bito-code-review[bot] commented on code in PR #41606:
URL: https://github.com/apache/superset/pull/41606#discussion_r3583952700


##########
tests/unit_tests/mcp_service/dashboard/tool/test_manage_dashboard_roles.py:
##########
@@ -0,0 +1,320 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for the manage_dashboard_roles MCP tool.
+
+"Roles" are ROLE-type Subjects in the dashboard's ``viewers`` list (the
+Subject-based model apache/superset#38831 introduced, replacing the legacy
+``roles``/``DASHBOARD_RBAC`` relationship), gated by ``ENABLE_VIEWERS``.
+"""
+
+from collections.abc import Iterator
+from unittest.mock import Mock, patch
+
+import pytest
+from fastmcp import Client
+
+from superset.mcp_service.app import mcp
+from superset.subjects.types import SubjectType
+from superset.utils import json
+
+DAO_GET = "superset.daos.dashboard.DashboardDAO.get_by_id_or_slug"
+SUBJECTS_FROM_ROLES = "superset.subjects.utils.subjects_from_roles"
+IS_FEATURE_ENABLED = "superset.is_feature_enabled"
+
+
[email protected]
+def mcp_server() -> object:
+    return mcp
+
+
[email protected](autouse=True)
+def mock_auth() -> Iterator[Mock]:
+    """Mock authentication for all tests in this module."""
+    with patch("superset.mcp_service.auth.get_user_from_request") as 
mock_get_user:
+        with patch("superset.security_manager.raise_for_editorship"):
+            mock_user = Mock()
+            mock_user.id = 1
+            mock_user.username = "admin"
+            mock_get_user.return_value = mock_user
+            yield mock_get_user
+
+
+def _mock_subject(id: int, role_id: int, label: str = "role") -> Mock:
+    subject = Mock()
+    subject.id = id
+    subject.type = SubjectType.ROLE
+    subject.user_id = None
+    subject.role_id = role_id
+    subject.label = label
+    subject.active = True
+    return subject
+
+
+def _mock_dashboard(
+    id: int = 42,
+    title: str = "Test Dashboard",
+    slug: str | None = "test-slug",
+    viewers: list[Mock] | None = None,
+) -> Mock:
+    dashboard = Mock()
+    dashboard.id = id
+    dashboard.dashboard_title = title
+    dashboard.slug = slug
+    dashboard.viewers = viewers if viewers is not None else []
+    return dashboard
+
+
+class TestManageDashboardRoles:
+    @patch(IS_FEATURE_ENABLED, return_value=True)
+    @patch(SUBJECTS_FROM_ROLES)
+    @patch(DAO_GET)
+    @patch("superset.extensions.db.session")
+    @pytest.mark.asyncio
+    async def test_add_role(
+        self,
+        mock_session: Mock,
+        mock_get: Mock,
+        mock_subjects_from_roles: Mock,
+        mock_flag: Mock,
+        mcp_server: object,
+    ) -> None:
+        new_subject = _mock_subject(200, 5, "Analyst")
+        dash = _mock_dashboard(viewers=[])
+        mock_get.return_value = dash
+        mock_subjects_from_roles.return_value = [new_subject]
+
+        async with Client(mcp_server) as client:
+            result = await client.call_tool(
+                "manage_dashboard_roles",
+                {"request": {"identifier": 42, "add_role_ids": [5]}},
+            )
+
+        mock_subjects_from_roles.assert_called_once_with([5])
+        assert dash.viewers == [new_subject]
+        assert mock_session.commit.call_count >= 1
+        payload = json.loads(result.content[0].text)
+        assert [r["id"] for r in payload["roles"]] == [200]
+        assert payload["added_role_ids"] == [5]
+        assert payload["viewers_enabled"] is True
+        assert payload["warnings"] == []

Review Comment:
   <!-- Bito Reply -->
   The addition of the test case 
`test_commit_failure_rolls_back_and_returns_error` effectively addresses the 
concern regarding missing test coverage for database commit failures. By 
mocking `db.session.commit` to raise a `SQLAlchemyError`, the test verifies 
that the application correctly triggers a rollback and returns a structured 
error response instead of failing unhandled.



##########
superset/mcp_service/dashboard/tool/manage_dashboard_roles.py:
##########
@@ -0,0 +1,309 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Manage dashboard access roles FastMCP tool
+
+Adds/removes role-based dashboard access via explicit operations. Companion
+to ``manage_dashboard_owners`` — dashboard access roles are dropped from the
+generic ``update_dashboard`` tool because a full-replacement access-control
+list silently widens or narrows who can see a dashboard.
+
+"Roles" are modeled as ROLE-type entries in the dashboard's Subject-based
+``viewers`` list (apache/superset#38831 replaced the legacy
+``roles``/``DASHBOARD_RBAC`` relationship with a unified Subject model
+covering User/Role/Group, gated by the ``ENABLE_VIEWERS`` feature flag
+instead of ``DASHBOARD_RBAC``). Any USER- or GROUP-type viewers already on
+the dashboard are preserved untouched by this tool.
+"""
+
+import logging
+from typing import Any
+
+from fastmcp import Context
+from sqlalchemy.exc import SQLAlchemyError
+from superset_core.mcp.decorators import tool, ToolAnnotations
+
+from superset.commands.dashboard.exceptions import DashboardNotFoundError
+from superset.exceptions import SupersetSecurityException
+from superset.extensions import db, event_logger
+from superset.mcp_service.dashboard.schemas import (
+    ManageDashboardRolesRequest,
+    ManageDashboardRolesResponse,
+)
+from superset.mcp_service.system.schemas import serialize_subject_object
+from superset.mcp_service.utils.url_utils import get_superset_base_url
+from superset.subjects.types import SubjectType
+
+logger: logging.Logger = logging.getLogger(__name__)
+
+
+def _find_and_authorize_dashboard(
+    identifier: int | str,
+) -> tuple[Any, ManageDashboardRolesResponse | None]:
+    """Return (dashboard, None) on success or (None, error_response) on 
failure.
+
+    Mirrors the helper in ``update_dashboard``: avoids ImportError before
+    Flask app initialisation by co-locating the imports it needs with the
+    call site rather than importing them at module load time.
+    """
+    from superset import security_manager
+    from superset.daos.dashboard import DashboardDAO
+
+    try:
+        dashboard = DashboardDAO.get_by_id_or_slug(identifier)
+    except (DashboardNotFoundError, SQLAlchemyError):
+        return None, ManageDashboardRolesResponse(
+            error=f"Dashboard not found: {identifier!r}",
+        )
+
+    if dashboard is None:
+        return None, ManageDashboardRolesResponse(
+            error=f"Dashboard not found: {identifier!r}",
+        )
+
+    try:
+        security_manager.raise_for_editorship(dashboard)
+    except SupersetSecurityException:
+        return None, ManageDashboardRolesResponse(
+            permission_denied=True,
+            error=(
+                f"You don't have permission to edit dashboard "
+                f"'{dashboard.dashboard_title}' (ID: {dashboard.id})."
+            ),
+        )

Review Comment:
   <!-- Bito Reply -->
   The suggestion is correct and should be applied. The current implementation 
only catches `DashboardNotFoundError` and `SQLAlchemyError`, which leads to an 
unhandled `DashboardAccessDeniedError` when a user lacks view access. Adding 
this exception to the `try-except` block ensures the tool returns a structured 
`permission_denied` response, consistent with other dashboard management tools.
   
   **superset/mcp_service/dashboard/tool/manage_dashboard_roles.py**
   ```
   try:
           dashboard = DashboardDAO.get_by_id_or_slug(identifier)
       except (DashboardNotFoundError, DashboardAccessDeniedError, 
SQLAlchemyError):
           return None, ManageDashboardRolesResponse(
               error=f"Dashboard not found: {identifier!r}",
           )
   ```



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