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


##########
superset/mcp_service/dashboard/tool/update_dashboard.py:
##########
@@ -164,14 +164,81 @@ def _collect_metadata_overrides(request: 
UpdateDashboardRequest) -> dict[str, An
     return overrides
 
 
-def _apply_field_updates(dashboard: Any, request: UpdateDashboardRequest) -> 
list[str]:
+def _resolve_owners(owner_ids: list[int]) -> tuple[list[Any], list[int]]:
+    """Resolve owner user IDs to user objects.
+
+    Returns ``(users, missing_ids)``, deduplicating IDs while preserving the
+    caller's order. ``security_manager`` is imported lazily for the same
+    app-context reason as ``_find_and_authorize_dashboard``.
+    """
+    from superset import security_manager
+
+    users: list[Any] = []
+    missing: list[int] = []
+    seen: set[int] = set()
+    for uid in owner_ids:
+        if uid in seen:
+            continue
+        seen.add(uid)
+        user = security_manager.get_user_by_id(uid)

Review Comment:
   Related to the line-289 issue: even once this targets `editors` instead of 
`owners`, `get_user_by_id` returns FAB `User` rows, but `editors` expects 
`Subject` rows — a different ID space. `generate_dashboard.py` in this same 
directory already has the right pattern (`get_user_subject(user.id)` before 
assigning to `dashboard.editors`), and REST's `UpdateDashboardCommand` goes 
through `compute_subjects` → `compute_subject_list` → 
`populate_subject_list`/`get_or_create_user_subject` in `commands/utils.py`. 
Reusing that helper here would fix this and the line-289 issue together and 
keep owner-resolution semantics in sync with REST.



##########
superset/mcp_service/dashboard/tool/update_dashboard.py:
##########
@@ -328,12 +408,18 @@ def update_dashboard(
     if validation_error is not None:
         return validation_error
 
+    # Resolve owners once up front so the same list is used for validation and
+    # the write (no second lookup that could drop a concurrently-removed user).
+    resolved_owners, owners_error = _resolve_and_validate_owners(request)

Review Comment:
   Building on msyavuz's question above: once this is wired to `editors` 
correctly, worth deciding explicitly whether to add the same 
`ensure_no_lockout=True` guard REST uses in `compute_subjects`, so a non-admin 
can't remove themselves from the editor list via this tool. If the omission is 
intentional it'd be good to say so, otherwise it's an easy way to get locked 
out of a dashboard you still need to edit.



##########
tests/unit_tests/mcp_service/dashboard/tool/test_update_dashboard.py:
##########
@@ -137,6 +137,119 @@ async def test_update_layout_theme_and_css(
         changed = set(payload.get("changed_fields") or [])
         assert {"position_json", "json_metadata", "css"} <= changed
 
+    @patch("superset.security_manager.get_user_by_id")
+    @patch("superset.daos.dashboard.DashboardDAO.get_by_id_or_slug")
+    @patch("superset.extensions.db.session")
+    @pytest.mark.asyncio
+    async def test_update_owners_replaces_list(

Review Comment:
   Good coverage overall on the input-handling side. One gap: 
`test_non_editor_gets_permission_denied` (further down in this file) only 
exercises `dashboard_title`, not `owners`. The editorship gate runs before any 
field-specific logic so it should cover `owners` too, but a direct test for 
that — ideally against something more realistic than `Mock()` for the dashboard 
— would likely have caught the persistence issue flagged above before this 
merged.



##########
superset/mcp_service/dashboard/tool/update_dashboard.py:
##########
@@ -214,6 +281,14 @@ def _apply_field_updates(dashboard: Any, request: 
UpdateDashboardRequest) -> lis
         update_tags(ObjectType.dashboard, dashboard.id, dashboard.tags, 
request.tags)
         changed.append("tags")
 
+    if request.owners is not None:
+        # Full replacement of owners (empty list clears them). The owners were
+        # resolved and validated exactly once by _resolve_and_validate_owners,
+        # so there is no second lookup here that could silently drop a user
+        # removed between validation and this write.
+        dashboard.owners = resolved_owners or []

Review Comment:
   Confirming and expanding on msyavuz's comment above: `Dashboard` doesn't 
actually have an `owners` relationship — only `editors`/`viewers` 
(Subject-based, see `superset/models/dashboard.py`). This line sets a plain, 
unmapped instance attribute, so `db.session.commit()` won't persist anything 
here against a real database. The unit tests pass only because `dash` is a 
`Mock()` that happily accepts any attribute assignment. `changed_fields` will 
still report `"owners"` and the tool will return success, so a caller has no 
way to tell the write silently no-op'd. This needs to route through `editors` 
the way the sibling `generate_dashboard.py` tool and the REST 
`UpdateDashboardCommand` do — see the note on line 183 below.



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