aminghadersohi commented on code in PR #43781:
URL: https://github.com/apache/superset/pull/43781#discussion_r3928617447


##########
superset/security/manager.py:
##########
@@ -4786,11 +4786,36 @@ def has_promiscuous_chart_access() -> bool:
             if dashboard.viewers:
                 if dashboard.published and self.is_viewer(dashboard):
                     return
-            elif not dashboard.datasources or any(
-                self.can_access_datasource(datasource)
-                for datasource in dashboard.datasources
-            ):
-                return
+            else:
+                # Datasource-based fallback. Member chart datasources are
+                # resolved across datasource types via
+                # ``Slice.resolved_datasource`` — ``Dashboard.datasources``
+                # only ever contains SqlaTable-backed datasources, so an
+                # unqualified emptiness check would grant every authenticated
+                # user access to a dashboard composed solely of, e.g.,
+                # semantic-view charts. A dashboard with no charts remains
+                # accessible; a chart whose datasource cannot be resolved
+                # counts as inaccessible, never as absent. Resolution is
+                # lazy and deduplicated per (type, id) so the first
+                # accessible datasource short-circuits the remaining lookups.
+                member_slices = dashboard.slices
+
+                def member_datasource_accessible() -> bool:
+                    seen: set[tuple[str | None, int | None]] = set()
+                    for slc in member_slices:
+                        key = (slc.datasource_type, slc.datasource_id)
+                        if key in seen:
+                            continue
+                        seen.add(key)
+                        resolved = slc.resolved_datasource
+                        if resolved is not None and self.can_access_datasource(

Review Comment:
   **Semantic-layer grants are dropped by this predicate.**
   
   `can_access_datasource(resolved)` routes to 
`SupersetSecurityManager.raise_for_access(datasource=...)`, which authorizes 
through `can_access_schema(...)`, `can_access("datasource_access", 
datasource.perm or "")` and `is_editor(...)`. For a `SemanticView`, 
`can_access_schema` short-circuits to `False` (it is not a `BaseDatasource`), 
so this reduces to **the view's own `perm` only**.
   
   Everywhere else, a semantic view is accessible via *either* its own perm 
*or* its parent layer's:
   
   - `SemanticView.raise_for_access()` — 
`superset/semantic_layers/models.py:576-581`
   - `SemanticLayerDAO.find_accessible()` — `or_(SemanticView.perm.in_(perms), 
SemanticLayer.perm.in_(perms))`, `superset/daos/semantic_layer.py:228-232`
   - `DatasourceDAO.build_semantic_view_query()` — same `or_`, 
`superset/daos/datasource.py:168-170`
   
   Both are real `datasource_access` PVMs — `semantic_layer_after_insert` 
(`superset/security/manager.py:3853-3854`) and `semantic_view_after_insert` 
(`:3974-3975`) each create one — so a role can hold the layer grant alone.
   
   Reproduced at this head with layer `finance` (`perm = [finance](id:<hex>)`) 
and view `revenue` (`perm = [finance].[revenue](id:101)`), granting **only** 
the layer perm:
   
   | check | result |
   | --- | --- |
   | `view.raise_for_access()` | allowed |
   | `raise_for_access(dashboard=...)` | **denied** |
   | `raise_for_access(chart=...)` | **denied** |
   
   For the dashboard gate that is a regression: pre-PR the same user was 
admitted by the `not dashboard.datasources` fail-open. For the chart gate it is 
an incomplete fix rather than a regression (semantic-view charts were already 
denied there).
   
   A blanket "call the datasource's own `raise_for_access()`" is not safe — 
`Query.raise_for_access()` (`superset/models/sql_lab.py:311`) re-parses 
`executed_sql` and enforces per-table dataset matching, which is both heavier 
and stricter than what this gate wants. Targeting the one type whose 
entitlement rule differs keeps it minimal:
   
   ```python
   def _datasource_accessible(self, datasource: Any) -> bool:
       from superset.semantic_layers.models import SemanticView
   
       if isinstance(datasource, SemanticView):
           try:
               datasource.raise_for_access()
               return True
           except SupersetSecurityException:
               return False
       return self.can_access_datasource(datasource)
   ```
   
   Both call sites need it — here and the chart branch below 
(`chart.resolved_datasource`). Worth a test with the view's own perm withheld 
and only the layer perm granted; the current fixtures give every view its own 
perm and leave the layer's unset, so this path is not exercised.



##########
UPDATING.md:
##########
@@ -39,6 +39,7 @@ payload. Clients must display the new impact and obtain 
renewed confirmation
 before retrying. Preview or recheck failures fail closed rather than treating
 unknown impact as zero. Chart and dashboard purge endpoints are unchanged.
 
+- The dashboard datasource-based visibility fallback now fails closed: a 
dashboard whose member charts’ datasources cannot be resolved (deleted 
datasource rows, missing `datasource_id`, or unsupported datasource types) is 
no longer accessible to users without explicit editor/viewer rights, and a 
dashboard composed of semantic-view charts now requires `datasource_access` on 
(at least one of) its semantic views — previously any authenticated user could 
open such a dashboard’s shell. Dashboards with no charts remain accessible, and 
dashboards with explicit viewers are unaffected. Conversely, holders of 
`all_datasource_access` now see every published no-viewer dashboard in the 
dashboard list — including chart-less ones previously hidden by the inner joins 
— matching what the object-level gate already allowed them to open.

Review Comment:
   This documents the restrictive direction and the `all_datasource_access` 
list widening, but not the object-gate widening from round 1: on a **mixed** 
dashboard the fallback is now an `any()` over *all* member charts rather than 
over table-backed ones only.
   
   Verified at this head — dashboard with one table chart (`perm = 
[d90].[t91](id:91)`) the user cannot access plus one semantic-view chart (`perm 
= [L2].[v90](id:90)`) they can:
   
   - pre-PR: `dashboard.datasources == {t91}` → `any(...)` is `False` → **403**
   - at head: the semantic view resolves and matches → **granted**
   
   So a user holding only semantic-view grants gains access to mixed dashboards 
that previously denied them. That follows from applying the pre-existing 
`any()` semantics consistently and looks intended, but it is user-visible and 
belongs in this note. One clause after the semantic-view sentence, e.g.:
   
   > Because the fallback now considers every member chart rather than only 
table-backed ones, a user holding `datasource_access` on any single member 
datasource — including a semantic view — can open a mixed dashboard that 
previously denied them.



##########
superset/dashboards/filters.py:
##########
@@ -167,12 +167,30 @@ def _apply_viewers(self, query: Query) -> Query:
             filters.append(Dashboard.id.in_(viewer_query))
 
         # (C) No-viewer fallback: dashboards with no viewers → dataset-based 
access
+        # Note: for ordinary users a dashboard with no charts is never yielded
+        # here (every access predicate is NULL-false after the outer joins)
+        # even though the object gate allows opening it — a deliberate,
+        # pre-existing asymmetry. For ``all_datasource_access`` holders the
+        # spliced literal True below yields such rows, matching the gate.
         dashboard_has_viewers = Dashboard.viewers.any()
         no_viewer_query = (
             db.session.query(Dashboard.id)
             .join(Dashboard.slices, isouter=True)
-            .join(SqlaTable, Slice.datasource_id == SqlaTable.id)
-            .join(Database, SqlaTable.database_id == Database.id)
+            # Type-aware datasource joins: the SqlaTable join is constrained
+            # to table-backed charts (an unconstrained id join can bind a
+            # semantic-view chart to an unrelated table sharing its numeric
+            # id) and kept outer so charts on other datasource types survive
+            # into the access filter — their access matches through the perm
+            # columns denormalized onto Slice by ``set_related_perm``.

Review Comment:
   This claim holds for a view-level grant but not a layer-level one. 
`set_related_perm` copies `ds.perm` onto `Slice.perm`, and for a semantic-view 
chart that is the **view's** perm — `SemanticView.catalog_perm` and 
`schema_perm` are both hardcoded `None` 
(`superset/semantic_layers/models.py:515-520`), so the other two denormalized 
columns never match either.
   
   A user holding only the parent layer's `datasource_access` therefore never 
matches branch C, and the dashboard stays out of their list. That is 
pre-existing here, but it becomes visible if the object gate is corrected as 
described on `superset/security/manager.py` — the gate would admit them while 
the list still hides the dashboard.
   
   Bringing this branch into line needs the layer perm in the disjunction too, 
e.g. an outer join to `SemanticView`/`SemanticLayer` on `Slice.datasource_type 
== 'semantic_view'` with `SemanticLayer.perm.in_(perms)` added — mirroring 
`DatasourceDAO.build_semantic_view_query`. Fine to defer to a follow-up, but 
the two surfaces should not drift in opposite directions.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to