codeant-ai-for-open-source[bot] commented on code in PR #40959: URL: https://github.com/apache/superset/pull/40959#discussion_r3482569042
########## superset/mcp_service/dashboard/tool/duplicate_dashboard.py: ########## @@ -0,0 +1,393 @@ +# 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. + +""" +MCP tool: duplicate_dashboard + +Duplicates an existing dashboard, optionally deep-copying its charts. +Canonical workflow: clone a template dashboard, then edit the copy +(e.g. to create a regional or staging variant). +""" + +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.extensions import event_logger +from superset.mcp_service.dashboard.schemas import ( + _sanitize_dashboard_info_for_llm_context, + DashboardInfo, + DuplicateDashboardRequest, + DuplicateDashboardResponse, + serialize_chart_summary, +) +from superset.mcp_service.privacy import user_can_view_data_model_metadata +from superset.mcp_service.utils.url_utils import get_superset_base_url +from superset.utils import json + +logger = logging.getLogger(__name__) + + +def _positions_reference_charts(positions: dict[str, Any]) -> bool: + """Return whether a layout maps any chart into the dashboard. + + ``DashboardDAO.set_dash_metadata`` rebuilds the new dashboard's slice + list solely from the chart IDs found in ``positions``, so a layout + with no ``CHART`` entries yields an empty dashboard regardless of the + source's ``slices`` relationship. + """ + return any( + isinstance(value, dict) + and value.get("type") == "CHART" + and value.get("meta", {}).get("chartId") + for value in positions.values() + ) Review Comment: **Suggestion:** The layout guard only checks whether any `CHART` node has a truthy `chartId`, but it does not verify that those IDs actually resolve to real charts on the source dashboard. If `position_json` contains stale/mismatched chart IDs, the guard passes and `set_dash_metadata` later rebuilds slices from non-existent IDs, yielding an empty copy despite source charts existing. Validate chart IDs against `source.slices` (or at least DB-resolved IDs) before allowing duplication. [logic error] <details> <summary><b>Severity Level:</b> Major ⚠️</summary> ```mdx - ❌ MCP duplicate_dashboard may return empty dashboards unexpectedly. - ⚠️ Users lose charts in copies when layout IDs go stale. - ⚠️ AI-assisted cloning workflow can silently produce broken dashboards. ``` </details> <details> <summary><b>Steps of Reproduction ✅ </b></summary> ```mdx 1. Create a dashboard whose `slices` relationship still contains charts (queried by `DashboardDAO.copy_dashboard` at `superset/daos/dashboard.py:5-21`), but whose persisted `position_json` only references chart IDs that do not correspond to any real `Slice` rows (e.g. manually edit `dashboards.position_json` to include `{"type": "CHART", "meta": {"chartId": 999999}}` where `Slice.id=999999` does not exist). 2. Start the Superset MCP service so that the `duplicate_dashboard` tool from `superset/mcp_service/dashboard/tool/__init__.py:18-32` is registered, and connect an MCP client. 3. Call the `duplicate_dashboard()` MCP tool at `superset/mcp_service/dashboard/tool/duplicate_dashboard.py:276-289` with `dashboard_id` pointing to the corrupted dashboard; inside the tool, `_build_copy_payload()` at `duplicate_dashboard.py:64-102` loads `position_json` and passes it to `_positions_reference_charts()` at `duplicate_dashboard.py:48-61`. 4. `_positions_reference_charts()` returns `True` at lines 56-61 because it only checks for any `CHART` node with a truthy `meta["chartId"]`, without verifying that those IDs map to existing slices on the source. The guard at `duplicate_dashboard.py:311-324` (`if getattr(source, "slices", None) and not layout_has_charts`) is therefore bypassed, `CopyDashboardCommand(source, data).run()` executes, and `DashboardDAO.set_dash_metadata()` at `superset/daos/dashboard.py:5-25` computes `slice_ids` from the stale `chartId`s and queries `Slice.id.in_(slice_ids)` at line 22. Because there are no matching `Slice` rows, `dashboard.slices` is set to an empty list at line 25, yielding a duplicated dashboard with no charts even though the source `source.slices` was non-empty and the tool did not return an error. ``` </details> [](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=970466adf4704544a0bd51164f40ccc3&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset) [](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=970466adf4704544a0bd51164f40ccc3&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset) *(Use Cmd/Ctrl + Click for best experience)* <details> <summary><b>Prompt for AI Agent 🤖 </b></summary> ```mdx This is a comment left during a code review. **Path:** superset/mcp_service/dashboard/tool/duplicate_dashboard.py **Line:** 56:61 **Comment:** *Logic Error: The layout guard only checks whether any `CHART` node has a truthy `chartId`, but it does not verify that those IDs actually resolve to real charts on the source dashboard. If `position_json` contains stale/mismatched chart IDs, the guard passes and `set_dash_metadata` later rebuilds slices from non-existent IDs, yielding an empty copy despite source charts existing. Validate chart IDs against `source.slices` (or at least DB-resolved IDs) before allowing duplication. Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise. Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix ``` </details> <a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F40959&comment_hash=20ca186b48b4321173e5003f77781b806b546e57d44b4001de06f52b7d4639f3&reaction=like'>👍</a> | <a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F40959&comment_hash=20ca186b48b4321173e5003f77781b806b546e57d44b4001de06f52b7d4639f3&reaction=dislike'>👎</a> ########## superset/mcp_service/dashboard/tool/duplicate_dashboard.py: ########## @@ -0,0 +1,393 @@ +# 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. + +""" +MCP tool: duplicate_dashboard + +Duplicates an existing dashboard, optionally deep-copying its charts. +Canonical workflow: clone a template dashboard, then edit the copy +(e.g. to create a regional or staging variant). +""" + +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.extensions import event_logger +from superset.mcp_service.dashboard.schemas import ( + _sanitize_dashboard_info_for_llm_context, + DashboardInfo, + DuplicateDashboardRequest, + DuplicateDashboardResponse, + serialize_chart_summary, +) +from superset.mcp_service.privacy import user_can_view_data_model_metadata +from superset.mcp_service.utils.url_utils import get_superset_base_url +from superset.utils import json + +logger = logging.getLogger(__name__) + + +def _positions_reference_charts(positions: dict[str, Any]) -> bool: + """Return whether a layout maps any chart into the dashboard. + + ``DashboardDAO.set_dash_metadata`` rebuilds the new dashboard's slice + list solely from the chart IDs found in ``positions``, so a layout + with no ``CHART`` entries yields an empty dashboard regardless of the + source's ``slices`` relationship. + """ + return any( + isinstance(value, dict) + and value.get("type") == "CHART" + and value.get("meta", {}).get("chartId") + for value in positions.values() + ) + + +def _build_copy_payload( + source: Any, dashboard_title: str, duplicate_slices: bool +) -> tuple[dict[str, Any], bool]: + """Build the data payload expected by ``CopyDashboardCommand``. + + Mirrors what the frontend "Save as" flow sends to the + ``/api/v1/dashboard/<id>/copy/`` endpoint: the source dashboard's + current ``json_metadata`` with a ``positions`` key holding the current + layout (``position_json``). ``DashboardCopySchema`` requires + ``json_metadata``, and ``DashboardDAO.copy_dashboard`` reads + ``positions`` from it to remap chart IDs when ``duplicate_slices`` + is enabled. + + Returns the payload and a flag indicating whether the layout maps any + chart, so the caller can refuse to produce a silently empty copy. + """ + try: + metadata = json.loads(source.json_metadata or "{}") + except (json.JSONDecodeError, TypeError): + metadata = {} + if not isinstance(metadata, dict): + metadata = {} Review Comment: **Suggestion:** Parsing failures for the source dashboard's `json_metadata` are silently converted to `{}`, so malformed persisted metadata produces a "successful" duplicate with defaulted/stripped settings instead of surfacing a repairable error. This can silently lose filter config, color config, and other dashboard behavior. Return a structured duplication error when `json_metadata` cannot be decoded (instead of defaulting), matching the tool's stated behavior of mirroring the source state. [logic error] <details> <summary><b>Severity Level:</b> Major ⚠️</summary> ```mdx - ❌ MCP duplicate_dashboard can drop all JSON-based settings. - ⚠️ Copied dashboards may lose filters and customizations silently. - ⚠️ AI workflows see copies behave differently than source dashboards. ``` </details> <details> <summary><b>Steps of Reproduction ✅ </b></summary> ```mdx 1. Create or identify a dashboard record whose persisted `json_metadata` column is malformed JSON (e.g. set `dashboards.json_metadata` to `"{invalid"` in the database so that `source.json_metadata` read at `superset/mcp_service/dashboard/tool/duplicate_dashboard.py:81` is not valid JSON). 2. Start the Superset MCP service and connect an MCP client that exposes the `duplicate_dashboard` tool from `superset/mcp_service/dashboard/tool/__init__.py:18-32`. 3. Invoke the MCP tool `duplicate_dashboard()` defined at `superset/mcp_service/dashboard/tool/duplicate_dashboard.py:276-289` with `dashboard_id` pointing to the malformed dashboard and a valid `dashboard_title`. 4. During execution, `_build_copy_payload()` at `superset/mcp_service/dashboard/tool/duplicate_dashboard.py:64-102` calls `json.loads(source.json_metadata or "{}")` inside the `try` at line 80; the malformed JSON raises `json.JSONDecodeError`, which is caught at line 82 and replaced with `metadata = {}` at lines 83-85. The function then builds a payload with `json_metadata=json.dumps(metadata)` (line 100), so `CopyDashboardCommand` (instantiated at line 327 and implemented in `superset/daos/dashboard.py:372-34`) receives a sanitized `{}` instead of failing. The duplicate dashboard is successfully created but loses any settings that were present in the original `json_metadata` (filter scopes, default filters, chart customization config as consumed in `superset/daos/dashboard.py:36-76` and `superset/views/utils.py:372-27`), contrary to the expectation that invalid stored metadata would surface as an error as handled in the `(ValueError, TypeError)` block at `duplicate_dashboard.py:370-387`. ``` </details> [](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=2626d74076644f1d9ee531e8a1c85a5b&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset) [](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=2626d74076644f1d9ee531e8a1c85a5b&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset) *(Use Cmd/Ctrl + Click for best experience)* <details> <summary><b>Prompt for AI Agent 🤖 </b></summary> ```mdx This is a comment left during a code review. **Path:** superset/mcp_service/dashboard/tool/duplicate_dashboard.py **Line:** 80:85 **Comment:** *Logic Error: Parsing failures for the source dashboard's `json_metadata` are silently converted to `{}`, so malformed persisted metadata produces a "successful" duplicate with defaulted/stripped settings instead of surfacing a repairable error. This can silently lose filter config, color config, and other dashboard behavior. Return a structured duplication error when `json_metadata` cannot be decoded (instead of defaulting), matching the tool's stated behavior of mirroring the source state. Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise. Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix ``` </details> <a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F40959&comment_hash=5f29b4444f3b5d5a64369dded2975ee81726238c3ebadcae5fcb27431fa8da9f&reaction=like'>👍</a> | <a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F40959&comment_hash=5f29b4444f3b5d5a64369dded2975ee81726238c3ebadcae5fcb27431fa8da9f&reaction=dislike'>👎</a> -- 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]
