codeant-ai-for-open-source[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891628140


##########
superset-frontend/src/pages/EmbeddedChartsList/index.tsx:
##########
@@ -0,0 +1,249 @@
+/**
+ * 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.
+ */
+
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { Link } from 'react-router-dom';
+import { SupersetClient } from '@superset-ui/core';
+import { styled, css, t } from '@apache-superset/core/ui';
+import { DeleteModal, Tooltip } from '@superset-ui/core/components';
+import { Icons } from '@superset-ui/core/components/Icons';
+import withToasts from 'src/components/MessageToasts/withToasts';
+import { ListView } from 'src/components';
+import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
+
+const PAGE_SIZE = 25;
+
+interface EmbeddedChart {
+  uuid: string;
+  chart_id: number;
+  chart_name: string | null;
+  allowed_domains: string[];
+  changed_on: string | null;
+}
+
+interface EmbeddedChartsListProps {
+  addDangerToast: (msg: string) => void;
+  addSuccessToast: (msg: string) => void;
+}
+
+const ActionsWrapper = styled.div`
+  display: flex;
+  gap: 8px;
+`;
+
+const ActionButton = styled.button`
+  ${({ theme }) => css`
+    background: none;
+    border: none;
+    cursor: pointer;
+    padding: 4px;
+    color: ${theme.colorTextSecondary};
+    &:hover {
+      color: ${theme.colorPrimary};
+    }
+  `}
+`;
+
+function EmbeddedChartsList({
+  addDangerToast,
+  addSuccessToast,
+}: EmbeddedChartsListProps) {
+  const [embeddedCharts, setEmbeddedCharts] = useState<EmbeddedChart[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [chartToDelete, setChartToDelete] = useState<EmbeddedChart | null>(
+    null,
+  );
+
+  const fetchEmbeddedCharts = useCallback(async () => {
+    setLoading(true);
+    try {
+      const response = await SupersetClient.get({
+        endpoint: '/api/v1/chart/embedded',
+      });
+      setEmbeddedCharts(response.json.result || []);
+    } catch (error) {
+      addDangerToast(t('Error loading embedded charts'));
+    } finally {
+      setLoading(false);
+    }
+  }, [addDangerToast]);
+
+  useEffect(() => {
+    fetchEmbeddedCharts();
+  }, [fetchEmbeddedCharts]);
+
+  const handleDelete = useCallback(
+    async (chart: EmbeddedChart) => {
+      try {
+        await SupersetClient.delete({
+          endpoint: `/api/v1/chart/${chart.chart_id}/embedded`,
+        });
+        addSuccessToast(t('Embedding disabled for %s', chart.chart_name));
+        fetchEmbeddedCharts();
+      } catch (error) {
+        addDangerToast(t('Error disabling embedding for %s', 
chart.chart_name));
+      } finally {
+        setChartToDelete(null);
+      }
+    },
+    [addDangerToast, addSuccessToast, fetchEmbeddedCharts],
+  );
+
+  const copyToClipboard = useCallback(
+    (text: string) => {
+      navigator.clipboard.writeText(text).then(
+        () => addSuccessToast(t('UUID copied to clipboard')),
+        () => addDangerToast(t('Failed to copy to clipboard')),
+      );
+    },
+    [addDangerToast, addSuccessToast],
+  );

Review Comment:
   **Suggestion:** Using `navigator.clipboard.writeText` without checking for 
`navigator.clipboard` will throw a runtime error in non-secure contexts or 
unsupported browsers, and the promise handlers won't run, so the user sees no 
toast but the click crashes. Guard against the Clipboard API being unavailable 
before calling `writeText` and fall back to an error toast. [possible bug]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Copy UUID action crashes Embedded Charts page.
   - ⚠️ Users cannot copy embed UUIDs in some environments.
   ```
   </details>
   
   ```suggestion
     const copyToClipboard = useCallback(
       (text: string) => {
         if (!navigator.clipboard || !navigator.clipboard.writeText) {
           addDangerToast(t('Failed to copy to clipboard'));
           return;
         }
         navigator.clipboard.writeText(text).then(
           () => addSuccessToast(t('UUID copied to clipboard')),
           () => addDangerToast(t('Failed to copy to clipboard')),
         );
       },
       [addDangerToast, addSuccessToast],
     );
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Run the Superset frontend in a non-secure context (e.g., default dev 
server over
   http://localhost) where the Clipboard API is not exposed, and open the 
Embedded Charts
   page rendered by `EmbeddedChartsList`
   (`superset-frontend/src/pages/EmbeddedChartsList/index.tsx`).
   
   2. Observe that `EmbeddedChartsList` defines `copyToClipboard` at lines 
108–116, and that
   this function is used in the UUID column cell renderer and in the Actions 
column "Copy
   UUID" button within the `columns` `useMemo` in the same file (lines 
~120–180).
   
   3. In the browser, click on the UUID `<code>` element in the "UUID" column 
or the "Copy
   UUID" icon button in the "Actions" column; both call `copyToClipboard(uuid)` 
without any
   feature detection.
   
   4. Because `navigator.clipboard` is `undefined` in this context, the 
expression
   `navigator.clipboard.writeText` at line 110 attempts to access `writeText` 
on `undefined`,
   throwing a synchronous `TypeError` before the promise is created, causing a 
React runtime
   error and preventing any success/error toast from being shown.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/pages/EmbeddedChartsList/index.tsx
   **Line:** 108:116
   **Comment:**
        *Possible Bug: Using `navigator.clipboard.writeText` without checking 
for `navigator.clipboard` will throw a runtime error in non-secure contexts or 
unsupported browsers, and the promise handlers won't run, so the user sees no 
toast but the click crashes. Guard against the Clipboard API being unavailable 
before calling `writeText` and fall back to an error toast.
   
   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.
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=6246c5ed08f02b19dff6416cdd8d619c8e90ba3f9dcf527d7fd44a66f8a9a876&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=6246c5ed08f02b19dff6416cdd8d619c8e90ba3f9dcf527d7fd44a66f8a9a876&reaction=dislike'>👎</a>



##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,136 @@
+# 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.
+import logging
+from typing import Callable
+from urllib.parse import urlparse
+
+from flask import abort, current_app, request
+from flask_appbuilder import expose
+from flask_login import AnonymousUserMixin, login_user
+
+from superset import event_logger
+from superset.daos.key_value import KeyValueDAO
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.key_value.shared_entries import get_permalink_salt
+from superset.key_value.types import (
+    KeyValueResource,
+    MarshmallowKeyValueCodec,
+    SharedKey,
+)
+from superset.key_value.utils import decode_permalink_id
+from superset.superset_typing import FlaskResponse
+from superset.utils import json
+from superset.views.base import BaseSupersetView, common_bootstrap_payload
+
+logger = logging.getLogger(__name__)
+
+
+def same_origin(url1: str | None, url2: str | None) -> bool:
+    """Check if two URLs have the same origin (scheme + netloc)."""
+    if not url1 or not url2:
+        return False
+    parsed1 = urlparse(url1)
+    parsed2 = urlparse(url2)
+    # For domain matching, we just check if the host matches
+    # url2 might just be a domain like "example.com"
+    if not parsed2.scheme:
+        # url2 is just a domain, check if it matches url1's netloc
+        return parsed1.netloc == url2 or parsed1.netloc.endswith(f".{url2}")
+    return (parsed1.scheme, parsed1.netloc) == (parsed2.scheme, parsed2.netloc)
+
+
+class EmbeddedChartView(BaseSupersetView):
+    """Server-side rendering for embedded chart pages."""
+
+    route_base = "/embedded/chart"
+
+    @expose("/")
+    @event_logger.log_this_with_extra_payload
+    def embedded_chart(
+        self,
+        add_extra_log_payload: Callable[..., None] = lambda **kwargs: None,
+    ) -> FlaskResponse:
+        """
+        Server side rendering for the embedded chart page.
+        Expects ?permalink_key=xxx query parameter.
+        """
+        # Get permalink_key from query params
+        permalink_key = request.args.get("permalink_key")
+        if not permalink_key:
+            logger.warning("Missing permalink_key in embedded chart request")
+            abort(404)
+
+        # Get permalink value to check allowed domains
+        try:
+            salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+            codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+            key = decode_permalink_id(permalink_key, salt=salt)
+            permalink_value = KeyValueDAO.get_value(
+                KeyValueResource.EXPLORE_PERMALINK,
+                key,
+                codec,
+            )
+        except (ValueError, KeyError) as ex:
+            logger.warning("Error fetching permalink for referrer validation: 
%s", ex)
+            permalink_value = None
+
+        # Validate request referrer against allowed domains (if configured)
+        if permalink_value:
+            state = permalink_value.get("state", {})
+            allowed_domains = state.get("allowedDomains", [])
+
+            if allowed_domains:
+                is_referrer_allowed = False
+                for domain in allowed_domains:
+                    if same_origin(request.referrer, domain):
+                        is_referrer_allowed = True
+                        break
+
+                if not is_referrer_allowed:
+                    logger.warning(
+                        "Embedded chart referrer not in allowed domains: %s",
+                        request.referrer,
+                    )
+                    abort(403)
+
+        # Log in as anonymous user for page rendering
+        # This view needs to be visible to all users,
+        # and building the page fails if g.user and/or ctx.user aren't present.
+        login_user(AnonymousUserMixin(), force=True)
+

Review Comment:
   **Suggestion:** Calling the login helper with an anonymous user on every 
embedded chart request will overwrite any existing logged-in user session in 
the browser, effectively logging users out just by visiting an embedded chart; 
the login should only occur when there is no authenticated user. [security]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Embedded chart view logs out authenticated Superset users.
   - ❌ Concurrent Superset and embed usage breaks active user sessions.
   - ⚠️ Confusing UX when authors preview their embedded charts.
   ```
   </details>
   
   ```suggestion
           # Log in as anonymous user for page rendering
           # This view needs to be visible to all users,
           # and building the page fails if g.user and/or ctx.user aren't 
present.
           from flask_login import current_user
           if not current_user.is_authenticated:
               login_user(AnonymousUserMixin(), force=True)
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Log in to Superset in a browser using the normal UI (any authenticated 
view that sets
   the Flask-Login user, e.g. `/superset/` via views registered in 
`superset/views/base.py`).
   
   2. In the same browser (same Superset domain, so cookies are shared), open 
or embed a
   chart using the new embedded chart endpoint 
`EmbeddedChartView.embedded_chart` at
   `superset/embedded_chart/view.py:61-67` with a valid `permalink_key` 
(permalink keys are
   resolved via `KeyValueDAO.get_value` in the same file at lines 76-86).
   
   3. The request to `/embedded/chart/?permalink_key=...` is routed by 
Flask-AppBuilder to
   `EmbeddedChartView.embedded_chart`, which unconditionally executes
   `login_user(AnonymousUserMixin(), force=True)` at
   `superset/embedded_chart/view.py:111-114`, replacing the existing 
authenticated user in
   the Flask-Login session with an anonymous user.
   
   4. Refresh any authenticated Superset page in another tab in the same 
browser; observe
   that the user is now treated as logged out/anonymous because the session 
user was
   overwritten by the embedded chart request.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/embedded_chart/view.py
   **Line:** 111:114
   **Comment:**
        *Security: Calling the login helper with an anonymous user on every 
embedded chart request will overwrite any existing logged-in user session in 
the browser, effectively logging users out just by visiting an embedded chart; 
the login should only occur when there is no authenticated user.
   
   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.
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=136efde2daa7158d68da2d5282ba3679ab2dc77fdf7032c8a9e8ee07f2857a29&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=136efde2daa7158d68da2d5282ba3679ab2dc77fdf7032c8a9e8ee07f2857a29&reaction=dislike'>👎</a>



##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,550 @@
+# 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.
+
+"""
+Unit tests for MCP get_embeddable_chart tool
+"""
+
+from datetime import datetime, timezone
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from superset.mcp_service.chart.schemas import (
+    ColumnRef,
+    FilterConfig,
+    TableChartConfig,
+    XYChartConfig,
+)
+from superset.mcp_service.embedded_chart.schemas import (
+    GetEmbeddableChartRequest,
+    GetEmbeddableChartResponse,
+)
+from superset.mcp_service.embedded_chart.tool.get_embeddable_chart import (
+    _ensure_guest_role_permissions,
+)
+
+
+class TestGetEmbeddableChartSchemas:
+    """Tests for get_embeddable_chart schemas."""
+
+    def test_request_with_xy_config(self):
+        """Test request with XY chart config (same as generate_chart)."""
+        config = XYChartConfig(
+            chart_type="xy",
+            x=ColumnRef(name="genre"),
+            y=[ColumnRef(name="sales", aggregate="SUM")],
+            kind="bar",
+        )
+        request = GetEmbeddableChartRequest(
+            datasource_id=22,
+            config=config,
+        )
+        assert request.datasource_id == 22
+        assert request.config.chart_type == "xy"
+        assert request.config.x.name == "genre"
+        assert request.config.y[0].aggregate == "SUM"
+        assert request.config.kind == "bar"
+        assert request.ttl_minutes == 60  # default
+        assert request.height == 400  # default
+
+    def test_request_with_table_config(self):
+        """Test request with table chart config."""
+        config = TableChartConfig(
+            chart_type="table",
+            columns=[
+                ColumnRef(name="genre"),
+                ColumnRef(name="sales", aggregate="SUM", label="Total Sales"),
+            ],
+        )
+        request = GetEmbeddableChartRequest(
+            datasource_id="dataset-uuid-here",
+            config=config,
+            ttl_minutes=120,
+            height=600,
+        )
+        assert request.datasource_id == "dataset-uuid-here"
+        assert request.config.chart_type == "table"
+        assert len(request.config.columns) == 2
+        assert request.ttl_minutes == 120
+        assert request.height == 600
+
+    def test_request_with_rls_rules(self):
+        """Test request with row-level security rules."""
+        config = XYChartConfig(
+            chart_type="xy",
+            x=ColumnRef(name="date"),
+            y=[ColumnRef(name="count", aggregate="COUNT")],
+            kind="line",
+        )
+        request = GetEmbeddableChartRequest(
+            datasource_id=123,
+            config=config,
+            rls_rules=[
+                {"dataset": 123, "clause": "tenant_id = 42"},
+                {"dataset": 123, "clause": "region = 'US'"},
+            ],
+        )
+        assert len(request.rls_rules) == 2
+        assert request.rls_rules[0]["clause"] == "tenant_id = 42"
+
+    def test_request_with_allowed_domains(self):
+        """Test request with allowed_domains for iframe security."""
+        config = TableChartConfig(
+            chart_type="table",
+            columns=[ColumnRef(name="col1")],
+        )
+        request = GetEmbeddableChartRequest(
+            datasource_id=1,
+            config=config,
+            allowed_domains=["https://example.com";, "https://app.example.com";],
+        )
+        assert request.allowed_domains == [
+            "https://example.com";,
+            "https://app.example.com";,
+        ]
+
+    def test_request_ttl_validation(self):
+        """Test TTL minutes validation bounds."""
+        config = TableChartConfig(
+            chart_type="table",
+            columns=[ColumnRef(name="col1")],
+        )
+        # Valid min TTL
+        request = GetEmbeddableChartRequest(
+            datasource_id=1,
+            config=config,
+            ttl_minutes=1,
+        )
+        assert request.ttl_minutes == 1
+
+        # Valid max TTL (1 week)
+        request = GetEmbeddableChartRequest(
+            datasource_id=1,
+            config=config,
+            ttl_minutes=10080,
+        )
+        assert request.ttl_minutes == 10080
+
+        # Invalid TTL should raise
+        with pytest.raises(ValueError, match="greater than or equal to 1"):
+            GetEmbeddableChartRequest(
+                datasource_id=1,
+                config=config,
+                ttl_minutes=0,  # below min
+            )
+
+        with pytest.raises(ValueError, match="less than or equal to 10080"):
+            GetEmbeddableChartRequest(
+                datasource_id=1,
+                config=config,
+                ttl_minutes=10081,  # above max

Review Comment:
   **Suggestion:** The tests for TTL validation expect a ValueError, but 
Pydantic's field constraints (ge/le on a BaseModel) raise a ValidationError, so 
these tests will fail even when the model behaves correctly; import and assert 
the correct exception type. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ TTL validation unit test fails on every test run.
   - ⚠️ CI for MCP embeddable chart schemas reports false negatives.
   ```
   </details>
   
   ```suggestion
       def test_request_ttl_validation(self):
           """Test TTL minutes validation bounds."""
           from pydantic import ValidationError
   
           config = TableChartConfig(
               chart_type="table",
               columns=[ColumnRef(name="col1")],
           )
           # Valid min TTL
           request = GetEmbeddableChartRequest(
               datasource_id=1,
               config=config,
               ttl_minutes=1,
           )
           assert request.ttl_minutes == 1
   
           # Valid max TTL (1 week)
           request = GetEmbeddableChartRequest(
               datasource_id=1,
               config=config,
               ttl_minutes=10080,
           )
           assert request.ttl_minutes == 10080
   
           # Invalid TTL should raise
           with pytest.raises(ValidationError, match="greater than or equal to 
1"):
               GetEmbeddableChartRequest(
                   datasource_id=1,
                   config=config,
                   ttl_minutes=0,  # below min
               )
   
           with pytest.raises(ValidationError, match="less than or equal to 
10080"):
               GetEmbeddableChartRequest(
                   datasource_id=1,
                   config=config,
                   ttl_minutes=10081,  # above max
               )
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Run the unit test module
   
`tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py` 
with
   pytest so that `TestGetEmbeddableChartSchemas.test_request_ttl_validation` 
(around line
   121) is executed.
   
   2. Inside this test, `GetEmbeddableChartRequest` from
   `superset/mcp_service/embedded_chart/schemas.py:26-68` is instantiated with
   `ttl_minutes=0` while the field is defined with `ge=1, le=10080` constraints.
   
   3. Pydantic validation for `ttl_minutes` fails and raises 
`pydantic.ValidationError`
   (model-level validation error), not a bare `ValueError`, during model 
construction.
   
   4. The `pytest.raises(ValueError, ...)` context in 
`test_request_ttl_validation` does not
   catch `ValidationError`, causing the test to error/fail even though the 
model enforces the
   constraints correctly.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** 
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py
   **Line:** 121:155
   **Comment:**
        *Logic Error: The tests for TTL validation expect a ValueError, but 
Pydantic's field constraints (ge/le on a BaseModel) raise a ValidationError, so 
these tests will fail even when the model behaves correctly; import and assert 
the correct exception type.
   
   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.
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=2e584557759d243529dee47fb773d1f8efe735dfe4d95db0743057bcb858ef2f&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=2e584557759d243529dee47fb773d1f8efe735dfe4d95db0743057bcb858ef2f&reaction=dislike'>👎</a>



##########
superset/commands/chart/delete.py:
##########
@@ -68,3 +69,23 @@ def validate(self) -> None:
                 security_manager.raise_for_ownership(model)
             except SupersetSecurityException as ex:
                 raise ChartForbiddenError() from ex
+
+
+class DeleteEmbeddedChartCommand(BaseCommand):
+    def __init__(self, chart: Slice):
+        self._chart = chart
+
+    @transaction(on_error=partial(on_error, 
reraise=ChartDeleteEmbeddedFailedError))
+    def run(self) -> None:
+        self.validate()
+        return EmbeddedChartDAO.delete(self._chart.embedded)

Review Comment:
   **Suggestion:** The delete method on the DAO base class expects an iterable 
(list) of items, but a single embedded chart object is passed directly, which 
will cause a TypeError when the method tries to iterate over it; wrap the 
embedded chart in a list before passing it to the DAO delete method. [type 
error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ DeleteEmbeddedChartCommand crashes, embedded charts never deleted.
   - ⚠️ Orphaned embedded chart records persist until manual cleanup.
   ```
   </details>
   
   ```suggestion
       @transaction(on_error=partial(on_error, 
reraise=ChartDeleteEmbeddedFailedError))
       def run(self) -> None:
           self.validate()
           return EmbeddedChartDAO.delete([self._chart.embedded])
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Create or fetch a chart `Slice` instance with an associated embedded 
chart relationship
   (`chart.embedded` not None) via the ORM (e.g. in a shell or any caller), as 
used by
   command classes in `superset.models.slice` and 
`superset/commands/chart/delete.py`.
   
   2. Instantiate the command defined in `superset/commands/chart/delete.py:73` 
as `cmd =
   DeleteEmbeddedChartCommand(chart)` where `chart` is the `Slice` from step 1.
   
   3. Call `cmd.run()` which executes the `run` method at
   `superset/commands/chart/delete.py:78-81`; after `self.validate()` succeeds, 
it calls
   `EmbeddedChartDAO.delete(self._chart.embedded)`.
   
   4. The `delete` implementation inherited from `BaseDAO.delete` at
   `superset/daos/base.py:432-449` executes `for item in items:` where `items` 
is the single
   `EmbeddedChart` instance; since this model is not iterable, Python raises 
`TypeError:
   'EmbeddedChart' object is not iterable`, the transaction rolls back in
   `superset/utils/decorators.py:235-275`, and the embedded chart is not 
deleted.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/commands/chart/delete.py
   **Line:** 78:81
   **Comment:**
        *Type Error: The delete method on the DAO base class expects an 
iterable (list) of items, but a single embedded chart object is passed 
directly, which will cause a TypeError when the method tries to iterate over 
it; wrap the embedded chart in a list before passing it to the DAO delete 
method.
   
   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.
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=a2752b9cd7f43d78cbf0d41a56d39846878b77847ec9f21782a031db7d20d25c&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=a2752b9cd7f43d78cbf0d41a56d39846878b77847ec9f21782a031db7d20d25c&reaction=dislike'>👎</a>



##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,550 @@
+# 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.
+
+"""
+Unit tests for MCP get_embeddable_chart tool
+"""
+
+from datetime import datetime, timezone
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from superset.mcp_service.chart.schemas import (
+    ColumnRef,
+    FilterConfig,
+    TableChartConfig,
+    XYChartConfig,
+)
+from superset.mcp_service.embedded_chart.schemas import (
+    GetEmbeddableChartRequest,
+    GetEmbeddableChartResponse,
+)
+from superset.mcp_service.embedded_chart.tool.get_embeddable_chart import (
+    _ensure_guest_role_permissions,
+)
+
+
+class TestGetEmbeddableChartSchemas:
+    """Tests for get_embeddable_chart schemas."""
+
+    def test_request_with_xy_config(self):
+        """Test request with XY chart config (same as generate_chart)."""
+        config = XYChartConfig(
+            chart_type="xy",
+            x=ColumnRef(name="genre"),
+            y=[ColumnRef(name="sales", aggregate="SUM")],
+            kind="bar",
+        )
+        request = GetEmbeddableChartRequest(
+            datasource_id=22,
+            config=config,
+        )
+        assert request.datasource_id == 22
+        assert request.config.chart_type == "xy"
+        assert request.config.x.name == "genre"
+        assert request.config.y[0].aggregate == "SUM"
+        assert request.config.kind == "bar"
+        assert request.ttl_minutes == 60  # default
+        assert request.height == 400  # default
+
+    def test_request_with_table_config(self):
+        """Test request with table chart config."""
+        config = TableChartConfig(
+            chart_type="table",
+            columns=[
+                ColumnRef(name="genre"),
+                ColumnRef(name="sales", aggregate="SUM", label="Total Sales"),
+            ],
+        )
+        request = GetEmbeddableChartRequest(
+            datasource_id="dataset-uuid-here",
+            config=config,
+            ttl_minutes=120,
+            height=600,
+        )
+        assert request.datasource_id == "dataset-uuid-here"
+        assert request.config.chart_type == "table"
+        assert len(request.config.columns) == 2
+        assert request.ttl_minutes == 120
+        assert request.height == 600
+
+    def test_request_with_rls_rules(self):
+        """Test request with row-level security rules."""
+        config = XYChartConfig(
+            chart_type="xy",
+            x=ColumnRef(name="date"),
+            y=[ColumnRef(name="count", aggregate="COUNT")],
+            kind="line",
+        )
+        request = GetEmbeddableChartRequest(
+            datasource_id=123,
+            config=config,
+            rls_rules=[
+                {"dataset": 123, "clause": "tenant_id = 42"},
+                {"dataset": 123, "clause": "region = 'US'"},
+            ],
+        )
+        assert len(request.rls_rules) == 2
+        assert request.rls_rules[0]["clause"] == "tenant_id = 42"
+
+    def test_request_with_allowed_domains(self):
+        """Test request with allowed_domains for iframe security."""
+        config = TableChartConfig(
+            chart_type="table",
+            columns=[ColumnRef(name="col1")],
+        )
+        request = GetEmbeddableChartRequest(
+            datasource_id=1,
+            config=config,
+            allowed_domains=["https://example.com";, "https://app.example.com";],
+        )
+        assert request.allowed_domains == [
+            "https://example.com";,
+            "https://app.example.com";,
+        ]
+
+    def test_request_ttl_validation(self):
+        """Test TTL minutes validation bounds."""
+        config = TableChartConfig(
+            chart_type="table",
+            columns=[ColumnRef(name="col1")],
+        )
+        # Valid min TTL
+        request = GetEmbeddableChartRequest(
+            datasource_id=1,
+            config=config,
+            ttl_minutes=1,
+        )
+        assert request.ttl_minutes == 1
+
+        # Valid max TTL (1 week)
+        request = GetEmbeddableChartRequest(
+            datasource_id=1,
+            config=config,
+            ttl_minutes=10080,
+        )
+        assert request.ttl_minutes == 10080
+
+        # Invalid TTL should raise
+        with pytest.raises(ValueError, match="greater than or equal to 1"):
+            GetEmbeddableChartRequest(
+                datasource_id=1,
+                config=config,
+                ttl_minutes=0,  # below min
+            )
+
+        with pytest.raises(ValueError, match="less than or equal to 10080"):
+            GetEmbeddableChartRequest(
+                datasource_id=1,
+                config=config,
+                ttl_minutes=10081,  # above max
+            )
+
+    def test_request_height_validation(self):
+        """Test height validation bounds."""
+        config = TableChartConfig(
+            chart_type="table",
+            columns=[ColumnRef(name="col1")],
+        )
+        # Valid min height
+        request = GetEmbeddableChartRequest(
+            datasource_id=1,
+            config=config,
+            height=100,
+        )
+        assert request.height == 100
+
+        # Valid max height
+        request = GetEmbeddableChartRequest(
+            datasource_id=1,
+            config=config,
+            height=2000,
+        )
+        assert request.height == 2000
+
+        # Invalid heights should raise
+        with pytest.raises(ValueError, match="greater than or equal to 100"):
+            GetEmbeddableChartRequest(
+                datasource_id=1,
+                config=config,
+                height=99,  # below min
+            )
+
+        with pytest.raises(ValueError, match="less than or equal to 2000"):
+            GetEmbeddableChartRequest(
+                datasource_id=1,
+                config=config,
+                height=2001,  # above max

Review Comment:
   **Suggestion:** The height validation tests also expect a ValueError, but 
Pydantic's ge/le constraints on the height field raise a ValidationError, so 
these tests will incorrectly fail; they should assert ValidationError instead. 
[logic error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Height validation unit test fails on every test run.
   - ⚠️ Embeddable chart request schema appears broken in CI.
   ```
   </details>
   
   ```suggestion
       def test_request_height_validation(self):
           """Test height validation bounds."""
           from pydantic import ValidationError
   
           config = TableChartConfig(
               chart_type="table",
               columns=[ColumnRef(name="col1")],
           )
           # Valid min height
           request = GetEmbeddableChartRequest(
               datasource_id=1,
               config=config,
               height=100,
           )
           assert request.height == 100
   
           # Valid max height
           request = GetEmbeddableChartRequest(
               datasource_id=1,
               config=config,
               height=2000,
           )
           assert request.height == 2000
   
           # Invalid heights should raise
           with pytest.raises(ValidationError, match="greater than or equal to 
100"):
               GetEmbeddableChartRequest(
                   datasource_id=1,
                   config=config,
                   height=99,  # below min
               )
   
           with pytest.raises(ValidationError, match="less than or equal to 
2000"):
               GetEmbeddableChartRequest(
                   datasource_id=1,
                   config=config,
                   height=2001,  # above max
               )
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Run the unit test module
   
`tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py` 
so that
   `TestGetEmbeddableChartSchemas.test_request_height_validation` (around line 
158) is
   executed.
   
   2. Within this test, construct `GetEmbeddableChartRequest` from
   `superset/mcp_service/embedded_chart/schemas.py:26-68` with `height=99`, 
where `height` is
   defined with `ge=100, le=2000` constraints.
   
   3. Pydantic enforces the `height` constraints and raises 
`pydantic.ValidationError` during
   model initialization for both `height=99` and `height=2001`.
   
   4. The surrounding `pytest.raises(ValueError, ...)` contexts do not match
   `ValidationError`, causing the test to fail even though the height field is 
correctly
   validated.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** 
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py
   **Line:** 158:192
   **Comment:**
        *Logic Error: The height validation tests also expect a ValueError, but 
Pydantic's ge/le constraints on the height field raise a ValidationError, so 
these tests will incorrectly fail; they should assert ValidationError instead.
   
   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.
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=c61c63ce5a952a73bbb195cebc1b8443c4e41aa47e3092ea2e49fb3685554a24&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=c61c63ce5a952a73bbb195cebc1b8443c4e41aa47e3092ea2e49fb3685554a24&reaction=dislike'>👎</a>



##########
superset/security/manager.py:
##########
@@ -3049,6 +3071,28 @@ def has_guest_access(self, dashboard: "Dashboard") -> 
bool:
                 return True
         return False
 
+    def has_guest_chart_permalink_access(self, datasource: Any) -> bool:
+        """
+        Check if a guest user has access to a datasource via a chart permalink 
resource.
+
+        For embedded charts, the guest token contains chart_permalink 
resources.
+        This grants datasource access when the user holds at least one
+        chart_permalink resource. The embedded chart page constrains the actual
+        query to the datasource specified in the permalink's formData.
+
+        TODO: For stricter security, resolve each permalink and compare the
+        datasource in its formData against the requested datasource. This would
+        prevent a guest user from crafting requests to other datasources.
+        """
+        user = self.get_current_guest_user_if_guest()
+        if not user or not isinstance(user, GuestUser):
+            return False
+
+        return any(
+            r.get("type") == GuestTokenResourceType.CHART_PERMALINK
+            for r in user.resources
+        )
+

Review Comment:
   **Suggestion:** The guest chart permalink access check compares the stored 
resource `"type"` (which uses the enum's `.value`) against the enum object 
itself, so the condition is never true and guest users with valid 
`CHART_PERMALINK` resources will never be granted datasource access via this 
path; switch the comparison to use 
`GuestTokenResourceType.CHART_PERMALINK.value`. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Embedded chart guest tokens can't access chart datasource.
   - ❌ Chart permalink-based embedding fails with datasource access errors.
   - ⚠️ MCP tool `get_embeddable_chart` unusable for chart permalinks.
   ```
   </details>
   
   ```suggestion
           r.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. A guest token is created for an embeddable chart using
   `SupersetSecurityManager.create_guest_access_token()` in
   `superset/security/manager.py:2957-2967`, with a `resources` entry like 
`{"type":
   GuestTokenResourceType.CHART_PERMALINK.value, "id": "<permalink_key>"}` (the 
`.value`
   usage is validated later).
   
   2. When this token is later validated,
   `SupersetSecurityManager.validate_guest_token_resources()` at
   `superset/security/manager.py:2921-2955` checks `resource["type"] ==
   GuestTokenResourceType.CHART_PERMALINK.value`, confirming that the stored 
`"type"` field
   is the enum `.value` (a string), not the enum object.
   
   3. An HTTP request for the embedded chart is made with the guest token in 
the configured
   header (default `X-GuestToken`), which is parsed by 
`get_guest_user_from_request()` at
   `superset/security/manager.py:2982-3013` and attached as `g.user` via the
   `request_loader()` at lines 477-483.
   
   4. The chart data request code (not shown in this diff, but in Superset it 
calls
   `SupersetSecurityManager.raise_for_access(...)`) invokes
   `raise_for_access(datasource=<chart datasource>, 
query_context=<QueryContext>, ...)` in
   `superset/security/manager.py:2061-2174`, which reaches the datasource 
permission block at
   lines 2539-2569.
   
   5. Inside that block, the code evaluates the `if not (` guard including `or
   self.has_guest_chart_permalink_access(datasource)` added at
   `superset/security/manager.py:2556-2557`; this calls 
`has_guest_chart_permalink_access()`
   at lines 3077-3097.
   
   6. `has_guest_chart_permalink_access()` iterates `user.resources` and 
compares
   `r.get("type") == GuestTokenResourceType.CHART_PERMALINK` (enum object) 
instead of
   `.value`, so none of the string `"type"` values match and the function 
always returns
   `False` for valid `CHART_PERMALINK` resources.
   
   7. Because the guest user typically lacks `datasource_access` or schema 
permissions, and
   `has_guest_chart_permalink_access()` incorrectly returns `False`, the `if 
not (...)`
   condition at `superset/security/manager.py:2552-2568` evaluates to `True` and
   `raise_for_access()` raises `SupersetSecurityException` with
   `DATASOURCE_SECURITY_ACCESS_ERROR`, causing the embedded chart request to 
fail instead of
   returning data.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/security/manager.py
   **Line:** 3095:3095
   **Comment:**
        *Logic Error: The guest chart permalink access check compares the 
stored resource `"type"` (which uses the enum's `.value`) against the enum 
object itself, so the condition is never true and guest users with valid 
`CHART_PERMALINK` resources will never be granted datasource access via this 
path; switch the comparison to use 
`GuestTokenResourceType.CHART_PERMALINK.value`.
   
   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.
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=df4a35d432b71630cfd43e93e1f6d09dbee455297ba1f179a1ae66b681c4b209&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=df4a35d432b71630cfd43e93e1f6d09dbee455297ba1f179a1ae66b681c4b209&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]


Reply via email to