sadpandajoe commented on code in PR #44215:
URL: https://github.com/apache/superset/pull/44215#discussion_r4451517751


##########
superset/security/manager.py:
##########
@@ -5715,154 +6013,34 @@ def raise_for_access(  # noqa: C901
             assert datasource
             self.raise_for_unsupported_guest_rls(datasource)
 
-            def has_promiscuous_chart_access() -> bool:
-                if not (
-                    form_data
-                    and is_feature_enabled("ENABLE_VIEWERS")
-                    and current_app.config.get("VIEWER_PROMISCUOUS_MODE")
-                    and (viewer_slice_id := form_data.get("slice_id"))
-                    and (
-                        viewer_slc := self.session.query(Slice)
-                        .filter(Slice.id == viewer_slice_id)
-                        .one_or_none()
-                    )
-                ):
-                    return False
-
-                if not _datasource_matches(
-                    datasource, viewer_slc.datasource_id, 
viewer_slc.datasource_type
-                ):
-                    return False
-
-                return self.is_viewer(viewer_slc) or self.is_editor(viewer_slc)
-
             if not (
                 self.can_access_schema(datasource)
                 or self.can_access("datasource_access", datasource.perm or "")
                 # A grant on a semantic view's parent layer covers the view,
                 # matching SemanticView.raise_for_access (sc-119501).
                 or self._semantic_layer_grant_allows(datasource)
-                # ``allow_editorship`` is honored here too so a caller that 
opts
-                # out of the editorship accept (e.g. dataset define/repoint
-                # validation) gets consistent behavior across both branches,
-                # rather than the flag silently applying only to the
-                # database+table / query branch above.
+                # ``allow_editorship`` is honored on both branches so a caller
+                # that opts out of the editorship accept (e.g. dataset
+                # define/repoint validation) gets consistent behavior.
                 or (allow_editorship and self.is_editor(datasource))
+                # Embedded guest / promiscuous viewer, bound to the specific
+                # dashboard request (native filter, member chart, multi-layer
+                # child, or drill). Keeps guests strictly scoped to the 
request.
+                or self._has_request_bound_dashboard_datasource_access(
+                    datasource, form_data
+                )
+                # Chart-viewer/editor promiscuous mode: a viewer or editor of 
the
+                # chart the request references may query that chart's 
datasource.
+                or self._has_promiscuous_chart_access(datasource, form_data)
+                # Dashboard-viewer promiscuous mode: a viewer or editor of a
+                # dashboard that uses this datasource inherits access to it,
+                # independent of the request's ``form_data``. This is what
+                # lets ``GET /dashboard/<id>/datasets`` serve full metadata to 
a
+                # promiscuous viewer.
                 or (
-                    # Grant access to the datasource only if dashboard RBAC is 
enabled
-                    # or the user is an embedded guest user with access to the 
dashboard
-                    # and said datasource is associated with the dashboard 
chart in
-                    # question.
-                    form_data
-                    and (dashboard_id := form_data.get("dashboardId"))
-                    and (
-                        dashboard_ := self.session.query(Dashboard)
-                        .filter(Dashboard.id == dashboard_id)
-                        .one_or_none()
-                    )
-                    and (
-                        (
-                            is_feature_enabled("EMBEDDED_SUPERSET")
-                            and self.is_guest_user()
-                        )
-                        or (
-                            is_feature_enabled("ENABLE_VIEWERS")
-                            and 
current_app.config.get("VIEWER_PROMISCUOUS_MODE")
-                            and self.is_viewer(dashboard_)
-                        )
-                    )
-                    and (
-                        (
-                            # Native filter.
-                            form_data.get("type") == "NATIVE_FILTER"
-                            and (native_filter_id := 
form_data.get("native_filter_id"))
-                            and dashboard_.json_metadata
-                            and (json_metadata := 
json.loads(dashboard_.json_metadata))
-                            and any(
-                                _datasource_matches(
-                                    datasource,
-                                    target.get("datasetId"),
-                                    target.get("datasourceType")
-                                    or DatasourceType.TABLE,
-                                )
-                                for fltr in json_metadata.get(
-                                    "native_filter_configuration",
-                                    [],
-                                )
-                                for target in fltr.get("targets", [])
-                                if native_filter_id == fltr.get("id")
-                            )
-                        )
-                        or (
-                            form_data.get("type") != "NATIVE_FILTER"
-                            and (
-                                (
-                                    # Chart.
-                                    (slice_id := form_data.get("slice_id"))
-                                    and (
-                                        # Direct chart access (no parent)
-                                        (
-                                            form_data.get("parent_slice_id") 
is None
-                                            and (
-                                                slc := 
self.session.query(Slice)
-                                                .filter(Slice.id == slice_id)
-                                                .one_or_none()
-                                            )
-                                            and slc in dashboard_.slices
-                                            and _datasource_matches(
-                                                datasource,
-                                                slc.datasource_id,
-                                                slc.datasource_type,
-                                            )
-                                        )
-                                        or
-                                        # Multi-layer chart child access (has 
parent)
-                                        (
-                                            (
-                                                parent_id := form_data.get(
-                                                    "parent_slice_id"
-                                                )
-                                            )
-                                            and (
-                                                parent_slc := 
self.session.query(Slice)
-                                                .filter(Slice.id == parent_id)
-                                                .one_or_none()
-                                            )
-                                            and parent_slc in dashboard_.slices
-                                            # Validate child is actually part 
of parent's config    # noqa: E501
-                                            and 
self._validate_child_in_parent_multilayer(  # noqa: E501
-                                                child_slice_id=slice_id,
-                                                parent_slice=parent_slc,
-                                            )
-                                            # Bind the request to the child
-                                            # chart's own datasource, mirroring
-                                            # the direct-chart leg above.
-                                            and (
-                                                child_slc := 
self.session.query(Slice)
-                                                .filter(Slice.id == slice_id)
-                                                .one_or_none()
-                                            )
-                                            and _datasource_matches(
-                                                datasource,
-                                                child_slc.datasource_id,
-                                                child_slc.datasource_type,
-                                            )
-                                        )
-                                    )
-                                )
-                                # D2D or Drill By
-                                or self.has_drill_access(
-                                    form_data, dashboard_, datasource
-                                )
-                            )
-                        )
-                    )
-                    and self.can_access_dashboard(dashboard_)
+                    allow_dashboard_inheritance
+                    and 
self._promiscuous_viewer_inherits_datasource(datasource)

Review Comment:
   This new dataset-inheritance leg also satisfies 
`CreateChartCommand.validate()` (`commands/chart/create.py:85`) and the 
datasource check on chart repoint (`commands/chart/update.py:262`), which both 
call `raise_for_access(datasource=...)` with inheritance allowed. A Gamma 
viewer of published dashboard A, with no grant on its dataset X, can POST a 
chart on X (or Save as from Explore) and becomes that chart's editor. Once they 
are removed from A or A is unpublished, `_has_promiscuous_chart_access` still 
admits `form_data.slice_id = <their chart>` through `is_editor(chart)`, so they 
keep querying X with no accessible dashboard. The attach gate's own comment 
says inherited access should last only as long as the dashboard grants it; 
should the create and repoint checks opt out with 
`allow_dashboard_inheritance=False` the same way?



##########
superset/daos/dashboard.py:
##########
@@ -343,8 +343,14 @@ def _raise_for_unreadable_new_charts(
             selectinload(Slice.editors),
             selectinload(Slice.viewers),
         ).filter(Slice.id.in_([chart.id for chart in candidates])).all()
+    # Access inherited through another dashboard doesn't count: it lasts only
+    # as long as that dashboard grants it, while membership here would keep it.
     denied: list[int] = sorted(
-        chart.id for chart in candidates if not 
security_manager.can_access_chart(chart)
+        chart.id
+        for chart in candidates
+        if not security_manager.can_access_chart(
+            chart, allow_dashboard_inheritance=False

Review Comment:
   This gate only runs when slices are attached through the DAO. 
`generate_dashboard` (`mcp_service/dashboard/tool/generate_dashboard.py:274`) 
checks `can_access_chart(chart)` with inheritance allowed and then assigns 
`dashboard.slices` directly (around line 412), so it never reaches 
`_raise_for_unreadable_new_charts`. A viewer of published dashboard D1 can call 
it with a chart from D1 and get a new dashboard D2 on which they are an editor; 
after they lose D1, the editors leg (no `published` requirement) keeps the 
chart and its dataset readable. Should that tool use 
`allow_dashboard_inheritance=False` or go through the DAO gate so the invariant 
described in the comment above holds there too?



##########
superset/security/manager.py:
##########
@@ -2670,46 +2670,324 @@ def _semantic_layer_grant_allows(
             return False
         return self.can_access("datasource_access", layer_perm)
 
-    def can_access_datasource(self, datasource: "BaseDatasource") -> bool:
+    def can_access_datasource(
+        self,
+        datasource: "BaseDatasource",
+        *,
+        allow_dashboard_inheritance: bool = True,
+    ) -> bool:
         """
         Return True if the user can access the specified datasource, False 
otherwise.
 
         :param datasource: The datasource
+        :param allow_dashboard_inheritance: Whether access inherited through a
+            dashboard under ``VIEWER_PROMISCUOUS_MODE`` counts; see
+            :meth:`raise_for_access`
         :returns: Whether the user can access the datasource
         """
 
         try:
-            self.raise_for_access(datasource=datasource)
+            if allow_dashboard_inheritance:
+                self.raise_for_access(datasource=datasource)
+            else:
+                # Passed only when set, so an operator's in-place replacement
+                # of ``raise_for_access`` with the older signature keeps
+                # working on the default path.
+                self.raise_for_access(

Review Comment:
   The compatibility comment says an older-signature `raise_for_access` 
override keeps working on the default path, but this branch is also reached on 
the default path internally: the chart fallback (around line 6143) and the 
dashboard fallback (around line 6116) call `can_access_datasource(..., 
allow_dashboard_inheritance=False)`. With an operator override that has the 
pre-PR signature and delegates to `super()`, an ordinary `can_access_chart(C)` 
for a user with an explicit grant on C's dataset (chart has no viewers, user 
not an editor) then raises an uncaught `TypeError` here instead of returning 
True. This happens even with `ENABLE_VIEWERS` off. Is the 
override-compatibility guarantee meant to cover these nested calls, and if so 
how should they avoid passing the keyword?



-- 
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