You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@superset.apache.org by "aminghadersohi (via GitHub)" <gi...@apache.org> on 2026/01/06 22:28:56 UTC

[PR] feat(mcp): add embeddable charts with guest token authentication [superset]

aminghadersohi opened a new pull request, #36933:
URL: https://github.com/apache/superset/pull/36933

   ### SUMMARY
   
   Add a new MCP tool `get_embeddable_chart` that enables AI agents to create ephemeral chart visualizations with guest token authentication. This allows charts to be embedded in external applications (Claude Desktop, ChatGPT, remote AI sessions) without persisting them to the database.
   
   **Key Features:**
   - **MCP Tool**: `get_embeddable_chart` creates chart iframes with guest tokens
   - **Ephemeral Charts**: Uses the permalink system with TTL (no database persistence)
   - **Guest Token Support**: New `CHART_PERMALINK` resource type for secure embedding
   - **StatefulChart Rendering**: Lightweight frontend component that handles data fetching internally
   
   **Architecture:**
   ```
   MCP Client (AI Agent) → get_embeddable_chart tool
                           ├── Creates permalink (KeyValue store + TTL)
                           ├── Issues guest token (chart_permalink resource)
                           └── Returns iframe_url + guest_token + iframe_html
   
   User opens iframe → /embedded/chart/?permalink_key=xxx
                       ├── Validates guest token via Switchboard
                       ├── Fetches formData from /api/v1/embedded_chart/<key>
                       └── Renders StatefulChart with formData
   ```
   
   **Files Changed:**
   - `superset/security/guest_token.py` - Add CHART_PERMALINK enum
   - `superset/security/manager.py` - Add validation logic
   - `superset/config.py` - Add EMBEDDABLE_CHARTS_MCP feature flag
   - `superset/embedded_chart/` - New backend module (api, view, exceptions)
   - `superset/mcp_service/embedded_chart/` - New MCP tool with schemas
   - `superset-frontend/src/embeddedChart/` - New frontend entry point
   
   ### BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
   N/A - New feature, no UI changes to existing functionality
   
   ### TESTING INSTRUCTIONS
   
   1. Enable the feature flag in `superset_config.py`:
      ```python
      FEATURE_FLAGS = {
          "EMBEDDABLE_CHARTS_MCP": True,
      }
      ```
   
   2. Use the MCP tool to create an embeddable chart:
      ```json
      {
          "datasource_id": 1,
          "viz_type": "echarts_timeseries_line",
          "form_data": {
              "metrics": ["count"],
              "groupby": ["category"],
              "time_range": "Last 7 days"
          },
          "ttl_minutes": 60,
          "height": 500
      }
      ```
   
   3. The tool returns `iframe_url`, `guest_token`, and `iframe_html`
   
   4. The `iframe_html` can be embedded in external applications
   
   ### ADDITIONAL INFORMATION
   - [ ] Has associated issue:
   - [x] Required feature flags: `EMBEDDABLE_CHARTS_MCP`
   - [x] Changes UI
   - [ ] Includes DB Migration (follow approval process in [SIP-59](https://github.com/apache/superset/issues/13351))
     - [ ] Migration is atomic, supports rollback & is backwards-compatible
     - [ ] Confirm DB migration upgrade and downgrade tested
     - [ ] Runtime estimates and downtime expectations provided
   - [x] Introduces new feature or API
   - [ ] Removes existing feature or API
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911859139


##########
superset/models/embedded_chart.py:
##########
@@ -0,0 +1,58 @@
+# 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 uuid
+
+from flask_appbuilder import Model
+from sqlalchemy import Column, ForeignKey, Integer, Text
+from sqlalchemy.orm import relationship
+from sqlalchemy_utils import UUIDType
+
+from superset.models.helpers import AuditMixinNullable
+
+
+class EmbeddedChart(Model, AuditMixinNullable):
+    """
+    A configuration of embedding for a chart.
+
+    References the chart (slice), and contains a config for embedding that chart.
+
+    This data model allows multiple configurations for a given chart,
+    but at this time the API only allows setting one.
+    """
+
+    __tablename__ = "embedded_charts"
+
+    uuid = Column(UUIDType(binary=True), default=uuid.uuid4, primary_key=True)
+    allow_domain_list = Column(Text)  # reference the `allowed_domains` property instead
+    chart_id = Column(
+        Integer,
+        ForeignKey("slices.id", ondelete="CASCADE"),
+        nullable=False,
+    )
+    chart = relationship(
+        "Slice",
+        back_populates="embedded",
+        foreign_keys=[chart_id],
+    )
+
+    @property
+    def allowed_domains(self) -> list[str]:
+        """
+        A list of domains which are allowed to embed the chart.
+        An empty list means any domain can embed.
+        """
+        return self.allow_domain_list.split(",") if self.allow_domain_list else []

Review Comment:
   <!-- Bito Reply -->
   Yes, the current code in embedded_chart.py at line 60 already implements the suggested domain parsing logic, stripping whitespace and filtering empty entries.



##########
superset/views/embedded_charts.py:
##########
@@ -0,0 +1,34 @@
+# 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.
+from flask_appbuilder import expose, has_access
+
+from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP
+from superset.superset_typing import FlaskResponse
+from superset.views.base import BaseSupersetView
+
+
+class EmbeddedChartsView(BaseSupersetView):
+    """View for managing embedded charts list."""
+
+    route_base = "/embeddedcharts"
+    class_permission_name = "Chart"
+    method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP
+
+    @expose("/list/")
+    @has_access
+    def list(self) -> FlaskResponse:
+        return super().render_app_template()

Review Comment:
   <!-- Bito Reply -->
   No, the suggestion isn’t fully correct — EmbeddedChartsView renders the admin list page within the main Superset SPA, so it correctly uses the default 'spa' entry. The embedded chart iframe is handled by a different view using 'embedded' entry.
   
   **superset/views/embedded_charts.py**
   ```
   @expose("/list/")
       @has_access
       def list(self) -> FlaskResponse:
           return super().render_app_template()
   ```



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911850020


##########
superset/views/embedded_charts.py:
##########
@@ -0,0 +1,34 @@
+# 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.
+from flask_appbuilder import expose, has_access
+
+from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP
+from superset.superset_typing import FlaskResponse
+from superset.views.base import BaseSupersetView
+
+
+class EmbeddedChartsView(BaseSupersetView):
+    """View for managing embedded charts list."""
+
+    route_base = "/embeddedcharts"
+    class_permission_name = "Chart"
+    method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP
+
+    @expose("/list/")
+    @has_access
+    def list(self) -> FlaskResponse:
+        return super().render_app_template()

Review Comment:
   Correct as-is — `embedded_charts.py` is the management UI (list view for managing embedded charts), which is part of the main SPA, hence `entry='spa'` (the default). The actual embedded rendering happens in `dashboard/views.py` which correctly uses `entry='embedded'` (line 125).



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4054517804

   ## **Sequence Diagram**
   
   This PR adds a new embeddable chart flow where the caller requests an ephemeral chart embed, Superset issues a permalink plus guest token, and the embedded iframe uses that token to securely fetch chart state and render.
   
   ```mermaid
   sequenceDiagram
       participant Caller
       participant SupersetAPI
       participant PermalinkStore
       participant TokenService
       participant HostPage
       participant EmbeddedIframe
   
       Caller->>SupersetAPI: Create embeddable chart with form data
       SupersetAPI->>PermalinkStore: Save permalink state with allowed domains
       SupersetAPI->>TokenService: Create guest token for chart permalink resource
       SupersetAPI-->>Caller: Return iframe URL and guest token
   
       Caller->>HostPage: Insert iframe embed snippet
       HostPage->>EmbeddedIframe: Send guest token message on iframe load
       EmbeddedIframe->>SupersetAPI: Fetch permalink state using guest token
       SupersetAPI-->>EmbeddedIframe: Return chart form data
       EmbeddedIframe-->>HostPage: Render chart in iframe
   ```
   
   ---
   *Generated by [CodeAnt AI](https://codeant.ai)*
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932184282


##########
superset-frontend/src/pages/EmbeddedChartsList/index.tsx:
##########
@@ -0,0 +1,254 @@
+/**
+ * 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 } from '@apache-superset/core/theme';
+import { t } from '@apache-superset/core/translation';
+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));

Review Comment:
   <!-- Bito Reply -->
   The diff shows the change adds `|| t('Untitled')` to the toasts in `handleDelete` (lines 1224, 1227), which lacked it before. Other uses like the chart name column (line 1262) and delete modal (line 1368) already have the fallback. The `EmbeddedChart` interface correctly types `chart_name` as `string | null`. Your statement is mostly accurate, but the toasts needed this fix.
   
   **superset-frontend/src/pages/EmbeddedChartsList/index.tsx**
   ```
   interface EmbeddedChart {
     uuid: string;
     chart_id: number;
     chart_name: string | null;
     allowed_domains: string[];
     changed_on: string | null;
   }
   ```
   
   **superset-frontend/src/pages/EmbeddedChartsList/index.tsx**
   ```
   addSuccessToast(t('Embedding disabled for %s', chart.chart_name || t('Untitled')));
   addDangerToast(t('Error disabling embedding for %s', chart.chart_name || t('Untitled')));
   ```



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4056445610

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2927561664


##########
superset/mcp_service/embedded_chart/tool/get_embeddable_chart.py:
##########
@@ -0,0 +1,286 @@
+# 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: get_embeddable_chart
+
+Creates an embeddable chart iframe with guest token authentication.
+This enables AI agents to generate charts that can be displayed in
+external applications via iframe.
+"""
+
+import logging
+from datetime import datetime, timedelta, timezone
+
+from fastmcp import Context
+from flask import g
+from superset_core.mcp.decorators import tool
+
+from superset.commands.exceptions import CommandException
+from superset.mcp_service.chart.chart_utils import (
+    map_config_to_form_data,
+    resolve_dataset,
+)
+from superset.mcp_service.embedded_chart.schemas import (
+    GetEmbeddableChartRequest,
+    GetEmbeddableChartResponse,
+)
+from superset.mcp_service.utils.schema_utils import parse_request
+from superset.mcp_service.utils.url_utils import get_superset_base_url
+from superset.security.guest_token import (
+    GuestTokenResource,

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Type-only imports should be in TYPE_CHECKING</b></div>
   <div id="fix">
   
   Move `GuestTokenResource`, `GuestTokenRlsRule`, and `GuestTokenUser` imports (lines 44-46) into a `TYPE_CHECKING` block to avoid runtime circular imports. These are used only for type annotations.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ```
     from datetime import datetime, timedelta, timezone
    
    +from typing import TYPE_CHECKING
     from fastmcp import Context
     from flask import g
     from superset_core.mcp.decorators import tool
    @@ -41,9 +42,12 @@
     from superset.mcp_service.utils.schema_utils import parse_request
     from superset.mcp_service.utils.url_utils import get_superset_base_url
    -from superset.security.guest_token import (
    -    GuestTokenResource,
    -    GuestTokenRlsRule,
    -    GuestTokenUser,
    -)
    +from superset.security.guest_token import GuestTokenResourceType
    +
    +if TYPE_CHECKING:
    +    from superset.security.guest_token import (
    +        GuestTokenResource,
    +        GuestTokenRlsRule,
    +        GuestTokenUser,
    +    )
   ```
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #d07566</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2666503443


##########
superset-frontend/src/embeddedChart/index.tsx:
##########
@@ -0,0 +1,303 @@
+/**
+ * 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 'src/public-path';
+
+import { useState, useEffect, useCallback } from 'react';
+import ReactDOM from 'react-dom';
+import { makeApi, t, logging, QueryFormData } from '@superset-ui/core';
+import { StatefulChart } from '@superset-ui/core';
+import Switchboard from '@superset-ui/switchboard';
+import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData';
+import setupClient from 'src/setup/setupClient';
+import setupPlugins from 'src/setup/setupPlugins';
+import { store, USER_LOADED } from 'src/views/store';
+import { Loading } from '@superset-ui/core/components';
+import { ErrorBoundary, DynamicPluginProvider } from 'src/components';
+import { addDangerToast } from 'src/components/MessageToasts/actions';
+import ToastContainer from 'src/components/MessageToasts/ToastContainer';
+import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
+import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
+import { ThemeController } from 'src/theme/ThemeController';
+import type { ThemeStorage } from '@apache-superset/core/ui';
+import { Provider as ReduxProvider } from 'react-redux';
+
+setupPlugins();
+
+const debugMode = process.env.WEBPACK_MODE === 'development';
+const bootstrapData = getBootstrapData();
+
+function log(...info: unknown[]) {
+  if (debugMode) logging.debug(`[superset-embedded-chart]`, ...info);
+}
+
+/**
+ * In-memory implementation of ThemeStorage interface for embedded contexts.
+ */
+class ThemeMemoryStorageAdapter implements ThemeStorage {
+  private storage = new Map<string, string>();
+
+  getItem(key: string): string | null {
+    return this.storage.get(key) || null;
+  }
+
+  setItem(key: string, value: string): void {
+    this.storage.set(key, value);
+  }
+
+  removeItem(key: string): void {
+    this.storage.delete(key);
+  }
+}
+
+const themeController = new ThemeController({
+  storage: new ThemeMemoryStorageAdapter(),
+});
+
+interface PermalinkState {
+  formData: QueryFormData;
+}
+
+const appMountPoint = document.getElementById('app')!;
+const MESSAGE_TYPE = '__embedded_comms__';
+
+function showFailureMessage(message: string) {
+  appMountPoint.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-family: sans-serif; color: #666;">${message}</div>`;
+}
+
+if (!window.parent || window.parent === window) {
+  showFailureMessage(
+    t(
+      'This page is intended to be embedded in an iframe, but it looks like that is not the case.',
+    ),
+  );
+}
+
+let displayedUnauthorizedToast = false;
+
+/**
+ * Handle unauthorized errors from the API.
+ */
+function guestUnauthorizedHandler() {
+  if (displayedUnauthorizedToast) return;
+  displayedUnauthorizedToast = true;
+  store.dispatch(
+    addDangerToast(
+      t(
+        'This session has encountered an interruption. The embedded chart may not load correctly.',
+      ),
+      {
+        duration: -1,
+        noDuplicate: true,
+      },
+    ),
+  );
+}
+
+/**
+ * Configures SupersetClient with the correct settings for the embedded chart page.
+ */
+function setupGuestClient(guestToken: string) {
+  setupClient({
+    appRoot: applicationRoot(),
+    guestToken,
+    guestTokenHeaderName: bootstrapData.config?.GUEST_TOKEN_HEADER_NAME,
+    unauthorizedHandler: guestUnauthorizedHandler,
+  });
+}
+
+function validateMessageEvent(event: MessageEvent) {
+  if (typeof event.data !== 'object' || event.data.type !== MESSAGE_TYPE) {

Review Comment:
   **Suggestion:** `validateMessageEvent` accesses `event.data.type` without guarding against `event.data` being null; because `typeof null === 'object'` the function will throw a TypeError when `event.data` is null (instead of throwing the intended validation error), leading to unexpected exceptions. [null pointer]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
     const data = event.data;
     if (data == null || typeof data !== 'object' || (data as any).type !== MESSAGE_TYPE) {
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   The current code will throw a runtime TypeError when event.data is null because typeof null === 'object' and the second operand accesses .type. The improved code guards for null/undefined first, which fixes a real bug in message validation.
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/embeddedChart/index.tsx
   **Line:** 125:125
   **Comment:**
   	*Null Pointer: `validateMessageEvent` accesses `event.data.type` without guarding against `event.data` being null; because `typeof null === 'object'` the function will throw a TypeError when `event.data` is null (instead of throwing the intended validation error), leading to unexpected exceptions.
   
   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>



##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,72 @@
+# 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.
+from typing import Callable
+
+from flask import abort, current_app
+from flask_appbuilder import expose
+from flask_login import AnonymousUserMixin, login_user
+
+from superset import event_logger, is_feature_enabled
+from superset.superset_typing import FlaskResponse
+from superset.utils import json
+from superset.views.base import BaseSupersetView, common_bootstrap_payload
+
+
+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.
+        """
+        if not is_feature_enabled("EMBEDDABLE_CHARTS_MCP"):
+            abort(404)
+
+        # 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)
+
+        add_extra_log_payload(
+            embedded_type="chart",
+        )
+
+        bootstrap_data = {
+            "config": {
+                "GUEST_TOKEN_HEADER_NAME": current_app.config[
+                    "GUEST_TOKEN_HEADER_NAME"
+                ],

Review Comment:
   **Suggestion:** Runtime error (KeyError): accessing `current_app.config["GUEST_TOKEN_HEADER_NAME"]` will raise a KeyError if the configuration key is not present, causing the request to 500. Use .get with a sensible default or validate/abort with a clear error. [possible bug]
   
   **Severity Level:** Critical 🚨
   ```suggestion
                   "GUEST_TOKEN_HEADER_NAME": current_app.config.get(
                       "GUEST_TOKEN_HEADER_NAME", ""
                   ),
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   This is a legitimate runtime risk: current_app.config[...] will raise KeyError if the key isn't set and crash the request. Using .get with a sensible default (or explicitly validating and aborting) avoids a 500. The suggested improved_code is a small, safe change that prevents crashes; it's relevant to the diff and fixes a real potential production bug.
   </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:** 58:60
   **Comment:**
   	*Possible Bug: Runtime error (KeyError): accessing `current_app.config["GUEST_TOKEN_HEADER_NAME"]` will raise a KeyError if the configuration key is not present, causing the request to 500. Use .get with a sensible default or validate/abort with a clear error.
   
   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>



##########
superset/security/manager.py:
##########
@@ -2834,6 +2838,15 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except Exception as ex:
+                    raise EmbeddedChartPermalinkNotFoundError() from ex

Review Comment:
   **Suggestion:** Re-raising EmbeddedChartPermalinkNotFoundError using "from ex" attaches the original exception as the cause, which can leak internal error details into logs or responses; if the intention is to normalize to a not-found error, re-raise without chaining or re-raise the original unexpected exception. [security]
   
   **Severity Level:** Critical 🚨
   ```suggestion
                   except Exception:
                       # Normalize missing permalink errors but avoid chaining unexpected internals
                       raise EmbeddedChartPermalinkNotFoundError()
   ```
   <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:** 2848:2849
   **Comment:**
   	*Security: Re-raising EmbeddedChartPermalinkNotFoundError using "from ex" attaches the original exception as the cause, which can leak internal error details into logs or responses; if the intention is to normalize to a not-found error, re-raise without chaining or re-raise the original unexpected exception.
   
   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>



##########
superset/mcp_service/embedded_chart/tool/__init__.py:
##########
@@ -0,0 +1,19 @@
+# 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.
+from .get_embeddable_chart import get_embeddable_chart
+
+__all__ = ["get_embeddable_chart"]

Review Comment:
   **Suggestion:** Eager top-level import can create a circular import if `get_embeddable_chart` imports from the `tool` package (or other sibling modules); this will raise ImportError at runtime when importing the package. Replace the direct import with a lazy loader (module-level __getattr__) so the function is imported only when first accessed, avoiding circular import problems. [possible bug]
   
   **Severity Level:** Critical 🚨
   ```suggestion
   from importlib import import_module
    
   __all__ = ["get_embeddable_chart"]
    
   def __getattr__(name):
       if name == "get_embeddable_chart":
           mod = import_module(".get_embeddable_chart", __package__)
           return getattr(mod, "get_embeddable_chart")
       raise AttributeError(f"module {__name__} has no attribute {name}")
   ```
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/mcp_service/embedded_chart/tool/__init__.py
   **Line:** 17:19
   **Comment:**
   	*Possible Bug: Eager top-level import can create a circular import if `get_embeddable_chart` imports from the `tool` package (or other sibling modules); this will raise ImportError at runtime when importing the package. Replace the direct import with a lazy loader (module-level __getattr__) so the function is imported only when first accessed, avoiding circular import problems.
   
   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>



##########
superset/config.py:
##########
@@ -559,6 +559,8 @@ class D3TimeFormat(TypedDict, total=False):
     # This feature flag is stil in beta and is not recommended for production use.
     "GLOBAL_ASYNC_QUERIES": False,
     "EMBEDDED_SUPERSET": False,
+    # Enables MCP tool for creating embeddable chart iframes with guest tokens
+    "EMBEDDABLE_CHARTS_MCP": False,

Review Comment:
   **Suggestion:** Logic/consistency issue: there's already a feature flag `EMBEDDABLE_CHARTS` set to True earlier in the file; adding `EMBEDDABLE_CHARTS_MCP` defaulting to False creates an ambiguous/conflicting default behavior (one embedding-related flag True, the MCP-specific flag False) which can lead to surprising runtime behavior if code paths check the wrong flag. Align the default or explicitly document the relationship; the minimal fix is to set the MCP flag to the same default as the generic embedding flag. [logic error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
       "EMBEDDABLE_CHARTS_MCP": True,
   ```
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/config.py
   **Line:** 563:563
   **Comment:**
   	*Logic Error: Logic/consistency issue: there's already a feature flag `EMBEDDABLE_CHARTS` set to True earlier in the file; adding `EMBEDDABLE_CHARTS_MCP` defaulting to False creates an ambiguous/conflicting default behavior (one embedding-related flag True, the MCP-specific flag False) which can lead to surprising runtime behavior if code paths check the wrong flag. Align the default or explicitly document the relationship; the minimal fix is to set the MCP flag to the same default as the generic embedding flag.
   
   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>



##########
superset-frontend/src/embeddedChart/index.tsx:
##########
@@ -0,0 +1,303 @@
+/**
+ * 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 'src/public-path';
+
+import { useState, useEffect, useCallback } from 'react';
+import ReactDOM from 'react-dom';
+import { makeApi, t, logging, QueryFormData } from '@superset-ui/core';
+import { StatefulChart } from '@superset-ui/core';
+import Switchboard from '@superset-ui/switchboard';
+import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData';
+import setupClient from 'src/setup/setupClient';
+import setupPlugins from 'src/setup/setupPlugins';
+import { store, USER_LOADED } from 'src/views/store';
+import { Loading } from '@superset-ui/core/components';
+import { ErrorBoundary, DynamicPluginProvider } from 'src/components';
+import { addDangerToast } from 'src/components/MessageToasts/actions';
+import ToastContainer from 'src/components/MessageToasts/ToastContainer';
+import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
+import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
+import { ThemeController } from 'src/theme/ThemeController';
+import type { ThemeStorage } from '@apache-superset/core/ui';
+import { Provider as ReduxProvider } from 'react-redux';
+
+setupPlugins();
+
+const debugMode = process.env.WEBPACK_MODE === 'development';
+const bootstrapData = getBootstrapData();
+
+function log(...info: unknown[]) {
+  if (debugMode) logging.debug(`[superset-embedded-chart]`, ...info);
+}
+
+/**
+ * In-memory implementation of ThemeStorage interface for embedded contexts.
+ */
+class ThemeMemoryStorageAdapter implements ThemeStorage {
+  private storage = new Map<string, string>();
+
+  getItem(key: string): string | null {
+    return this.storage.get(key) || null;
+  }
+
+  setItem(key: string, value: string): void {
+    this.storage.set(key, value);
+  }
+
+  removeItem(key: string): void {
+    this.storage.delete(key);
+  }
+}
+
+const themeController = new ThemeController({
+  storage: new ThemeMemoryStorageAdapter(),
+});
+
+interface PermalinkState {
+  formData: QueryFormData;
+}
+
+const appMountPoint = document.getElementById('app')!;
+const MESSAGE_TYPE = '__embedded_comms__';
+
+function showFailureMessage(message: string) {
+  appMountPoint.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-family: sans-serif; color: #666;">${message}</div>`;
+}
+
+if (!window.parent || window.parent === window) {
+  showFailureMessage(
+    t(
+      'This page is intended to be embedded in an iframe, but it looks like that is not the case.',
+    ),
+  );
+}
+
+let displayedUnauthorizedToast = false;
+
+/**
+ * Handle unauthorized errors from the API.
+ */
+function guestUnauthorizedHandler() {
+  if (displayedUnauthorizedToast) return;
+  displayedUnauthorizedToast = true;
+  store.dispatch(
+    addDangerToast(
+      t(
+        'This session has encountered an interruption. The embedded chart may not load correctly.',
+      ),
+      {
+        duration: -1,
+        noDuplicate: true,
+      },
+    ),
+  );
+}
+
+/**
+ * Configures SupersetClient with the correct settings for the embedded chart page.
+ */
+function setupGuestClient(guestToken: string) {
+  setupClient({
+    appRoot: applicationRoot(),
+    guestToken,
+    guestTokenHeaderName: bootstrapData.config?.GUEST_TOKEN_HEADER_NAME,
+    unauthorizedHandler: guestUnauthorizedHandler,
+  });
+}
+
+function validateMessageEvent(event: MessageEvent) {
+  if (typeof event.data !== 'object' || event.data.type !== MESSAGE_TYPE) {
+    throw new Error(`Message type does not match type used for embedded comms`);
+  }
+}
+
+/**
+ * Embedded Chart component that fetches formData from permalink and renders StatefulChart.
+ */
+function EmbeddedChartApp() {
+  const [formData, setFormData] = useState<QueryFormData | null>(null);
+  const [error, setError] = useState<string | null>(null);
+  const [loading, setLoading] = useState(true);
+
+  const fetchPermalinkData = useCallback(async () => {
+    const urlParams = new URLSearchParams(window.location.search);
+    const permalinkKey = urlParams.get('permalink_key');
+
+    if (!permalinkKey) {
+      setError(t('Missing permalink_key parameter'));
+      setLoading(false);
+      return;
+    }
+
+    try {
+      const getPermalinkData = makeApi<void, { state: PermalinkState }>({
+        method: 'GET',
+        endpoint: `/api/v1/embedded_chart/${permalinkKey}`,
+      });
+
+      const response = await getPermalinkData();
+      const { state } = response;
+
+      if (!state?.formData) {
+        setError(t('Invalid permalink data: missing formData'));
+        setLoading(false);
+        return;
+      }
+
+      log('Loaded formData from permalink:', state.formData);
+      setFormData(state.formData);
+      setLoading(false);
+    } catch (err) {
+      logging.error('Failed to load permalink data:', err);
+      setError(
+        t('Failed to load chart data. The permalink may have expired.'),
+      );
+      setLoading(false);
+    }
+  }, []);
+
+  useEffect(() => {
+    fetchPermalinkData();
+  }, [fetchPermalinkData]);
+
+  if (loading) {
+    return (
+      <div
+        style={{ width: '100%', height: '100vh', position: 'relative' }}
+      >
+        <Loading position="floating" />
+      </div>
+    );
+  }
+
+  if (error) {
+    return (
+      <div
+        style={{
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'center',
+          height: '100vh',
+          fontFamily: 'sans-serif',
+          color: '#666',
+          padding: '20px',
+          textAlign: 'center',
+        }}
+      >
+        {error}
+      </div>
+    );
+  }
+
+  if (!formData) {
+    return null;
+  }
+
+  return (
+    <div style={{ width: '100%', height: '100vh' }}>
+      <StatefulChart
+        formData={formData}
+        width="100%"
+        height="100%"
+        showLoading
+        enableNoResults
+      />
+    </div>
+  );
+}
+
+/**
+ * Context providers wrapper for the embedded chart.
+ */
+function EmbeddedChartWithProviders() {
+  return (
+    <SupersetThemeProvider themeController={themeController}>
+      <ReduxProvider store={store}>
+        <DynamicPluginProvider>
+          <ErrorBoundary>
+            <EmbeddedChartApp />
+          </ErrorBoundary>
+          <ToastContainer position="top" />
+        </DynamicPluginProvider>
+      </ReduxProvider>
+    </SupersetThemeProvider>
+  );
+}
+
+function start() {
+  const getMeWithRole = makeApi<void, { result: UserWithPermissionsAndRoles }>({
+    method: 'GET',
+    endpoint: '/api/v1/me/roles/',
+  });
+  return getMeWithRole().then(
+    ({ result }) => {
+      bootstrapData.user = result;
+      store.dispatch({
+        type: USER_LOADED,
+        user: result,
+      });
+      ReactDOM.render(<EmbeddedChartWithProviders />, appMountPoint);
+    },
+    err => {
+      logging.error(err);
+      showFailureMessage(
+        t(
+          'Something went wrong with embedded authentication. Check the dev console for details.',
+        ),
+      );
+    },
+  );
+}
+
+window.addEventListener('message', function embeddedChartInitializer(event) {
+  try {
+    validateMessageEvent(event);
+  } catch (err) {
+    log('ignoring message unrelated to embedded comms', err, event);
+    return;
+  }
+
+  const port = event.ports?.[0];
+  if (event.data.handshake === 'port transfer' && port) {
+    log('message port received', event);
+
+    Switchboard.init({
+      port,
+      name: 'superset-embedded-chart',
+      debug: debugMode,
+    });
+
+    let started = false;
+
+    Switchboard.defineMethod(
+      'guestToken',
+      ({ guestToken }: { guestToken: string }) => {
+        setupGuestClient(guestToken);
+        if (!started) {
+          start();
+          started = true;

Review Comment:
   **Suggestion:** The `started` flag is set immediately after calling `start()`; if `start()` fails, `started` remains true and the app will not retry on later `guestToken` messages. Set `started` only after `start()` completes successfully (or reset it on failure). [race condition]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
               // call start and set started to true only after a successful start
               start()
                 .then(() => {
                   started = true;
                 })
                 .catch(err => {
                   logging.error('Failed to start after receiving guestToken', err);
                 });
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   start() returns a promise; setting started synchronously means a failure leaves started true and blocks subsequent retries. Setting started only after start resolves (or resetting on failure) fixes a real initialization/retry bug.
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/embeddedChart/index.tsx
   **Line:** 294:294
   **Comment:**
   	*Race Condition: The `started` flag is set immediately after calling `start()`; if `start()` fails, `started` remains true and the app will not retry on later `guestToken` messages. Set `started` only after `start()` completes successfully (or reset it on failure).
   
   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>



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,119 @@
+# 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 flask import Response
+from flask_appbuilder.api import expose, safe
+from flask_appbuilder.hooks import before_request
+
+from superset import is_feature_enabled
+from superset.commands.explore.permalink.get import GetExplorePermalinkCommand
+from superset.embedded_chart.exceptions import EmbeddedChartPermalinkNotFoundError
+from superset.extensions import event_logger
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    @before_request
+    def ensure_feature_enabled(self) -> Response | None:
+        if not is_feature_enabled("EMBEDDABLE_CHARTS_MCP"):
+            return self.response_404()
+        return None
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart form_data
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        type: object
+                        properties:
+                          form_data:
+                            type: object
+                            description: The chart configuration form_data
+                          datasource_id:
+                            type: integer
+                            description: The datasource ID
+                          datasource_type:
+                            type: string
+                            description: The datasource type
+                          chart_id:
+                            type: integer
+                            description: Optional chart ID if based on saved chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            state = permalink_value.get("state", {})
+            form_data = state.get("formData", {})
+
+            return self.response(
+                200,
+                result={
+                    "form_data": form_data,
+                    "datasource_id": permalink_value.get("datasourceId"),
+                    "datasource_type": permalink_value.get("datasourceType"),
+                    "chart_id": permalink_value.get("chartId"),
+                },
+            )
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception as ex:
+            logger.exception("Error fetching embedded chart: %s", ex)
+            return self.response_500(message=str(ex))

Review Comment:
   **Suggestion:** Security: the code returns the raw exception message to API clients in a 500 response (message=str(ex)), which can leak internal details and sensitive information. Return a generic 500 response and log the exception server-side instead. [security]
   
   **Severity Level:** Critical 🚨
   ```suggestion
           except Exception:
               logger.exception("Error fetching embedded chart", exc_info=True)
               return self.response_500()
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   This is a valid security concern: returning str(ex) in a 500 response can leak internal state / sensitive details.
   Replacing it with a generic response and logging the full exception server-side fixes a real information-leak.
   It's low-risk and localized to this handler.
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/embedded_chart/api.py
   **Line:** 115:119
   **Comment:**
   	*Security: Security: the code returns the raw exception message to API clients in a 500 response (message=str(ex)), which can leak internal details and sensitive information. Return a generic 500 response and log the exception server-side 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>



##########
superset-frontend/webpack.config.js:
##########
@@ -300,6 +300,7 @@ const config = {
     menu: addPreamble('src/views/menu.tsx'),
     spa: addPreamble('/src/views/index.tsx'),
     embedded: addPreamble('/src/embedded/index.tsx'),
+    embeddedChart: addPreamble('/src/embeddedChart/index.tsx'),

Review Comment:
   **Suggestion:** Logic bug: the entry path is passed with a leading slash to `addPreamble`, which calls `path.join(APP_DIR, entry)`. When `entry` starts with '/', `path.join` treats it as an absolute path and discards `APP_DIR`, producing an incorrect path; remove the leading slash so the path is joined relative to `APP_DIR`. [logic error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
       embeddedChart: addPreamble('src/embeddedChart/index.tsx'),
   ```
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/webpack.config.js
   **Line:** 303:303
   **Comment:**
   	*Logic Error: Logic bug: the entry path is passed with a leading slash to `addPreamble`, which calls `path.join(APP_DIR, entry)`. When `entry` starts with '/', `path.join` treats it as an absolute path and discards `APP_DIR`, producing an incorrect path; remove the leading slash so the path is joined relative to `APP_DIR`.
   
   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>



##########
superset/mcp_service/embedded_chart/tool/get_embeddable_chart.py:
##########
@@ -0,0 +1,228 @@
+# 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: get_embeddable_chart
+
+Creates an embeddable chart iframe with guest token authentication.
+This enables AI agents to generate charts that can be displayed in
+external applications via iframe.
+"""
+
+import logging
+from datetime import datetime, timedelta, timezone
+
+from fastmcp import Context
+from flask import g
+from superset_core.mcp import tool
+
+from superset import is_feature_enabled
+from superset.mcp_service.auth import has_dataset_access
+from superset.mcp_service.embedded_chart.schemas import (
+    GetEmbeddableChartRequest,
+    GetEmbeddableChartResponse,
+)
+from superset.mcp_service.utils.schema_utils import parse_request
+from superset.mcp_service.utils.url_utils import get_superset_base_url
+
+logger = logging.getLogger(__name__)
+
+
+@tool(tags=["core"])
+@parse_request(GetEmbeddableChartRequest)
+async def get_embeddable_chart(
+    request: GetEmbeddableChartRequest,
+    ctx: Context,
+) -> GetEmbeddableChartResponse:
+    """Create an embeddable chart iframe URL with guest token authentication.
+
+    This tool creates an ephemeral chart visualization that can be embedded
+    in external applications via iframe. The chart is configured via form_data
+    and stored as a permalink with TTL.
+
+    IMPORTANT:
+    - Requires EMBEDDABLE_CHARTS_MCP feature flag to be enabled
+    - The iframe_html can be directly embedded in web pages
+    - Guest token must be passed to the iframe for authentication
+    - Chart expires after ttl_minutes (default: 60 minutes)
+
+    Example usage:
+    ```json
+    {
+        "datasource_id": 123,
+        "viz_type": "echarts_timeseries_line",
+        "form_data": {
+            "metrics": ["count"],
+            "groupby": ["category"],
+            "time_range": "Last 7 days"
+        },
+        "ttl_minutes": 120,
+        "height": 500
+    }
+    ```
+
+    Returns iframe_url, guest_token, and ready-to-use iframe_html snippet.
+    """
+    await ctx.info(
+        f"Creating embeddable chart: datasource_id={request.datasource_id}, "
+        f"viz_type={request.viz_type}"
+    )
+
+    # Check feature flag
+    if not is_feature_enabled("EMBEDDABLE_CHARTS_MCP"):
+        await ctx.error("EMBEDDABLE_CHARTS_MCP feature flag is not enabled")
+        return GetEmbeddableChartResponse(
+            success=False,
+            error="EMBEDDABLE_CHARTS_MCP feature flag is not enabled",
+        )
+
+    try:
+        # Import here to avoid circular imports
+        from superset.commands.explore.permalink.create import (
+            CreateExplorePermalinkCommand,
+        )
+        from superset.daos.dataset import DatasetDAO
+        from superset.extensions import security_manager
+        from superset.security.guest_token import GuestTokenResourceType
+
+        # Resolve dataset
+        dataset = None
+        if isinstance(request.datasource_id, int) or (
+            isinstance(request.datasource_id, str) and request.datasource_id.isdigit()
+        ):
+            dataset_id = (
+                int(request.datasource_id)
+                if isinstance(request.datasource_id, str)
+                else request.datasource_id
+            )
+            dataset = DatasetDAO.find_by_id(dataset_id)
+        else:
+            # Try UUID lookup
+            dataset = DatasetDAO.find_by_id(request.datasource_id, id_column="uuid")
+
+        if not dataset:
+            await ctx.error(f"Dataset not found: {request.datasource_id}")
+            return GetEmbeddableChartResponse(
+                success=False,
+                error=f"Dataset not found: {request.datasource_id}",
+            )
+
+        # Check access
+        if not has_dataset_access(dataset):
+            await ctx.error("Access denied to dataset")
+            return GetEmbeddableChartResponse(
+                success=False,
+                error="Access denied to dataset",
+            )
+
+        # Build complete form_data
+        form_data = {
+            **request.form_data,
+            "viz_type": request.viz_type,
+            "datasource": f"{dataset.id}__table",
+        }
+
+        # Apply overrides if provided
+        if request.form_data_overrides:
+            form_data.update(request.form_data_overrides)
+
+        # Create permalink
+        state = {"formData": form_data}
+        permalink_key = CreateExplorePermalinkCommand(state).run()
+
+        await ctx.debug(f"Created permalink: {permalink_key}")
+
+        # Calculate expiration
+        expires_at = datetime.now(timezone.utc) + timedelta(minutes=request.ttl_minutes)

Review Comment:
   **Suggestion:** TypeError when `ttl_minutes` is None: `timedelta(minutes=request.ttl_minutes)` will raise if `request.ttl_minutes` is None; ensure a default is used (e.g., 60) before constructing the timedelta. [type error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
           # Calculate expiration (use a safe default if ttl_minutes is missing)
           ttl_minutes = (
               request.ttl_minutes
               if getattr(request, "ttl_minutes", None) is not None
               else 60
           )
           expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
   ```
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/mcp_service/embedded_chart/tool/get_embeddable_chart.py
   **Line:** 148:149
   **Comment:**
   	*Type Error: TypeError when `ttl_minutes` is None: `timedelta(minutes=request.ttl_minutes)` will raise if `request.ttl_minutes` is None; ensure a default is used (e.g., 60) before constructing the timedelta.
   
   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>



##########
superset/mcp_service/app.py:
##########
@@ -324,6 +324,9 @@ def create_mcp_app(
     get_instance_info,
     health_check,
 )
+from superset.mcp_service.embedded_chart.tool import (  # noqa: F401, E402
+    get_embeddable_chart,
+)

Review Comment:
   **Suggestion:** The import is used only for its decorator side-effects (tool registration) and the symbol is otherwise unused; importing the module (not the symbol) makes that intent explicit and avoids suggesting the symbol is referenced in this file. [code quality]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
   # Import module for side-effects (registers tools via decorators) — symbol is not used directly here
   import superset.mcp_service.embedded_chart.tool  # noqa: F401
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   This is a good, minimal-quality suggestion: importing the module (rather than a symbol) makes the intent of importing for side-effects explicit and avoids implying the symbol is used in this file.
   It reduces accidental future removal by other maintainers and is safe here because the symbol isn't referenced elsewhere in this module.
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/mcp_service/app.py
   **Line:** 327:329
   **Comment:**
   	*Code Quality: The import is used only for its decorator side-effects (tool registration) and the symbol is otherwise unused; importing the module (not the symbol) makes that intent explicit and avoids suggesting the symbol is referenced in this file.
   
   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>



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891596507


##########
superset/charts/api.py:
##########
@@ -1183,3 +1195,252 @@ def import_(self) -> Response:
         )
         command.run()
         return self.response(200, message="OK")
+
+    @expose("/<id_or_uuid>/embedded", methods=("GET",))
+    @protect()
+    @safe
+    @permission_name("read")
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_embedded",
+        log_to_statsd=False,
+    )
+    def get_embedded(self, id_or_uuid: str) -> Response:
+        """Get the chart's embedded configuration.
+        ---
+        get:
+          summary: Get the chart's embedded configuration
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: id_or_uuid
+            description: The chart id or uuid
+          responses:
+            200:
+              description: Result contains the embedded chart config
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        $ref: '#/components/schemas/EmbeddedChartResponseSchema'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            chart = ChartDAO.get_by_id_or_uuid(id_or_uuid)
+        except ChartNotFoundError:
+            return self.response_404()
+
+        if not chart.embedded:
+            return self.response(404)
+        embedded: EmbeddedChart = chart.embedded[0]
+        result = self.embedded_response_schema.dump(embedded)
+        return self.response(200, result=result)
+
+    @expose("/<id_or_uuid>/embedded", methods=["POST", "PUT"])
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.set_embedded",
+        log_to_statsd=False,
+    )
+    def set_embedded(self, id_or_uuid: str) -> Response:
+        """Set a chart's embedded configuration.
+        ---
+        post:
+          summary: Set a chart's embedded configuration
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: id_or_uuid
+            description: The chart id or uuid
+          requestBody:
+            description: The embedded configuration to set
+            required: true
+            content:
+              application/json:
+                schema: EmbeddedChartConfigSchema
+          responses:
+            200:
+              description: Successfully set the configuration
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        $ref: '#/components/schemas/EmbeddedChartResponseSchema'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        put:
+          description: >-
+            Sets a chart's embedded configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: id_or_uuid
+            description: The chart id or uuid
+          requestBody:
+            description: The embedded configuration to set
+            required: true
+            content:
+              application/json:
+                schema: EmbeddedChartConfigSchema
+          responses:
+            200:
+              description: Successfully set the configuration
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        $ref: '#/components/schemas/EmbeddedChartResponseSchema'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            chart = ChartDAO.get_by_id_or_uuid(id_or_uuid)
+        except ChartNotFoundError:
+            return self.response_404()
+
+        try:
+            body = self.embedded_config_schema.load(request.json)
+
+            embedded = EmbeddedChartDAO.upsert(
+                chart,
+                body["allowed_domains"],
+            )
+            db.session.commit()  # pylint: disable=consider-using-transaction
+
+            result = self.embedded_response_schema.dump(embedded)
+            return self.response(200, result=result)
+        except ValidationError as error:
+            db.session.rollback()  # pylint: disable=consider-using-transaction
+            return self.response_400(message=error.messages)
+

Review Comment:
   The endpoint uses `@protect()` which is Flask-AppBuilder's permission decorator. The embedded chart API endpoints are registered with the FAB security system and access is controlled via the RBAC model.



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,304 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
+
+            # Generate guest token
+            username = g.user.username if hasattr(g, "user") and g.user else "anonymous"
+            guest_user: GuestTokenUser = {
+                "username": f"embed_{username}",
+                "first_name": "Embed",
+                "last_name": "User",
+            }
+
+            resources: list[GuestTokenResource] = [
+                {
+                    "type": GuestTokenResourceType.CHART_PERMALINK,
+                    "id": permalink_key,
+                }
+            ]
+
+            rls_rules: list[GuestTokenRlsRule] = []
+
+            guest_token_result = security_manager.create_guest_access_token(
+                user=guest_user,
+                resources=resources,
+                rls=rls_rules,
+            )
+
+            # Handle both bytes (older PyJWT) and string (PyJWT 2.0+)
+            guest_token = (
+                guest_token_result.decode("utf-8")
+                if isinstance(guest_token_result, bytes)
+                else guest_token_result
+            )
+
+            # Build iframe URL using request host
+            base_url = request.host_url.rstrip("/")
+            iframe_url = f"{base_url}/embedded/chart/?permalink_key={permalink_key}"

Review Comment:
   The iframe URL is correct. It points to `EmbeddedChartView` (`/embedded/chart/`) which uses `?permalink_key=xxx` as a query parameter to serve the HTML page. The API endpoint `/<permalink_key>` is a separate REST endpoint used by the JS inside the iframe.



##########
superset/charts/schemas.py:
##########
@@ -345,6 +345,17 @@ class ChartGetCachedScreenshotResponseSchema(Schema):
     )
 
 
+class EmbeddedChartConfigSchema(Schema):
+    allowed_domains = fields.List(fields.String(), required=True)
+
+
+class EmbeddedChartResponseSchema(Schema):
+    uuid = fields.String()
+    allowed_domains = fields.List(fields.String())
+    chart_id = fields.Integer()
+    changed_on = fields.DateTime()

Review Comment:
   The `changed_on = fields.DateTime()` in the schema is correct — Marshmallow serializes DateTime to ISO string format, which matches the frontend `EmbeddedChart` TypeScript type where `changed_on: string`.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894977612


##########
superset/models/embedded_chart.py:
##########
@@ -0,0 +1,58 @@
+# 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 uuid
+
+from flask_appbuilder import Model
+from sqlalchemy import Column, ForeignKey, Integer, Text
+from sqlalchemy.orm import relationship
+from sqlalchemy_utils import UUIDType
+
+from superset.models.helpers import AuditMixinNullable
+
+
+class EmbeddedChart(Model, AuditMixinNullable):
+    """
+    A configuration of embedding for a chart.
+
+    References the chart (slice), and contains a config for embedding that chart.
+
+    This data model allows multiple configurations for a given chart,
+    but at this time the API only allows setting one.
+    """
+
+    __tablename__ = "embedded_charts"
+
+    uuid = Column(UUIDType(binary=True), default=uuid.uuid4, primary_key=True)
+    allow_domain_list = Column(Text)  # reference the `allowed_domains` property instead
+    chart_id = Column(
+        Integer,
+        ForeignKey("slices.id", ondelete="CASCADE"),
+        nullable=False,
+    )
+    chart = relationship(
+        "Slice",
+        back_populates="embedded",
+        foreign_keys=[chart_id],
+    )
+
+    @property
+    def allowed_domains(self) -> list[str]:
+        """
+        A list of domains which are allowed to embed the chart.
+        An empty list means any domain can embed.
+        """
+        return self.allow_domain_list.split(",") if self.allow_domain_list else []

Review Comment:
   Thanks for confirming the domain parsing fix.



##########
superset/embedded_chart/exceptions.py:
##########
@@ -0,0 +1,35 @@
+# 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.
+from superset.exceptions import SupersetException
+
+
+class EmbeddedChartPermalinkNotFoundError(SupersetException):
+    """Raised when an embedded chart permalink is not found or has expired."""
+
+    message = "The embedded chart permalink could not be found or has expired."

Review Comment:
   Thanks for verifying the i18n support and HTTP status code mappings.



##########
superset/views/embedded_charts.py:
##########
@@ -0,0 +1,34 @@
+# 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.
+from flask_appbuilder import expose, has_access
+
+from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP
+from superset.superset_typing import FlaskResponse
+from superset.views.base import BaseSupersetView
+
+
+class EmbeddedChartsView(BaseSupersetView):
+    """View for managing embedded charts list."""
+
+    route_base = "/embeddedcharts"
+    class_permission_name = "Chart"
+    method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP
+
+    @expose("/list/")
+    @has_access
+    def list(self) -> FlaskResponse:
+        return super().render_app_template()

Review Comment:
   Great clarification — you're right that EmbeddedChartsView (the admin list) and EmbeddedChartView (the iframe page) serve different purposes and use different entry points. Thanks for the thorough analysis.



##########
superset-frontend/src/explore/components/EmbedCodeContent.tsx:
##########
@@ -57,43 +61,73 @@ const EmbedCodeContent: FC<EmbedCodeContentProps> = ({
     }
   }, []);
 
-  const updateUrl = useCallback(() => {
-    setUrl('');
-    if (!formData?.datasource) return;
-    getChartPermalink(formData as { datasource: string })
-      .then(result => {
-        if (result?.url) {
-          setUrl(result.url);
-          setErrorMessage('');
-        }
-      })
-      .catch(() => {
-        setErrorMessage(t('Error'));
-        addDangerToast?.(t('Sorry, something went wrong. Try again later.'));
+  const generateEmbedCode = useCallback(async () => {
+    if (!formData) return;
+
+    setLoading(true);
+    setErrorMessage('');
+
+    try {
+      const createEmbeddedChart = makeApi<
+        {
+          form_data: LatestQueryFormData;
+          allowed_domains: string[];
+          ttl_minutes: number;
+        },
+        EmbedData
+      >({
+        method: 'POST',
+        endpoint: '/api/v1/embedded_chart/',
       });
-  }, [addDangerToast, formData]);
+
+      const response = await createEmbeddedChart({
+        form_data: formData,
+        allowed_domains: [],
+        ttl_minutes: 60,
+      });
+
+      setEmbedData(response);
+    } catch {
+      setErrorMessage(t('Error generating embed code'));
+      addDangerToast?.(t('Sorry, something went wrong. Try again later.'));
+    } finally {
+      setLoading(false);
+    }
+  }, [formData, addDangerToast]);
 
   useEffect(() => {
-    updateUrl();
-  }, []);
+    generateEmbedCode();
+  }, [generateEmbedCode]);
 
   const html = useMemo(() => {
-    if (!url) return '';
-    const srcLink = `${url}?${URL_PARAMS.standalone.name}=1&height=${height}`;
-    return (
-      '<iframe\n' +
-      `  width="${width}"\n` +
-      `  height="${height}"\n` +
-      '  seamless\n' +
-      '  frameBorder="0"\n' +
-      '  scrolling="no"\n' +
-      `  src="${srcLink}"\n` +
-      '>\n' +
-      '</iframe>'
+    if (!embedData?.iframe_url || !embedData?.guest_token) return '';
+
+    const { origin } = new URL(embedData.iframe_url);
+
+    return `<!-- Superset Embedded Chart -->
+<iframe
+  id="superset-chart"
+  src="${embedData.iframe_url}"
+  width="${width}"
+  height="${height}"
+  frameborder="0"
+  data-guest-token="${embedData.guest_token}"
+  sandbox="allow-scripts allow-same-origin allow-popups"
+></iframe>
+<script>
+  document.getElementById('superset-chart').onload = function() {
+    this.contentWindow.postMessage(
+      { type: '__embedded_comms__', guestToken: '${embedData.guest_token}' },
+      '${origin}'
     );
-  }, [height, url, width]);
+  };
+</script>`;

Review Comment:
   Good point about the hard-coded iframe id causing issues when embedding multiple charts on the same page. Using a unique id per chart (e.g., based on the permalink key) would be the right fix. I'll update the embed code generation to use unique identifiers. Thanks for the catch.



##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,137 @@
+# 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, current_user, 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

Review Comment:
   Good observation. You're right that decode_permalink_id could raise exceptions beyond ValueError or KeyError for malformed input. I'll broaden the exception handling in that specific code path to catch any decoding failures gracefully instead of letting them bubble up as 500 errors. Thanks for the review.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "github-advanced-security[bot] (via GitHub)" <gi...@apache.org>.
github-advanced-security[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2892620116


##########
embed-demo.html:
##########
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Superset Embedded Chart Demo</title>
+  <style>
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+      max-width: 1200px;
+      margin: 0 auto;
+      padding: 40px;
+      background: #f5f5f5;
+    }
+    h1 { color: #333; }
+    .input-section {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      margin-bottom: 20px;
+    }
+    input[type="text"] {
+      width: 100%;
+      padding: 10px;
+      font-family: monospace;
+      font-size: 13px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      box-sizing: border-box;
+      margin-bottom: 12px;
+    }
+    button {
+      background: #20a7c9;
+      color: white;
+      border: none;
+      padding: 12px 24px;
+      font-size: 16px;
+      border-radius: 4px;
+      cursor: pointer;
+      margin-top: 10px;
+    }
+    button:hover {
+      background: #1a8fa8;
+    }
+    .chart-container {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      min-height: 450px;
+    }
+    label {
+      font-weight: 600;
+      display: block;
+      margin-bottom: 8px;
+    }
+  </style>
+</head>
+<body>
+  <h1>Superset Embedded Chart Demo</h1>
+
+  <div class="input-section">
+    <label for="iframe-url-input">iframe_url (from get_embeddable_chart response):</label>
+    <input type="text" id="iframe-url-input" placeholder="http://localhost:8088/embedded/chart/?permalink_key=..." />
+
+    <label for="guest-token-input">guest_token (from get_embeddable_chart response):</label>
+    <input type="text" id="guest-token-input" placeholder="eyJ..." />
+
+    <button onclick="embedChart()">Embed Chart</button>
+  </div>
+
+  <div class="chart-container" id="chart-container">
+    <p style="color: #999; text-align: center; margin-top: 200px;">Chart will appear here</p>
+  </div>
+
+  <script>
+    function embedChart() {
+      var iframeUrl = document.getElementById('iframe-url-input').value.trim();
+      var guestToken = document.getElementById('guest-token-input').value.trim().replace(/\s/g, '');
+      var container = document.getElementById('chart-container');
+
+      if (!iframeUrl || !guestToken) {
+        container.textContent = 'Error: Both iframe_url and guest_token are required.';
+        return;
+      }
+
+      // Validate URL scheme to prevent javascript: or data: URIs
+      if (!iframeUrl.startsWith('http://') && !iframeUrl.startsWith('https://')) {
+        container.textContent = 'Error: iframe_url must use http:// or https://';
+        return;
+      }
+
+      var targetOrigin;
+      try {
+        targetOrigin = new URL(iframeUrl).origin;
+      } catch (e) {
+        container.textContent = 'Error: Invalid iframe_url';
+        return;
+      }
+
+      // Build iframe programmatically (no HTML parsing)
+      var iframe = document.createElement('iframe');
+      iframe.setAttribute('src', iframeUrl);

Review Comment:
   ## DOM text reinterpreted as HTML
   
   [DOM text](1) is reinterpreted as HTML without escaping meta-characters.
   
   [Show more details](https://github.com/apache/superset/security/code-scanning/2250)



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2892230159


##########
superset/models/embedded_chart.py:
##########
@@ -0,0 +1,58 @@
+# 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 uuid
+
+from flask_appbuilder import Model
+from sqlalchemy import Column, ForeignKey, Integer, Text
+from sqlalchemy.orm import relationship
+from sqlalchemy_utils import UUIDType
+
+from superset.models.helpers import AuditMixinNullable
+
+
+class EmbeddedChart(Model, AuditMixinNullable):
+    """
+    A configuration of embedding for a chart.
+
+    References the chart (slice), and contains a config for embedding that chart.
+
+    This data model allows multiple configurations for a given chart,
+    but at this time the API only allows setting one.
+    """
+
+    __tablename__ = "embedded_charts"
+
+    uuid = Column(UUIDType(binary=True), default=uuid.uuid4, primary_key=True)
+    allow_domain_list = Column(Text)  # reference the `allowed_domains` property instead
+    chart_id = Column(
+        Integer,
+        ForeignKey("slices.id", ondelete="CASCADE"),
+        nullable=False,
+    )
+    chart = relationship(
+        "Slice",
+        back_populates="embedded",
+        foreign_keys=[chart_id],
+    )
+
+    @property
+    def allowed_domains(self) -> list[str]:
+        """
+        A list of domains which are allowed to embed the chart.
+        An empty list means any domain can embed.
+        """
+        return self.allow_domain_list.split(",") if self.allow_domain_list else []

Review Comment:
   <!-- Bito Reply -->
   The fix in commit 50ab323 addresses the domain parsing robustness issue by stripping whitespace and filtering empty entries, matching the suggestion to prevent validation problems.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891913407


##########
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:
   No change needed. Same as above — Pydantic v2's `ValidationError` inherits from `ValueError`, so `pytest.raises(ValueError)` works correctly for both TTL and height validation tests.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4007831810

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894974060


##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,548 @@
+# 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 len(request.allowed_domains) == 2
+        assert "https://example.com" in request.allowed_domains
+
+    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:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911847702


##########
embed-demo.html:
##########
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Superset Embedded Chart Demo</title>
+  <style>
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+      max-width: 1200px;
+      margin: 0 auto;
+      padding: 40px;
+      background: #f5f5f5;
+    }
+    h1 { color: #333; }
+    .input-section {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      margin-bottom: 20px;
+    }
+    textarea {
+      width: 100%;
+      height: 150px;
+      font-family: monospace;
+      font-size: 12px;
+      padding: 10px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      resize: vertical;
+      box-sizing: border-box;
+    }
+    button {
+      background: #20a7c9;
+      color: white;
+      border: none;
+      padding: 12px 24px;
+      font-size: 16px;
+      border-radius: 4px;
+      cursor: pointer;
+      margin-top: 10px;
+    }
+    button:hover {
+      background: #1a8fa8;
+    }
+    .chart-container {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      min-height: 450px;
+    }
+    label {
+      font-weight: 600;
+      display: block;
+      margin-bottom: 8px;
+    }
+  </style>
+</head>
+<body>
+  <h1>Superset Embedded Chart Demo</h1>
+
+  <div class="input-section">
+    <label for="iframe-input">Paste iframe_html response here:</label>
+    <textarea id="iframe-input" placeholder="Paste the iframe_html value from get_embeddable_chart response..."></textarea>
+    <button onclick="embedChart()">Embed Chart</button>
+  </div>
+
+  <div class="chart-container" id="chart-container">
+    <p style="color: #999; text-align: center; margin-top: 200px;">Chart will appear here</p>
+  </div>
+
+  <script>
+    function embedChart() {
+      const input = document.getElementById('iframe-input').value.trim();
+      const container = document.getElementById('chart-container');
+
+      if (!input) {
+        alert('Please paste the iframe_html first');
+        return;
+      }
+
+      // Safely parse input and only allow iframe elements (prevent XSS)
+      const parser = new DOMParser();
+      const doc = parser.parseFromString(input, 'text/html');

Review Comment:
   Fixed — `embed-demo.html` was removed in commit c7903163 to address this CodeQL alert. The file no longer exists in the repository.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932059975


##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,138 @@
+# 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, current_user, 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.exceptions import KeyValueParseKeyError
+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)

Review Comment:
   Fixed in ca5633381f. The origin matcher now correctly handles scheme-less domains with ports (e.g. `localhost:3000`) by detecting when urlparse misinterprets the input and comparing directly against the netloc.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932226980


##########
superset/mcp_service/embedded_chart/schemas.py:
##########
@@ -0,0 +1,101 @@
+# 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.
+from datetime import datetime
+from typing import Any
+
+from pydantic import BaseModel, Field
+
+# Import the same ChartConfig used by generate_chart
+from superset.mcp_service.chart.schemas import ChartConfig
+
+
+class GetEmbeddableChartRequest(BaseModel):
+    """Request schema for get_embeddable_chart tool.
+
+    Uses the same simplified ChartConfig schema as generate_chart for consistency.
+    """
+
+    datasource_id: int | str = Field(
+        ...,
+        description="Dataset ID (numeric) or UUID",
+    )
+    config: ChartConfig = Field(
+        ...,
+        description=(
+            "Chart configuration using simplified schema. Use chart_type='xy' for "
+            "line/bar/area/scatter charts, or chart_type='table' for tables. "
+            'Example: {"chart_type": "xy", "x": {"name": "genre"}, '
+            '"y": [{"name": "sales", "aggregate": "SUM"}], "kind": "bar"}'
+        ),
+    )
+    ttl_minutes: int = Field(
+        default=60,
+        ge=1,
+        le=10080,  # max 1 week
+        description="Permalink TTL in minutes (default: 60, max: 10080 = 1 week)",
+    )
+    height: int = Field(
+        default=400,
+        ge=100,
+        le=2000,
+        description="Chart height in pixels for iframe (default: 400)",
+    )
+    rls_rules: list[dict[str, Any]] = Field(
+        default_factory=list,
+        description="Row-level security rules to apply to the guest token",
+    )
+    allowed_domains: list[str] = Field(

Review Comment:
   The `allowed_domains` field intentionally accepts bare hostnames (e.g., `localhost:3000`, `example.com`) rather than full URLs, because the embedding use case is domain-matching (not URL navigation). Using Pydantic's `HttpUrl` would require users to specify `https://example.com` for every domain, which is more verbose and doesn't match the existing Superset embedded dashboard pattern. The actual validation happens at the API level (`embedded_chart/api.py` lines 247-252) which ensures `allowed_domains` is a list of strings. The domain matching uses `same_origin()` which handles the scheme/port normalization.



##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,139 @@
+# 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, current_user, 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.exceptions import KeyValueParseKeyError
+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 or (not parsed2.netloc and ":" in url2):
+        # url2 is a bare domain or host:port (e.g., "localhost:3000"),
+        # which urlparse misinterprets. Compare directly with 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, KeyValueParseKeyError) as ex:
+            logger.warning("Error fetching permalink for referrer validation: %s", ex)
+            permalink_value = None
+

Review Comment:
   When `permalink_value` is None (invalid/expired permalink), the endpoint raises `EmbeddedChartPermalinkNotFoundError()` which returns a 404 — not unrestricted access. See the `get_embedded_chart` endpoint which checks: `permalink_value = self._get_permalink_value(permalink_key)` followed by `if not permalink_value: raise EmbeddedChartPermalinkNotFoundError()`. The domain check is only relevant when a valid permalink exists and has `allowedDomains` configured. An invalid permalink can't bypass domain checks because the request is rejected with 404 before reaching any chart data.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932171782


##########
superset/security/manager.py:
##########
@@ -3109,6 +3131,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.value
+            for r in user.resources
+        )

Review Comment:
   This is a valid concern and is explicitly documented in the code as a known trade-off. See the docstring and TODO at lines 3135-3145:
   
   ```python
   def has_guest_chart_permalink_access(self, datasource: Any) -> bool:
       """
       ...
       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.
       """
   ```
   
   The current approach grants datasource access to any guest user holding a `chart_permalink` resource. The rationale is that the embedded chart frontend only queries the specific datasource from the permalink's `formData`, so in normal usage the scope is constrained. The TODO acknowledges that for stricter security, each permalink should be resolved and the datasource compared — this is a deliberate design choice to avoid the performance cost of permalink resolution on every datasource access check, with a planned follow-up for tighter validation.



##########
superset-frontend/src/pages/EmbeddedChartsList/index.tsx:
##########
@@ -0,0 +1,254 @@
+/**
+ * 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 } from '@apache-superset/core/theme';
+import { t } from '@apache-superset/core/translation';
+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));

Review Comment:
   This is already handled. All places where `chart_name` is used have an `|| t('Untitled')` fallback:
   
   - **Delete success toast** (line 98): `chart.chart_name || t('Untitled')`
   - **Delete error toast** (line 101): `chart.chart_name || t('Untitled')`
   - **Chart name column** (line 136): `chart_name || t('Untitled')`
   - **Delete modal description** (line 242): `chartToDelete.chart_name || t('Untitled')`
   
   The `EmbeddedChart` interface correctly types `chart_name` as `string | null` (line 36), and every usage provides the `|| t('Untitled')` fallback to prevent 'null' from appearing in the UI.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "netlify[bot] (via GitHub)" <gi...@apache.org>.
netlify[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3716594432

   ### <span aria-hidden="true">👷</span> Deploy Preview for *superset-docs-preview* processing.
   
   
   |  Name | Link |
   |:-:|------------------------|
   |<span aria-hidden="true">🔨</span> Latest commit | 99b1a97fc41b7d389018af29ef5ab9959d4d45a5 |
   |<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/projects/superset-docs-preview/deploys/695d8ca9d659480008d19810 |


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3730164115

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3717355865

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3720809017

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3727350418

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3729956978

   CodeAnt AI is running Incremental review
   
   ---
   
   ### Thanks for using CodeAnt! 🎉
   
   We're free for open-source projects. if you're enjoying it, help us grow by sharing.
   
   [Share on X](https://twitter.com/intent/tweet?text=Just%20tried%20%40CodeAntAI%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A&url=https%3A//codeant.ai) ·
   [Reddit](https://www.reddit.com/submit?title=Check%20out%20CodeAnt%20for%20automated%20code%20review&text=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A%20https%3A//codeant.ai) ·
   [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcodeant.ai&mini=true&title=Check%20out%20CodeAnt%20for%20automated%20code%20review&summary=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos)
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3893998312

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3892112011

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3755764499

   CodeAnt AI is running Incremental review
   
   ---
   
   ### Thanks for using CodeAnt! 🎉
   
   We're free for open-source projects. if you're enjoying it, help us grow by sharing.
   
   [Share on X](https://twitter.com/intent/tweet?text=Just%20tried%20%40CodeAntAI%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A&url=https%3A//codeant.ai) ·
   [Reddit](https://www.reddit.com/submit?title=Check%20out%20CodeAnt%20for%20automated%20code%20review&text=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A%20https%3A//codeant.ai) ·
   [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcodeant.ai&mini=true&title=Check%20out%20CodeAnt%20for%20automated%20code%20review&summary=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos)
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4054557650

   @villebro @michael-s-molina Just confirming — the separate \`EMBEDDABLE_CHARTS_MCP\` feature flag has been removed as promised. The PR now uses the existing \`EMBEDDABLE_CHARTS\` flag (which was already in \`config.py\`) to control frontend UI visibility only. No new feature flags were added.


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932057836


##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,321 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.exceptions import CommandException
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+from superset.key_value.shared_entries import get_permalink_salt
+from superset.key_value.types import (
+    KeyValueResource,
+    MarshmallowKeyValueCodec,
+    SharedKey,
+)
+from superset.key_value.exceptions import KeyValueParseKeyError
+from superset.key_value.utils import decode_permalink_id
+from superset.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except (ValueError, KeyError, KeyValueParseKeyError) as ex:
+            logger.warning("Error fetching embedded chart: %s", ex)
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)

Review Comment:
   Fixed in ca5633381f. Added explicit type validation for `allowed_domains` — now returns 400 if it's not a list of strings.



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,321 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.exceptions import CommandException
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+from superset.key_value.shared_entries import get_permalink_salt
+from superset.key_value.types import (
+    KeyValueResource,
+    MarshmallowKeyValueCodec,
+    SharedKey,
+)
+from superset.key_value.exceptions import KeyValueParseKeyError
+from superset.key_value.utils import decode_permalink_id
+from superset.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except (ValueError, KeyError, KeyValueParseKeyError) as ex:
+            logger.warning("Error fetching embedded chart: %s", ex)
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate ttl_minutes bounds
+            if (
+                not isinstance(ttl_minutes, int)
+                or ttl_minutes < 1
+                or ttl_minutes > 10080
+            ):
+                return self.response_400(
+                    message="ttl_minutes must be between 1 and 10080"
+                )
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)

Review Comment:
   Valid point. The guest token itself has a TTL (controlled by `GUEST_TOKEN_JWT_EXP_SECONDS` config), so the token expires and the embed becomes inaccessible after that window. The `expires_at` in the API response communicates this to the caller. The permalink entry persists longer, but without a valid guest token it can't be accessed via the embedded chart GET endpoint. Adding `expires_on` to the permalink itself would require changes to `CreateExplorePermalinkCommand` — tracked as a follow-up improvement.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4030550141

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911853771


##########
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:
   Already guarded — line 111 checks `if (\!navigator.clipboard?.writeText)` and shows a danger toast before returning early. The optional chaining (`?.`) safely handles both `navigator.clipboard` being undefined and `writeText` being unavailable.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891590137


##########
superset/config.py:
##########
@@ -559,6 +559,8 @@ class D3TimeFormat(TypedDict, total=False):
     # This feature flag is stil in beta and is not recommended for production use.
     "GLOBAL_ASYNC_QUERIES": False,
     "EMBEDDED_SUPERSET": False,
+    # Enables MCP tool for creating embeddable chart iframes with guest tokens
+    "EMBEDDABLE_CHARTS_MCP": False,

Review Comment:
   Fixed in a8b866e. Changed `EMBEDDABLE_CHARTS` to default to `False` so operators must explicitly opt in after configuring a strong `GUEST_TOKEN_JWT_SECRET`.



##########
superset/mcp_service/embedded_chart/tool/__init__.py:
##########
@@ -0,0 +1,19 @@
+# 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.
+from .get_embeddable_chart import get_embeddable_chart
+
+__all__ = ["get_embeddable_chart"]

Review Comment:
   This pattern is the same used by all other tool `__init__.py` files in the MCP service (chart/tool/__init__.py, dashboard/tool/__init__.py, etc.). The circular import risk doesn't apply here because `get_embeddable_chart` only imports from external modules, not back into the tool package.



##########
superset/mcp_service/embedded_chart/tool/get_embeddable_chart.py:
##########
@@ -0,0 +1,228 @@
+# 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: get_embeddable_chart
+
+Creates an embeddable chart iframe with guest token authentication.
+This enables AI agents to generate charts that can be displayed in
+external applications via iframe.
+"""
+
+import logging
+from datetime import datetime, timedelta, timezone
+
+from fastmcp import Context
+from flask import g
+from superset_core.mcp import tool
+
+from superset import is_feature_enabled
+from superset.mcp_service.auth import has_dataset_access
+from superset.mcp_service.embedded_chart.schemas import (
+    GetEmbeddableChartRequest,
+    GetEmbeddableChartResponse,
+)
+from superset.mcp_service.utils.schema_utils import parse_request
+from superset.mcp_service.utils.url_utils import get_superset_base_url
+
+logger = logging.getLogger(__name__)
+
+
+@tool(tags=["core"])
+@parse_request(GetEmbeddableChartRequest)
+async def get_embeddable_chart(
+    request: GetEmbeddableChartRequest,
+    ctx: Context,
+) -> GetEmbeddableChartResponse:
+    """Create an embeddable chart iframe URL with guest token authentication.
+
+    This tool creates an ephemeral chart visualization that can be embedded
+    in external applications via iframe. The chart is configured via form_data
+    and stored as a permalink with TTL.
+
+    IMPORTANT:
+    - Requires EMBEDDABLE_CHARTS_MCP feature flag to be enabled
+    - The iframe_html can be directly embedded in web pages
+    - Guest token must be passed to the iframe for authentication
+    - Chart expires after ttl_minutes (default: 60 minutes)
+
+    Example usage:
+    ```json
+    {
+        "datasource_id": 123,
+        "viz_type": "echarts_timeseries_line",
+        "form_data": {
+            "metrics": ["count"],
+            "groupby": ["category"],
+            "time_range": "Last 7 days"
+        },
+        "ttl_minutes": 120,
+        "height": 500
+    }
+    ```
+
+    Returns iframe_url, guest_token, and ready-to-use iframe_html snippet.
+    """
+    await ctx.info(
+        f"Creating embeddable chart: datasource_id={request.datasource_id}, "
+        f"viz_type={request.viz_type}"
+    )
+
+    # Check feature flag
+    if not is_feature_enabled("EMBEDDABLE_CHARTS_MCP"):
+        await ctx.error("EMBEDDABLE_CHARTS_MCP feature flag is not enabled")
+        return GetEmbeddableChartResponse(
+            success=False,
+            error="EMBEDDABLE_CHARTS_MCP feature flag is not enabled",
+        )
+
+    try:
+        # Import here to avoid circular imports
+        from superset.commands.explore.permalink.create import (
+            CreateExplorePermalinkCommand,
+        )
+        from superset.daos.dataset import DatasetDAO
+        from superset.extensions import security_manager
+        from superset.security.guest_token import GuestTokenResourceType
+
+        # Resolve dataset
+        dataset = None
+        if isinstance(request.datasource_id, int) or (
+            isinstance(request.datasource_id, str) and request.datasource_id.isdigit()
+        ):
+            dataset_id = (
+                int(request.datasource_id)
+                if isinstance(request.datasource_id, str)
+                else request.datasource_id
+            )
+            dataset = DatasetDAO.find_by_id(dataset_id)
+        else:
+            # Try UUID lookup
+            dataset = DatasetDAO.find_by_id(request.datasource_id, id_column="uuid")
+
+        if not dataset:
+            await ctx.error(f"Dataset not found: {request.datasource_id}")
+            return GetEmbeddableChartResponse(
+                success=False,
+                error=f"Dataset not found: {request.datasource_id}",
+            )
+
+        # Check access
+        if not has_dataset_access(dataset):
+            await ctx.error("Access denied to dataset")
+            return GetEmbeddableChartResponse(
+                success=False,
+                error="Access denied to dataset",
+            )
+
+        # Build complete form_data
+        form_data = {
+            **request.form_data,
+            "viz_type": request.viz_type,
+            "datasource": f"{dataset.id}__table",
+        }
+
+        # Apply overrides if provided
+        if request.form_data_overrides:
+            form_data.update(request.form_data_overrides)
+
+        # Create permalink
+        state = {"formData": form_data}
+        permalink_key = CreateExplorePermalinkCommand(state).run()
+
+        await ctx.debug(f"Created permalink: {permalink_key}")
+
+        # Calculate expiration
+        expires_at = datetime.now(timezone.utc) + timedelta(minutes=request.ttl_minutes)

Review Comment:
   Fixed in a8b866e. Added a safe default: `ttl = request.ttl_minutes if request.ttl_minutes else 60`.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4011119007

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894980914


##########
superset/security/manager.py:
##########
@@ -2810,6 +2821,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except (ExplorePermalinkGetFailedError, ValueError) as ex:
+                    raise EmbeddedChartPermalinkNotFoundError() from ex

Review Comment:
   <!-- Bito Reply -->
   Yes, ChartAccessDeniedError can bubble up from GetExplorePermalinkCommand.run() as it may be raised during chart access checks. Adding it to the catch list ensures consistent error handling by always raising EmbeddedChartPermalinkNotFoundError.
   
   **superset/security/manager.py**
   ```
   from superset.embedded_chart.exceptions import (
       EmbeddedChartPermalinkNotFoundError,
       ChartAccessDeniedError,
   )
   
                   try:
                       permalink_value = GetExplorePermalinkCommand(permalink_key).run()
                       if not permalink_value:
                           raise EmbeddedChartPermalinkNotFoundError()
                   except EmbeddedChartPermalinkNotFoundError:
                       raise
                   except (ExplorePermalinkGetFailedError, ValueError, ChartAccessDeniedError):
                       raise EmbeddedChartPermalinkNotFoundError() from None
   ```



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891588286


##########
superset-frontend/src/embeddedChart/index.tsx:
##########
@@ -0,0 +1,303 @@
+/**
+ * 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 'src/public-path';
+
+import { useState, useEffect, useCallback } from 'react';
+import ReactDOM from 'react-dom';
+import { makeApi, t, logging, QueryFormData } from '@superset-ui/core';
+import { StatefulChart } from '@superset-ui/core';
+import Switchboard from '@superset-ui/switchboard';
+import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData';
+import setupClient from 'src/setup/setupClient';
+import setupPlugins from 'src/setup/setupPlugins';
+import { store, USER_LOADED } from 'src/views/store';
+import { Loading } from '@superset-ui/core/components';
+import { ErrorBoundary, DynamicPluginProvider } from 'src/components';
+import { addDangerToast } from 'src/components/MessageToasts/actions';
+import ToastContainer from 'src/components/MessageToasts/ToastContainer';
+import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
+import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
+import { ThemeController } from 'src/theme/ThemeController';
+import type { ThemeStorage } from '@apache-superset/core/ui';
+import { Provider as ReduxProvider } from 'react-redux';
+
+setupPlugins();
+
+const debugMode = process.env.WEBPACK_MODE === 'development';
+const bootstrapData = getBootstrapData();
+
+function log(...info: unknown[]) {
+  if (debugMode) logging.debug(`[superset-embedded-chart]`, ...info);
+}
+
+/**
+ * In-memory implementation of ThemeStorage interface for embedded contexts.
+ */
+class ThemeMemoryStorageAdapter implements ThemeStorage {
+  private storage = new Map<string, string>();
+
+  getItem(key: string): string | null {
+    return this.storage.get(key) || null;
+  }
+
+  setItem(key: string, value: string): void {
+    this.storage.set(key, value);
+  }
+
+  removeItem(key: string): void {
+    this.storage.delete(key);
+  }
+}
+
+const themeController = new ThemeController({
+  storage: new ThemeMemoryStorageAdapter(),
+});
+
+interface PermalinkState {
+  formData: QueryFormData;
+}
+
+const appMountPoint = document.getElementById('app')!;
+const MESSAGE_TYPE = '__embedded_comms__';
+
+function showFailureMessage(message: string) {
+  appMountPoint.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-family: sans-serif; color: #666;">${message}</div>`;
+}
+
+if (!window.parent || window.parent === window) {
+  showFailureMessage(
+    t(
+      'This page is intended to be embedded in an iframe, but it looks like that is not the case.',
+    ),
+  );
+}
+
+let displayedUnauthorizedToast = false;
+
+/**
+ * Handle unauthorized errors from the API.
+ */
+function guestUnauthorizedHandler() {
+  if (displayedUnauthorizedToast) return;
+  displayedUnauthorizedToast = true;
+  store.dispatch(
+    addDangerToast(
+      t(
+        'This session has encountered an interruption. The embedded chart may not load correctly.',
+      ),
+      {
+        duration: -1,
+        noDuplicate: true,
+      },
+    ),
+  );
+}
+
+/**
+ * Configures SupersetClient with the correct settings for the embedded chart page.
+ */
+function setupGuestClient(guestToken: string) {
+  setupClient({
+    appRoot: applicationRoot(),
+    guestToken,
+    guestTokenHeaderName: bootstrapData.config?.GUEST_TOKEN_HEADER_NAME,
+    unauthorizedHandler: guestUnauthorizedHandler,
+  });
+}
+
+function validateMessageEvent(event: MessageEvent) {
+  if (typeof event.data !== 'object' || event.data.type !== MESSAGE_TYPE) {

Review Comment:
   Fixed in a8b866e. Relaxed `validateMessageEvent` to accept messages carrying `guestToken` or `handshake` without requiring `data.type === MESSAGE_TYPE`. Also added explicit null guard (`data == null`) before the typeof check.



##########
superset-frontend/src/embeddedChart/index.tsx:
##########
@@ -0,0 +1,303 @@
+/**
+ * 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 'src/public-path';
+
+import { useState, useEffect, useCallback } from 'react';
+import ReactDOM from 'react-dom';
+import { makeApi, t, logging, QueryFormData } from '@superset-ui/core';
+import { StatefulChart } from '@superset-ui/core';
+import Switchboard from '@superset-ui/switchboard';
+import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData';
+import setupClient from 'src/setup/setupClient';
+import setupPlugins from 'src/setup/setupPlugins';
+import { store, USER_LOADED } from 'src/views/store';
+import { Loading } from '@superset-ui/core/components';
+import { ErrorBoundary, DynamicPluginProvider } from 'src/components';
+import { addDangerToast } from 'src/components/MessageToasts/actions';
+import ToastContainer from 'src/components/MessageToasts/ToastContainer';
+import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
+import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
+import { ThemeController } from 'src/theme/ThemeController';
+import type { ThemeStorage } from '@apache-superset/core/ui';
+import { Provider as ReduxProvider } from 'react-redux';
+
+setupPlugins();
+
+const debugMode = process.env.WEBPACK_MODE === 'development';
+const bootstrapData = getBootstrapData();
+
+function log(...info: unknown[]) {
+  if (debugMode) logging.debug(`[superset-embedded-chart]`, ...info);
+}
+
+/**
+ * In-memory implementation of ThemeStorage interface for embedded contexts.
+ */
+class ThemeMemoryStorageAdapter implements ThemeStorage {
+  private storage = new Map<string, string>();
+
+  getItem(key: string): string | null {
+    return this.storage.get(key) || null;
+  }
+
+  setItem(key: string, value: string): void {
+    this.storage.set(key, value);
+  }
+
+  removeItem(key: string): void {
+    this.storage.delete(key);
+  }
+}
+
+const themeController = new ThemeController({
+  storage: new ThemeMemoryStorageAdapter(),
+});
+
+interface PermalinkState {
+  formData: QueryFormData;
+}
+
+const appMountPoint = document.getElementById('app')!;
+const MESSAGE_TYPE = '__embedded_comms__';
+
+function showFailureMessage(message: string) {
+  appMountPoint.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-family: sans-serif; color: #666;">${message}</div>`;
+}
+
+if (!window.parent || window.parent === window) {
+  showFailureMessage(
+    t(
+      'This page is intended to be embedded in an iframe, but it looks like that is not the case.',
+    ),
+  );
+}
+
+let displayedUnauthorizedToast = false;
+
+/**
+ * Handle unauthorized errors from the API.
+ */
+function guestUnauthorizedHandler() {
+  if (displayedUnauthorizedToast) return;
+  displayedUnauthorizedToast = true;
+  store.dispatch(
+    addDangerToast(
+      t(
+        'This session has encountered an interruption. The embedded chart may not load correctly.',
+      ),
+      {
+        duration: -1,
+        noDuplicate: true,
+      },
+    ),
+  );
+}
+
+/**
+ * Configures SupersetClient with the correct settings for the embedded chart page.
+ */
+function setupGuestClient(guestToken: string) {
+  setupClient({
+    appRoot: applicationRoot(),
+    guestToken,
+    guestTokenHeaderName: bootstrapData.config?.GUEST_TOKEN_HEADER_NAME,
+    unauthorizedHandler: guestUnauthorizedHandler,
+  });
+}
+
+function validateMessageEvent(event: MessageEvent) {
+  if (typeof event.data !== 'object' || event.data.type !== MESSAGE_TYPE) {
+    throw new Error(`Message type does not match type used for embedded comms`);
+  }
+}
+
+/**
+ * Embedded Chart component that fetches formData from permalink and renders StatefulChart.
+ */
+function EmbeddedChartApp() {
+  const [formData, setFormData] = useState<QueryFormData | null>(null);
+  const [error, setError] = useState<string | null>(null);
+  const [loading, setLoading] = useState(true);
+
+  const fetchPermalinkData = useCallback(async () => {
+    const urlParams = new URLSearchParams(window.location.search);
+    const permalinkKey = urlParams.get('permalink_key');
+
+    if (!permalinkKey) {
+      setError(t('Missing permalink_key parameter'));
+      setLoading(false);
+      return;
+    }
+
+    try {
+      const getPermalinkData = makeApi<void, { state: PermalinkState }>({
+        method: 'GET',
+        endpoint: `/api/v1/embedded_chart/${permalinkKey}`,
+      });
+
+      const response = await getPermalinkData();
+      const { state } = response;
+
+      if (!state?.formData) {
+        setError(t('Invalid permalink data: missing formData'));
+        setLoading(false);
+        return;
+      }
+
+      log('Loaded formData from permalink:', state.formData);
+      setFormData(state.formData);
+      setLoading(false);
+    } catch (err) {
+      logging.error('Failed to load permalink data:', err);
+      setError(
+        t('Failed to load chart data. The permalink may have expired.'),
+      );
+      setLoading(false);
+    }
+  }, []);
+
+  useEffect(() => {
+    fetchPermalinkData();
+  }, [fetchPermalinkData]);
+
+  if (loading) {
+    return (
+      <div
+        style={{ width: '100%', height: '100vh', position: 'relative' }}
+      >
+        <Loading position="floating" />
+      </div>
+    );
+  }
+
+  if (error) {
+    return (
+      <div
+        style={{
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'center',
+          height: '100vh',
+          fontFamily: 'sans-serif',
+          color: '#666',
+          padding: '20px',
+          textAlign: 'center',
+        }}
+      >
+        {error}
+      </div>
+    );
+  }
+
+  if (!formData) {
+    return null;
+  }
+
+  return (
+    <div style={{ width: '100%', height: '100vh' }}>
+      <StatefulChart
+        formData={formData}
+        width="100%"
+        height="100%"
+        showLoading
+        enableNoResults
+      />
+    </div>
+  );
+}
+
+/**
+ * Context providers wrapper for the embedded chart.
+ */
+function EmbeddedChartWithProviders() {
+  return (
+    <SupersetThemeProvider themeController={themeController}>
+      <ReduxProvider store={store}>
+        <DynamicPluginProvider>
+          <ErrorBoundary>
+            <EmbeddedChartApp />
+          </ErrorBoundary>
+          <ToastContainer position="top" />
+        </DynamicPluginProvider>
+      </ReduxProvider>
+    </SupersetThemeProvider>
+  );
+}
+
+function start() {
+  const getMeWithRole = makeApi<void, { result: UserWithPermissionsAndRoles }>({
+    method: 'GET',
+    endpoint: '/api/v1/me/roles/',
+  });
+  return getMeWithRole().then(
+    ({ result }) => {
+      bootstrapData.user = result;
+      store.dispatch({
+        type: USER_LOADED,
+        user: result,
+      });
+      ReactDOM.render(<EmbeddedChartWithProviders />, appMountPoint);
+    },
+    err => {
+      logging.error(err);
+      showFailureMessage(
+        t(
+          'Something went wrong with embedded authentication. Check the dev console for details.',
+        ),
+      );
+    },
+  );
+}
+
+window.addEventListener('message', function embeddedChartInitializer(event) {
+  try {
+    validateMessageEvent(event);
+  } catch (err) {
+    log('ignoring message unrelated to embedded comms', err, event);
+    return;
+  }
+
+  const port = event.ports?.[0];
+  if (event.data.handshake === 'port transfer' && port) {
+    log('message port received', event);
+
+    Switchboard.init({
+      port,
+      name: 'superset-embedded-chart',
+      debug: debugMode,
+    });
+
+    let started = false;
+
+    Switchboard.defineMethod(
+      'guestToken',
+      ({ guestToken }: { guestToken: string }) => {
+        setupGuestClient(guestToken);
+        if (!started) {
+          start();
+          started = true;

Review Comment:
   Already handled — the `started` flag is reset to `false` in the `.catch()` handler (lines 316/341), so failed starts will be retried on subsequent guestToken messages.



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,119 @@
+# 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 flask import Response
+from flask_appbuilder.api import expose, safe
+from flask_appbuilder.hooks import before_request
+
+from superset import is_feature_enabled
+from superset.commands.explore.permalink.get import GetExplorePermalinkCommand
+from superset.embedded_chart.exceptions import EmbeddedChartPermalinkNotFoundError
+from superset.extensions import event_logger
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    @before_request
+    def ensure_feature_enabled(self) -> Response | None:
+        if not is_feature_enabled("EMBEDDABLE_CHARTS_MCP"):
+            return self.response_404()
+        return None
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart form_data
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        type: object
+                        properties:
+                          form_data:
+                            type: object
+                            description: The chart configuration form_data
+                          datasource_id:
+                            type: integer
+                            description: The datasource ID
+                          datasource_type:
+                            type: string
+                            description: The datasource type
+                          chart_id:
+                            type: integer
+                            description: Optional chart ID if based on saved chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            state = permalink_value.get("state", {})
+            form_data = state.get("formData", {})
+
+            return self.response(
+                200,
+                result={
+                    "form_data": form_data,
+                    "datasource_id": permalink_value.get("datasourceId"),
+                    "datasource_type": permalink_value.get("datasourceType"),
+                    "chart_id": permalink_value.get("chartId"),
+                },
+            )
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception as ex:
+            logger.exception("Error fetching embedded chart: %s", ex)
+            return self.response_500(message=str(ex))

Review Comment:
   Fixed in a8b866e. Changed to return a generic error message instead of `str(ex)` to avoid leaking internal details.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891589207


##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,72 @@
+# 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.
+from typing import Callable
+
+from flask import abort, current_app
+from flask_appbuilder import expose
+from flask_login import AnonymousUserMixin, login_user
+
+from superset import event_logger, is_feature_enabled
+from superset.superset_typing import FlaskResponse
+from superset.utils import json
+from superset.views.base import BaseSupersetView, common_bootstrap_payload
+
+
+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.
+        """
+        if not is_feature_enabled("EMBEDDABLE_CHARTS_MCP"):
+            abort(404)
+
+        # 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)
+
+        add_extra_log_payload(
+            embedded_type="chart",
+        )
+
+        bootstrap_data = {
+            "config": {
+                "GUEST_TOKEN_HEADER_NAME": current_app.config[
+                    "GUEST_TOKEN_HEADER_NAME"
+                ],

Review Comment:
   Already addressed — line 122 uses `current_app.config.get('GUEST_TOKEN_HEADER_NAME', 'X-GuestToken')` with a sensible default.



##########
superset/mcp_service/app.py:
##########
@@ -324,6 +324,9 @@ def create_mcp_app(
     get_instance_info,
     health_check,
 )
+from superset.mcp_service.embedded_chart.tool import (  # noqa: F401, E402
+    get_embeddable_chart,
+)

Review Comment:
   This follows the same pattern used by all other MCP tool imports in app.py (e.g. generate_chart, get_chart_data, etc.). The `# noqa: F401, E402` comments already signal these are side-effect imports.



##########
superset-frontend/webpack.config.js:
##########
@@ -300,6 +300,7 @@ const config = {
     menu: addPreamble('src/views/menu.tsx'),
     spa: addPreamble('/src/views/index.tsx'),
     embedded: addPreamble('/src/embedded/index.tsx'),
+    embeddedChart: addPreamble('/src/embeddedChart/index.tsx'),

Review Comment:
   Already addressed — the entry uses `addPreamble('src/embeddedChart/index.tsx')` without a leading slash, matching the convention of all other entries.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2907999433


##########
superset/mcp_service/chart/chart_utils.py:
##########
@@ -39,6 +40,117 @@
 from superset.mcp_service.utils.url_utils import get_superset_base_url
 from superset.utils import json
 
+# =============================================================================
+# Visualization Type Constants
+# =============================================================================
+
+# Time series viz types that require a datetime column on the x-axis
+TIMESERIES_VIZ_TYPES = {
+    "echarts_timeseries",
+    "echarts_timeseries_line",
+    "echarts_timeseries_bar",
+    "echarts_timeseries_area",
+    "echarts_timeseries_scatter",
+    "echarts_timeseries_smooth",
+    "echarts_timeseries_step",
+    "mixed_timeseries",

Review Comment:
   **Suggestion:** The timeseries visualization type set includes a non-existent value `"echarts_timeseries_area"` but omits the real area viz type `"echarts_area"`, so any area chart using `viz_type="echarts_area"` will not be recognized as a timeseries by `validate_timeseries_config`, causing missing datetime validation and allowing misconfigured charts that can later fail at query/render time. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ⚠️ Area charts skip datetime validation in MCP chart utilities.
   - ❌ Misconfigured area charts may fail when executing Superset queries.
   - ⚠️ Inconsistent validation behavior across different timeseries chart types.
   ```
   </details>
   
   ```suggestion
       "echarts_area",
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Open `superset/mcp_service/chart/chart_utils.py` and inspect the constant
   `TIMESERIES_VIZ_TYPES` at lines 48–59, which includes `"echarts_timeseries_*"` types but
   not `"echarts_area"`.
   
   2. In the same file, inspect `validate_timeseries_config` at lines 114–151, which
   immediately returns `None` (skipping validation) when `viz_type not in
   TIMESERIES_VIZ_TYPES`.
   
   3. Further down in the same module, inspect `map_xy_config` (in `chart_utils.py`, after
   the helper functions) and note that it maps `XYChartConfig.kind == "area"` to
   `viz_type="echarts_area"` in the generated `form_data`.
   
   4. When any caller (e.g., the MCP tools mentioned in the module docstring) generates an
   area chart via `XYChartConfig(kind="area")`, and then passes `viz_type="echarts_area"`
   plus `form_data` into `validate_timeseries_config`, the initial membership check fails
   because `"echarts_area"` is not in `TIMESERIES_VIZ_TYPES`, so no datetime validation runs;
   this allows misconfigured area charts without a proper datetime x‑axis to pass validation
   and later fail at query/render time inside Superset.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/mcp_service/chart/chart_utils.py
   **Line:** 56:56
   **Comment:**
   	*Logic Error: The timeseries visualization type set includes a non-existent value `"echarts_timeseries_area"` but omits the real area viz type `"echarts_area"`, so any area chart using `viz_type="echarts_area"` will not be recognized as a timeseries by `validate_timeseries_config`, causing missing datetime validation and allowing misconfigured charts that can later fail at query/render time.
   
   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=b686961363e0fae02cd5594e9f95bd043e91e837008b30fc208e769a7fbf460c&reaction=like'>👍</a> | <a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=b686961363e0fae02cd5594e9f95bd043e91e837008b30fc208e769a7fbf460c&reaction=dislike'>👎</a>



##########
superset/config.py:
##########
@@ -730,7 +731,7 @@ class D3TimeFormat(TypedDict, total=False):
     # Enable sharing charts with embedding
     # @lifecycle: stable
     # @category: runtime_config
-    "EMBEDDABLE_CHARTS": True,
+    "EMBEDDABLE_CHARTS": False,

Review Comment:
   **Suggestion:** The `EMBEDDABLE_CHARTS` feature flag is defaulted to `False`, which contradicts the documented/default behavior described in the PR (where embeddable charts should be enabled by default), so the embed UI and related functionality will remain hidden unless users manually override the config, effectively breaking the advertised feature out of the box. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ "Embed code" button missing in Explore Share menu.
   - ⚠️ MCP tool can't generate embeddable charts via guest token.
   ```
   </details>
   
   ```suggestion
       "EMBEDDABLE_CHARTS": True,
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Configure Superset following the PR docs: in `superset_config.py` set `FEATURE_FLAGS =
   {"EMBEDDED_SUPERSET": True}` but do NOT define `EMBEDDABLE_CHARTS` (relying on the
   documented default of True).
   
   2. Start the Superset webserver so it loads `superset/config.py`, including
   `DEFAULT_FEATURE_FLAGS` where line `734 "EMBEDDABLE_CHARTS": False,` defines the default
   value for this flag.
   
   3. Since `FEATURE_FLAGS` (defined later in `superset/config.py`) is empty by default and
   no `SUPERSET_FEATURE_EMBEDDABLE_CHARTS` env var is set, the effective feature flags dict
   is built using `DEFAULT_FEATURE_FLAGS` with `"EMBEDDABLE_CHARTS": False`, so
   `is_feature_enabled("EMBEDDABLE_CHARTS")` will evaluate to False across the app.
   
   4. Access a chart in Explore or a Dashboard, open the Share menu, and observe that the
   "Embed code" option described in the PR (embeddable charts UI) does not appear because the
   `EMBEDDABLE_CHARTS` flag is disabled by default despite the documentation stating it
   should be enabled.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/config.py
   **Line:** 734:734
   **Comment:**
   	*Logic Error: The `EMBEDDABLE_CHARTS` feature flag is defaulted to `False`, which contradicts the documented/default behavior described in the PR (where embeddable charts should be enabled by default), so the embed UI and related functionality will remain hidden unless users manually override the config, effectively breaking the advertised feature out of the box.
   
   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=fda5e6e5f01ff479f8723030b7a2d06ca7ba5e5a23d44b37029ff7288cc71b60&reaction=like'>👍</a> | <a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=fda5e6e5f01ff479f8723030b7a2d06ca7ba5e5a23d44b37029ff7288cc71b60&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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894971316


##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,548 @@
+# 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 len(request.allowed_domains) == 2
+        assert "https://example.com" in request.allowed_domains
+
+    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:
   Thanks for the catch — this was already fixed in a follow-up commit. The test expectations were updated from ValueError to ValidationError to align with Pydantic v2's exception handling for constraint violations.



##########
superset/security/manager.py:
##########
@@ -2834,6 +2842,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError() from None

Review Comment:
   Thanks for confirming the fix looks good.



##########
superset/security/manager.py:
##########
@@ -2834,6 +2838,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError()

Review Comment:
   Appreciate the thorough review. Glad the specific exception handling approach aligns well.



##########
superset-frontend/webpack.config.js:
##########
@@ -300,6 +300,7 @@ const config = {
     menu: addPreamble('src/views/menu.tsx'),
     spa: addPreamble('/src/views/index.tsx'),
     embedded: addPreamble('/src/embedded/index.tsx'),
+    embeddedChart: addPreamble('/src/embeddedChart/index.tsx'),
   },

Review Comment:
   Thanks for verifying — correct, no leading slash needed to match the existing entries.



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,298 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:

Review Comment:
   Thanks for confirming the validation logic looks correct.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894982892


##########
superset/commands/chart/delete.py:
##########
@@ -68,3 +69,16 @@ 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)
+
+    def validate(self) -> None:
+        pass

Review Comment:
   <!-- Bito Reply -->
   The ownership verification has been added to DeleteEmbeddedChartCommand.validate() for better security, addressing the defense-in-depth concern you raised.
   
   **superset/commands/chart/delete.py**
   ```
   def validate(self) -> None:
           if not self._chart:
               raise ChartNotFoundError()
           if not self._chart.embedded:
               raise ChartNotFoundError()
           try:
               security_manager.raise_for_ownership(self._chart)
           except SupersetSecurityException as ex:
               raise ChartForbiddenError() from ex
   ```



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,298 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:

Review Comment:
   <!-- Bito Reply -->
   Adding explicit validation in the POST handler for ttl_minutes and datasource can provide faster error feedback. For ttl_minutes, since Pydantic enforces the range, explicit checks ensure clarity. For datasource, early validation prevents downstream failures. Here's a suggested addition after extracting body parameters.
   
   **superset/embedded_chart/api.py**
   ```
   if not (1 <= ttl_minutes <= 10080):
                   return self.response_400("ttl_minutes must be between 1 and 10080")
               if 'datasource' not in form_data or not form_data['datasource']:
                   return self.response_400("form_data must contain a valid datasource")
   ```



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4054510356

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2930418948


##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,321 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.exceptions import CommandException
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+from superset.key_value.shared_entries import get_permalink_salt
+from superset.key_value.types import (
+    KeyValueResource,
+    MarshmallowKeyValueCodec,
+    SharedKey,
+)
+from superset.key_value.exceptions import KeyValueParseKeyError
+from superset.key_value.utils import decode_permalink_id
+from superset.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except (ValueError, KeyError, KeyValueParseKeyError) as ex:
+            logger.warning("Error fetching embedded chart: %s", ex)
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate ttl_minutes bounds
+            if (
+                not isinstance(ttl_minutes, int)
+                or ttl_minutes < 1
+                or ttl_minutes > 10080
+            ):
+                return self.response_400(
+                    message="ttl_minutes must be between 1 and 10080"
+                )
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)

Review Comment:
   **Suggestion:** `ttl_minutes` is only used to compute the returned timestamp, but the permalink itself is created without expiration, so embeds can remain valid beyond the advertised TTL. Persist the permalink entry with `expires_on` to enforce actual expiry. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ TTL contract in embed API is not enforced.
   - ⚠️ Security expectations for ephemeral links are weakened.
   - ⚠️ UI displays expiry timestamp that backend ignores.
   ```
   </details>
   
   ```suggestion
               # Create permalink with the form_data
                       state = {
                           "formData": form_data,
                           "allowedDomains": allowed_domains,
                       }
                       permalink_key = CreateExplorePermalinkCommand(state).run()
   
                       # Calculate expiration and enforce it on the permalink entry
                       expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
                       permalink_id = decode_permalink_id(
                           permalink_key,
                           salt=get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT),
                       )
                       KeyValueDAO.update_entry(
                           KeyValueResource.EXPLORE_PERMALINK,
                           state,
                           MarshmallowKeyValueCodec(ExplorePermalinkSchema()),
                           permalink_id,
                           expires_on=expires_at.replace(tzinfo=None),
                       )
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. UI embed flow (`superset-frontend/src/explore/components/EmbedCodeContent.tsx:71-87`)
   always posts `ttl_minutes` to `POST /api/v1/embedded_chart/`.
   
   2. Backend computes `expires_at` (`superset/embedded_chart/api.py:270`) but creates
   permalink through `CreateExplorePermalinkCommand.run()`
   (`superset/commands/explore/permalink/create.py:56-74`) without `expires_on`.
   
   3. Stored permalink is read through `KeyValueDAO.get_value()`
   (`superset/daos/key_value.py:50-60`), which only expires entries when `entry.expires_on`
   is set (`:57`).
   
   4. Result: TTL is informational only in this endpoint; permalink lifetime is not bounded
   by `ttl_minutes`, so expiration behavior diverges from returned `expires_at`.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/embedded_chart/api.py
   **Line:** 262:270
   **Comment:**
   	*Logic Error: `ttl_minutes` is only used to compute the returned timestamp, but the permalink itself is created without expiration, so embeds can remain valid beyond the advertised TTL. Persist the permalink entry with `expires_on` to enforce actual expiry.
   
   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=761d79f7ad5e77d4389efb47944df223efcf1b074498749c2c030fe540412148&reaction=like'>👍</a> | <a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=761d79f7ad5e77d4389efb47944df223efcf1b074498749c2c030fe540412148&reaction=dislike'>👎</a>



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,321 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.exceptions import CommandException
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+from superset.key_value.shared_entries import get_permalink_salt
+from superset.key_value.types import (
+    KeyValueResource,
+    MarshmallowKeyValueCodec,
+    SharedKey,
+)
+from superset.key_value.exceptions import KeyValueParseKeyError
+from superset.key_value.utils import decode_permalink_id
+from superset.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except (ValueError, KeyError, KeyValueParseKeyError) as ex:
+            logger.warning("Error fetching embedded chart: %s", ex)
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)

Review Comment:
   **Suggestion:** `allowed_domains` is accepted without type validation, but downstream code iterates it as a list of domain strings. If a string/object is sent, domain validation in the embedded view behaves incorrectly and valid embeds can be rejected. [type error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Embedded chart page can reject valid referrers.
   - ⚠️ API consumers can store malformed domain payloads.
   - ⚠️ Frontend default uses list, reducing accidental hits.
   ```
   </details>
   
   ```suggestion
               allowed_domains = body.get("allowed_domains", [])
                       if not isinstance(allowed_domains, list) or any(
                           not isinstance(domain, str) for domain in allowed_domains
                       ):
                           return self.response_400(
                               message="allowed_domains must be a list of strings"
                           )
                       ttl_minutes: int = body.get("ttl_minutes", 60)
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Authenticated caller hits `POST /api/v1/embedded_chart/` handled by
   `EmbeddedChartRestApi.post()` (`superset/embedded_chart/api.py:171-321`).
   
   2. Send malformed payload like `allowed_domains: "https://example.com"`; code accepts it
   unchanged (`api.py:240`) and persists as `state["allowedDomains"]` (`api.py:263-266`).
   
   3. Embedded page (`superset/embedded_chart/view.py:64-109`) loads permalink and iterates
   `allowed_domains` (`:95-100`); string values iterate character-by-character.
   
   4. `same_origin()` (`view.py:43-54`) never matches per-character entries, leading to
   `abort(403)` (`view.py:104-109`) and blocked embedding.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/embedded_chart/api.py
   **Line:** 240:241
   **Comment:**
   	*Type Error: `allowed_domains` is accepted without type validation, but downstream code iterates it as a list of domain strings. If a string/object is sent, domain validation in the embedded view behaves incorrectly and valid embeds can be rejected.
   
   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=59bce833dfa04654ee78437e9a922e977b9d2cd273b96900e207e191f404b40b&reaction=like'>👍</a> | <a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=59bce833dfa04654ee78437e9a922e977b9d2cd273b96900e207e191f404b40b&reaction=dislike'>👎</a>



##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,138 @@
+# 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, current_user, 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.exceptions import KeyValueParseKeyError
+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)

Review Comment:
   **Suggestion:** The origin matcher parses scheme-less domains with ports incorrectly (for example `localhost:3000`), because `urlparse` interprets the prefix as a scheme and loses the host. This causes legitimate allowed domains to be rejected at runtime; parse scheme-less entries as netlocs and compare normalized host/port values. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Legitimate localhost:port embeds rejected with HTTP 403.
   - ⚠️ `allowedDomains` host:port entries behave inconsistently.
   - ⚠️ MCP embedding setup fails for common local dev.
   ```
   </details>
   
   ```suggestion
   
       parsed1 = urlparse(url1)
       has_scheme = "://" in url2
       parsed2 = urlparse(url2 if has_scheme else f"//{url2}")
   
       host1 = (parsed1.hostname or "").lower()
       host2 = (parsed2.hostname or "").lower()
       if not host1 or not host2:
           return False
   
       if not has_scheme:
           host_match = host1 == host2 or host1.endswith(f".{host2}")
           return host_match and (parsed2.port is None or parsed1.port == parsed2.port)
   
       return (
           parsed1.scheme == parsed2.scheme
           and host1 == host2
           and (parsed2.port is None or parsed1.port == parsed2.port)
       )
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Create an embeddable chart with an `allowed_domains` entry like `localhost:3000` via
   MCP tool request schema `superset/mcp_service/embedded_chart/schemas.py:61-68`; this value
   is persisted into permalink state at
   `superset/mcp_service/embedded_chart/tool/get_embeddable_chart.py:26-31`.
   
   2. Load returned iframe URL `/embedded/chart/?permalink_key=...` from an app running at
   `http://localhost:3000` so `request.referrer` includes that origin; request reaches
   `EmbeddedChartView.embedded_chart` in `superset/embedded_chart/view.py:64`.
   
   3. Domain validation loop executes at `superset/embedded_chart/view.py:97-101` and calls
   `same_origin(request.referrer, domain)`.
   
   4. Current parser logic `urlparse("localhost:3000")` misinterprets it
   (`scheme='localhost', netloc=''`; reproducible via Python), so comparison fails and
   request is rejected with `abort(403)` at `superset/embedded_chart/view.py:104-109` even
   for legitimate host:port embeds.
   ```
   </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:** 47:54
   **Comment:**
   	*Logic Error: The origin matcher parses scheme-less domains with ports incorrectly (for example `localhost:3000`), because `urlparse` interprets the prefix as a scheme and loses the host. This causes legitimate allowed domains to be rejected at runtime; parse scheme-less entries as netlocs and compare normalized host/port values.
   
   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=e69e8196b34ef63ee95956349328db9dcd1e8f48ad96a7cc58117edd10c6f937&reaction=like'>👍</a> | <a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=e69e8196b34ef63ee95956349328db9dcd1e8f48ad96a7cc58117edd10c6f937&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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2931768675


##########
embed-demo.html:
##########
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Superset Embedded Chart Demo</title>
+  <style>
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+      max-width: 1200px;
+      margin: 0 auto;
+      padding: 40px;
+      background: #f5f5f5;
+    }
+    h1 { color: #333; }
+    .input-section {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      margin-bottom: 20px;
+    }
+    textarea {
+      width: 100%;
+      height: 150px;
+      font-family: monospace;
+      font-size: 12px;
+      padding: 10px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      resize: vertical;
+      box-sizing: border-box;
+    }
+    button {
+      background: #20a7c9;
+      color: white;
+      border: none;
+      padding: 12px 24px;
+      font-size: 16px;
+      border-radius: 4px;
+      cursor: pointer;
+      margin-top: 10px;
+    }
+    button:hover {
+      background: #1a8fa8;
+    }
+    .chart-container {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      min-height: 450px;
+    }
+    label {
+      font-weight: 600;
+      display: block;
+      margin-bottom: 8px;
+    }
+  </style>
+</head>
+<body>
+  <h1>Superset Embedded Chart Demo</h1>
+
+  <div class="input-section">
+    <label for="iframe-input">Paste iframe_html response here:</label>
+    <textarea id="iframe-input" placeholder="Paste the iframe_html value from get_embeddable_chart response..."></textarea>
+    <button onclick="embedChart()">Embed Chart</button>
+  </div>
+
+  <div class="chart-container" id="chart-container">
+    <p style="color: #999; text-align: center; margin-top: 200px;">Chart will appear here</p>
+  </div>
+
+  <script>
+    function embedChart() {
+      const input = document.getElementById('iframe-input').value.trim();
+      const container = document.getElementById('chart-container');
+
+      if (!input) {
+        alert('Please paste the iframe_html first');
+        return;
+      }
+
+      // Safely parse input and only allow iframe elements (prevent XSS)
+      const parser = new DOMParser();
+      const doc = parser.parseFromString(input, 'text/html');

Review Comment:
   Acknowledged. The embedded chart view uses `document.domain` for origin-matching but does not inject untrusted HTML into the DOM. The origin comparison is string-based only.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2799641316


##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,304 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
+
+            # Generate guest token
+            username = g.user.username if hasattr(g, "user") and g.user else "anonymous"
+            guest_user: GuestTokenUser = {
+                "username": f"embed_{username}",
+                "first_name": "Embed",
+                "last_name": "User",
+            }
+
+            resources: list[GuestTokenResource] = [
+                {

Review Comment:
   Fixed in commit 3bbe33738c by using `.value` to ensure string serialization in the JWT payload.



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,304 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
+
+            # Generate guest token
+            username = g.user.username if hasattr(g, "user") and g.user else "anonymous"
+            guest_user: GuestTokenUser = {
+                "username": f"embed_{username}",
+                "first_name": "Embed",
+                "last_name": "User",
+            }
+
+            resources: list[GuestTokenResource] = [
+                {
+                    "type": GuestTokenResourceType.CHART_PERMALINK,
+                    "id": permalink_key,
+                }
+            ]
+
+            rls_rules: list[GuestTokenRlsRule] = []
+
+            guest_token_result = security_manager.create_guest_access_token(
+                user=guest_user,
+                resources=resources,
+                rls=rls_rules,
+            )
+
+            # Handle both bytes (older PyJWT) and string (PyJWT 2.0+)
+            guest_token = (
+                guest_token_result.decode("utf-8")
+                if isinstance(guest_token_result, bytes)
+                else guest_token_result
+            )
+
+            # Build iframe URL using request host
+            base_url = request.host_url.rstrip("/")
+            iframe_url = f"{base_url}/embedded/chart/?permalink_key={permalink_key}"
+

Review Comment:
   This is not a bug. The iframe_url (line 292) points to the VIEW endpoint `/embedded/chart/` which uses query parameters, not the API endpoint. The API endpoint at line 94 (`@expose("/<permalink_key>")`) is for fetching chart data via AJAX, while the iframe_url is for loading the HTML page. These are two different endpoints serving different purposes.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codecov[bot] (via GitHub)" <gi...@apache.org>.
codecov[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3716618892

   ## [Codecov](https://app.codecov.io/gh/apache/superset/pull/36933?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
   :x: Patch coverage is `29.67742%` with `109 lines` in your changes missing coverage. Please review.
   :white_check_mark: Project coverage is 48.43%. Comparing base ([`9aff89c`](https://app.codecov.io/gh/apache/superset/commit/9aff89c1b4b783c7cd8809ea8d46a273c12f6f4e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)) to head ([`99b1a97`](https://app.codecov.io/gh/apache/superset/commit/99b1a97fc41b7d389018af29ef5ab9959d4d45a5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)).
   
   | [Files with missing lines](https://app.codecov.io/gh/apache/superset/pull/36933?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Patch % | Lines |
   |---|---|---|
   | [...ervice/embedded\_chart/tool/get\_embeddable\_chart.py](https://app.codecov.io/gh/apache/superset/pull/36933?src=pr&el=tree&filepath=superset%2Fmcp_service%2Fembedded_chart%2Ftool%2Fget_embeddable_chart.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvbWNwX3NlcnZpY2UvZW1iZWRkZWRfY2hhcnQvdG9vbC9nZXRfZW1iZWRkYWJsZV9jaGFydC5weQ==) | 0.00% | [56 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/36933?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [superset/mcp\_service/embedded\_chart/schemas.py](https://app.codecov.io/gh/apache/superset/pull/36933?src=pr&el=tree&filepath=superset%2Fmcp_service%2Fembedded_chart%2Fschemas.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvbWNwX3NlcnZpY2UvZW1iZWRkZWRfY2hhcnQvc2NoZW1hcy5weQ==) | 0.00% | [19 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/36933?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [superset/embedded\_chart/api.py](https://app.codecov.io/gh/apache/superset/pull/36933?src=pr&el=tree&filepath=superset%2Fembedded_chart%2Fapi.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvZW1iZWRkZWRfY2hhcnQvYXBpLnB5) | 58.33% | [15 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/36933?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [superset/security/manager.py](https://app.codecov.io/gh/apache/superset/pull/36933?src=pr&el=tree&filepath=superset%2Fsecurity%2Fmanager.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvc2VjdXJpdHkvbWFuYWdlci5weQ==) | 0.00% | [10 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/36933?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [superset/embedded\_chart/view.py](https://app.codecov.io/gh/apache/superset/pull/36933?src=pr&el=tree&filepath=superset%2Fembedded_chart%2Fview.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvZW1iZWRkZWRfY2hhcnQvdmlldy5weQ==) | 68.42% | [6 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/36933?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [...perset/mcp\_service/embedded\_chart/tool/\_\_init\_\_.py](https://app.codecov.io/gh/apache/superset/pull/36933?src=pr&el=tree&filepath=superset%2Fmcp_service%2Fembedded_chart%2Ftool%2F__init__.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvbWNwX3NlcnZpY2UvZW1iZWRkZWRfY2hhcnQvdG9vbC9fX2luaXRfXy5weQ==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/36933?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [superset/mcp\_service/app.py](https://app.codecov.io/gh/apache/superset/pull/36933?src=pr&el=tree&filepath=superset%2Fmcp_service%2Fapp.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvbWNwX3NlcnZpY2UvYXBwLnB5) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/36933?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   
   <details><summary>Additional details and impacted files</summary>
   
   
   
   ```diff
   @@             Coverage Diff             @@
   ##           master   #36933       +/-   ##
   ===========================================
   + Coverage        0   48.43%   +48.43%     
   ===========================================
     Files           0      645      +645     
     Lines           0    47812    +47812     
     Branches        0     5215     +5215     
   ===========================================
   + Hits            0    23157    +23157     
   - Misses          0    24000    +24000     
   - Partials        0      655      +655     
   ```
   
   | [Flag](https://app.codecov.io/gh/apache/superset/pull/36933/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | |
   |---|---|---|
   | [hive](https://app.codecov.io/gh/apache/superset/pull/36933/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `43.05% <29.67%> (?)` | |
   | [presto](https://app.codecov.io/gh/apache/superset/pull/36933/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `46.64% <29.67%> (?)` | |
   | [python](https://app.codecov.io/gh/apache/superset/pull/36933/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `48.42% <29.67%> (?)` | |
   | [unit](https://app.codecov.io/gh/apache/superset/pull/36933/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `100.00% <ø> (?)` | |
   
   Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#carryforward-flags-in-the-pull-request-comment) to find out more.
   </details>
   
   [:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/apache/superset/pull/36933?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).   
   :loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
   <details><summary> :rocket: New features to boost your workflow: </summary>
   
   - :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
   - :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
   </details>


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3725680204

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2790728242


##########
embed-demo.html:
##########
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Superset Embedded Chart Demo</title>
+  <style>
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+      max-width: 1200px;
+      margin: 0 auto;
+      padding: 40px;
+      background: #f5f5f5;
+    }
+    h1 { color: #333; }
+    .input-section {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      margin-bottom: 20px;
+    }
+    textarea {
+      width: 100%;
+      height: 150px;
+      font-family: monospace;
+      font-size: 12px;
+      padding: 10px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      resize: vertical;
+      box-sizing: border-box;
+    }
+    button {
+      background: #20a7c9;
+      color: white;
+      border: none;
+      padding: 12px 24px;
+      font-size: 16px;
+      border-radius: 4px;
+      cursor: pointer;
+      margin-top: 10px;
+    }
+    button:hover {
+      background: #1a8fa8;
+    }
+    .chart-container {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      min-height: 450px;
+    }
+    label {
+      font-weight: 600;
+      display: block;
+      margin-bottom: 8px;
+    }
+  </style>
+</head>
+<body>
+  <h1>Superset Embedded Chart Demo</h1>
+
+  <div class="input-section">
+    <label for="iframe-input">Paste iframe_html response here:</label>
+    <textarea id="iframe-input" placeholder="Paste the iframe_html value from get_embeddable_chart response..."></textarea>
+    <button onclick="embedChart()">Embed Chart</button>
+  </div>
+
+  <div class="chart-container" id="chart-container">
+    <p style="color: #999; text-align: center; margin-top: 200px;">Chart will appear here</p>
+  </div>
+
+  <script>
+    function embedChart() {
+      const input = document.getElementById('iframe-input').value.trim();
+      const container = document.getElementById('chart-container');
+
+      if (!input) {
+        alert('Please paste the iframe_html first');
+        return;
+      }
+
+      // Insert the iframe HTML
+      container.innerHTML = input;

Review Comment:
   This is a local testing/demo file not intended for production use. It's already excluded from the license check via `.rat-excludes`. The XSS concern is noted but not applicable since this file is only used for local development testing.



##########
superset/security/manager.py:
##########
@@ -2834,6 +2838,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError()

Review Comment:
   Addressed in commit d3d0875 - replaced broad `except Exception` with specific `ExplorePermalinkGetFailedError, ValueError` to match the exceptions that `GetExplorePermalinkCommand.run()` can raise.



##########
superset/security/manager.py:
##########
@@ -2834,6 +2842,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError() from None

Review Comment:
   Addressed in commit d3d0875 - replaced broad `except Exception` with specific `ExplorePermalinkGetFailedError, ValueError` to match the exceptions that `GetExplorePermalinkCommand.run()` can raise.



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,304 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
+
+            # Generate guest token
+            username = g.user.username if hasattr(g, "user") and g.user else "anonymous"
+            guest_user: GuestTokenUser = {
+                "username": f"embed_{username}",
+                "first_name": "Embed",
+                "last_name": "User",
+            }
+
+            resources: list[GuestTokenResource] = [
+                {

Review Comment:
   Addressed in commit d3d0875 - changed to use `GuestTokenResourceType.CHART_PERMALINK.value` (string) for consistency with how `security/manager.py` compares resource types.



##########
superset-frontend/webpack.config.js:
##########
@@ -300,6 +300,7 @@ const config = {
     menu: addPreamble('src/views/menu.tsx'),
     spa: addPreamble('/src/views/index.tsx'),
     embedded: addPreamble('/src/embedded/index.tsx'),
+    embeddedChart: addPreamble('/src/embeddedChart/index.tsx'),
   },

Review Comment:
   This was already fixed in a previous commit - the entry path is now `'src/embeddedChart/index.tsx'` without a leading slash.



##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,72 @@
+# 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.
+from typing import Callable
+
+from flask import abort, current_app
+from flask_appbuilder import expose
+from flask_login import AnonymousUserMixin, login_user
+
+from superset import event_logger, is_feature_enabled
+from superset.superset_typing import FlaskResponse
+from superset.utils import json
+from superset.views.base import BaseSupersetView, common_bootstrap_payload
+
+
+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.
+        """
+        if not is_feature_enabled("EMBEDDABLE_CHARTS_MCP"):
+            abort(404)
+
+        # 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)
+
+        add_extra_log_payload(
+            embedded_type="chart",
+        )
+
+        bootstrap_data = {
+            "config": {
+                "GUEST_TOKEN_HEADER_NAME": current_app.config[
+                    "GUEST_TOKEN_HEADER_NAME"
+                ],

Review Comment:
   Already fixed - using `.get()` with default value `"X-GuestToken"` instead of bracket notation.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2790728789


##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,298 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:

Review Comment:
   Already addressed - the POST endpoint validates both `form_data` presence and `datasource` field before processing.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3886514772

   Addressed message validation issue in commit c7f47d7 - relaxed validation to accept messages with `guestToken` or `handshake` fields in addition to typed messages. This allows legitimate initialization messages to pass through correctly.


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2795360824


##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,548 @@
+# 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 len(request.allowed_domains) == 2
+        assert "https://example.com" in request.allowed_domains
+
+    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:
   Fixed in commit c7f47d7 - updated tests to expect `ValidationError` instead of `ValueError` for Pydantic v2 validation failures (ttl_minutes and height constraints).



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3716594373

   CodeAnt AI is reviewing your PR.
   
   ---
   
   ### Thanks for using CodeAnt! 🎉
   
   We're free for open-source projects. if you're enjoying it, help us grow by sharing.
   
   [Share on X](https://twitter.com/intent/tweet?text=Just%20tried%20%40CodeAntAI%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A&url=https%3A//codeant.ai) ·
   [Reddit](https://www.reddit.com/submit?title=Check%20out%20CodeAnt%20for%20automated%20code%20review&text=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A%20https%3A//codeant.ai) ·
   [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcodeant.ai&mini=true&title=Check%20out%20CodeAnt%20for%20automated%20code%20review&summary=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos)
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "michael-s-molina (via GitHub)" <gi...@apache.org>.
michael-s-molina commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3720496886

   > We have lots of FFs
   
   +1 If we can avoid a feature flag it would be better.


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3724404512

   CodeAnt AI is running Incremental review
   
   ---
   
   ### Thanks for using CodeAnt! 🎉
   
   We're free for open-source projects. if you're enjoying it, help us grow by sharing.
   
   [Share on X](https://twitter.com/intent/tweet?text=Just%20tried%20%40CodeAntAI%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A&url=https%3A//codeant.ai) ·
   [Reddit](https://www.reddit.com/submit?title=Check%20out%20CodeAnt%20for%20automated%20code%20review&text=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A%20https%3A//codeant.ai) ·
   [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcodeant.ai&mini=true&title=Check%20out%20CodeAnt%20for%20automated%20code%20review&summary=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos)
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2673116369


##########
superset/security/manager.py:
##########
@@ -2834,6 +2842,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError() from None

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Blind exception catch too broad</b></div>
   <div id="fix">
   
   Replace the broad `except Exception` on line 2854 with specific exception types. Catch only `EmbeddedChartPermalinkNotFoundError` or other expected exceptions to avoid masking unexpected errors.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
                   except (EmbeddedChartPermalinkNotFoundError, ValueError, KeyError):
                       raise EmbeddedChartPermalinkNotFoundError() from None
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #68cad7</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3886515888

   Re: modal prop naming - The `show` and `onHide` props are correct according to the Modal component API in `superset-ui-core/components/Modal/types.ts`. Other modals in the same file (e.g., DrillDetailModal) also use `show`, not `showModal`. This comment appears to be a false positive.


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3886514008

   Addressed validation test issues in commit c7f47d7 - fixed tests to expect `ValidationError` instead of `ValueError` for Pydantic v2 validation failures (ttl_minutes and height constraints).


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932237030


##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,139 @@
+# 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, current_user, 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.exceptions import KeyValueParseKeyError
+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 or (not parsed2.netloc and ":" in url2):
+        # url2 is a bare domain or host:port (e.g., "localhost:3000"),
+        # which urlparse misinterprets. Compare directly with 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, KeyValueParseKeyError) as ex:
+            logger.warning("Error fetching permalink for referrer validation: %s", ex)
+            permalink_value = None
+

Review Comment:
   <!-- Bito Reply -->
   Yes, the suggestion is valid — it ensures invalid permalinks are rejected with a 404 before any domain checks, preventing unrestricted access and aligning the view with the API's behavior.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932239891


##########
superset/mcp_service/embedded_chart/schemas.py:
##########
@@ -0,0 +1,101 @@
+# 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.
+from datetime import datetime
+from typing import Any
+
+from pydantic import BaseModel, Field
+
+# Import the same ChartConfig used by generate_chart
+from superset.mcp_service.chart.schemas import ChartConfig
+
+
+class GetEmbeddableChartRequest(BaseModel):
+    """Request schema for get_embeddable_chart tool.
+
+    Uses the same simplified ChartConfig schema as generate_chart for consistency.
+    """
+
+    datasource_id: int | str = Field(
+        ...,
+        description="Dataset ID (numeric) or UUID",
+    )
+    config: ChartConfig = Field(
+        ...,
+        description=(
+            "Chart configuration using simplified schema. Use chart_type='xy' for "
+            "line/bar/area/scatter charts, or chart_type='table' for tables. "
+            'Example: {"chart_type": "xy", "x": {"name": "genre"}, '
+            '"y": [{"name": "sales", "aggregate": "SUM"}], "kind": "bar"}'
+        ),
+    )
+    ttl_minutes: int = Field(
+        default=60,
+        ge=1,
+        le=10080,  # max 1 week
+        description="Permalink TTL in minutes (default: 60, max: 10080 = 1 week)",
+    )
+    height: int = Field(
+        default=400,
+        ge=100,
+        le=2000,
+        description="Chart height in pixels for iframe (default: 400)",
+    )
+    rls_rules: list[dict[str, Any]] = Field(
+        default_factory=list,
+        description="Row-level security rules to apply to the guest token",
+    )
+    allowed_domains: list[str] = Field(

Review Comment:
   <!-- Bito Reply -->
   No, the suggestion to change allowed_domains to list[HttpUrl] isn’t fully correct — it would require full URLs like 'https://example.com', but the embedding use case supports bare hostnames (e.g., 'localhost:3000', 'example.com') for domain matching. API validation at lines 247-252 ensures it's a list of strings, and same_origin() handles scheme/port normalization by comparing hosts.
   
   **superset/embedded_chart/api.py**
   ```
   # Validate allowed_domains is a list of strings
   if not isinstance(allowed_domains, list) or not all(
       isinstance(d, str) for d in allowed_domains
   ):
       return self.response_400(
           message="allowed_domains must be a list of strings"
       )
   ```
   
   **superset/embedded_chart/api.py**
   ```
   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"
   ```



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932182158


##########
tests/unit_tests/extensions/test_types.py:
##########
@@ -61,6 +61,18 @@ def test_extension_config_full():
             "description": "A query insights extension",
             "dependencies": ["other-extension"],
             "permissions": ["can_read", "can_view"],
+            "frontend": {
+                "contributions": {
+                    "views": {
+                        "sqllab": {
+                            "panels": [
+                                {"id": "query_insights.main", "name": "Query Insights"}
+                            ]
+                        }
+                    }
+                },
+                "moduleFederation": {"exposes": ["./index"]},
+            },

Review Comment:
   <!-- Bito Reply -->
   No, the suggestion isn’t fully correct — because the ExtensionConfig model does not define a frontend field, so accessing config.frontend will raise an AttributeError, causing the test to fail.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2931179284


##########
superset/security/manager.py:
##########
@@ -3109,6 +3131,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.value
+            for r in user.resources
+        )

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Potential security risk in datasource access</b></div>
   <div id="fix">
   
   The has_guest_chart_permalink_access method grants datasource access if the guest user has any chart_permalink resource, without checking if that permalink corresponds to the requested datasource. This could allow unauthorized access to datasources from other permalinks. Consider implementing stricter validation as noted in the TODO.
   </div>
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #1f05a6</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset-frontend/src/pages/EmbeddedChartsList/index.tsx:
##########
@@ -0,0 +1,254 @@
+/**
+ * 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 } from '@apache-superset/core/theme';
+import { t } from '@apache-superset/core/translation';
+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));

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Null chart name in delete toasts</b></div>
   <div id="fix">
   
   The success and error toasts in handleDelete use chart.chart_name directly, but since chart_name can be null (as defined in the EmbeddedChart interface), this will display 'null' in the message. Use a fallback like chart.chart_name || t('Untitled') to match the pattern used elsewhere in the component for consistency.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ```
    -  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 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 || t('Untitled')));
    -         fetchEmbeddedCharts();
    -       } catch (error) {
    -         addDangerToast(t('Error disabling embedding for %s', chart.chart_name || t('Untitled')));
    -       } finally {
    -         setChartToDelete(null);
    -       }
    -     },
    -     [addDangerToast, addSuccessToast, fetchEmbeddedCharts],
    -   );
   ```
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #1f05a6</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,321 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.exceptions import CommandException
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+from superset.key_value.exceptions import KeyValueParseKeyError
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except (ValueError, KeyError, KeyValueParseKeyError) as ex:
+            logger.warning("Error fetching embedded chart: %s", ex)
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Missing i18n for error messages</b></div>
   <div id="fix">
   
   Import `gettext` as `_` from `flask_babel` at the top of the file and wrap all user-facing error message strings in calls to `_()`, e.g., `self.response_400(message=_('form_data is required'))`.
   </div>
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #1f05a6</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset/charts/api.py:
##########
@@ -1184,3 +1196,253 @@ def import_(self) -> Response:
         )
         command.run()
         return self.response(200, message="OK")
+
+    @expose("/<id_or_uuid>/embedded", methods=("GET",))
+    @protect()
+    @safe
+    @permission_name("read")
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_embedded",
+        log_to_statsd=False,
+    )
+    def get_embedded(self, id_or_uuid: str) -> Response:
+        """Get the chart's embedded configuration.
+        ---
+        get:
+          summary: Get the chart's embedded configuration
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: id_or_uuid
+            description: The chart id or uuid
+          responses:
+            200:
+              description: Result contains the embedded chart config
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        $ref: '#/components/schemas/EmbeddedChartResponseSchema'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            chart = ChartDAO.get_by_id_or_uuid(id_or_uuid)
+        except ChartNotFoundError:
+            return self.response_404()
+
+        if not chart.embedded:
+            return self.response(404)
+        embedded: EmbeddedChart = chart.embedded[0]
+        result = self.embedded_response_schema.dump(embedded)
+        return self.response(200, result=result)
+
+    @expose("/<id_or_uuid>/embedded", methods=["POST", "PUT"])
+    @protect()
+    @safe
+    @permission_name("set_embedded")
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.set_embedded",
+        log_to_statsd=False,
+    )
+    def set_embedded(self, id_or_uuid: str) -> Response:
+        """Set a chart's embedded configuration.
+        ---
+        post:
+          summary: Set a chart's embedded configuration
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: id_or_uuid
+            description: The chart id or uuid
+          requestBody:
+            description: The embedded configuration to set
+            required: true
+            content:
+              application/json:
+                schema: EmbeddedChartConfigSchema
+          responses:
+            200:
+              description: Successfully set the configuration
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        $ref: '#/components/schemas/EmbeddedChartResponseSchema'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        put:
+          description: >-
+            Sets a chart's embedded configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: id_or_uuid
+            description: The chart id or uuid
+          requestBody:
+            description: The embedded configuration to set
+            required: true
+            content:
+              application/json:
+                schema: EmbeddedChartConfigSchema
+          responses:
+            200:
+              description: Successfully set the configuration
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        $ref: '#/components/schemas/EmbeddedChartResponseSchema'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            chart = ChartDAO.get_by_id_or_uuid(id_or_uuid)
+        except ChartNotFoundError:
+            return self.response_404()
+
+        try:
+            body = self.embedded_config_schema.load(request.json)
+
+            embedded = EmbeddedChartDAO.upsert(
+                chart,
+                body["allowed_domains"],
+            )
+            db.session.commit()  # pylint: disable=consider-using-transaction
+
+            result = self.embedded_response_schema.dump(embedded)
+            return self.response(200, result=result)
+        except ValidationError as error:
+            db.session.rollback()  # pylint: disable=consider-using-transaction
+            return self.response_400(message=error.messages)
+
+    @expose("/<id_or_uuid>/embedded", methods=("DELETE",))
+    @protect()
+    @safe
+    @permission_name("set_embedded")
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self,
+        *args,
+        **kwargs: f"{self.__class__.__name__}.delete_embedded",
+        log_to_statsd=False,
+    )
+    def delete_embedded(self, id_or_uuid: str) -> Response:
+        """Delete a chart's embedded configuration.
+        ---
+        delete:
+          summary: Delete a chart's embedded configuration
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: id_or_uuid
+            description: The chart id or uuid
+          responses:
+            200:
+              description: Successfully removed the configuration
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      message:
+                        type: string
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            chart = ChartDAO.get_by_id_or_uuid(id_or_uuid)
+        except ChartNotFoundError:
+            return self.response_404()
+
+        DeleteEmbeddedChartCommand(chart).run()
+        return self.response(200, message="OK")
+
+    @expose("/embedded", methods=("GET",))
+    @protect()
+    @safe
+    @permission_name("read")
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.list_embedded",
+        log_to_statsd=False,
+    )
+    def list_embedded(self) -> Response:
+        """List all embedded chart configurations.
+        ---
+        get:
+          summary: List all embedded chart configurations
+          responses:
+            200:
+              description: List of embedded charts
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        type: array
+                        items:
+                          type: object
+                          properties:
+                            uuid:
+                              type: string
+                            chart_id:
+                              type: integer
+                            chart_name:
+                              type: string
+                            allowed_domains:
+                              type: array
+                              items:
+                                type: string
+                            changed_on:
+                              type: string
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        embedded_charts = (
+            db.session.query(EmbeddedChart)
+            .join(Slice, EmbeddedChart.chart_id == Slice.id)
+            .all()
+        )
+        result = [
+            {
+                "uuid": str(ec.uuid),
+                "chart_id": ec.chart_id,
+                "chart_name": ec.chart.slice_name if ec.chart else None,
+                "allowed_domains": ec.allowed_domains,
+                "changed_on": ec.changed_on.isoformat() if ec.changed_on else None,
+            }
+            for ec in embedded_charts
+        ]

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Security: Missing permission filters in list endpoint</b></div>
   <div id="fix">
   
   The list_embedded endpoint queries all EmbeddedChart records without applying user permission filters, potentially exposing embedded configuration for charts the user cannot access. Other chart endpoints like get_embedded use ChartDAO.get_by_id_or_uuid, which applies ChartFilter for permissions, but this list method does not. This could lead to information disclosure if embedded charts contain sensitive data.
   </div>
   
   
   </div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Performance: N+1 query in list endpoint</b></div>
   <div id="fix">
   
   The query in list_embedded joins Slice but does not eagerly load the EmbeddedChart.chart relationship, causing N+1 queries when accessing ec.chart.slice_name for each embedded chart. This can degrade performance with many records. Use joinedload to populate the relationship in the initial query.
   </div>
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #1f05a6</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4055937336

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4003884755

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4003252674

   Good call @villebro @michael-s-molina. Since MCP is already behind its own enablement config, I'll replace the `EMBEDDABLE_CHARTS_MCP` feature flag with a simple config option (e.g., `MCP_EMBEDDABLE_CHARTS_ENABLED`) that controls whether the tool is exposed when MCP is enabled. Will update.


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911843014


##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,548 @@
+# 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 len(request.allowed_domains) == 2
+        assert "https://example.com" in request.allowed_domains

Review Comment:
   This is a test file using `https://example.com` as a fixture value — it's not user-controlled input and the URL is never rendered in a browser context. The test simply verifies the response contains the expected domain.



##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,548 @@
+# 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 len(request.allowed_domains) == 2
+        assert "https://example.com" in request.allowed_domains
+
+    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:
   This is correct as-is — Pydantic v2's `ValidationError` inherits from `ValueError` (`class ValidationError(ValueError)`), so `pytest.raises(ValueError)` correctly catches constraint validation errors from `ge=100` / `le=2000`.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911848782


##########
superset/models/embedded_chart.py:
##########
@@ -0,0 +1,58 @@
+# 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 uuid
+
+from flask_appbuilder import Model
+from sqlalchemy import Column, ForeignKey, Integer, Text
+from sqlalchemy.orm import relationship
+from sqlalchemy_utils import UUIDType
+
+from superset.models.helpers import AuditMixinNullable
+
+
+class EmbeddedChart(Model, AuditMixinNullable):
+    """
+    A configuration of embedding for a chart.
+
+    References the chart (slice), and contains a config for embedding that chart.
+
+    This data model allows multiple configurations for a given chart,
+    but at this time the API only allows setting one.
+    """
+
+    __tablename__ = "embedded_charts"
+
+    uuid = Column(UUIDType(binary=True), default=uuid.uuid4, primary_key=True)
+    allow_domain_list = Column(Text)  # reference the `allowed_domains` property instead
+    chart_id = Column(
+        Integer,
+        ForeignKey("slices.id", ondelete="CASCADE"),
+        nullable=False,
+    )
+    chart = relationship(
+        "Slice",
+        back_populates="embedded",
+        foreign_keys=[chart_id],
+    )
+
+    @property
+    def allowed_domains(self) -> list[str]:
+        """
+        A list of domains which are allowed to embed the chart.
+        An empty list means any domain can embed.
+        """
+        return self.allow_domain_list.split(",") if self.allow_domain_list else []

Review Comment:
   Already handled — line 60 of `embedded_chart.py` uses `[d.strip() for d in self.allow_domain_list.split(',') if d.strip()]` which strips whitespace and filters out empty entries.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911846394


##########
superset/security/manager.py:
##########
@@ -2810,6 +2821,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except (ExplorePermalinkGetFailedError, ValueError) as ex:
+                    raise EmbeddedChartPermalinkNotFoundError() from ex

Review Comment:
   The exception handling is intentionally specific. If `GetExplorePermalinkCommand.run()` raises a `ChartAccessDeniedError`, that should propagate up as an access denial rather than being converted into a 'permalink not found' error — masking an access denial as 'not found' would be a security concern. Only genuine lookup failures (`ExplorePermalinkGetFailedError`, `ValueError`) should be treated as 'not found'.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911862575


##########
superset/commands/chart/delete.py:
##########
@@ -68,3 +69,16 @@ 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)
+
+    def validate(self) -> None:
+        pass

Review Comment:
   <!-- Bito Reply -->
   Yes, the suggestion is valid — it correctly implements ownership validation for embedded chart deletion by catching SupersetSecurityException and raising ChartForbiddenError with proper exception chaining.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911855118


##########
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:
   This is correct as-is — `self._chart.embedded` is a SQLAlchemy relationship defined without `uselist=False` (see `models/slice.py:123`), so it returns a list by default. `EmbeddedChartDAO.delete()` receives a list, matching `BaseDAO.delete(cls, items: list[T])`.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891688730


##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,304 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
+
+            # Generate guest token
+            username = g.user.username if hasattr(g, "user") and g.user else "anonymous"
+            guest_user: GuestTokenUser = {
+                "username": f"embed_{username}",
+                "first_name": "Embed",
+                "last_name": "User",
+            }
+
+            resources: list[GuestTokenResource] = [
+                {

Review Comment:
   Correction: reverted to using the enum directly (without `.value`) because the TypedDict expects `GuestTokenResourceType`, not `str`. PyJWT handles serialization to string when encoding. The comparison in `validate_guest_token_resources` correctly uses `.value` because decoded JWT values are strings.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891602312


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

Review Comment:
   <!-- Bito Reply -->
   Yes, the referrer validation in `EmbeddedChartView.embedded_chart` uses `same_origin()` to check scheme+netloc or domain-suffix matching against configured `allowed_domains`. This serves as defense-in-depth, with the guest token providing primary security. The check only applies when `allowed_domains` are set, aborting with 403 if the referrer doesn't match.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891593721


##########
superset-frontend/webpack.config.js:
##########
@@ -300,6 +300,7 @@ const config = {
     menu: addPreamble('src/views/menu.tsx'),
     spa: addPreamble('/src/views/index.tsx'),
     embedded: addPreamble('/src/embedded/index.tsx'),
+    embeddedChart: addPreamble('/src/embeddedChart/index.tsx'),
   },

Review Comment:
   Already addressed — the entry uses `'src/embeddedChart/index.tsx'` without leading slash, matching all other entries.



##########
superset/security/manager.py:
##########
@@ -2834,6 +2838,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError()

Review Comment:
   Fixed in a8b866e. Removed `from ex` exception chaining in `validate_guest_token_resources`.



##########
docs/docs/configuration/databases.mdx:
##########
@@ -1137,6 +1194,28 @@ More information about PostgreSQL connection options can be found in the
 and the
 [PostgreSQL docs](https://www.postgresql.org/docs/9.1/libpq-connect.html#LIBPQ-PQCONNECTDBPARAMS).
 
+:::resources
+- [Blog: Data Visualization in PostgreSQL With Apache Superset](https://www.tigerdata.com/blog/data-visualization-in-postgresql-with-apache-superset)
+:::
+
+#### QuestDB
+
+[QuestDB](https://questdb.io/) is a high-performance, open-source time-series database with SQL support.
+The recommended connector library is the PostgreSQL driver [psycopg2](https://www.psycopg.org/docs/),
+as QuestDB supports the PostgreSQL wire protocol.
+
+The connection string is formatted as follows:
+
+```
+postgresql+psycopg2://{username}:{password}@{hostname}:{port}/{database}
+```
+
+The default port for QuestDB's PostgreSQL interface is `8812`.
+
+:::resources
+- [QuestDB Docs: Apache Superset Integration](https://questdb.com/docs/third-party-tools/superset/)

Review Comment:
   This is a pre-existing issue in the documentation, not introduced by this PR.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891481915


##########
superset/models/embedded_chart.py:
##########
@@ -0,0 +1,58 @@
+# 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 uuid
+
+from flask_appbuilder import Model
+from sqlalchemy import Column, ForeignKey, Integer, Text
+from sqlalchemy.orm import relationship
+from sqlalchemy_utils import UUIDType
+
+from superset.models.helpers import AuditMixinNullable
+
+
+class EmbeddedChart(Model, AuditMixinNullable):
+    """
+    A configuration of embedding for a chart.
+
+    References the chart (slice), and contains a config for embedding that chart.
+
+    This data model allows multiple configurations for a given chart,
+    but at this time the API only allows setting one.
+    """
+
+    __tablename__ = "embedded_charts"
+
+    uuid = Column(UUIDType(binary=True), default=uuid.uuid4, primary_key=True)
+    allow_domain_list = Column(Text)  # reference the `allowed_domains` property instead
+    chart_id = Column(
+        Integer,
+        ForeignKey("slices.id", ondelete="CASCADE"),
+        nullable=False,
+    )
+    chart = relationship(
+        "Slice",
+        back_populates="embedded",
+        foreign_keys=[chart_id],
+    )
+
+    @property
+    def allowed_domains(self) -> list[str]:
+        """
+        A list of domains which are allowed to embed the chart.
+        An empty list means any domain can embed.
+        """
+        return self.allow_domain_list.split(",") if self.allow_domain_list else []

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Domain parsing robustness</b></div>
   <div id="fix">
   
   The allowed_domains property splits the comma-separated domain list but does not handle whitespace around domains or empty entries, which could lead to incorrect embedding validation. Consider stripping whitespace and filtering empties for robustness.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
       return [domain.strip() for domain in self.allow_domain_list.split(",") if domain.strip()] if self.allow_domain_list else []
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #04cac8</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset/views/embedded_charts.py:
##########
@@ -0,0 +1,34 @@
+# 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.
+from flask_appbuilder import expose, has_access
+
+from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP
+from superset.superset_typing import FlaskResponse
+from superset.views.base import BaseSupersetView
+
+
+class EmbeddedChartsView(BaseSupersetView):
+    """View for managing embedded charts list."""
+
+    route_base = "/embeddedcharts"
+    class_permission_name = "Chart"
+    method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP
+
+    @expose("/list/")
+    @has_access
+    def list(self) -> FlaskResponse:
+        return super().render_app_template()

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Incorrect frontend entry for embedded view</b></div>
   <div id="fix">
   
   The render_app_template call defaults to entry="spa", but for the embedded charts view, it appears entry="embedded" should be used to load the appropriate frontend application, similar to how embedded dashboards specify this parameter.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
   return super().render_app_template(entry="embedded")
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #04cac8</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset/embedded_chart/exceptions.py:
##########
@@ -0,0 +1,35 @@
+# 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.
+from superset.exceptions import SupersetException
+
+
+class EmbeddedChartPermalinkNotFoundError(SupersetException):
+    """Raised when an embedded chart permalink is not found or has expired."""
+
+    message = "The embedded chart permalink could not be found or has expired."

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>I18n and HTTP status codes missing</b></div>
   <div id="fix">
   
   Exception messages should use lazy_gettext for i18n support, and classes need status codes for proper HTTP responses. The api.py handlers already return 401/404, but status attributes ensure consistency.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ```
    - from superset.exceptions import SupersetException
    - 
    - 
    - class EmbeddedChartPermalinkNotFoundError(SupersetException):
    + from superset.exceptions import SupersetException
    + from flask_babel import lazy_gettext as _
    + 
    + 
    + class EmbeddedChartPermalinkNotFoundError(SupersetException):
    @@ -23,2 +24,3 @@
    -    message = "The embedded chart permalink could not be found or has expired."
    -
    +    message = _("The embedded chart permalink could not be found or has expired.")
    +    status = 404
    +
    @@ -29,1 +31,2 @@
    -    message = "Access to this embedded chart is denied."
    +    message = _("Access to this embedded chart is denied.")
    +    status = 403
    @@ -35,1 +38,2 @@
    -    message = "The embeddable charts feature is not enabled."
    +    message = _("The embeddable charts feature is not enabled.")
    +    status = 403
   ```
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #04cac8</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset/commands/chart/delete.py:
##########
@@ -68,3 +69,16 @@ 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)
+
+    def validate(self) -> None:
+        pass

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Security: Missing Ownership Check</b></div>
   <div id="fix">
   
   The validate method lacks ownership checking, unlike DeleteChartCommand. This could allow unauthorized deletion of embedded charts, posing a security risk.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
           try:
               security_manager.raise_for_ownership(self._chart)
           except SupersetSecurityException as ex:
               raise ChartForbiddenError() from ex
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #04cac8</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4007364642

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894973072


##########
superset/security/manager.py:
##########
@@ -2834,6 +2842,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError() from None

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,298 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



##########
superset-frontend/webpack.config.js:
##########
@@ -300,6 +300,7 @@ const config = {
     menu: addPreamble('src/views/menu.tsx'),
     spa: addPreamble('/src/views/index.tsx'),
     embedded: addPreamble('/src/embedded/index.tsx'),
+    embeddedChart: addPreamble('/src/embeddedChart/index.tsx'),
   },

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



##########
superset/security/manager.py:
##########
@@ -2834,6 +2838,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError()

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894976205


##########
superset/security/manager.py:
##########
@@ -2834,6 +2838,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError()

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,116 @@
+# 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 Any, Callable, cast
+
+from flask import abort, current_app, request
+from flask_appbuilder import expose
+from flask_login import AnonymousUserMixin, login_user
+from flask_wtf.csrf import same_origin
+
+from superset import event_logger, is_feature_enabled
+from superset.commands.explore.permalink.get import GetExplorePermalinkCommand
+from superset.superset_typing import FlaskResponse
+from superset.utils import json
+from superset.views.base import BaseSupersetView, common_bootstrap_payload
+
+logger = logging.getLogger(__name__)
+
+
+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.
+        """
+        if not is_feature_enabled("EMBEDDABLE_CHARTS_MCP"):
+            abort(404)
+
+        # 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)
+
+        # Fetch permalink to get allowed_domains for referrer validation
+        try:
+            permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+        except Exception:
+            logger.exception("Error fetching permalink for embedded chart")
+            permalink_value = None
+
+        if not permalink_value:
+            logger.warning("Permalink not found for embedded chart: %s", permalink_key)
+            abort(404)
+
+        assert permalink_value is not None  # for mypy

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



##########
superset-frontend/webpack.config.js:
##########
@@ -300,6 +300,7 @@ const config = {
     menu: addPreamble('src/views/menu.tsx'),
     spa: addPreamble('/src/views/index.tsx'),
     embedded: addPreamble('/src/embedded/index.tsx'),
+    embeddedChart: addPreamble('/src/embeddedChart/index.tsx'),
   },

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



##########
superset-frontend/src/dashboard/components/EmbeddedChartModal/index.tsx:
##########
@@ -0,0 +1,289 @@
+/**
+ * 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, useState } from 'react';
+import { logging, makeApi, SupersetApiError, t } from '@superset-ui/core';
+import { styled, css, Alert } from '@apache-superset/core/ui';
+import {
+  Button,
+  FormItem,
+  InfoTooltip,
+  Input,
+  Modal,
+  Loading,
+  Form,
+  Space,
+} from '@superset-ui/core/components';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { Typography } from '@superset-ui/core/components/Typography';
+import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
+
+type Props = {
+  chartId: number;
+  formData: Record<string, unknown>;
+  show: boolean;
+  onHide: () => void;
+};
+
+type EmbeddedChart = {
+  uuid: string;
+  allowed_domains: string[];
+  chart_id: number;
+  changed_on: string;
+};
+
+type EmbeddedApiPayload = { allowed_domains: string[] };
+
+const stringToList = (stringyList: string): string[] =>
+  stringyList.split(/(?:\s|,)+/).filter(x => x);
+
+const ButtonRow = styled.div`
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+`;
+
+export const ChartEmbedControls = ({ chartId, onHide }: Props) => {
+  const { addInfoToast, addDangerToast } = useToasts();
+  const [ready, setReady] = useState(true);
+  const [loading, setLoading] = useState(false);
+  const [embedded, setEmbedded] = useState<EmbeddedChart | null>(null);
+  const [allowedDomains, setAllowedDomains] = useState<string>('');
+  const [showDeactivateConfirm, setShowDeactivateConfirm] = useState(false);
+
+  const endpoint = `/api/v1/chart/${chartId}/embedded`;
+  const isDirty =
+    !embedded ||
+    stringToList(allowedDomains).join() !== embedded.allowed_domains.join();
+
+  const enableEmbedded = useCallback(() => {
+    setLoading(true);
+    makeApi<EmbeddedApiPayload, { result: EmbeddedChart }>({
+      method: 'POST',
+      endpoint,
+    })({
+      allowed_domains: stringToList(allowedDomains),
+    })
+      .then(
+        ({ result }) => {
+          setEmbedded(result);
+          setAllowedDomains(result.allowed_domains.join(', '));
+          addInfoToast(t('Changes saved.'));
+        },
+        err => {
+          logging.error(err);
+          addDangerToast(
+            t('Sorry, something went wrong. The changes could not be saved.'),
+          );
+        },
+      )
+      .finally(() => {
+        setLoading(false);
+      });
+  }, [endpoint, allowedDomains, addInfoToast, addDangerToast]);
+
+  const disableEmbedded = useCallback(() => {
+    setShowDeactivateConfirm(true);
+  }, []);
+
+  const confirmDeactivate = useCallback(() => {
+    setLoading(true);
+    makeApi<object>({ method: 'DELETE', endpoint })({})
+      .then(
+        () => {
+          setEmbedded(null);
+          setAllowedDomains('');
+          setShowDeactivateConfirm(false);
+          addInfoToast(t('Embedding deactivated.'));
+          onHide();
+        },
+        err => {
+          logging.error(err);
+          addDangerToast(
+            t(
+              'Sorry, something went wrong. Embedding could not be deactivated.',
+            ),
+          );
+        },
+      )
+      .finally(() => {
+        setLoading(false);
+      });
+  }, [endpoint, addInfoToast, addDangerToast, onHide]);
+
+  useEffect(() => {
+    setReady(false);
+    makeApi<object, { result: EmbeddedChart }>({
+      method: 'GET',
+      endpoint,
+    })({})
+      .catch(err => {
+        if ((err as SupersetApiError).status === 404) {
+          return { result: null };
+        }
+        addDangerToast(t('Sorry, something went wrong. Please try again.'));
+        throw err;
+      })
+      .then(({ result }) => {
+        setReady(true);
+        setEmbedded(result);
+        setAllowedDomains(result ? result.allowed_domains.join(', ') : '');
+      });

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894979952


##########
superset/models/embedded_chart.py:
##########
@@ -0,0 +1,58 @@
+# 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 uuid
+
+from flask_appbuilder import Model
+from sqlalchemy import Column, ForeignKey, Integer, Text
+from sqlalchemy.orm import relationship
+from sqlalchemy_utils import UUIDType
+
+from superset.models.helpers import AuditMixinNullable
+
+
+class EmbeddedChart(Model, AuditMixinNullable):
+    """
+    A configuration of embedding for a chart.
+
+    References the chart (slice), and contains a config for embedding that chart.
+
+    This data model allows multiple configurations for a given chart,
+    but at this time the API only allows setting one.
+    """
+
+    __tablename__ = "embedded_charts"
+
+    uuid = Column(UUIDType(binary=True), default=uuid.uuid4, primary_key=True)
+    allow_domain_list = Column(Text)  # reference the `allowed_domains` property instead
+    chart_id = Column(
+        Integer,
+        ForeignKey("slices.id", ondelete="CASCADE"),
+        nullable=False,
+    )
+    chart = relationship(
+        "Slice",
+        back_populates="embedded",
+        foreign_keys=[chart_id],
+    )
+
+    @property
+    def allowed_domains(self) -> list[str]:
+        """
+        A list of domains which are allowed to embed the chart.
+        An empty list means any domain can embed.
+        """
+        return self.allow_domain_list.split(",") if self.allow_domain_list else []

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



##########
superset/embedded_chart/exceptions.py:
##########
@@ -0,0 +1,35 @@
+# 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.
+from superset.exceptions import SupersetException
+
+
+class EmbeddedChartPermalinkNotFoundError(SupersetException):
+    """Raised when an embedded chart permalink is not found or has expired."""
+
+    message = "The embedded chart permalink could not be found or has expired."

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



##########
docs/docs/configuration/databases.mdx:
##########
@@ -1137,6 +1194,28 @@ More information about PostgreSQL connection options can be found in the
 and the
 [PostgreSQL docs](https://www.postgresql.org/docs/9.1/libpq-connect.html#LIBPQ-PQCONNECTDBPARAMS).
 
+:::resources
+- [Blog: Data Visualization in PostgreSQL With Apache Superset](https://www.tigerdata.com/blog/data-visualization-in-postgresql-with-apache-superset)
+:::
+
+#### QuestDB
+
+[QuestDB](https://questdb.io/) is a high-performance, open-source time-series database with SQL support.
+The recommended connector library is the PostgreSQL driver [psycopg2](https://www.psycopg.org/docs/),
+as QuestDB supports the PostgreSQL wire protocol.
+
+The connection string is formatted as follows:
+
+```
+postgresql+psycopg2://{username}:{password}@{hostname}:{port}/{database}
+```
+
+The default port for QuestDB's PostgreSQL interface is `8812`.
+
+:::resources
+- [QuestDB Docs: Apache Superset Integration](https://questdb.com/docs/third-party-tools/superset/)

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894977405


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

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



##########
superset/config.py:
##########
@@ -591,8 +593,6 @@ class D3TimeFormat(TypedDict, total=False):
     "CACHE_IMPERSONATION": False,
     # Enable caching per user key for Superset cache (not database cache impersonation)
     "CACHE_QUERY_BY_USER": False,

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



##########
superset/security/manager.py:
##########
@@ -2834,6 +2842,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError() from None

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4030148569

   ## **Sequence Diagram**
   
   This diagram shows how the new MCP tool and embedded chart frontend work together to create an ephemeral chart, issue a guest token, and render the chart inside an embedded iframe using a permalink-backed state.
   
   ```mermaid
   sequenceDiagram
       participant Agent
       participant MCPServer
       participant SupersetAPI
       participant HostApp
       participant EmbeddedChartPage
   
       Agent->>MCPServer: Call get_embeddable_chart with dataset and config
       MCPServer->>SupersetAPI: Create permalink and guest token for form data
       SupersetAPI-->>MCPServer: Return iframe URL, guest token, expiry
       MCPServer-->>Agent: Return embed details and iframe HTML snippet
   
       HostApp->>EmbeddedChartPage: Load iframe URL
       HostApp->>EmbeddedChartPage: postMessage with guest token
       EmbeddedChartPage->>SupersetAPI: Fetch embedded chart state by permalink key
       SupersetAPI-->>EmbeddedChartPage: Return chart form data
       EmbeddedChartPage->>EmbeddedChartPage: Render chart with StatefulChart
   ```
   
   ---
   *Generated by [CodeAnt AI](https://codeant.ai)*
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932190435


##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,321 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.exceptions import CommandException
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+from superset.key_value.exceptions import KeyValueParseKeyError
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except (ValueError, KeyError, KeyValueParseKeyError) as ex:
+            logger.warning("Error fetching embedded chart: %s", ex)
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")

Review Comment:
   <!-- Bito Reply -->
   The codebase shows inconsistent i18n usage for API error messages—most endpoints use plain strings, but some like charts/data/api.py and the new embedded_chart exceptions use gettext. Since the exceptions in this PR already use lazy_gettext, applying i18n to the API messages would improve consistency within the new feature and align with best practices for future maintainability.
   
   **superset/embedded_chart/api.py**
   ```
   from flask_babel import gettext as _
   
   # Then wrap messages:
   return self.response_400(message=_("form_data is required"))
   ```



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4054846937

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3716594566

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3725793237

   CodeAnt AI is running Incremental review
   
   ---
   
   ### Thanks for using CodeAnt! 🎉
   
   We're free for open-source projects. if you're enjoying it, help us grow by sharing.
   
   [Share on X](https://twitter.com/intent/tweet?text=Just%20tried%20%40CodeAntAI%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A&url=https%3A//codeant.ai) ·
   [Reddit](https://www.reddit.com/submit?title=Check%20out%20CodeAnt%20for%20automated%20code%20review&text=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A%20https%3A//codeant.ai) ·
   [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcodeant.ai&mini=true&title=Check%20out%20CodeAnt%20for%20automated%20code%20review&summary=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos)
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3726990353

   CodeAnt AI is running Incremental review
   
   ---
   
   ### Thanks for using CodeAnt! 🎉
   
   We're free for open-source projects. if you're enjoying it, help us grow by sharing.
   
   [Share on X](https://twitter.com/intent/tweet?text=Just%20tried%20%40CodeAntAI%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A&url=https%3A//codeant.ai) ·
   [Reddit](https://www.reddit.com/submit?title=Check%20out%20CodeAnt%20for%20automated%20code%20review&text=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A%20https%3A//codeant.ai) ·
   [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcodeant.ai&mini=true&title=Check%20out%20CodeAnt%20for%20automated%20code%20review&summary=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos)
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2674086182


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

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Security Bug in Referrer Validation</b></div>
   <div id="fix">
   
   The same_origin function incorrectly uses netloc (which includes ports) for domain matching, causing it to reject valid referrers with non-standard ports like 'https://example.com:8080' when 'example.com' is allowed. It should use hostname for host-only comparison and perform case-insensitive matching, as hosts are case-insensitive per RFC.
   </div>
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #b9e5b1</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3886516864

   Re: iframe URL format - There's no mismatch. The `/api/v1/embedded_chart/<permalink_key>` endpoint is the API for fetching data (used by frontend), while `/embedded/chart/?permalink_key=XXX` is for page rendering (handled by view.py). Both are correct and serve different purposes.


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3881322415

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3716796933

   ## Code Review Responses
   
   ### ✅ Fixed Issues
   
   | Comment | Fix |
   |---------|-----|
   | **Null pointer in validateMessageEvent** | Added null check: `if (data == null \|\| typeof data !== 'object' \|\| data.type !== MESSAGE_TYPE)` |
   | **Race condition with started flag** | Now sets `started = true` before `start()` and resets to `false` on failure |
   | **Security: exception message leak in 500** | Returns generic `response_500()` without exposing exception message |
   | **Exception chaining leaks internal details** | Removed `from ex`, re-raises directly or creates new instance |
   | **Missing docstrings in exceptions** | Added docstrings to all exception classes |
   
   ### ❌ Not Applicable / Incorrect Suggestions
   
   | Suggestion | Reason |
   |------------|--------|
   | **GUEST_TOKEN_HEADER_NAME KeyError** | Config value exists in default `config.py` (line 2121). Existing `embedded/view.py` uses same pattern. |
   | **Webpack path format** | The leading slash format matches existing `spa` and `embedded` entries which work correctly. |
   | **Feature flag confusion** | No `EMBEDDABLE_CHARTS` flag exists - only `EMBEDDED_SUPERSET`. The new `EMBEDDABLE_CHARTS_MCP` is intentionally separate. |
   | **Lazy import in __init__.py** | Follows existing pattern used throughout the MCP service codebase. |
   | **ttl_minutes None check** | Pydantic model provides `default=60`, so value is never None. |
   | **Import style in app.py** | Follows existing pattern for other tool imports in the file. |


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3716611033

   ## 💡 Enhance Your PR Reviews
   
   We noticed that **3 feature(s)** are not configured for this repository. Enabling these features can help improve your code quality and workflow:
   
   ### 🚦 Quality Gates
   **Status:** Quality Gates are not enabled at the organization level
   Learn more about [Quality Gates](https://docs.codeant.ai/quality-gates)
   
   ### 🎫 Jira Ticket Compliance
   **Status:** Jira credentials file not found. Please configure Jira integration in your settings
   Learn more about [Jira Integration](https://docs.codeant.ai/integrations/jira)
   
   ### ⚙️ Custom Rules
   **Status:** No custom rules configured. Add rules via organization settings or .codeant/review.json in your repository
   Learn more about [Custom Rules](https://docs.codeant.ai/custom-rules)
   
   ---
   
   **Want to enable these features?** Contact your organization admin or check our documentation for setup instructions.
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3721525407

   > > We have lots of FFs
   > 
   > +1 If we can avoid a feature flag it would be better.
   
   yeah you're both 100% right. let me get rid of it


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891591888


##########
superset-frontend/src/embeddedChart/index.tsx:
##########
@@ -0,0 +1,338 @@
+/**
+ * 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 'src/public-path';
+
+import { useState, useEffect } from 'react';
+import ReactDOM from 'react-dom';
+import {
+  makeApi,
+  t,
+  logging,
+  QueryFormData,
+  StatefulChart,
+} from '@superset-ui/core';
+import Switchboard from '@superset-ui/switchboard';
+import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData';
+import setupClient from 'src/setup/setupClient';
+import setupPlugins from 'src/setup/setupPlugins';
+import { store, USER_LOADED } from 'src/views/store';
+import { Loading } from '@superset-ui/core/components';
+import { ErrorBoundary, DynamicPluginProvider } from 'src/components';
+import { addDangerToast } from 'src/components/MessageToasts/actions';
+import ToastContainer from 'src/components/MessageToasts/ToastContainer';
+import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
+import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
+import { ThemeController } from 'src/theme/ThemeController';
+import type { ThemeStorage } from '@apache-superset/core/ui';
+import { Provider as ReduxProvider } from 'react-redux';
+
+setupPlugins();
+
+const debugMode = process.env.WEBPACK_MODE === 'development';
+const bootstrapData = getBootstrapData();
+
+function log(...info: unknown[]) {
+  if (debugMode) logging.debug(`[superset-embedded-chart]`, ...info);
+}
+
+/**
+ * In-memory implementation of ThemeStorage interface for embedded contexts.
+ */
+class ThemeMemoryStorageAdapter implements ThemeStorage {
+  private storage = new Map<string, string>();
+
+  getItem(key: string): string | null {
+    return this.storage.get(key) || null;
+  }
+
+  setItem(key: string, value: string): void {
+    this.storage.set(key, value);
+  }
+
+  removeItem(key: string): void {
+    this.storage.delete(key);
+  }
+}
+
+const themeController = new ThemeController({
+  storage: new ThemeMemoryStorageAdapter(),
+});
+
+interface PermalinkState {
+  formData: QueryFormData;
+}
+
+const appMountPoint = document.getElementById('app')!;
+const MESSAGE_TYPE = '__embedded_comms__';
+
+function showFailureMessage(message: string) {
+  appMountPoint.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-family: sans-serif;">${message}</div>`;
+}
+
+if (!window.parent || window.parent === window) {
+  showFailureMessage(
+    t(
+      'This page is intended to be embedded in an iframe, but it looks like that is not the case.',
+    ),
+  );
+}
+
+let displayedUnauthorizedToast = false;
+
+/**
+ * Handle unauthorized errors from the API.
+ */
+function guestUnauthorizedHandler() {
+  if (displayedUnauthorizedToast) return;
+  displayedUnauthorizedToast = true;
+  store.dispatch(
+    addDangerToast(
+      t(
+        'This session has encountered an interruption. The embedded chart may not load correctly.',
+      ),
+      {
+        duration: -1,
+        noDuplicate: true,
+      },
+    ),
+  );
+}
+
+/**
+ * Configures SupersetClient with the correct settings for the embedded chart page.
+ */
+function setupGuestClient(guestToken: string) {
+  setupClient({
+    appRoot: applicationRoot(),
+    guestToken,
+    guestTokenHeaderName: bootstrapData.config?.GUEST_TOKEN_HEADER_NAME,
+    unauthorizedHandler: guestUnauthorizedHandler,
+  });
+}
+
+function validateMessageEvent(event: MessageEvent) {
+  const { data } = event;
+  if (data == null || typeof data !== 'object' || data.type !== MESSAGE_TYPE) {

Review Comment:
   Fixed in a8b866e. Relaxed `validateMessageEvent` to accept messages carrying `guestToken` or `handshake` properties in addition to typed messages. This ensures direct token delivery and Switchboard handshakes both work.



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,304 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
+
+            # Generate guest token
+            username = g.user.username if hasattr(g, "user") and g.user else "anonymous"
+            guest_user: GuestTokenUser = {
+                "username": f"embed_{username}",
+                "first_name": "Embed",
+                "last_name": "User",
+            }
+
+            resources: list[GuestTokenResource] = [
+                {

Review Comment:
   Fixed in a8b866e. Changed to use `GuestTokenResourceType.CHART_PERMALINK.value` so the string value is stored in the guest token, matching what `validate_guest_token_resources` and the GET endpoint expect.



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,304 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
+
+            # Generate guest token
+            username = g.user.username if hasattr(g, "user") and g.user else "anonymous"
+            guest_user: GuestTokenUser = {
+                "username": f"embed_{username}",
+                "first_name": "Embed",
+                "last_name": "User",
+            }
+
+            resources: list[GuestTokenResource] = [
+                {
+                    "type": GuestTokenResourceType.CHART_PERMALINK,
+                    "id": permalink_key,
+                }
+            ]
+
+            rls_rules: list[GuestTokenRlsRule] = []
+
+            guest_token_result = security_manager.create_guest_access_token(
+                user=guest_user,
+                resources=resources,
+                rls=rls_rules,
+            )
+
+            # Handle both bytes (older PyJWT) and string (PyJWT 2.0+)
+            guest_token = (
+                guest_token_result.decode("utf-8")
+                if isinstance(guest_token_result, bytes)
+                else guest_token_result
+            )
+
+            # Build iframe URL using request host
+            base_url = request.host_url.rstrip("/")
+            iframe_url = f"{base_url}/embedded/chart/?permalink_key={permalink_key}"
+

Review Comment:
   The `iframe_url` points to the `EmbeddedChartView` (line 59-61 of view.py: `route_base = '/embedded/chart'`, `@expose('/')`) which uses a query parameter `?permalink_key=xxx` (line 72). This is correct — the iframe loads the server-rendered page, not the REST API. The REST API endpoint `/<permalink_key>` is a separate endpoint used by the JS app inside the iframe to fetch chart data.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891594612


##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,116 @@
+# 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 Any, Callable, cast
+
+from flask import abort, current_app, request
+from flask_appbuilder import expose
+from flask_login import AnonymousUserMixin, login_user
+from flask_wtf.csrf import same_origin
+
+from superset import event_logger, is_feature_enabled
+from superset.commands.explore.permalink.get import GetExplorePermalinkCommand
+from superset.superset_typing import FlaskResponse
+from superset.utils import json
+from superset.views.base import BaseSupersetView, common_bootstrap_payload
+
+logger = logging.getLogger(__name__)
+
+
+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.
+        """
+        if not is_feature_enabled("EMBEDDABLE_CHARTS_MCP"):
+            abort(404)
+
+        # 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)
+
+        # Fetch permalink to get allowed_domains for referrer validation
+        try:
+            permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+        except Exception:
+            logger.exception("Error fetching permalink for embedded chart")
+            permalink_value = None
+
+        if not permalink_value:
+            logger.warning("Permalink not found for embedded chart: %s", permalink_key)
+            abort(404)
+
+        assert permalink_value is not None  # for mypy

Review Comment:
   There are no assert statements in view.py for our code. The `login_user(AnonymousUserMixin(), force=True)` call is a Flask-Login API call, not an assertion.



##########
superset/security/manager.py:
##########
@@ -2834,6 +2842,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError() from None

Review Comment:
   Fixed in a8b866e. Removed exception chaining and narrowed the catch scope.



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,298 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:

Review Comment:
   Already addressed — the POST endpoint validates `form_data` is non-empty, `datasource` field exists, and `ttl_minutes` is between 1-10080.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891595567


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

Review Comment:
   The referrer validation uses `same_origin()` which checks scheme+netloc or domain-suffix matching. This is defense-in-depth — the primary security is the guest token. The referrer check is an additional layer when `allowed_domains` are configured.



##########
superset/config.py:
##########
@@ -591,8 +593,6 @@ class D3TimeFormat(TypedDict, total=False):
     "CACHE_IMPERSONATION": False,
     # Enable caching per user key for Superset cache (not database cache impersonation)
     "CACHE_QUERY_BY_USER": False,

Review Comment:
   Fixed in a8b866e. Changed `EMBEDDABLE_CHARTS` to default to `False` — operators must explicitly opt in.



##########
superset-frontend/src/dashboard/components/EmbeddedChartModal/index.tsx:
##########
@@ -0,0 +1,289 @@
+/**
+ * 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, useState } from 'react';
+import { logging, makeApi, SupersetApiError, t } from '@superset-ui/core';
+import { styled, css, Alert } from '@apache-superset/core/ui';
+import {
+  Button,
+  FormItem,
+  InfoTooltip,
+  Input,
+  Modal,
+  Loading,
+  Form,
+  Space,
+} from '@superset-ui/core/components';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { Typography } from '@superset-ui/core/components/Typography';
+import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
+
+type Props = {
+  chartId: number;
+  formData: Record<string, unknown>;
+  show: boolean;
+  onHide: () => void;
+};
+
+type EmbeddedChart = {
+  uuid: string;
+  allowed_domains: string[];
+  chart_id: number;
+  changed_on: string;
+};
+
+type EmbeddedApiPayload = { allowed_domains: string[] };
+
+const stringToList = (stringyList: string): string[] =>
+  stringyList.split(/(?:\s|,)+/).filter(x => x);
+
+const ButtonRow = styled.div`
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+`;
+
+export const ChartEmbedControls = ({ chartId, onHide }: Props) => {
+  const { addInfoToast, addDangerToast } = useToasts();
+  const [ready, setReady] = useState(true);
+  const [loading, setLoading] = useState(false);
+  const [embedded, setEmbedded] = useState<EmbeddedChart | null>(null);
+  const [allowedDomains, setAllowedDomains] = useState<string>('');
+  const [showDeactivateConfirm, setShowDeactivateConfirm] = useState(false);
+
+  const endpoint = `/api/v1/chart/${chartId}/embedded`;
+  const isDirty =
+    !embedded ||
+    stringToList(allowedDomains).join() !== embedded.allowed_domains.join();
+
+  const enableEmbedded = useCallback(() => {
+    setLoading(true);
+    makeApi<EmbeddedApiPayload, { result: EmbeddedChart }>({
+      method: 'POST',
+      endpoint,
+    })({
+      allowed_domains: stringToList(allowedDomains),
+    })
+      .then(
+        ({ result }) => {
+          setEmbedded(result);
+          setAllowedDomains(result.allowed_domains.join(', '));
+          addInfoToast(t('Changes saved.'));
+        },
+        err => {
+          logging.error(err);
+          addDangerToast(
+            t('Sorry, something went wrong. The changes could not be saved.'),
+          );
+        },
+      )
+      .finally(() => {
+        setLoading(false);
+      });
+  }, [endpoint, allowedDomains, addInfoToast, addDangerToast]);
+
+  const disableEmbedded = useCallback(() => {
+    setShowDeactivateConfirm(true);
+  }, []);
+
+  const confirmDeactivate = useCallback(() => {
+    setLoading(true);
+    makeApi<object>({ method: 'DELETE', endpoint })({})
+      .then(
+        () => {
+          setEmbedded(null);
+          setAllowedDomains('');
+          setShowDeactivateConfirm(false);
+          addInfoToast(t('Embedding deactivated.'));
+          onHide();
+        },
+        err => {
+          logging.error(err);
+          addDangerToast(
+            t(
+              'Sorry, something went wrong. Embedding could not be deactivated.',
+            ),
+          );
+        },
+      )
+      .finally(() => {
+        setLoading(false);
+      });
+  }, [endpoint, addInfoToast, addDangerToast, onHide]);
+
+  useEffect(() => {
+    setReady(false);
+    makeApi<object, { result: EmbeddedChart }>({
+      method: 'GET',
+      endpoint,
+    })({})
+      .catch(err => {
+        if ((err as SupersetApiError).status === 404) {
+          return { result: null };
+        }
+        addDangerToast(t('Sorry, something went wrong. Please try again.'));
+        throw err;
+      })
+      .then(({ result }) => {
+        setReady(true);
+        setEmbedded(result);
+        setAllowedDomains(result ? result.allowed_domains.join(', ') : '');
+      });

Review Comment:
   Fixed in a8b866e. Changed `ready` state to start as `false` so the Loading spinner shows during initial fetch.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891592817


##########
superset/commands/chart/delete.py:
##########
@@ -68,3 +69,16 @@ 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)
+
+    def validate(self) -> None:
+        pass

Review Comment:
   Fixed in a8b866e. Added existence, embedded-record, and ownership validation to `DeleteEmbeddedChartCommand.validate()`.



##########
superset/config.py:
##########
@@ -559,6 +559,8 @@ class D3TimeFormat(TypedDict, total=False):
     # This feature flag is stil in beta and is not recommended for production use.
     "GLOBAL_ASYNC_QUERIES": False,
     "EMBEDDED_SUPERSET": False,
+    # Enables the "Embed code" and "Embed chart" options in the Share menu
+    "EMBEDDABLE_CHARTS": True,

Review Comment:
   Fixed in a8b866e. Changed `EMBEDDABLE_CHARTS` to default to `False` so the feature is opt-in.



##########
embed-demo.html:
##########
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Superset Embedded Chart Demo</title>
+  <style>
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+      max-width: 1200px;
+      margin: 0 auto;
+      padding: 40px;
+      background: #f5f5f5;
+    }
+    h1 { color: #333; }
+    .input-section {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      margin-bottom: 20px;
+    }
+    textarea {
+      width: 100%;
+      height: 150px;
+      font-family: monospace;
+      font-size: 12px;
+      padding: 10px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      resize: vertical;
+      box-sizing: border-box;
+    }
+    button {
+      background: #20a7c9;
+      color: white;
+      border: none;
+      padding: 12px 24px;
+      font-size: 16px;
+      border-radius: 4px;
+      cursor: pointer;
+      margin-top: 10px;
+    }
+    button:hover {
+      background: #1a8fa8;
+    }
+    .chart-container {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      min-height: 450px;
+    }
+    label {
+      font-weight: 600;
+      display: block;
+      margin-bottom: 8px;
+    }
+  </style>
+</head>
+<body>
+  <h1>Superset Embedded Chart Demo</h1>
+
+  <div class="input-section">
+    <label for="iframe-input">Paste iframe_html response here:</label>
+    <textarea id="iframe-input" placeholder="Paste the iframe_html value from get_embeddable_chart response..."></textarea>
+    <button onclick="embedChart()">Embed Chart</button>
+  </div>
+
+  <div class="chart-container" id="chart-container">
+    <p style="color: #999; text-align: center; margin-top: 200px;">Chart will appear here</p>
+  </div>
+
+  <script>
+    function embedChart() {
+      const input = document.getElementById('iframe-input').value.trim();
+      const container = document.getElementById('chart-container');
+
+      if (!input) {
+        alert('Please paste the iframe_html first');
+        return;
+      }
+
+      // Insert the iframe HTML
+      container.innerHTML = input;

Review Comment:
   Fixed in a8b866e. Added URL scheme validation (only `http://` or `https://` allowed) and `sandbox` attribute to the created iframe to prevent javascript: URI injection.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891603314


##########
superset/security/manager.py:
##########
@@ -2834,6 +2842,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError() from None

Review Comment:
   <!-- Bito Reply -->
   This fix improves exception handling in `validate_guest_token_resources` by narrowing the catch scope to specific exceptions (EmbeddedChartPermalinkNotFoundError, ValueError, KeyError) that can arise from permalink access or resource parsing, and removes exception chaining with `from None` to avoid leaking internal error details in stack traces.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2892220206


##########
superset/models/embedded_chart.py:
##########
@@ -0,0 +1,58 @@
+# 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 uuid
+
+from flask_appbuilder import Model
+from sqlalchemy import Column, ForeignKey, Integer, Text
+from sqlalchemy.orm import relationship
+from sqlalchemy_utils import UUIDType
+
+from superset.models.helpers import AuditMixinNullable
+
+
+class EmbeddedChart(Model, AuditMixinNullable):
+    """
+    A configuration of embedding for a chart.
+
+    References the chart (slice), and contains a config for embedding that chart.
+
+    This data model allows multiple configurations for a given chart,
+    but at this time the API only allows setting one.
+    """
+
+    __tablename__ = "embedded_charts"
+
+    uuid = Column(UUIDType(binary=True), default=uuid.uuid4, primary_key=True)
+    allow_domain_list = Column(Text)  # reference the `allowed_domains` property instead
+    chart_id = Column(
+        Integer,
+        ForeignKey("slices.id", ondelete="CASCADE"),
+        nullable=False,
+    )
+    chart = relationship(
+        "Slice",
+        back_populates="embedded",
+        foreign_keys=[chart_id],
+    )
+
+    @property
+    def allowed_domains(self) -> list[str]:
+        """
+        A list of domains which are allowed to embed the chart.
+        An empty list means any domain can embed.
+        """
+        return self.allow_domain_list.split(",") if self.allow_domain_list else []

Review Comment:
   Fixed in 50ab323. `allowed_domains` now strips whitespace and filters empty entries: `[d.strip() for d in self.allow_domain_list.split(",") if d.strip()]`



##########
superset/views/embedded_charts.py:
##########
@@ -0,0 +1,34 @@
+# 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.
+from flask_appbuilder import expose, has_access
+
+from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP
+from superset.superset_typing import FlaskResponse
+from superset.views.base import BaseSupersetView
+
+
+class EmbeddedChartsView(BaseSupersetView):
+    """View for managing embedded charts list."""
+
+    route_base = "/embeddedcharts"
+    class_permission_name = "Chart"
+    method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP
+
+    @expose("/list/")
+    @has_access
+    def list(self) -> FlaskResponse:
+        return super().render_app_template()

Review Comment:
   No change needed. `EmbeddedChartsView` is the admin list page at `/embeddedcharts/list/` — it renders within the main Superset SPA (not in an iframe). The SPA entry is correct here. The embedded chart iframe page is served by `EmbeddedChartView` in `embedded_chart/view.py` which uses a separate webpack entry point (`embeddedChart`).



##########
superset/embedded_chart/exceptions.py:
##########
@@ -0,0 +1,35 @@
+# 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.
+from superset.exceptions import SupersetException
+
+
+class EmbeddedChartPermalinkNotFoundError(SupersetException):
+    """Raised when an embedded chart permalink is not found or has expired."""
+
+    message = "The embedded chart permalink could not be found or has expired."

Review Comment:
   Fixed in 50ab323. Added `lazy_gettext` for i18n and proper HTTP status codes to all exception classes: `EmbeddedChartPermalinkNotFoundError` (404), `EmbeddedChartAccessDeniedError` (401), `EmbeddedChartFeatureDisabledError` (403).



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2790840091


##########
superset/security/manager.py:
##########
@@ -2810,6 +2821,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except (ExplorePermalinkGetFailedError, ValueError) as ex:
+                    raise EmbeddedChartPermalinkNotFoundError() from ex

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Incomplete Exception Handling</b></div>
   <div id="fix">
   
   The exception handling catches ExplorePermalinkGetFailedError and ValueError, but GetExplorePermalinkCommand.run() can raise other exceptions like ChartAccessDeniedError from check_chart_access, leading to inconsistent error types instead of always raising EmbeddedChartPermalinkNotFoundError.
   </div>
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #1fca91</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2795376159


##########
superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx:
##########
@@ -603,6 +618,13 @@ const SliceHeaderControls = (
         dataset={datasetWithVerboseMap}
       />
 
+      <EmbeddedChartModal
+        chartId={slice.slice_id}
+        formData={props.formData}
+        show={embedModalIsOpen}
+        onHide={() => setEmbedModalIsOpen(false)}

Review Comment:
   This is not a bug. The `EmbeddedChartModal` component correctly accepts `show` and `onHide` props (defined in Props interface at superset-frontend/src/dashboard/components/EmbeddedChartModal/index.tsx:37-42) and passes them to the Modal component (lines 281-282). The Modal component from @superset-ui/core/components uses `show` and `onHide` as its interface, not `showModal` and `onHideModal`.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891606573


##########
superset/charts/schemas.py:
##########
@@ -345,6 +345,17 @@ class ChartGetCachedScreenshotResponseSchema(Schema):
     )
 
 
+class EmbeddedChartConfigSchema(Schema):
+    allowed_domains = fields.List(fields.String(), required=True)
+
+
+class EmbeddedChartResponseSchema(Schema):
+    uuid = fields.String()
+    allowed_domains = fields.List(fields.String())
+    chart_id = fields.Integer()
+    changed_on = fields.DateTime()

Review Comment:
   <!-- Bito Reply -->
   Yes, that's accurate. Marshmallow's `fields.DateTime()` serializes DateTime objects to ISO 8601 string format by default, which matches the `changed_on: string` type in the frontend `EmbeddedChart` TypeScript interface.
   
   **superset/charts/schemas.py**
   ```
   class EmbeddedChartResponseSchema(Schema):
       uuid = fields.UUID()
       allowed_domains = fields.List(fields.String())
       chart_id = fields.Int()
       changed_on = fields.DateTime()
   ```



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,298 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:

Review Comment:
   <!-- Bito Reply -->
   The POST endpoint checks if `form_data` is non-empty, but lacks validation for the `datasource` field in `form_data` and the `ttl_minutes` range (1-10080).



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911880946


##########
superset/embedded_chart/exceptions.py:
##########
@@ -0,0 +1,35 @@
+# 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.
+from superset.exceptions import SupersetException
+
+
+class EmbeddedChartPermalinkNotFoundError(SupersetException):
+    """Raised when an embedded chart permalink is not found or has expired."""
+
+    message = "The embedded chart permalink could not be found or has expired."

Review Comment:
   <!-- Bito Reply -->
   Partially — the suggestion adds lazy_gettext for i18n and status codes to the exception classes, which improves internationalization and error handling, but the status code for EmbeddedChartAccessDeniedError should be 401 instead of 403 to match standard HTTP semantics for unauthorized access.
   
   **superset/embedded_chart/exceptions.py**
   ```
   from flask_babel import lazy_gettext as _
   
   from superset.exceptions import SupersetException
   
   
   class EmbeddedChartPermalinkNotFoundError(SupersetException):
       """Raised when an embedded chart permalink is not found or has expired."""
   
       status = 404
       message = _("The embedded chart permalink could not be found or has expired.")
   
   
   class EmbeddedChartAccessDeniedError(SupersetException):
       """Raised when access to an embedded chart is denied."""
   
       status = 401
       message = _("Access to this embedded chart is denied.")
   
   
   class EmbeddedChartFeatureDisabledError(SupersetException):
       """Raised when the embeddable charts feature is disabled."""
   
       status = 403
       message = _("The embeddable charts feature is not enabled.")
   ```



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911861349


##########
superset/security/manager.py:
##########
@@ -2810,6 +2821,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except (ExplorePermalinkGetFailedError, ValueError) as ex:
+                    raise EmbeddedChartPermalinkNotFoundError() from ex

Review Comment:
   <!-- Bito Reply -->
   The exception handling is correctly designed to propagate access denials like ChartAccessDeniedError while converting only genuine lookup failures (ExplorePermalinkGetFailedError, ValueError) to 'not found' errors, preventing security issues from masking access restrictions.
   
   **superset/security/manager.py**
   ```
   elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
                   # Validate that the chart permalink exists
                   permalink_key = str(resource["id"])
                   try:
                       permalink_value = GetExplorePermalinkCommand(permalink_key).run()
                       if not permalink_value:
                           raise EmbeddedChartPermalinkNotFoundError()
                   except EmbeddedChartPermalinkNotFoundError:
                       raise
                   except (ExplorePermalinkGetFailedError, ValueError) as ex:
                       raise EmbeddedChartPermalinkNotFoundError() from ex
   ```



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911110412


##########
tests/unit_tests/extensions/test_types.py:
##########
@@ -61,6 +61,18 @@ def test_extension_config_full():
             "description": "A query insights extension",
             "dependencies": ["other-extension"],
             "permissions": ["can_read", "can_view"],
+            "frontend": {
+                "contributions": {
+                    "views": {
+                        "sqllab": {
+                            "panels": [
+                                {"id": "query_insights.main", "name": "Query Insights"}
+                            ]
+                        }
+                    }
+                },
+                "moduleFederation": {"exposes": ["./index"]},
+            },

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Missing frontend assertions</b></div>
   <div id="fix">
   
   The test validates ExtensionConfig parsing with frontend but lacks assertions for frontend values, violating the rule to assert actual behavior logic rather than just component rendering/parsing.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ```
    --- a/tests/unit_tests/extensions/test_types.py
    +++ b/tests/unit_tests/extensions/test_types.py
    @@ -86,6 +86,10 @@ def test_extension_config_full():
         assert config.permissions == ["can_read", "can_view"]
         assert config.backend is not None
         assert config.backend.files == ["backend/src/query_insights/**/*.py"]
    +    assert config.frontend is not None
    +    assert config.frontend.contributions["views"]["sqllab"]["panels"][0]["id"] == "query_insights.main"
    +    assert config.frontend.contributions["views"]["sqllab"]["panels"][0]["name"] == "Query Insights"
    +    assert config.frontend.moduleFederation["exposes"] == ["./index"]
   ```
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #786a36</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4031815383

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2674837633


##########
superset/charts/api.py:
##########
@@ -1183,3 +1195,252 @@ def import_(self) -> Response:
         )
         command.run()
         return self.response(200, message="OK")
+
+    @expose("/<id_or_uuid>/embedded", methods=("GET",))
+    @protect()
+    @safe
+    @permission_name("read")
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_embedded",
+        log_to_statsd=False,
+    )
+    def get_embedded(self, id_or_uuid: str) -> Response:
+        """Get the chart's embedded configuration.
+        ---
+        get:
+          summary: Get the chart's embedded configuration
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: id_or_uuid
+            description: The chart id or uuid
+          responses:
+            200:
+              description: Result contains the embedded chart config
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        $ref: '#/components/schemas/EmbeddedChartResponseSchema'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            chart = ChartDAO.get_by_id_or_uuid(id_or_uuid)
+        except ChartNotFoundError:
+            return self.response_404()
+
+        if not chart.embedded:
+            return self.response(404)
+        embedded: EmbeddedChart = chart.embedded[0]
+        result = self.embedded_response_schema.dump(embedded)
+        return self.response(200, result=result)
+
+    @expose("/<id_or_uuid>/embedded", methods=["POST", "PUT"])
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.set_embedded",
+        log_to_statsd=False,
+    )
+    def set_embedded(self, id_or_uuid: str) -> Response:
+        """Set a chart's embedded configuration.
+        ---
+        post:
+          summary: Set a chart's embedded configuration
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: id_or_uuid
+            description: The chart id or uuid
+          requestBody:
+            description: The embedded configuration to set
+            required: true
+            content:
+              application/json:
+                schema: EmbeddedChartConfigSchema
+          responses:
+            200:
+              description: Successfully set the configuration
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        $ref: '#/components/schemas/EmbeddedChartResponseSchema'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        put:
+          description: >-
+            Sets a chart's embedded configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: id_or_uuid
+            description: The chart id or uuid
+          requestBody:
+            description: The embedded configuration to set
+            required: true
+            content:
+              application/json:
+                schema: EmbeddedChartConfigSchema
+          responses:
+            200:
+              description: Successfully set the configuration
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        $ref: '#/components/schemas/EmbeddedChartResponseSchema'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            chart = ChartDAO.get_by_id_or_uuid(id_or_uuid)
+        except ChartNotFoundError:
+            return self.response_404()
+
+        try:
+            body = self.embedded_config_schema.load(request.json)
+
+            embedded = EmbeddedChartDAO.upsert(
+                chart,
+                body["allowed_domains"],
+            )
+            db.session.commit()  # pylint: disable=consider-using-transaction
+
+            result = self.embedded_response_schema.dump(embedded)
+            return self.response(200, result=result)
+        except ValidationError as error:
+            db.session.rollback()  # pylint: disable=consider-using-transaction
+            return self.response_400(message=error.messages)
+

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Missing Permission Decorator</b></div>
   <div id="fix">
   
   The set_embedded endpoint lacks a @permission_name decorator, unlike get_embedded ('read') and delete_embedded ('set_embedded'). This could allow unauthorized config changes if permissions aren't enforced elsewhere.
   </div>
   
   
   </div>
   
   <details>
   <summary><b>Citations</b></summary>
   <ul>
   
   <li>
   Rule Violated: <a href="https://github.com/apache/superset/blob/66ad889/AGENTS.md#L85">AGENTS.md:85</a>
   </li>
   
   </ul>
   </details>
   
   
   
   
   <small><i>Code Review Run #4834de</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset/config.py:
##########
@@ -591,8 +593,6 @@ class D3TimeFormat(TypedDict, total=False):
     "CACHE_IMPERSONATION": False,
     # Enable caching per user key for Superset cache (not database cache impersonation)
     "CACHE_QUERY_BY_USER": False,

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Breaking change in stable feature flag</b></div>
   <div id="fix">
   
   Removing the stable EMBEDDABLE_CHARTS feature flag from DEFAULT_FEATURE_FLAGS disables chart embedding by default, which is a breaking change for users relying on the default configuration. According to FEATURE_FLAGS.md and official docs, this flag is still stable and not deprecated. If this removal is intentional, consider adding a deprecation notice first.
   </div>
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #4834de</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset-frontend/src/dashboard/components/EmbeddedChartModal/index.tsx:
##########
@@ -0,0 +1,289 @@
+/**
+ * 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, useState } from 'react';
+import { logging, makeApi, SupersetApiError, t } from '@superset-ui/core';
+import { styled, css, Alert } from '@apache-superset/core/ui';
+import {
+  Button,
+  FormItem,
+  InfoTooltip,
+  Input,
+  Modal,
+  Loading,
+  Form,
+  Space,
+} from '@superset-ui/core/components';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { Typography } from '@superset-ui/core/components/Typography';
+import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
+
+type Props = {
+  chartId: number;
+  formData: Record<string, unknown>;
+  show: boolean;
+  onHide: () => void;
+};
+
+type EmbeddedChart = {
+  uuid: string;
+  allowed_domains: string[];
+  chart_id: number;
+  changed_on: string;
+};
+
+type EmbeddedApiPayload = { allowed_domains: string[] };
+
+const stringToList = (stringyList: string): string[] =>
+  stringyList.split(/(?:\s|,)+/).filter(x => x);
+
+const ButtonRow = styled.div`
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+`;
+
+export const ChartEmbedControls = ({ chartId, onHide }: Props) => {
+  const { addInfoToast, addDangerToast } = useToasts();
+  const [ready, setReady] = useState(true);
+  const [loading, setLoading] = useState(false);
+  const [embedded, setEmbedded] = useState<EmbeddedChart | null>(null);
+  const [allowedDomains, setAllowedDomains] = useState<string>('');
+  const [showDeactivateConfirm, setShowDeactivateConfirm] = useState(false);
+
+  const endpoint = `/api/v1/chart/${chartId}/embedded`;
+  const isDirty =
+    !embedded ||
+    stringToList(allowedDomains).join() !== embedded.allowed_domains.join();
+
+  const enableEmbedded = useCallback(() => {
+    setLoading(true);
+    makeApi<EmbeddedApiPayload, { result: EmbeddedChart }>({
+      method: 'POST',
+      endpoint,
+    })({
+      allowed_domains: stringToList(allowedDomains),
+    })
+      .then(
+        ({ result }) => {
+          setEmbedded(result);
+          setAllowedDomains(result.allowed_domains.join(', '));
+          addInfoToast(t('Changes saved.'));
+        },
+        err => {
+          logging.error(err);
+          addDangerToast(
+            t('Sorry, something went wrong. The changes could not be saved.'),
+          );
+        },
+      )
+      .finally(() => {
+        setLoading(false);
+      });
+  }, [endpoint, allowedDomains, addInfoToast, addDangerToast]);
+
+  const disableEmbedded = useCallback(() => {
+    setShowDeactivateConfirm(true);
+  }, []);
+
+  const confirmDeactivate = useCallback(() => {
+    setLoading(true);
+    makeApi<object>({ method: 'DELETE', endpoint })({})
+      .then(
+        () => {
+          setEmbedded(null);
+          setAllowedDomains('');
+          setShowDeactivateConfirm(false);
+          addInfoToast(t('Embedding deactivated.'));
+          onHide();
+        },
+        err => {
+          logging.error(err);
+          addDangerToast(
+            t(
+              'Sorry, something went wrong. Embedding could not be deactivated.',
+            ),
+          );
+        },
+      )
+      .finally(() => {
+        setLoading(false);
+      });
+  }, [endpoint, addInfoToast, addDangerToast, onHide]);
+
+  useEffect(() => {
+    setReady(false);
+    makeApi<object, { result: EmbeddedChart }>({
+      method: 'GET',
+      endpoint,
+    })({})
+      .catch(err => {
+        if ((err as SupersetApiError).status === 404) {
+          return { result: null };
+        }
+        addDangerToast(t('Sorry, something went wrong. Please try again.'));
+        throw err;
+      })
+      .then(({ result }) => {
+        setReady(true);
+        setEmbedded(result);
+        setAllowedDomains(result ? result.allowed_domains.join(', ') : '');
+      });

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Error Handling Issue</b></div>
   <div id="fix">
   
   The useEffect fails to set ready to true on API errors, potentially leaving the component stuck in loading state. Add a .finally block to ensure ready is always set.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ```
    @@ -141,6 +141,7 @@
           .then(({ result }) => {
             setReady(true);
             setEmbedded(result);
             setAllowedDomains(result ? result.allowed_domains.join(', ') : '');
           })
    +      .finally(() => setReady(true));
    
   ```
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #4834de</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4050199016

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932133595


##########
superset/mcp_service/embedded_chart/schemas.py:
##########
@@ -0,0 +1,101 @@
+# 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.
+from datetime import datetime
+from typing import Any
+
+from pydantic import BaseModel, Field
+
+# Import the same ChartConfig used by generate_chart
+from superset.mcp_service.chart.schemas import ChartConfig
+
+
+class GetEmbeddableChartRequest(BaseModel):
+    """Request schema for get_embeddable_chart tool.
+
+    Uses the same simplified ChartConfig schema as generate_chart for consistency.
+    """
+
+    datasource_id: int | str = Field(
+        ...,
+        description="Dataset ID (numeric) or UUID",
+    )
+    config: ChartConfig = Field(
+        ...,
+        description=(
+            "Chart configuration using simplified schema. Use chart_type='xy' for "
+            "line/bar/area/scatter charts, or chart_type='table' for tables. "
+            'Example: {"chart_type": "xy", "x": {"name": "genre"}, '
+            '"y": [{"name": "sales", "aggregate": "SUM"}], "kind": "bar"}'
+        ),
+    )
+    ttl_minutes: int = Field(
+        default=60,
+        ge=1,
+        le=10080,  # max 1 week
+        description="Permalink TTL in minutes (default: 60, max: 10080 = 1 week)",
+    )
+    height: int = Field(
+        default=400,
+        ge=100,
+        le=2000,
+        description="Chart height in pixels for iframe (default: 400)",
+    )
+    rls_rules: list[dict[str, Any]] = Field(
+        default_factory=list,
+        description="Row-level security rules to apply to the guest token",
+    )
+    allowed_domains: list[str] = Field(

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Missing URL validation</b></div>
   <div id="fix">
   
   The allowed_domains field accepts any strings, but for security in embedding, it should validate that inputs are valid HTTP URLs. Using pydantic's HttpUrl type would prevent invalid domains from being accepted.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ```
    - from pydantic import BaseModel, Field
    + from pydantic import BaseModel, Field, HttpUrl
    @@ -57,12 +57,12 @@
    -    rls_rules: list[dict[str, Any]] = Field(
    -        default_factory=list,
    -        description="Row-level security rules to apply to the guest token",
    -    )
    -    allowed_domains: list[str] = Field(
    -        default_factory=list,
    -        description=(
    -            "List of domains allowed to embed this chart. "
    -            "If empty, any domain can embed (less secure). "
    -            "Example: ['https://example.com', 'https://app.example.com']"
    -        ),
    -    )
    +    rls_rules: list[dict[str, Any]] = Field(
    +        default_factory=list,
    +        description="Row-level security rules to apply to the guest token",
    -    )
    +    allowed_domains: list[HttpUrl] = Field(
    +        default_factory=list,
    +        description=(
    +            "List of domains allowed to embed this chart. "
    +            "If empty, any domain can embed (less secure). "
    +            "Example: ['https://example.com', 'https://app.example.com']"
    -        ),
    -    )
   ```
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #1ea619</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,139 @@
+# 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, current_user, 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.exceptions import KeyValueParseKeyError
+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 or (not parsed2.netloc and ":" in url2):
+        # url2 is a bare domain or host:port (e.g., "localhost:3000"),
+        # which urlparse misinterprets. Compare directly with 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, KeyValueParseKeyError) as ex:
+            logger.warning("Error fetching permalink for referrer validation: %s", ex)
+            permalink_value = None
+

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Security: Invalid permalink bypasses domain check</b></div>
   <div id="fix">
   
   The code skips domain validation when permalink_value is None, creating a security vulnerability where invalid permalink keys allow unrestricted access to embedded charts. Add a check to abort with 404 for invalid permalinks, consistent with how invalid embedded objects are handled elsewhere.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
               permalink_value = None
    
           # Validate request referrer against allowed domains (if configured)
           if not permalink_value:
               abort(404)
   
           # Validate request referrer against allowed domains (if configured)
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #1ea619</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932056295


##########
superset/config.py:
##########
@@ -730,7 +731,7 @@ class D3TimeFormat(TypedDict, total=False):
     # Enable sharing charts with embedding
     # @lifecycle: stable
     # @category: runtime_config
-    "EMBEDDABLE_CHARTS": True,
+    "EMBEDDABLE_CHARTS": False,

Review Comment:
   Thanks for flagging this. The `EMBEDDABLE_CHARTS` feature flag is set to `True` by default in the current code (`superset/config.py:739`), so this concern has been addressed.



##########
superset/mcp_service/chart/chart_utils.py:
##########
@@ -39,6 +40,117 @@
 from superset.mcp_service.utils.url_utils import get_superset_base_url
 from superset.utils import json
 
+# =============================================================================
+# Visualization Type Constants
+# =============================================================================
+
+# Time series viz types that require a datetime column on the x-axis
+TIMESERIES_VIZ_TYPES = {
+    "echarts_timeseries",
+    "echarts_timeseries_line",
+    "echarts_timeseries_bar",
+    "echarts_timeseries_area",
+    "echarts_timeseries_scatter",
+    "echarts_timeseries_smooth",
+    "echarts_timeseries_step",
+    "mixed_timeseries",

Review Comment:
   Good catch. The code already uses `echarts_area` (not `echarts_timeseries_area`) in all viz type sets and mappings. This was corrected in a prior revision.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2695414296


##########
superset/charts/schemas.py:
##########
@@ -345,6 +345,17 @@ class ChartGetCachedScreenshotResponseSchema(Schema):
     )
 
 
+class EmbeddedChartConfigSchema(Schema):
+    allowed_domains = fields.List(fields.String(), required=True)
+
+
+class EmbeddedChartResponseSchema(Schema):
+    uuid = fields.String()
+    allowed_domains = fields.List(fields.String())
+    chart_id = fields.Integer()
+    changed_on = fields.DateTime()

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Schema field type inconsistency</b></div>
   <div id="fix">
   
   The uuid field should use fields.UUID() to ensure proper UUID validation during serialization, matching the model's UUIDType column. Similarly, chart_id should use fields.Int() for consistency with other schemas like ChartGetResponseSchema, as fields.Integer() is an alias but not the standard used in this file.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
   class EmbeddedChartResponseSchema(Schema):
       uuid = fields.UUID()
       allowed_domains = fields.List(fields.String())
       chart_id = fields.Int()
       changed_on = fields.DateTime()
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #5db474</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,304 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
+
+            # Generate guest token
+            username = g.user.username if hasattr(g, "user") and g.user else "anonymous"
+            guest_user: GuestTokenUser = {
+                "username": f"embed_{username}",
+                "first_name": "Embed",
+                "last_name": "User",
+            }
+
+            resources: list[GuestTokenResource] = [
+                {
+                    "type": GuestTokenResourceType.CHART_PERMALINK,
+                    "id": permalink_key,
+                }
+            ]
+
+            rls_rules: list[GuestTokenRlsRule] = []
+
+            guest_token_result = security_manager.create_guest_access_token(
+                user=guest_user,
+                resources=resources,
+                rls=rls_rules,
+            )
+
+            # Handle both bytes (older PyJWT) and string (PyJWT 2.0+)
+            guest_token = (
+                guest_token_result.decode("utf-8")
+                if isinstance(guest_token_result, bytes)
+                else guest_token_result
+            )
+
+            # Build iframe URL using request host
+            base_url = request.host_url.rstrip("/")
+            iframe_url = f"{base_url}/embedded/chart/?permalink_key={permalink_key}"

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Incorrect iframe URL format</b></div>
   <div id="fix">
   
   The iframe_url uses a query parameter (?permalink_key=), but the GET endpoint expects a path parameter (/<permalink_key>). This mismatch will cause embedded charts to fail loading, as the URL won't match the exposed route.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
               # Build iframe URL using request host
               base_url = request.host_url.rstrip("/")
               iframe_url = f"{base_url}/embedded/chart/{permalink_key}"
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #5db474</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "github-advanced-security[bot] (via GitHub)" <gi...@apache.org>.
github-advanced-security[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2790293185


##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,548 @@
+# 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 len(request.allowed_domains) == 2
+        assert "https://example.com" in request.allowed_domains

Review Comment:
   ## Incomplete URL substring sanitization
   
   The string [https://example.com](1) may be at an arbitrary position in the sanitized URL.
   
   [Show more details](https://github.com/apache/superset/security/code-scanning/2233)



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3880942438

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2790732887


##########
superset/security/manager.py:
##########
@@ -2834,6 +2842,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError() from None

Review Comment:
   <!-- Bito Reply -->
   Yes, replacing the broad `except Exception` with specific `ExplorePermalinkGetFailedError, ValueError` addresses the review comment about overly broad exception handling. This improves error specificity without masking unexpected issues.



##########
superset/security/manager.py:
##########
@@ -2834,6 +2838,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError()

Review Comment:
   <!-- Bito Reply -->
   The implemented change improves exception handling by catching the specific exceptions `GetExplorePermalinkCommand.run()` can raise, rather than the broader `Exception`. This is more precise and aligns with the method's documented behavior.
   
   **superset/security/manager.py**
   ```
   except EmbeddedChartPermalinkNotFoundError:
                       raise
                   except (ExplorePermalinkGetFailedError, ValueError) as ex:
                       raise EmbeddedChartPermalinkNotFoundError() from ex
   ```



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3724697903

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3727193962

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "villebro (via GitHub)" <gi...@apache.org>.
villebro commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3720413278

   @aminghadersohi qq, WRT the `EMBEDDABLE_CHARTS_MCP` FF, is that expected to be long lived, or just temporary? We have lots of FFs, and as MCP is already feature flagged, I think it may make more sense to make this a generic config flag to configure if this tool gets exposed or not when MCP is enabled, rather than adding yet another FF.


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3720494555

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2666512910


##########
superset-frontend/webpack.config.js:
##########
@@ -300,6 +300,7 @@ const config = {
     menu: addPreamble('src/views/menu.tsx'),
     spa: addPreamble('/src/views/index.tsx'),
     embedded: addPreamble('/src/embedded/index.tsx'),
+    embeddedChart: addPreamble('/src/embeddedChart/index.tsx'),
   },

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Path format inconsistency</b></div>
   <div id="fix">
   
   The new embeddedChart entry uses a leading slash in the path, which differs from the 'menu' entry and causes path.join(APP_DIR, '/src/...') to return an absolute path '/src/...' instead of the intended APP_DIR-relative path. This inconsistency could lead to webpack failing to resolve the file correctly. Change to 'src/embeddedChart/index.tsx' for consistency.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
       menu: addPreamble('src/views/menu.tsx'),
       spa: addPreamble('/src/views/index.tsx'),
       embedded: addPreamble('/src/embedded/index.tsx'),
       embeddedChart: addPreamble('src/embeddedChart/index.tsx'),
     },
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #e877b0</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3716610954

   CodeAnt AI finished reviewing your PR.


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891600171


##########
superset/security/manager.py:
##########
@@ -2834,6 +2838,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError()

Review Comment:
   <!-- Bito Reply -->
   The fix in commit a8b866e changes the exception handling in `validate_guest_token_resources` to use `from None`, suppressing the exception chain to avoid leaking internal error details in tracebacks. This improves security by not exposing underlying errors.
   
   **superset/security/manager.py**
   ```
   except (ExplorePermalinkGetFailedError, ValueError) as ex:
                       raise EmbeddedChartPermalinkNotFoundError() from ex
   ```
   
   **superset/security/manager.py**
   ```
   except (ValueError, KeyError, AttributeError):
                       raise EmbeddedChartPermalinkNotFoundError() from None
   ```



##########
docs/docs/configuration/databases.mdx:
##########
@@ -1137,6 +1194,28 @@ More information about PostgreSQL connection options can be found in the
 and the
 [PostgreSQL docs](https://www.postgresql.org/docs/9.1/libpq-connect.html#LIBPQ-PQCONNECTDBPARAMS).
 
+:::resources
+- [Blog: Data Visualization in PostgreSQL With Apache Superset](https://www.tigerdata.com/blog/data-visualization-in-postgresql-with-apache-superset)
+:::
+
+#### QuestDB
+
+[QuestDB](https://questdb.io/) is a high-performance, open-source time-series database with SQL support.
+The recommended connector library is the PostgreSQL driver [psycopg2](https://www.psycopg.org/docs/),
+as QuestDB supports the PostgreSQL wire protocol.
+
+The connection string is formatted as follows:
+
+```
+postgresql+psycopg2://{username}:{password}@{hostname}:{port}/{database}
+```
+
+The default port for QuestDB's PostgreSQL interface is `8812`.
+
+:::resources
+- [QuestDB Docs: Apache Superset Integration](https://questdb.com/docs/third-party-tools/superset/)

Review Comment:
   <!-- Bito Reply -->
   Yes, the pull request does not include any changes to docs/docs/configuration/databases.mdx, so any issues in that documentation file are pre-existing and not introduced by this PR.



##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,116 @@
+# 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 Any, Callable, cast
+
+from flask import abort, current_app, request
+from flask_appbuilder import expose
+from flask_login import AnonymousUserMixin, login_user
+from flask_wtf.csrf import same_origin
+
+from superset import event_logger, is_feature_enabled
+from superset.commands.explore.permalink.get import GetExplorePermalinkCommand
+from superset.superset_typing import FlaskResponse
+from superset.utils import json
+from superset.views.base import BaseSupersetView, common_bootstrap_payload
+
+logger = logging.getLogger(__name__)
+
+
+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.
+        """
+        if not is_feature_enabled("EMBEDDABLE_CHARTS_MCP"):
+            abort(404)
+
+        # 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)
+
+        # Fetch permalink to get allowed_domains for referrer validation
+        try:
+            permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+        except Exception:
+            logger.exception("Error fetching permalink for embedded chart")
+            permalink_value = None
+
+        if not permalink_value:
+            logger.warning("Permalink not found for embedded chart: %s", permalink_key)
+            abort(404)
+
+        assert permalink_value is not None  # for mypy

Review Comment:
   <!-- Bito Reply -->
   You're right—the code in view.py now uses `if permalink_value is None: abort(500)` instead of an assert statement for better production error handling. The `login_user(AnonymousUserMixin(), force=True)` is indeed a Flask-Login API call to log in an anonymous user for page rendering, not an assertion.



##########
superset-frontend/webpack.config.js:
##########
@@ -300,6 +300,7 @@ const config = {
     menu: addPreamble('src/views/menu.tsx'),
     spa: addPreamble('/src/views/index.tsx'),
     embedded: addPreamble('/src/embedded/index.tsx'),
+    embeddedChart: addPreamble('/src/embeddedChart/index.tsx'),
   },

Review Comment:
   <!-- Bito Reply -->
   The PR adds the embeddedChart entry as `addPreamble('src/embeddedChart/index.tsx')` without a leading slash, consistent with the other entries (menu, spa, embedded) in the webpack config.



##########
superset-frontend/src/dashboard/components/EmbeddedChartModal/index.tsx:
##########
@@ -0,0 +1,289 @@
+/**
+ * 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, useState } from 'react';
+import { logging, makeApi, SupersetApiError, t } from '@superset-ui/core';
+import { styled, css, Alert } from '@apache-superset/core/ui';
+import {
+  Button,
+  FormItem,
+  InfoTooltip,
+  Input,
+  Modal,
+  Loading,
+  Form,
+  Space,
+} from '@superset-ui/core/components';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { Typography } from '@superset-ui/core/components/Typography';
+import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
+
+type Props = {
+  chartId: number;
+  formData: Record<string, unknown>;
+  show: boolean;
+  onHide: () => void;
+};
+
+type EmbeddedChart = {
+  uuid: string;
+  allowed_domains: string[];
+  chart_id: number;
+  changed_on: string;
+};
+
+type EmbeddedApiPayload = { allowed_domains: string[] };
+
+const stringToList = (stringyList: string): string[] =>
+  stringyList.split(/(?:\s|,)+/).filter(x => x);
+
+const ButtonRow = styled.div`
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+`;
+
+export const ChartEmbedControls = ({ chartId, onHide }: Props) => {
+  const { addInfoToast, addDangerToast } = useToasts();
+  const [ready, setReady] = useState(true);
+  const [loading, setLoading] = useState(false);
+  const [embedded, setEmbedded] = useState<EmbeddedChart | null>(null);
+  const [allowedDomains, setAllowedDomains] = useState<string>('');
+  const [showDeactivateConfirm, setShowDeactivateConfirm] = useState(false);
+
+  const endpoint = `/api/v1/chart/${chartId}/embedded`;
+  const isDirty =
+    !embedded ||
+    stringToList(allowedDomains).join() !== embedded.allowed_domains.join();
+
+  const enableEmbedded = useCallback(() => {
+    setLoading(true);
+    makeApi<EmbeddedApiPayload, { result: EmbeddedChart }>({
+      method: 'POST',
+      endpoint,
+    })({
+      allowed_domains: stringToList(allowedDomains),
+    })
+      .then(
+        ({ result }) => {
+          setEmbedded(result);
+          setAllowedDomains(result.allowed_domains.join(', '));
+          addInfoToast(t('Changes saved.'));
+        },
+        err => {
+          logging.error(err);
+          addDangerToast(
+            t('Sorry, something went wrong. The changes could not be saved.'),
+          );
+        },
+      )
+      .finally(() => {
+        setLoading(false);
+      });
+  }, [endpoint, allowedDomains, addInfoToast, addDangerToast]);
+
+  const disableEmbedded = useCallback(() => {
+    setShowDeactivateConfirm(true);
+  }, []);
+
+  const confirmDeactivate = useCallback(() => {
+    setLoading(true);
+    makeApi<object>({ method: 'DELETE', endpoint })({})
+      .then(
+        () => {
+          setEmbedded(null);
+          setAllowedDomains('');
+          setShowDeactivateConfirm(false);
+          addInfoToast(t('Embedding deactivated.'));
+          onHide();
+        },
+        err => {
+          logging.error(err);
+          addDangerToast(
+            t(
+              'Sorry, something went wrong. Embedding could not be deactivated.',
+            ),
+          );
+        },
+      )
+      .finally(() => {
+        setLoading(false);
+      });
+  }, [endpoint, addInfoToast, addDangerToast, onHide]);
+
+  useEffect(() => {
+    setReady(false);
+    makeApi<object, { result: EmbeddedChart }>({
+      method: 'GET',
+      endpoint,
+    })({})
+      .catch(err => {
+        if ((err as SupersetApiError).status === 404) {
+          return { result: null };
+        }
+        addDangerToast(t('Sorry, something went wrong. Please try again.'));
+        throw err;
+      })
+      .then(({ result }) => {
+        setReady(true);
+        setEmbedded(result);
+        setAllowedDomains(result ? result.allowed_domains.join(', ') : '');
+      });

Review Comment:
   <!-- Bito Reply -->
   This change adds a `.finally()` block to ensure the `ready` state is set to `true` after the initial fetch, regardless of success or failure. Combined with setting `ready` to `false` at the start of the effect, it displays the loading spinner during the API call.
   
   **superset-frontend/src/dashboard/components/EmbeddedChartModal/index.tsx**
   ```
   .then(({ result }) => {
           setReady(true);
           setEmbedded(result);
           setAllowedDomains(result ? result.allowed_domains.join(', ') : '');
         })
         .finally(() => setReady(true));
   ```



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891605622


##########
superset/config.py:
##########
@@ -591,8 +593,6 @@ class D3TimeFormat(TypedDict, total=False):
     "CACHE_IMPERSONATION": False,
     # Enable caching per user key for Superset cache (not database cache impersonation)
     "CACHE_QUERY_BY_USER": False,

Review Comment:
   <!-- Bito Reply -->
   This change updates the EMBEDDABLE_CHARTS feature flag in superset/config.py from True to False by default. It makes chart embedding opt-in, requiring operators to explicitly enable it after setting up a secure GUEST_TOKEN_JWT_SECRET. This addresses security concerns by preventing accidental exposure of embedded charts without proper configuration.



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,304 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
+
+            # Generate guest token
+            username = g.user.username if hasattr(g, "user") and g.user else "anonymous"
+            guest_user: GuestTokenUser = {
+                "username": f"embed_{username}",
+                "first_name": "Embed",
+                "last_name": "User",
+            }
+
+            resources: list[GuestTokenResource] = [
+                {
+                    "type": GuestTokenResourceType.CHART_PERMALINK,
+                    "id": permalink_key,
+                }
+            ]
+
+            rls_rules: list[GuestTokenRlsRule] = []
+
+            guest_token_result = security_manager.create_guest_access_token(
+                user=guest_user,
+                resources=resources,
+                rls=rls_rules,
+            )
+
+            # Handle both bytes (older PyJWT) and string (PyJWT 2.0+)
+            guest_token = (
+                guest_token_result.decode("utf-8")
+                if isinstance(guest_token_result, bytes)
+                else guest_token_result
+            )
+
+            # Build iframe URL using request host
+            base_url = request.host_url.rstrip("/")
+            iframe_url = f"{base_url}/embedded/chart/?permalink_key={permalink_key}"

Review Comment:
   <!-- Bito Reply -->
   The explanation is accurate. The iframe URL points to `/embedded/chart/?permalink_key=xxx` for serving the HTML page via EmbeddedChartView, while the REST API endpoint `/<permalink_key>` delivers chart formData to the embedded JavaScript.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891912825


##########
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:
   Fixed in ee13c3f. Added `navigator.clipboard?.writeText` guard with fallback error toast for non-secure contexts.



##########
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:
   No change needed. `chart.embedded` is a SQLAlchemy relationship which returns a list (defined as `embedded = relationship("EmbeddedChart", ...)` in `models/slice.py:123`). `BaseDAO.delete()` expects `list[T]` and `chart.embedded` already is a list, so no wrapping is needed.



##########
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:
   Fixed in ee13c3f. Added `if not current_user.is_authenticated:` guard before calling `login_user(AnonymousUserMixin(), force=True)`. This prevents overwriting an existing authenticated session when a logged-in user visits the embedded chart URL directly.



##########
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:
   Fixed in ee13c3f. Changed to use `.value` for the comparison: `r.get("type") == GuestTokenResourceType.CHART_PERMALINK.value`. While both work with StrEnum (string equality), using `.value` is more explicit and consistent with the pattern in `api.py:72` and `security/manager.py:2938`.



##########
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:
   No change needed. Pydantic v2's `ValidationError` is a subclass of `ValueError` (`issubclass(ValidationError, ValueError)` returns `True`), so `pytest.raises(ValueError)` correctly catches Pydantic validation errors. This is by design in Pydantic v2 for backwards compatibility.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2892231067


##########
superset/embedded_chart/exceptions.py:
##########
@@ -0,0 +1,35 @@
+# 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.
+from superset.exceptions import SupersetException
+
+
+class EmbeddedChartPermalinkNotFoundError(SupersetException):
+    """Raised when an embedded chart permalink is not found or has expired."""
+
+    message = "The embedded chart permalink could not be found or has expired."

Review Comment:
   <!-- Bito Reply -->
   The fix adds i18n support with lazy_gettext and appropriate HTTP status codes. EmbeddedChartAccessDeniedError correctly uses 401 (matching the response_401() in the code), while PermalinkNotFound uses 404 and FeatureDisabled uses 403.



##########
superset/views/embedded_charts.py:
##########
@@ -0,0 +1,34 @@
+# 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.
+from flask_appbuilder import expose, has_access
+
+from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP
+from superset.superset_typing import FlaskResponse
+from superset.views.base import BaseSupersetView
+
+
+class EmbeddedChartsView(BaseSupersetView):
+    """View for managing embedded charts list."""
+
+    route_base = "/embeddedcharts"
+    class_permission_name = "Chart"
+    method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP
+
+    @expose("/list/")
+    @has_access
+    def list(self) -> FlaskResponse:
+        return super().render_app_template()

Review Comment:
   <!-- Bito Reply -->
   No, the suggestion isn’t fully correct — EmbeddedChartsView renders the admin list page at `/embeddedcharts/list/` within the main Superset SPA, so it correctly uses the default entry. The embedded chart iframe page is served by EmbeddedChartView in `embedded_chart/view.py`, which uses entry="embeddedChart".



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894550959


##########
superset-frontend/src/explore/components/EmbedCodeContent.tsx:
##########
@@ -57,43 +61,73 @@ const EmbedCodeContent: FC<EmbedCodeContentProps> = ({
     }
   }, []);
 
-  const updateUrl = useCallback(() => {
-    setUrl('');
-    if (!formData?.datasource) return;
-    getChartPermalink(formData as { datasource: string })
-      .then(result => {
-        if (result?.url) {
-          setUrl(result.url);
-          setErrorMessage('');
-        }
-      })
-      .catch(() => {
-        setErrorMessage(t('Error'));
-        addDangerToast?.(t('Sorry, something went wrong. Try again later.'));
+  const generateEmbedCode = useCallback(async () => {
+    if (!formData) return;
+
+    setLoading(true);
+    setErrorMessage('');
+
+    try {
+      const createEmbeddedChart = makeApi<
+        {
+          form_data: LatestQueryFormData;
+          allowed_domains: string[];
+          ttl_minutes: number;
+        },
+        EmbedData
+      >({
+        method: 'POST',
+        endpoint: '/api/v1/embedded_chart/',
       });
-  }, [addDangerToast, formData]);
+
+      const response = await createEmbeddedChart({
+        form_data: formData,
+        allowed_domains: [],
+        ttl_minutes: 60,
+      });
+
+      setEmbedData(response);
+    } catch {
+      setErrorMessage(t('Error generating embed code'));
+      addDangerToast?.(t('Sorry, something went wrong. Try again later.'));
+    } finally {
+      setLoading(false);
+    }
+  }, [formData, addDangerToast]);
 
   useEffect(() => {
-    updateUrl();
-  }, []);
+    generateEmbedCode();
+  }, [generateEmbedCode]);
 
   const html = useMemo(() => {
-    if (!url) return '';
-    const srcLink = `${url}?${URL_PARAMS.standalone.name}=1&height=${height}`;
-    return (
-      '<iframe\n' +
-      `  width="${width}"\n` +
-      `  height="${height}"\n` +
-      '  seamless\n' +
-      '  frameBorder="0"\n' +
-      '  scrolling="no"\n' +
-      `  src="${srcLink}"\n` +
-      '>\n' +
-      '</iframe>'
+    if (!embedData?.iframe_url || !embedData?.guest_token) return '';
+
+    const { origin } = new URL(embedData.iframe_url);
+
+    return `<!-- Superset Embedded Chart -->
+<iframe
+  id="superset-chart"
+  src="${embedData.iframe_url}"
+  width="${width}"
+  height="${height}"
+  frameborder="0"
+  data-guest-token="${embedData.guest_token}"
+  sandbox="allow-scripts allow-same-origin allow-popups"
+></iframe>
+<script>
+  document.getElementById('superset-chart').onload = function() {
+    this.contentWindow.postMessage(
+      { type: '__embedded_comms__', guestToken: '${embedData.guest_token}' },
+      '${origin}'
     );
-  }, [height, url, width]);
+  };
+</script>`;

Review Comment:
   **Suggestion:** The embed HTML hard-codes the iframe id as `superset-chart` and uses `document.getElementById` to attach the onload handler, so if a page embeds more than one chart, all iframes will share the same id and only the first iframe will receive the guest token message, causing other embedded charts on the same page to fail to authenticate and render correctly. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Multiple embedded charts on same page fail authentication.
   - ⚠️ Only one chart per page reliably supports guest tokens.
   ```
   </details>
   
   ```suggestion
       return `<!-- Superset Embedded Chart -->
       <iframe
         src="${embedData.iframe_url}"
         width="${width}"
         height="${height}"
         frameborder="0"
         data-guest-token="${embedData.guest_token}"
         sandbox="allow-scripts allow-same-origin allow-popups"
       ></iframe>
       <script>
         (function() {
           var script = document.currentScript;
           var iframe = script && script.previousElementSibling;
           if (!iframe || iframe.tagName !== 'IFRAME') {
             return;
           }
           iframe.onload = function() {
             this.contentWindow.postMessage(
               { type: '__embedded_comms__', guestToken: '${embedData.guest_token}' },
               '${origin}'
             );
           };
         })();
       </script>`;
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. In the Superset UI, open a chart's Share → Embed flow which renders `EmbedCodeContent`
   in `superset-frontend/src/explore/components/EmbedCodeContent.tsx:41-211` and generates
   the HTML string in the `useMemo` block at lines 102-124.
   
   2. Copy the generated embed HTML for Chart A (includes `<iframe id="superset-chart">` and
   the inline `<script>` calling `document.getElementById('superset-chart')`).
   
   3. Repeat for Chart B, then place both embed snippets sequentially into the same external
   HTML page so the DOM contains two `<iframe id="superset-chart">` elements (duplicate IDs)
   and two identical inline scripts.
   
   4. Load that HTML page in a browser: each script executes
   `document.getElementById('superset-chart')` (line 114) which always returns the first
   iframe in DOM order, so only the first iframe receives the `postMessage` with a guest
   token; the second chart never gets its token and fails to authenticate/render.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/explore/components/EmbedCodeContent.tsx
   **Line:** 107:124
   **Comment:**
   	*Logic Error: The embed HTML hard-codes the iframe id as `superset-chart` and uses `document.getElementById` to attach the onload handler, so if a page embeds more than one chart, all iframes will share the same id and only the first iframe will receive the guest token message, causing other embedded charts on the same page to fail to authenticate and render correctly.
   
   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=6b0404bd5fbc9233b1bf14dd0fe95c2b4bad605120e9bd217d2ad08f19199a22&reaction=like'>👍</a> | <a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=6b0404bd5fbc9233b1bf14dd0fe95c2b4bad605120e9bd217d2ad08f19199a22&reaction=dislike'>👎</a>



##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,137 @@
+# 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, current_user, 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

Review Comment:
   **Suggestion:** In the embedded chart view, a malformed `permalink_key` query parameter can cause `decode_permalink_id` or the codec to raise an exception that is not a `ValueError` or `KeyError`, leading to an unhandled exception and a 500 error during referrer validation; broadening the exception handler to catch all exceptions ensures such bad keys are handled gracefully and simply skip referrer-based checks as intended. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Embedded chart iframe requests return 500 for bad keys.
   - ⚠️ Error handling inconsistent with missing permalink_key behavior.
   - ⚠️ Logs lack clear differentiation of invalid share links.
   ```
   </details>
   
   ```suggestion
           # 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 Exception as ex:  # pylint: disable=broad-except
               logger.warning("Error fetching permalink for referrer validation: %s", ex)
               permalink_value = None
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Run the Superset web server including `EmbeddedChartView` defined in
   `superset/embedded_chart/view.py` (route_base `/embedded/chart`, method `embedded_chart`
   at approx. lines 61–88).
   
   2. Send an HTTP GET request to `/embedded/chart/?permalink_key=abc` where `abc` is a
   malformed key that cannot be decoded by `hashids` (e.g., `curl
   http://localhost:8088/embedded/chart/?permalink_key=abc`).
   
   3. The request enters `EmbeddedChartView.embedded_chart`
   (`superset/embedded_chart/view.py:61`), reaches the block starting at line 77, and calls
   `decode_permalink_id(permalink_key, salt=salt)` from `superset/key_value/utils.py:63`.
   
   4. In `decode_permalink_id`, `hashids.Hashids.decode("abc")` returns an empty list,
   causing `KeyValueParseKeyError(_("Invalid permalink key"))` to be raised at
   `superset/key_value/utils.py:67`; this exception is *not* a `ValueError` or `KeyError`, so
   it bypasses the current `except (ValueError, KeyError)` in
   `EmbeddedChartView.embedded_chart` and propagates to Flask, resulting in an uncaught
   exception and an HTTP 500 response instead of gracefully skipping referrer validation and
   rendering or rejecting the 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:** 77:89
   **Comment:**
   	*Logic Error: In the embedded chart view, a malformed `permalink_key` query parameter can cause `decode_permalink_id` or the codec to raise an exception that is not a `ValueError` or `KeyError`, leading to an unhandled exception and a 500 error during referrer validation; broadening the exception handler to catch all exceptions ensures such bad keys are handled gracefully and simply skip referrer-based checks as intended.
   
   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=069b454cd9f4f3a365e31128ff51dd0fde2c1c4deeec059ebda0bd0683f32e56&reaction=like'>👍</a> | <a href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F36933&comment_hash=069b454cd9f4f3a365e31128ff51dd0fde2c1c4deeec059ebda0bd0683f32e56&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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894975777


##########
superset/security/manager.py:
##########
@@ -2834,6 +2842,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError() from None

Review Comment:
   Thanks for the detailed confirmation. Glad the narrowed exception handling approach looks solid.



##########
superset/config.py:
##########
@@ -591,8 +593,6 @@ class D3TimeFormat(TypedDict, total=False):
     "CACHE_IMPERSONATION": False,
     # Enable caching per user key for Superset cache (not database cache impersonation)
     "CACHE_QUERY_BY_USER": False,

Review Comment:
   Thanks for the clear summary. Making chart embedding opt-in with a proper GUEST_TOKEN_JWT_SECRET setup is the right security posture.



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,304 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
+
+            # Generate guest token
+            username = g.user.username if hasattr(g, "user") and g.user else "anonymous"
+            guest_user: GuestTokenUser = {
+                "username": f"embed_{username}",
+                "first_name": "Embed",
+                "last_name": "User",
+            }
+
+            resources: list[GuestTokenResource] = [
+                {
+                    "type": GuestTokenResourceType.CHART_PERMALINK,
+                    "id": permalink_key,
+                }
+            ]
+
+            rls_rules: list[GuestTokenRlsRule] = []
+
+            guest_token_result = security_manager.create_guest_access_token(
+                user=guest_user,
+                resources=resources,
+                rls=rls_rules,
+            )
+
+            # Handle both bytes (older PyJWT) and string (PyJWT 2.0+)
+            guest_token = (
+                guest_token_result.decode("utf-8")
+                if isinstance(guest_token_result, bytes)
+                else guest_token_result
+            )
+
+            # Build iframe URL using request host
+            base_url = request.host_url.rstrip("/")
+            iframe_url = f"{base_url}/embedded/chart/?permalink_key={permalink_key}"

Review Comment:
   Thanks for confirming the URL architecture. The separation between the iframe HTML page and the REST API endpoint is intentional.



##########
superset/charts/schemas.py:
##########
@@ -345,6 +345,17 @@ class ChartGetCachedScreenshotResponseSchema(Schema):
     )
 
 
+class EmbeddedChartConfigSchema(Schema):
+    allowed_domains = fields.List(fields.String(), required=True)
+
+
+class EmbeddedChartResponseSchema(Schema):
+    uuid = fields.String()
+    allowed_domains = fields.List(fields.String())
+    chart_id = fields.Integer()
+    changed_on = fields.DateTime()

Review Comment:
   Thanks for confirming the DateTime serialization compatibility between Marshmallow and the frontend TypeScript interface.



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,298 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:

Review Comment:
   Good observation. The datasource field validation happens downstream when the permalink is resolved and the chart is rendered — if the datasource is invalid, the chart will fail to load. For ttl_minutes, the Pydantic schema enforces the 1-10080 range before we reach the API handler. But adding explicit validation in the POST handler would make errors clearer and faster to surface. I'll consider adding that.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894974443


##########
docs/docs/configuration/databases.mdx:
##########
@@ -1137,6 +1194,28 @@ More information about PostgreSQL connection options can be found in the
 and the
 [PostgreSQL docs](https://www.postgresql.org/docs/9.1/libpq-connect.html#LIBPQ-PQCONNECTDBPARAMS).
 
+:::resources
+- [Blog: Data Visualization in PostgreSQL With Apache Superset](https://www.tigerdata.com/blog/data-visualization-in-postgresql-with-apache-superset)
+:::
+
+#### QuestDB
+
+[QuestDB](https://questdb.io/) is a high-performance, open-source time-series database with SQL support.
+The recommended connector library is the PostgreSQL driver [psycopg2](https://www.psycopg.org/docs/),
+as QuestDB supports the PostgreSQL wire protocol.
+
+The connection string is formatted as follows:
+
+```
+postgresql+psycopg2://{username}:{password}@{hostname}:{port}/{database}
+```
+
+The default port for QuestDB's PostgreSQL interface is `8812`.
+
+:::resources
+- [QuestDB Docs: Apache Superset Integration](https://questdb.com/docs/third-party-tools/superset/)

Review Comment:
   Correct — the docs file wasn't changed in this PR. Any issues there are pre-existing.



##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,116 @@
+# 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 Any, Callable, cast
+
+from flask import abort, current_app, request
+from flask_appbuilder import expose
+from flask_login import AnonymousUserMixin, login_user
+from flask_wtf.csrf import same_origin
+
+from superset import event_logger, is_feature_enabled
+from superset.commands.explore.permalink.get import GetExplorePermalinkCommand
+from superset.superset_typing import FlaskResponse
+from superset.utils import json
+from superset.views.base import BaseSupersetView, common_bootstrap_payload
+
+logger = logging.getLogger(__name__)
+
+
+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.
+        """
+        if not is_feature_enabled("EMBEDDABLE_CHARTS_MCP"):
+            abort(404)
+
+        # 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)
+
+        # Fetch permalink to get allowed_domains for referrer validation
+        try:
+            permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+        except Exception:
+            logger.exception("Error fetching permalink for embedded chart")
+            permalink_value = None
+
+        if not permalink_value:
+            logger.warning("Permalink not found for embedded chart: %s", permalink_key)
+            abort(404)
+
+        assert permalink_value is not None  # for mypy

Review Comment:
   Thanks for confirming the approach. Using abort(500) instead of assert is more appropriate for production error handling.



##########
superset-frontend/webpack.config.js:
##########
@@ -300,6 +300,7 @@ const config = {
     menu: addPreamble('src/views/menu.tsx'),
     spa: addPreamble('/src/views/index.tsx'),
     embedded: addPreamble('/src/embedded/index.tsx'),
+    embeddedChart: addPreamble('/src/embeddedChart/index.tsx'),
   },

Review Comment:
   Thanks for verifying consistency with the other webpack entries.



##########
superset-frontend/src/dashboard/components/EmbeddedChartModal/index.tsx:
##########
@@ -0,0 +1,289 @@
+/**
+ * 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, useState } from 'react';
+import { logging, makeApi, SupersetApiError, t } from '@superset-ui/core';
+import { styled, css, Alert } from '@apache-superset/core/ui';
+import {
+  Button,
+  FormItem,
+  InfoTooltip,
+  Input,
+  Modal,
+  Loading,
+  Form,
+  Space,
+} from '@superset-ui/core/components';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { Typography } from '@superset-ui/core/components/Typography';
+import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
+
+type Props = {
+  chartId: number;
+  formData: Record<string, unknown>;
+  show: boolean;
+  onHide: () => void;
+};
+
+type EmbeddedChart = {
+  uuid: string;
+  allowed_domains: string[];
+  chart_id: number;
+  changed_on: string;
+};
+
+type EmbeddedApiPayload = { allowed_domains: string[] };
+
+const stringToList = (stringyList: string): string[] =>
+  stringyList.split(/(?:\s|,)+/).filter(x => x);
+
+const ButtonRow = styled.div`
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+`;
+
+export const ChartEmbedControls = ({ chartId, onHide }: Props) => {
+  const { addInfoToast, addDangerToast } = useToasts();
+  const [ready, setReady] = useState(true);
+  const [loading, setLoading] = useState(false);
+  const [embedded, setEmbedded] = useState<EmbeddedChart | null>(null);
+  const [allowedDomains, setAllowedDomains] = useState<string>('');
+  const [showDeactivateConfirm, setShowDeactivateConfirm] = useState(false);
+
+  const endpoint = `/api/v1/chart/${chartId}/embedded`;
+  const isDirty =
+    !embedded ||
+    stringToList(allowedDomains).join() !== embedded.allowed_domains.join();
+
+  const enableEmbedded = useCallback(() => {
+    setLoading(true);
+    makeApi<EmbeddedApiPayload, { result: EmbeddedChart }>({
+      method: 'POST',
+      endpoint,
+    })({
+      allowed_domains: stringToList(allowedDomains),
+    })
+      .then(
+        ({ result }) => {
+          setEmbedded(result);
+          setAllowedDomains(result.allowed_domains.join(', '));
+          addInfoToast(t('Changes saved.'));
+        },
+        err => {
+          logging.error(err);
+          addDangerToast(
+            t('Sorry, something went wrong. The changes could not be saved.'),
+          );
+        },
+      )
+      .finally(() => {
+        setLoading(false);
+      });
+  }, [endpoint, allowedDomains, addInfoToast, addDangerToast]);
+
+  const disableEmbedded = useCallback(() => {
+    setShowDeactivateConfirm(true);
+  }, []);
+
+  const confirmDeactivate = useCallback(() => {
+    setLoading(true);
+    makeApi<object>({ method: 'DELETE', endpoint })({})
+      .then(
+        () => {
+          setEmbedded(null);
+          setAllowedDomains('');
+          setShowDeactivateConfirm(false);
+          addInfoToast(t('Embedding deactivated.'));
+          onHide();
+        },
+        err => {
+          logging.error(err);
+          addDangerToast(
+            t(
+              'Sorry, something went wrong. Embedding could not be deactivated.',
+            ),
+          );
+        },
+      )
+      .finally(() => {
+        setLoading(false);
+      });
+  }, [endpoint, addInfoToast, addDangerToast, onHide]);
+
+  useEffect(() => {
+    setReady(false);
+    makeApi<object, { result: EmbeddedChart }>({
+      method: 'GET',
+      endpoint,
+    })({})
+      .catch(err => {
+        if ((err as SupersetApiError).status === 404) {
+          return { result: null };
+        }
+        addDangerToast(t('Sorry, something went wrong. Please try again.'));
+        throw err;
+      })
+      .then(({ result }) => {
+        setReady(true);
+        setEmbedded(result);
+        setAllowedDomains(result ? result.allowed_domains.join(', ') : '');
+      });

Review Comment:
   Exactly — the .finally() ensures the spinner is dismissed regardless of success or failure. Glad the pattern reads well.



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

Review Comment:
   Thanks for the thorough analysis. You've captured the security model well — referrer validation is defense-in-depth while the guest token provides the primary security boundary.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4027625200

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932169790


##########
tests/unit_tests/extensions/test_types.py:
##########
@@ -61,6 +61,18 @@ def test_extension_config_full():
             "description": "A query insights extension",
             "dependencies": ["other-extension"],
             "permissions": ["can_read", "can_view"],
+            "frontend": {
+                "contributions": {
+                    "views": {
+                        "sqllab": {
+                            "panels": [
+                                {"id": "query_insights.main", "name": "Query Insights"}
+                            ]
+                        }
+                    }
+                },
+                "moduleFederation": {"exposes": ["./index"]},
+            },

Review Comment:
   This is a false positive. The test `test_extension_config_full` is testing the `ExtensionConfig` model, which does **not** have a `frontend` field — only `backend`. Looking at the Pydantic model in `superset-core/src/superset_core/extensions/types.py`:
   
   ```python
   class ExtensionConfig(BaseExtension):
       backend: ExtensionConfigBackend | None = Field(default=None, ...)
   ```
   
   The `frontend` key in the test input data (line 64) is simply ignored by Pydantic's `model_validate()` since `ExtensionConfig` doesn't define that field. The test correctly asserts on fields that `ExtensionConfig` actually has (`publisher`, `name`, `version`, `backend`, etc.). Frontend assertions exist in the separate `test_manifest_with_frontend` test (line 192), which tests the `Manifest` model that does have a `frontend` field.
   
   The `frontend` data in this test input is intentionally there to verify that extra fields don't cause validation errors — it's testing robustness, not frontend parsing.



##########
superset/mcp_service/embedded_chart/tool/get_embeddable_chart.py:
##########
@@ -0,0 +1,286 @@
+# 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: get_embeddable_chart
+
+Creates an embeddable chart iframe with guest token authentication.
+This enables AI agents to generate charts that can be displayed in
+external applications via iframe.
+"""
+
+import logging
+from datetime import datetime, timedelta, timezone
+
+from fastmcp import Context
+from flask import g
+from superset_core.mcp.decorators import tool
+
+from superset.commands.exceptions import CommandException
+from superset.mcp_service.chart.chart_utils import (
+    map_config_to_form_data,
+    resolve_dataset,
+)
+from superset.mcp_service.embedded_chart.schemas import (
+    GetEmbeddableChartRequest,
+    GetEmbeddableChartResponse,
+)
+from superset.mcp_service.utils.schema_utils import parse_request
+from superset.mcp_service.utils.url_utils import get_superset_base_url
+from superset.security.guest_token import (
+    GuestTokenResource,

Review Comment:
   The circular import concern here is incorrect. `superset.security.guest_token` is a leaf module with no imports from the `mcp_service` package, so there is no circular import risk.
   
   These TypedDicts (`GuestTokenResource`, `GuestTokenRlsRule`, `GuestTokenUser`) are used at runtime to construct dict literals with type annotations (lines 210, 216, 224):
   
   ```python
   guest_user: GuestTokenUser = {
       "username": f"mcp_embed_{username}",
       ...
   }
   resources: list[GuestTokenResource] = [
       {"type": GuestTokenResourceType.CHART_PERMALINK, "id": permalink_key}
   ]
   rls_rules: list[GuestTokenRlsRule] = [...]
   ```
   
   While local variable annotations aren't evaluated at runtime in Python 3.10+, these TypedDicts serve as documentation and enable static type checking on the dict structure. Moving them to `TYPE_CHECKING` would save a negligible import overhead for a module that has no circular dependency issue. This is consistent with how the rest of the Superset codebase imports these types (e.g., `superset/embedded_chart/api.py` lines 42-46 import the same types at the module level).



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932176161


##########
.rat-excludes:
##########
@@ -86,3 +86,4 @@ CLAUDE.md
 CURSOR.md
 GEMINI.md
 GPT.md
+embed-demo.html

Review Comment:
   <!-- Bito Reply -->
   Your explanation aligns with the PR's intent. The embed-demo.html file is a local testing artifact, and excluding it from RAT license checks follows the same logic as excluding other non-source files like *.json or *.csv that don't require ASF headers.



##########
superset/embedded_chart/api.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.
+import logging
+from datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.exceptions import CommandException
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )

Review Comment:
   <!-- Bito Reply -->
   Yes, the suggestion is valid — it catches KeyValueParseKeyError in _get_permalink_value and returns None, causing the get() method to raise EmbeddedChartPermalinkNotFoundError for invalid keys, returning a 404 instead of 500. This improves semantic correctness for malformed permalinks without affecting other error handling.
   
   **superset/embedded_chart/api.py**
   ```
   try:
           key = decode_permalink_id(permalink_key, salt=salt)
       except KeyValueParseKeyError:
           return None
       return KeyValueDAO.get_value(
           KeyValueResource.EXPLORE_PERMALINK,
           key,
           codec,
       )
   ```



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932180394


##########
superset/security/manager.py:
##########
@@ -3109,6 +3131,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.value
+            for r in user.resources
+        )

Review Comment:
   <!-- Bito Reply -->
   Yes, the code in superset/security/manager.py matches your description. The has_guest_chart_permalink_access method grants datasource access to guests with any chart_permalink resource, as documented in the docstring and TODO for performance reasons, with a planned stricter check.
   
   **superset/security/manager.py**
   ```
   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.value
               for r in user.resources
           )
   ```



##########
superset/mcp_service/embedded_chart/tool/get_embeddable_chart.py:
##########
@@ -0,0 +1,286 @@
+# 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: get_embeddable_chart
+
+Creates an embeddable chart iframe with guest token authentication.
+This enables AI agents to generate charts that can be displayed in
+external applications via iframe.
+"""
+
+import logging
+from datetime import datetime, timedelta, timezone
+
+from fastmcp import Context
+from flask import g
+from superset_core.mcp.decorators import tool
+
+from superset.commands.exceptions import CommandException
+from superset.mcp_service.chart.chart_utils import (
+    map_config_to_form_data,
+    resolve_dataset,
+)
+from superset.mcp_service.embedded_chart.schemas import (
+    GetEmbeddableChartRequest,
+    GetEmbeddableChartResponse,
+)
+from superset.mcp_service.utils.schema_utils import parse_request
+from superset.mcp_service.utils.url_utils import get_superset_base_url
+from superset.security.guest_token import (
+    GuestTokenResource,

Review Comment:
   <!-- Bito Reply -->
   The circular import concern is incorrect. `superset.security.guest_token` is a leaf module with no imports from `mcp_service`, so no circular dependency exists. Moving these TypedDicts to `TYPE_CHECKING` saves negligible overhead and isn't consistent with how the rest of Superset imports them.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi closed pull request #36933: feat(mcp): add embeddable charts with guest token authentication
URL: https://github.com/apache/superset/pull/36933


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2908608016


##########
.rat-excludes:
##########
@@ -86,3 +86,4 @@ CLAUDE.md
 CURSOR.md
 GEMINI.md
 GPT.md
+embed-demo.html

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>License Header Exclusion Inconsistency</b></div>
   <div id="fix">
   
   Excluding embed-demo.html from license checks appears inconsistent with the project's standards in .cursor/rules/dev-standard.mdc and AGENTS.md, which permit exclusions only for LLM instruction files like LLMS.md or CLAUDE.md. If this file requires a license header, it should not be excluded; consider removing this entry.
   </div>
   
   
   </div>
   
   <details>
   <summary><b>Citations</b></summary>
   <ul>
   
   <li>
   Rule Violated: <a href="https://github.com/apache/superset/blob/44c9c94/.cursor/rules/dev-standard.mdc#L48">dev-standard.mdc:48</a>
   </li>
   
   </ul>
   </details>
   
   
   
   
   <small><i>Code Review Run #2ff7b5</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset/embedded_chart/api.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.
+import logging
+from datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.exceptions import CommandException
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Missing exception import for permalink validation</b></div>
   <div id="fix">
   
   The _get_permalink_value method calls decode_permalink_id, which can raise KeyValueParseKeyError for invalid keys, but this exception isn't imported or caught, potentially leading to unhandled 500 errors instead of proper 404 responses. It looks like similar permalink APIs catch this explicitly.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
    from superset.key_value.utils import decode_permalink_id
    from superset.key_value.exceptions import KeyValueParseKeyError
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #2ff7b5</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset/embedded_chart/api.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.
+import logging
+from datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.exceptions import CommandException
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Unhandled exception in permalink decoding</b></div>
   <div id="fix">
   
   Without catching KeyValueParseKeyError here, invalid permalink keys would propagate up and cause 500 errors rather than returning None for 404 handling. Other permalink getters in the codebase handle this similarly.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
       try:
           key = decode_permalink_id(permalink_key, salt=salt)
       except KeyValueParseKeyError:
           return None
       return KeyValueDAO.get_value(
           KeyValueResource.EXPLORE_PERMALINK,
           key,
           codec,
       )
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #2ff7b5</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932177991


##########
superset/embedded_chart/api.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.
+import logging
+from datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.exceptions import CommandException
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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

Review Comment:
   <!-- Bito Reply -->
   You're correct. The KeyValueParseKeyError is already imported at line 33 and caught in the get() method's exception handler at line 167, so the concern about it not being handled does not apply.
   
   **superset/embedded_chart/api.py**
   ```
   from superset.key_value.exceptions import KeyValueParseKeyError
   ```
   
   **superset/embedded_chart/api.py**
   ```
   except (ValueError, KeyError, KeyValueParseKeyError) as ex:
       logger.warning("Error fetching embedded chart: %s", ex)
       return self.response_500()
   ```



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932603700


##########
superset/mcp_service/chart/chart_utils.py:
##########
@@ -39,6 +40,117 @@
 from superset.mcp_service.utils.url_utils import get_superset_base_url
 from superset.utils import json
 
+# =============================================================================
+# Visualization Type Constants
+# =============================================================================
+
+# Time series viz types that require a datetime column on the x-axis
+TIMESERIES_VIZ_TYPES = {
+    "echarts_timeseries",
+    "echarts_timeseries_line",
+    "echarts_timeseries_bar",
+    "echarts_area",
+    "echarts_timeseries_scatter",
+    "echarts_timeseries_smooth",
+    "echarts_timeseries_step",
+    "mixed_timeseries",
+    "line",
+    "area",
+}
+
+# Categorical viz types for bar/pie charts (use these for non-time data)
+# Note: dist_bar is deprecated, use 'bar' instead
+CATEGORICAL_VIZ_TYPES = {
+    "bar",
+    "pie",
+    "big_number",
+    "big_number_total",
+    "table",
+    "pivot_table_v2",
+}
+
+
+# =============================================================================
+# Dataset Resolution
+# =============================================================================
+
+
+def resolve_dataset(
+    dataset_id: int | str, check_access: bool = True
+) -> tuple[Any | None, str | None]:
+    """
+    Resolve a dataset by ID (numeric) or UUID with optional access checking.
+
+    Args:
+        dataset_id: Numeric dataset ID or UUID string
+        check_access: Whether to check user has access to the dataset
+
+    Returns:
+        Tuple of (dataset, error_message). If successful, error_message is None.
+        If failed, dataset is None and error_message contains the reason.
+    """
+    from superset.daos.dataset import DatasetDAO
+
+    dataset = None
+
+    if isinstance(dataset_id, int) or (
+        isinstance(dataset_id, str) and dataset_id.isdigit()
+    ):
+        numeric_id = int(dataset_id) if isinstance(dataset_id, str) else dataset_id
+        dataset = DatasetDAO.find_by_id(numeric_id)
+    else:
+        # Try UUID lookup
+        dataset = DatasetDAO.find_by_id(dataset_id, id_column="uuid")
+
+    if not dataset:
+        return None, f"Dataset not found: {dataset_id}"
+
+    if check_access and not has_dataset_access(dataset):
+        return None, "Access denied to dataset"
+
+    return dataset, None
+
+
+def validate_timeseries_config(
+    viz_type: str, form_data: Dict[str, Any], dataset: Any
+) -> str | None:
+    """
+    Validate that timeseries chart types have required datetime configuration.
+
+    Args:
+        viz_type: The visualization type
+        form_data: Chart form_data configuration
+        dataset: The dataset object
+
+    Returns:
+        Error message if validation fails, None if valid.
+    """
+    if viz_type not in TIMESERIES_VIZ_TYPES:
+        return None
+
+    has_x_axis = form_data.get("x_axis")
+    has_granularity = form_data.get("granularity_sqla")
+    has_time_column = form_data.get("time_column")
+
+    if has_x_axis or has_granularity or has_time_column:
+        return None
+
+    # Check if dataset has a main temporal column configured
+    main_dttm_col = getattr(dataset, "main_dttm_col", None)
+    if main_dttm_col:
+        return None
+
+    # Suggest appropriate categorical chart type
+    categorical_alternative = "bar" if "bar" in viz_type else "pie"
+
+    return (
+        f"Time series chart type '{viz_type}' requires a datetime column on the "
+        f"x-axis. For categorical data like 'genre', use "
+        f"viz_type='{categorical_alternative}' instead. Alternatively, add 'x_axis' "
+        f"or 'granularity_sqla' to form_data with a datetime column name."

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Error Message Inconsistency</b></div>
   <div id="fix">
   
   The validate_timeseries_config function checks for 'time_column' as a valid way to specify a datetime column, but the error message only mentions 'x_axis' and 'granularity_sqla' as alternatives. This inconsistency could confuse users trying to fix the configuration.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
       f"viz_type='{categorical_alternative}' instead. Alternatively, add 'x_axis' "
       f"or 'granularity_sqla' or 'time_column' to form_data with a datetime column name."
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #902933</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset/embedded_chart/exceptions.py:
##########
@@ -0,0 +1,40 @@
+# 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.
+from flask_babel import lazy_gettext as _
+
+from superset.exceptions import SupersetException
+
+
+class EmbeddedChartPermalinkNotFoundError(SupersetException):
+    """Raised when an embedded chart permalink is not found or has expired."""
+
+    status = 404
+    message = _("The embedded chart permalink could not be found or has expired.")
+
+
+class EmbeddedChartAccessDeniedError(SupersetException):
+    """Raised when access to an embedded chart is denied."""
+
+    status = 401

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Incorrect HTTP Status Code</b></div>
   <div id="fix">
   
   The status code for EmbeddedChartAccessDeniedError is set to 401, but this should be 403 since it represents access being denied to an authenticated user, not an authentication failure. Similar exceptions like DatasetAccessDeniedError use 403.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
       status = 403
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #902933</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932167333


##########
.rat-excludes:
##########
@@ -86,3 +86,4 @@ CLAUDE.md
 CURSOR.md
 GEMINI.md
 GPT.md
+embed-demo.html

Review Comment:
   The `embed-demo.html` entry is not inconsistent with the LLM file pattern. The PR's testing instructions direct users to paste generated iframe HTML into a local HTML file for testing. The `.rat-excludes` entry is a proactive exclusion so that if a developer creates `embed-demo.html` locally (following the testing steps), it won't fail the Apache RAT license header check. This is the same rationale as excluding `*.json`, `*.csv`, and other non-source files that cannot contain license headers — HTML files generated for testing don't need ASF headers.



##########
superset/embedded_chart/api.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.
+import logging
+from datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.exceptions import CommandException
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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

Review Comment:
   `KeyValueParseKeyError` is imported at line 33 of this file:
   
   ```python
   from superset.key_value.exceptions import KeyValueParseKeyError
   ```
   
   It's both imported and caught in the `get()` method's exception handler at line 167:
   
   ```python
   except (ValueError, KeyError, KeyValueParseKeyError) as ex:
       logger.warning("Error fetching embedded chart: %s", ex)
       return self.response_500()
   ```
   
   This concern does not apply.



##########
superset/embedded_chart/api.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.
+import logging
+from datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.exceptions import CommandException
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )

Review Comment:
   `KeyValueParseKeyError` is caught at line 167 of the `get()` method:
   
   ```python
   except (ValueError, KeyError, KeyValueParseKeyError) as ex:
       logger.warning("Error fetching embedded chart: %s", ex)
       return self.response_500()
   ```
   
   Invalid permalink keys will not propagate as unhandled 500 errors — they are caught and handled here. The `_get_permalink_value` method at line 89 calls `decode_permalink_id(permalink_key, salt=salt)`, and if that raises `KeyValueParseKeyError`, it's caught by the handler above and logged with a controlled 500 response (not an unhandled traceback). A 404 might be more semantically appropriate than 500 for this case, but the exception is handled.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911855850


##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,548 @@
+# 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 len(request.allowed_domains) == 2
+        assert "https://example.com" in request.allowed_domains
+
+    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:
   <!-- Bito Reply -->
   Yes, that's correct. Pydantic v2's ValidationError inherits from ValueError, so pytest.raises(ValueError) properly catches constraint validation errors from ge=100 and le=2000 in the schemas.
   
   **superset/mcp_service/embedded_chart/schemas.py**
   ```
   from pydantic import BaseModel, Field
   
   class GetEmbeddableChartRequest(BaseModel):
       ttl_minutes: int = Field(
           default=60,
           ge=1,
           le=10080,
           description="Permalink TTL in minutes (default: 60, max: 10080 = 1 week)",
       )
       height: int = Field(
           default=400,
           ge=100,
           le=2000,
           description="Chart height in pixels for iframe (default: 400)",
       )
   ```



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911852592


##########
superset/embedded_chart/exceptions.py:
##########
@@ -0,0 +1,35 @@
+# 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.
+from superset.exceptions import SupersetException
+
+
+class EmbeddedChartPermalinkNotFoundError(SupersetException):
+    """Raised when an embedded chart permalink is not found or has expired."""
+
+    message = "The embedded chart permalink could not be found or has expired."

Review Comment:
   Already implemented — all exception classes in `embedded_chart/exceptions.py` use `lazy_gettext` for i18n and have `status` codes set (404 for NotFound, 401 for AccessDenied, 403 for FeatureDisabled).



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2911851278


##########
superset/commands/chart/delete.py:
##########
@@ -68,3 +69,16 @@ 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)
+
+    def validate(self) -> None:
+        pass

Review Comment:
   The ownership check is present at line 89: `security_manager.raise_for_ownership(self._chart)` which raises `ChartForbiddenError` (via `SupersetSecurityException`) if the current user doesn't own the chart.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894975243


##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,548 @@
+# 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 len(request.allowed_domains) == 2
+        assert "https://example.com" in request.allowed_domains
+
+    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:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2891590984


##########
superset/security/manager.py:
##########
@@ -2834,6 +2838,15 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except Exception as ex:
+                    raise EmbeddedChartPermalinkNotFoundError() from ex

Review Comment:
   Fixed in a8b866e. Removed `from ex` exception chaining to avoid leaking internal error details.



##########
superset-frontend/src/dashboard/components/EmbeddedChartModal/index.tsx:
##########
@@ -0,0 +1,291 @@
+/**
+ * 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, useState } from 'react';
+import { logging, makeApi, SupersetApiError, t } from '@superset-ui/core';
+import { styled, css, Alert } from '@apache-superset/core/ui';
+import {
+  Button,
+  FormItem,
+  InfoTooltip,
+  Input,
+  Modal,
+  Loading,
+  Form,
+  Space,
+} from '@superset-ui/core/components';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { Typography } from '@superset-ui/core/components/Typography';
+import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
+
+type Props = {
+  chartId: number;
+  formData: Record<string, unknown>;
+  show: boolean;
+  onHide: () => void;
+};
+
+type EmbeddedChart = {
+  uuid: string;
+  allowed_domains: string[];
+  chart_id: number;
+  changed_on: string;
+};
+
+type EmbeddedApiPayload = { allowed_domains: string[] };
+
+const stringToList = (stringyList: string): string[] =>
+  stringyList.split(/(?:\s|,)+/).filter(x => x);
+
+const ButtonRow = styled.div`
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+`;
+
+export const ChartEmbedControls = ({ chartId, onHide }: Props) => {
+  const { addInfoToast, addDangerToast } = useToasts();
+  const [ready, setReady] = useState(true);

Review Comment:
   Fixed in a8b866e. Changed `useState(true)` to `useState(false)` so the Loading spinner shows until the fetch completes.



##########
superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx:
##########
@@ -603,6 +618,13 @@ const SliceHeaderControls = (
         dataset={datasetWithVerboseMap}
       />
 
+      <EmbeddedChartModal
+        chartId={slice.slice_id}
+        formData={props.formData}
+        show={embedModalIsOpen}
+        onHide={() => setEmbedModalIsOpen(false)}

Review Comment:
   The `EmbeddedChartModal` component accepts `show` and `onHide` props (defined in its Props type at line 37-42). It passes them to the inner `Modal` component which also uses `show`/`onHide`. This is correct and different from `DrillDetailModal` which uses its own naming.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894542661


##########
embed-demo.html:
##########
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Superset Embedded Chart Demo</title>
+  <style>
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+      max-width: 1200px;
+      margin: 0 auto;
+      padding: 40px;
+      background: #f5f5f5;
+    }
+    h1 { color: #333; }
+    .input-section {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      margin-bottom: 20px;
+    }
+    input[type="text"] {
+      width: 100%;
+      padding: 10px;
+      font-family: monospace;
+      font-size: 13px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      box-sizing: border-box;
+      margin-bottom: 12px;
+    }
+    button {
+      background: #20a7c9;
+      color: white;
+      border: none;
+      padding: 12px 24px;
+      font-size: 16px;
+      border-radius: 4px;
+      cursor: pointer;
+      margin-top: 10px;
+    }
+    button:hover {
+      background: #1a8fa8;
+    }
+    .chart-container {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      min-height: 450px;
+    }
+    label {
+      font-weight: 600;
+      display: block;
+      margin-bottom: 8px;
+    }
+  </style>
+</head>
+<body>
+  <h1>Superset Embedded Chart Demo</h1>
+
+  <div class="input-section">
+    <label for="iframe-url-input">iframe_url (from get_embeddable_chart response):</label>
+    <input type="text" id="iframe-url-input" placeholder="http://localhost:8088/embedded/chart/?permalink_key=..." />
+
+    <label for="guest-token-input">guest_token (from get_embeddable_chart response):</label>
+    <input type="text" id="guest-token-input" placeholder="eyJ..." />
+
+    <button onclick="embedChart()">Embed Chart</button>
+  </div>
+
+  <div class="chart-container" id="chart-container">
+    <p style="color: #999; text-align: center; margin-top: 200px;">Chart will appear here</p>
+  </div>
+
+  <script>
+    function embedChart() {
+      var iframeUrl = document.getElementById('iframe-url-input').value.trim();
+      var guestToken = document.getElementById('guest-token-input').value.trim().replace(/\s/g, '');
+      var container = document.getElementById('chart-container');
+
+      if (!iframeUrl || !guestToken) {
+        container.textContent = 'Error: Both iframe_url and guest_token are required.';
+        return;
+      }
+
+      // Validate URL scheme to prevent javascript: or data: URIs
+      if (!iframeUrl.startsWith('http://') && !iframeUrl.startsWith('https://')) {
+        container.textContent = 'Error: iframe_url must use http:// or https://';
+        return;
+      }
+
+      var targetOrigin;
+      try {
+        targetOrigin = new URL(iframeUrl).origin;
+      } catch (e) {
+        container.textContent = 'Error: Invalid iframe_url';
+        return;
+      }
+
+      // Build iframe programmatically (no HTML parsing)
+      var iframe = document.createElement('iframe');
+      iframe.setAttribute('src', iframeUrl);

Review Comment:
   Fixed in a9daf71. Removed `embed-demo.html` entirely — it was a standalone developer demo file, not part of the application. Eliminating the file removes the CodeQL alert completely.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3716811667

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3721536946

   CodeAnt AI is running Incremental review
   
   ---
   
   ### Thanks for using CodeAnt! 🎉
   
   We're free for open-source projects. if you're enjoying it, help us grow by sharing.
   
   [Share on X](https://twitter.com/intent/tweet?text=Just%20tried%20%40CodeAntAI%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A&url=https%3A//codeant.ai) ·
   [Reddit](https://www.reddit.com/submit?title=Check%20out%20CodeAnt%20for%20automated%20code%20review&text=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A%20https%3A//codeant.ai) ·
   [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcodeant.ai&mini=true&title=Check%20out%20CodeAnt%20for%20automated%20code%20review&summary=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos)
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3721652661

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2669891286


##########
docs/docs/configuration/databases.mdx:
##########
@@ -1137,6 +1194,28 @@ More information about PostgreSQL connection options can be found in the
 and the
 [PostgreSQL docs](https://www.postgresql.org/docs/9.1/libpq-connect.html#LIBPQ-PQCONNECTDBPARAMS).
 
+:::resources
+- [Blog: Data Visualization in PostgreSQL With Apache Superset](https://www.tigerdata.com/blog/data-visualization-in-postgresql-with-apache-superset)
+:::
+
+#### QuestDB
+
+[QuestDB](https://questdb.io/) is a high-performance, open-source time-series database with SQL support.
+The recommended connector library is the PostgreSQL driver [psycopg2](https://www.psycopg.org/docs/),
+as QuestDB supports the PostgreSQL wire protocol.
+
+The connection string is formatted as follows:
+
+```
+postgresql+psycopg2://{username}:{password}@{hostname}:{port}/{database}
+```
+
+The default port for QuestDB's PostgreSQL interface is `8812`.
+
+:::resources
+- [QuestDB Docs: Apache Superset Integration](https://questdb.com/docs/third-party-tools/superset/)

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Broken Documentation Link</b></div>
   <div id="fix">
   
   The link to QuestDB's Superset integration docs returns a 404 error. Please verify and update to the current valid URL, as broken links reduce documentation usability.
   </div>
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #e3850d</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset/embedded_chart/view.py:
##########
@@ -0,0 +1,116 @@
+# 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 Any, Callable, cast
+
+from flask import abort, current_app, request
+from flask_appbuilder import expose
+from flask_login import AnonymousUserMixin, login_user
+from flask_wtf.csrf import same_origin
+
+from superset import event_logger, is_feature_enabled
+from superset.commands.explore.permalink.get import GetExplorePermalinkCommand
+from superset.superset_typing import FlaskResponse
+from superset.utils import json
+from superset.views.base import BaseSupersetView, common_bootstrap_payload
+
+logger = logging.getLogger(__name__)
+
+
+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.
+        """
+        if not is_feature_enabled("EMBEDDABLE_CHARTS_MCP"):
+            abort(404)
+
+        # 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)
+
+        # Fetch permalink to get allowed_domains for referrer validation
+        try:
+            permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+        except Exception:
+            logger.exception("Error fetching permalink for embedded chart")
+            permalink_value = None
+
+        if not permalink_value:
+            logger.warning("Permalink not found for embedded chart: %s", permalink_key)
+            abort(404)
+
+        assert permalink_value is not None  # for mypy

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Use of assert statement detected</b></div>
   <div id="fix">
   
   Replace `assert` statement with explicit error handling. Use a conditional check and `abort()` or raise an exception instead of `assert` for production code.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
           if permalink_value is None:
               abort(500)
           # permalink_value is guaranteed to be not None for mypy
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #e3850d</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2795361973


##########
superset-frontend/src/embeddedChart/index.tsx:
##########
@@ -0,0 +1,338 @@
+/**
+ * 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 'src/public-path';
+
+import { useState, useEffect } from 'react';
+import ReactDOM from 'react-dom';
+import {
+  makeApi,
+  t,
+  logging,
+  QueryFormData,
+  StatefulChart,
+} from '@superset-ui/core';
+import Switchboard from '@superset-ui/switchboard';
+import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData';
+import setupClient from 'src/setup/setupClient';
+import setupPlugins from 'src/setup/setupPlugins';
+import { store, USER_LOADED } from 'src/views/store';
+import { Loading } from '@superset-ui/core/components';
+import { ErrorBoundary, DynamicPluginProvider } from 'src/components';
+import { addDangerToast } from 'src/components/MessageToasts/actions';
+import ToastContainer from 'src/components/MessageToasts/ToastContainer';
+import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
+import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
+import { ThemeController } from 'src/theme/ThemeController';
+import type { ThemeStorage } from '@apache-superset/core/ui';
+import { Provider as ReduxProvider } from 'react-redux';
+
+setupPlugins();
+
+const debugMode = process.env.WEBPACK_MODE === 'development';
+const bootstrapData = getBootstrapData();
+
+function log(...info: unknown[]) {
+  if (debugMode) logging.debug(`[superset-embedded-chart]`, ...info);
+}
+
+/**
+ * In-memory implementation of ThemeStorage interface for embedded contexts.
+ */
+class ThemeMemoryStorageAdapter implements ThemeStorage {
+  private storage = new Map<string, string>();
+
+  getItem(key: string): string | null {
+    return this.storage.get(key) || null;
+  }
+
+  setItem(key: string, value: string): void {
+    this.storage.set(key, value);
+  }
+
+  removeItem(key: string): void {
+    this.storage.delete(key);
+  }
+}
+
+const themeController = new ThemeController({
+  storage: new ThemeMemoryStorageAdapter(),
+});
+
+interface PermalinkState {
+  formData: QueryFormData;
+}
+
+const appMountPoint = document.getElementById('app')!;
+const MESSAGE_TYPE = '__embedded_comms__';
+
+function showFailureMessage(message: string) {
+  appMountPoint.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-family: sans-serif;">${message}</div>`;
+}
+
+if (!window.parent || window.parent === window) {
+  showFailureMessage(
+    t(
+      'This page is intended to be embedded in an iframe, but it looks like that is not the case.',
+    ),
+  );
+}
+
+let displayedUnauthorizedToast = false;
+
+/**
+ * Handle unauthorized errors from the API.
+ */
+function guestUnauthorizedHandler() {
+  if (displayedUnauthorizedToast) return;
+  displayedUnauthorizedToast = true;
+  store.dispatch(
+    addDangerToast(
+      t(
+        'This session has encountered an interruption. The embedded chart may not load correctly.',
+      ),
+      {
+        duration: -1,
+        noDuplicate: true,
+      },
+    ),
+  );
+}
+
+/**
+ * Configures SupersetClient with the correct settings for the embedded chart page.
+ */
+function setupGuestClient(guestToken: string) {
+  setupClient({
+    appRoot: applicationRoot(),
+    guestToken,
+    guestTokenHeaderName: bootstrapData.config?.GUEST_TOKEN_HEADER_NAME,
+    unauthorizedHandler: guestUnauthorizedHandler,
+  });
+}
+
+function validateMessageEvent(event: MessageEvent) {
+  const { data } = event;
+  if (data == null || typeof data !== 'object' || data.type !== MESSAGE_TYPE) {

Review Comment:
   Fixed in commit c7f47d7 - relaxed validation to accept messages with `guestToken` or `handshake` fields in addition to typed messages. This allows legitimate initialization messages (both direct guest token and Switchboard-based handshake) to pass through correctly.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2795374499


##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,548 @@
+# 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 len(request.allowed_domains) == 2
+        assert "https://example.com" in request.allowed_domains
+
+    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:
   <!-- Bito Reply -->
   The fix updates test expectations from ValueError to ValidationError for ttl_minutes and height constraint validations, aligning with Pydantic v2's exception handling.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3885900493

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "github-advanced-security[bot] (via GitHub)" <gi...@apache.org>.
github-advanced-security[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2794637666


##########
embed-demo.html:
##########
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Superset Embedded Chart Demo</title>
+  <style>
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+      max-width: 1200px;
+      margin: 0 auto;
+      padding: 40px;
+      background: #f5f5f5;
+    }
+    h1 { color: #333; }
+    .input-section {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      margin-bottom: 20px;
+    }
+    textarea {
+      width: 100%;
+      height: 150px;
+      font-family: monospace;
+      font-size: 12px;
+      padding: 10px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      resize: vertical;
+      box-sizing: border-box;
+    }
+    button {
+      background: #20a7c9;
+      color: white;
+      border: none;
+      padding: 12px 24px;
+      font-size: 16px;
+      border-radius: 4px;
+      cursor: pointer;
+      margin-top: 10px;
+    }
+    button:hover {
+      background: #1a8fa8;
+    }
+    .chart-container {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      min-height: 450px;
+    }
+    label {
+      font-weight: 600;
+      display: block;
+      margin-bottom: 8px;
+    }
+  </style>
+</head>
+<body>
+  <h1>Superset Embedded Chart Demo</h1>
+
+  <div class="input-section">
+    <label for="iframe-input">Paste iframe_html response here:</label>
+    <textarea id="iframe-input" placeholder="Paste the iframe_html value from get_embeddable_chart response..."></textarea>
+    <button onclick="embedChart()">Embed Chart</button>
+  </div>
+
+  <div class="chart-container" id="chart-container">
+    <p style="color: #999; text-align: center; margin-top: 200px;">Chart will appear here</p>
+  </div>
+
+  <script>
+    function embedChart() {
+      const input = document.getElementById('iframe-input').value.trim();
+      const container = document.getElementById('chart-container');
+
+      if (!input) {
+        alert('Please paste the iframe_html first');
+        return;
+      }
+
+      // Safely parse input and only allow iframe elements (prevent XSS)
+      const parser = new DOMParser();
+      const doc = parser.parseFromString(input, 'text/html');

Review Comment:
   ## DOM text reinterpreted as HTML
   
   [DOM text](1) is reinterpreted as HTML without escaping meta-characters.
   
   [Show more details](https://github.com/apache/superset/security/code-scanning/2234)



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3716963233

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3717246042

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3726032625

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi closed pull request #36933: feat(mcp): add embeddable charts with guest token authentication
URL: https://github.com/apache/superset/pull/36933


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2666672912


##########
superset/security/manager.py:
##########
@@ -2834,6 +2838,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError()

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Blind exception catch too broad</b></div>
   <div id="fix">
   
   Catching bare `Exception` is too broad and masks errors. Specify the exact exception type(s) expected or use `raise ... from None` to clarify intent.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
               except (ValueError, KeyError, AttributeError):
                   raise EmbeddedChartPermalinkNotFoundError() from None
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #a3d995</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3717254381

   CodeAnt AI is running Incremental review
   
   ---
   
   ### Thanks for using CodeAnt! 🎉
   
   We're free for open-source projects. if you're enjoying it, help us grow by sharing.
   
   [Share on X](https://twitter.com/intent/tweet?text=Just%20tried%20%40CodeAntAI%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A&url=https%3A//codeant.ai) ·
   [Reddit](https://www.reddit.com/submit?title=Check%20out%20CodeAnt%20for%20automated%20code%20review&text=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A%20https%3A//codeant.ai) ·
   [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcodeant.ai&mini=true&title=Check%20out%20CodeAnt%20for%20automated%20code%20review&summary=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos)
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3720255759

   CodeAnt AI is running Incremental review
   
   ---
   
   ### Thanks for using CodeAnt! 🎉
   
   We're free for open-source projects. if you're enjoying it, help us grow by sharing.
   
   [Share on X](https://twitter.com/intent/tweet?text=Just%20tried%20%40CodeAntAI%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A&url=https%3A//codeant.ai) ·
   [Reddit](https://www.reddit.com/submit?title=Check%20out%20CodeAnt%20for%20automated%20code%20review&text=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos.%20Worth%20checking%20out%3A%20https%3A//codeant.ai) ·
   [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcodeant.ai&mini=true&title=Check%20out%20CodeAnt%20for%20automated%20code%20review&summary=Just%20tried%20CodeAnt%20for%20automated%20code%20review%20and%20I%27m%20impressed%21%20Free%20for%20open%20source%20with%20a%20free%20trial%20for%20private%20repos)
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2673836029


##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,298 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Input Validation Gap</b></div>
   <div id="fix">
   
   The code checks if form_data is truthy but doesn't validate its structure. Since CreateExplorePermalinkCommand expects 'datasource' in formData, invalid input could cause runtime exceptions. Add early validation to prevent this.
   </div>
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #e87f28</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3756099184

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2695173806


##########
superset-frontend/src/dashboard/components/EmbeddedChartModal/index.tsx:
##########
@@ -0,0 +1,291 @@
+/**
+ * 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, useState } from 'react';
+import { logging, makeApi, SupersetApiError, t } from '@superset-ui/core';
+import { styled, css, Alert } from '@apache-superset/core/ui';
+import {
+  Button,
+  FormItem,
+  InfoTooltip,
+  Input,
+  Modal,
+  Loading,
+  Form,
+  Space,
+} from '@superset-ui/core/components';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { Typography } from '@superset-ui/core/components/Typography';
+import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
+
+type Props = {
+  chartId: number;
+  formData: Record<string, unknown>;
+  show: boolean;
+  onHide: () => void;
+};
+
+type EmbeddedChart = {
+  uuid: string;
+  allowed_domains: string[];
+  chart_id: number;
+  changed_on: string;
+};
+
+type EmbeddedApiPayload = { allowed_domains: string[] };
+
+const stringToList = (stringyList: string): string[] =>
+  stringyList.split(/(?:\s|,)+/).filter(x => x);
+
+const ButtonRow = styled.div`
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+`;
+
+export const ChartEmbedControls = ({ chartId, onHide }: Props) => {
+  const { addInfoToast, addDangerToast } = useToasts();
+  const [ready, setReady] = useState(true);

Review Comment:
   **Suggestion:** The component initializes the `ready` state to true, so the UI renders as if data is loaded before the GET request completes; this causes a flash of the main UI and bypasses the intended loading indicator. Initialize `ready` to false so the Loading spinner is shown until the fetch finishes. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Embed Chart modal flashes uninitialized content.
   - ⚠️ Visual flicker when opening Embed Chart modal.
   ```
   </details>
   
   ```suggestion
     const [ready, setReady] = useState(false);
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Open the Dashboard UI and click the chart "Embed chart" action that mounts the
   EmbeddedChartModal component (UI flow described in PR). The modal renders the
   ChartEmbedControls component defined at
   `superset-frontend/src/dashboard/components/EmbeddedChartModal/index.tsx:61`.
   
   2. On the initial render of ChartEmbedControls (component start at line 61), the code at
   `index.tsx:63` initializes ready with `useState(true)`. Because ready is true, the
   component proceeds to render the main UI instead of the Loading indicator.
   
   3. Immediately after mount the useEffect at `index.tsx:129` runs and calls
   `setReady(false)` to start the GET request for `/api/v1/chart/${chartId}/embedded` (see
   lines 129-134). This causes a visible flash: the main UI is shown briefly, then the
   component re-renders into the Loading state (`if (!ready) { return <Loading />; }` at
   lines 151-153).
   
   4. Expected behavior: Loading should show until the GET completes. The root cause is the
   initial value at `index.tsx:63`. Initializing ready to false prevents the UI flash. If you
   believe this pattern is intentional, note that the effect already sets ready false on
   mount, but that still allows an initial render with ready=true and causes the flash.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/dashboard/components/EmbeddedChartModal/index.tsx
   **Line:** 63:63
   **Comment:**
   	*Logic Error: The component initializes the `ready` state to true, so the UI renders as if data is loaded before the GET request completes; this causes a flash of the main UI and bypasses the intended loading indicator. Initialize `ready` to false so the Loading spinner is shown until the fetch finishes.
   
   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>



##########
superset-frontend/src/embeddedChart/index.tsx:
##########
@@ -0,0 +1,338 @@
+/**
+ * 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 'src/public-path';
+
+import { useState, useEffect } from 'react';
+import ReactDOM from 'react-dom';
+import {
+  makeApi,
+  t,
+  logging,
+  QueryFormData,
+  StatefulChart,
+} from '@superset-ui/core';
+import Switchboard from '@superset-ui/switchboard';
+import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData';
+import setupClient from 'src/setup/setupClient';
+import setupPlugins from 'src/setup/setupPlugins';
+import { store, USER_LOADED } from 'src/views/store';
+import { Loading } from '@superset-ui/core/components';
+import { ErrorBoundary, DynamicPluginProvider } from 'src/components';
+import { addDangerToast } from 'src/components/MessageToasts/actions';
+import ToastContainer from 'src/components/MessageToasts/ToastContainer';
+import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
+import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
+import { ThemeController } from 'src/theme/ThemeController';
+import type { ThemeStorage } from '@apache-superset/core/ui';
+import { Provider as ReduxProvider } from 'react-redux';
+
+setupPlugins();
+
+const debugMode = process.env.WEBPACK_MODE === 'development';
+const bootstrapData = getBootstrapData();
+
+function log(...info: unknown[]) {
+  if (debugMode) logging.debug(`[superset-embedded-chart]`, ...info);
+}
+
+/**
+ * In-memory implementation of ThemeStorage interface for embedded contexts.
+ */
+class ThemeMemoryStorageAdapter implements ThemeStorage {
+  private storage = new Map<string, string>();
+
+  getItem(key: string): string | null {
+    return this.storage.get(key) || null;
+  }
+
+  setItem(key: string, value: string): void {
+    this.storage.set(key, value);
+  }
+
+  removeItem(key: string): void {
+    this.storage.delete(key);
+  }
+}
+
+const themeController = new ThemeController({
+  storage: new ThemeMemoryStorageAdapter(),
+});
+
+interface PermalinkState {
+  formData: QueryFormData;
+}
+
+const appMountPoint = document.getElementById('app')!;
+const MESSAGE_TYPE = '__embedded_comms__';
+
+function showFailureMessage(message: string) {
+  appMountPoint.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-family: sans-serif;">${message}</div>`;
+}
+
+if (!window.parent || window.parent === window) {
+  showFailureMessage(
+    t(
+      'This page is intended to be embedded in an iframe, but it looks like that is not the case.',
+    ),
+  );
+}
+
+let displayedUnauthorizedToast = false;
+
+/**
+ * Handle unauthorized errors from the API.
+ */
+function guestUnauthorizedHandler() {
+  if (displayedUnauthorizedToast) return;
+  displayedUnauthorizedToast = true;
+  store.dispatch(
+    addDangerToast(
+      t(
+        'This session has encountered an interruption. The embedded chart may not load correctly.',
+      ),
+      {
+        duration: -1,
+        noDuplicate: true,
+      },
+    ),
+  );
+}
+
+/**
+ * Configures SupersetClient with the correct settings for the embedded chart page.
+ */
+function setupGuestClient(guestToken: string) {
+  setupClient({
+    appRoot: applicationRoot(),
+    guestToken,
+    guestTokenHeaderName: bootstrapData.config?.GUEST_TOKEN_HEADER_NAME,
+    unauthorizedHandler: guestUnauthorizedHandler,
+  });
+}
+
+function validateMessageEvent(event: MessageEvent) {
+  const { data } = event;
+  if (data == null || typeof data !== 'object' || data.type !== MESSAGE_TYPE) {

Review Comment:
   **Suggestion:** Logic/compatibility bug: `validateMessageEvent` currently rejects any message that does not have `data.type === MESSAGE_TYPE`, which will also reject legitimate direct guest-token or handshake messages that omit `type`. As a result, valid messages carrying `guestToken` or `handshake` may be ignored. Relax validation to allow messages that carry `guestToken` or `handshake` in addition to the typed messages. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Embed auth initialization fails; guest token ignored.
   - ❌ Embedded charts fail to load without handshake.
   ```
   </details>
   
   ```suggestion
     // Accept messages that explicitly use our MESSAGE_TYPE, or messages that
     // carry a guestToken or handshake for backward compatibility.
     if (
       data == null ||
       typeof data !== 'object' ||
       (data.type !== MESSAGE_TYPE && !('guestToken' in data) && !('handshake' in data))
     ) {
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Host an iframe pointing to the embeddable page
   (superset-frontend/src/embeddedChart/index.tsx).
   
   2. From the parent page, send a postMessage to the iframe using window.postMessage({
   guestToken: 'abc' }, targetOrigin) without a `type` field.
   
   3. The embedded page receives the message and the listener at line 287
   (window.addEventListener) immediately calls validateMessageEvent at lines 129-134.
   
   4. validateMessageEvent enforces `data.type === MESSAGE_TYPE` and throws because `type` is
   absent; the listener's try/catch (lines 287-293) catches this and logs "ignoring message
   unrelated to embedded comms" and returns, so the guestToken handling branches (lines
   296-307 / Switchboard flow at 310+) never run.
   
   5. Observed outcome: guest token is ignored and the embedded app does not initialize
   authentication or render charts.
   
   Note: This is a concrete failure when parent code posts guestToken without adding a `type`
   property (a likely integration pattern).
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/embeddedChart/index.tsx
   **Line:** 131:131
   **Comment:**
   	*Logic Error: Logic/compatibility bug: `validateMessageEvent` currently rejects any message that does not have `data.type === MESSAGE_TYPE`, which will also reject legitimate direct guest-token or handshake messages that omit `type`. As a result, valid messages carrying `guestToken` or `handshake` may be ignored. Relax validation to allow messages that carry `guestToken` or `handshake` in addition to the typed messages.
   
   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>



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,304 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
+
+            # Generate guest token
+            username = g.user.username if hasattr(g, "user") and g.user else "anonymous"
+            guest_user: GuestTokenUser = {
+                "username": f"embed_{username}",
+                "first_name": "Embed",
+                "last_name": "User",
+            }
+
+            resources: list[GuestTokenResource] = [
+                {

Review Comment:
   **Suggestion:** The resource `type` is passed as an enum object when creating the guest token resources, but later code (and token payloads) expect the string value; this can cause a mismatch during serialization or when validating the guest token (access will be denied because the types won't match). Change the resource `type` to use the enum's `.value` so a string is stored. [type error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ POST /api/v1/embedded_chart/ creation fails.
   - ❌ Generated guest token rejected by access checks.
   - ⚠️ UI \"Embed chart\" flow returns errors.
   - ⚠️ MCP tool embed creation may error.
   ```
   </details>
   
   ```suggestion
           "type": GuestTokenResourceType.CHART_PERMALINK.value,
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Call the embeddable-chart creation API: POST /api/v1/embedded_chart/ which maps to
   EmbeddedChartRestApi.post in superset/embedded_chart/api.py (post method defined around
   lines 177-301 in the PR).
   
   2. Execution reaches the guest-token creation block at
   superset/embedded_chart/api.py:lines 261-283 (guest_user and resources construction),
   specifically the resources dict at lines 267-272 is built with
   GuestTokenResourceType.CHART_PERMALINK (enum instance).
   
   3. The code calls security_manager.create_guest_access_token(...) at
   superset/embedded_chart/api.py:lines 276-281. The guest token creation serializes the
   resources payload into a JWT JSON body.
   
   4. Because the resource \"type\" contains an Enum object
   (GuestTokenResourceType.CHART_PERMALINK) rather than a primitive string, JSON
   serialization or downstream validation either (a) raises a serialization error or (b)
   produces a payload that does not match the string-based checks in
   _validate_guest_token_access (superset/embedded_chart/api.py:_validate_guest_token_access,
   lines ~58-75). The POST request therefore either returns HTTP 500 (serialization error) or
   issues a guest token that will fail access checks when used to fetch the embed (leading to
   401).
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/embedded_chart/api.py
   **Line:** 269:269
   **Comment:**
   	*Type Error: The resource `type` is passed as an enum object when creating the guest token resources, but later code (and token payloads) expect the string value; this can cause a mismatch during serialization or when validating the guest token (access will be denied because the types won't match). Change the resource `type` to use the enum's `.value` so a string is stored.
   
   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>



##########
superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx:
##########
@@ -603,6 +618,13 @@ const SliceHeaderControls = (
         dataset={datasetWithVerboseMap}
       />
 
+      <EmbeddedChartModal
+        chartId={slice.slice_id}
+        formData={props.formData}
+        show={embedModalIsOpen}
+        onHide={() => setEmbedModalIsOpen(false)}

Review Comment:
   **Suggestion:** Prop-name mismatch on the rendered modal: other modals in this file use `showModal` / `onHideModal` naming (e.g. `DrillDetailModal`), but the new `EmbeddedChartModal` is rendered with `show` and `onHide`. If `EmbeddedChartModal` follows the same API as other dashboard modals, passing `show`/`onHide` will do nothing and the modal won't open/close as expected. Change the prop names to match the modal API used elsewhere (use `showModal` and `onHideModal`) so the component receives the visible state and close callback. [possible bug]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Dashboard "Embed chart" modal fails to open.
   - ⚠️ Dashboard embed UI unusable for sharable charts.
   ```
   </details>
   
   ```suggestion
           showModal={embedModalIsOpen}
           onHideModal={() => setEmbedModalIsOpen(false)}
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Open a dashboard and open a chart's dropdown. The dropdown Menu is rendered with
   onClick={handleMenuClick} in this file (SliceHeaderControls). The handleMenuClick switch
   includes the EmbedChart branch at src/dashboard/components/SliceHeaderControls/index.tsx
   lines ~292-299 where it calls setEmbedModalIsOpen(true).
   
   2. Select the "Embed chart" menu entry (MenuKeys.EmbedChart). The case MenuKeys.EmbedChart
   in handleMenuClick at index.tsx: ~294-298 executes setEmbedModalIsOpen(true).
   
   3. The component tree includes <EmbeddedChartModal ... /> rendered at index.tsx:621-626.
   That component instance receives props show and onHide (lines 623-625), not the
   showModal/onHideModal shape used by other modals in this file (for example
   DrillDetailModal in the same component uses showModal and onHideModal).
   
   4. Because EmbeddedChartModal is passed show/onHide instead of showModal/onHideModal, the
   modal's visibility API is not driven by the local state. Result: clicking "Embed chart"
   sets embedModalIsOpen to true (verified at handleMenuClick: index.tsx:~294-298) but the
   EmbeddedChartModal does not open. This reproduces the broken UI where the embed modal
   never appears when the menu item is used.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
   **Line:** 624:625
   **Comment:**
   	*Possible Bug: Prop-name mismatch on the rendered modal: other modals in this file use `showModal` / `onHideModal` naming (e.g. `DrillDetailModal`), but the new `EmbeddedChartModal` is rendered with `show` and `onHide`. If `EmbeddedChartModal` follows the same API as other dashboard modals, passing `show`/`onHide` will do nothing and the modal won't open/close as expected. Change the prop names to match the modal API used elsewhere (use `showModal` and `onHideModal`) so the component receives the visible state and close callback.
   
   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>



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,304 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
+
+            # Generate guest token
+            username = g.user.username if hasattr(g, "user") and g.user else "anonymous"
+            guest_user: GuestTokenUser = {
+                "username": f"embed_{username}",
+                "first_name": "Embed",
+                "last_name": "User",
+            }
+
+            resources: list[GuestTokenResource] = [
+                {
+                    "type": GuestTokenResourceType.CHART_PERMALINK,
+                    "id": permalink_key,
+                }
+            ]
+
+            rls_rules: list[GuestTokenRlsRule] = []
+
+            guest_token_result = security_manager.create_guest_access_token(
+                user=guest_user,
+                resources=resources,
+                rls=rls_rules,
+            )
+
+            # Handle both bytes (older PyJWT) and string (PyJWT 2.0+)
+            guest_token = (
+                guest_token_result.decode("utf-8")
+                if isinstance(guest_token_result, bytes)
+                else guest_token_result
+            )
+
+            # Build iframe URL using request host
+            base_url = request.host_url.rstrip("/")
+            iframe_url = f"{base_url}/embedded/chart/?permalink_key={permalink_key}"
+

Review Comment:
   **Suggestion:** The GET endpoint expects the permalink as a path parameter, but the generated `iframe_url` places the permalink as a query parameter; this mismatch means the iframe URL will not match the API route and the iframe will fail to load the embed. Construct the iframe URL using the path-style route (embed path + permalink) to match the GET route. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Embedded iframe fails to load (404).
   - ⚠️ Dashboard UI \"Embed chart\" unusable.
   - ⚠️ Third-party embeds get invalid URLs.
   ```
   </details>
   
   ```suggestion
               iframe_url = f"{base_url}/embedded/chart/{permalink_key}"
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Create an embed by calling POST /api/v1/embedded_chart/ (EmbeddedChartRestApi.post in
   superset/embedded_chart/api.py, method body lines ~177-301). The response includes the
   iframe_url built at superset/embedded_chart/api.py:lines 291-292.
   
   2. Take the returned iframe_url (example: https://host/embedded/chart/?permalink_key=XYZ)
   and load it in a browser iframe.
   
   3. Browser requests GET https://host/embedded/chart/ without a path parameter; the GET
   handler is defined as @expose("/<permalink_key>", methods=("GET",)) in
   superset/embedded_chart/api.py (get method at lines ~94-166). Because the path parameter
   is missing, Flask routing does not match the GET endpoint for the permalink and the iframe
   request yields 404/invalid route instead of returning the chart state.
   
   4. Result: embedded iframe fails to load chart configuration; frontend cannot render
   embed. The mismatch is reproducible using the API response from step 1 and loading that
   iframe_url.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/embedded_chart/api.py
   **Line:** 293:293
   **Comment:**
   	*Logic Error: The GET endpoint expects the permalink as a path parameter, but the generated `iframe_url` places the permalink as a query parameter; this mismatch means the iframe URL will not match the API route and the iframe will fail to load the embed. Construct the iframe URL using the path-style route (embed path + permalink) to match the GET route.
   
   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>



##########
superset/commands/chart/delete.py:
##########
@@ -68,3 +69,16 @@ 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)
+
+    def validate(self) -> None:
+        pass

Review Comment:
   **Suggestion:** The `validate` method is a no-op, allowing deletion to proceed without verifying the embedded chart exists, whether there are associated reports, or whether the caller has ownership; implement the same existence, report-association, and ownership checks used in `DeleteChartCommand.validate` to prevent unauthorized or invalid deletes. [security]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Embedded deletion bypasses ownership checks.
   - ❌ Deletion can proceed despite associated reports.
   - ❌ TypeError when embedded is missing.
   - ⚠️ Affects command layer: superset/commands/chart/delete.py.
   ```
   </details>
   
   ```suggestion
           # Ensure chart and embedded object exist
           if not self._chart or getattr(self._chart, "embedded", None) is None:
               raise ChartNotFoundError()
           # Prevent deletion if there are associated ReportSchedules
           if reports := ReportScheduleDAO.find_by_chart_ids([self._chart.id]):
               report_names = [report.name for report in reports]
               raise ChartDeleteFailedReportsExistError(
                   _(
                       "There are associated alerts or reports: %(report_names)s",
                       report_names=",".join(report_names),
                   )
               )
           # Check ownership
           try:
               security_manager.raise_for_ownership(self._chart)
           except SupersetSecurityException as ex:
               raise ChartForbiddenError() from ex
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Call DeleteEmbeddedChartCommand.run():
   
      - File: superset/commands/chart/delete.py
   
      - run() is defined at lines 78-81 and calls self.validate() then proceeds.
   
      - Current validate() at lines 83-84 is a no-op.
   
   2. Create a chart that has no embedded object (embedded is None) and call run():
   
      - Because validate() is a no-op, run() will call EmbeddedChartDAO.delete(None),
   
        which then raises TypeError in superset/daos/base.py:423-440 when iterating None.
   
   3. Create a chart that has associated reports and call run():
   
      - The DAO ReportSchedule check exists in DeleteChartCommand.validate (lines 52-71),
   
        but not here. ReportScheduleDAO.find_by_chart_ids is implemented at
        superset/daos/report.py:54-59.
   
      - Because validate() is empty, deletion will proceed even if reports exist, bypassing
      the
   
        safeguards that prevent deletion in DeleteChartCommand.
   
   4. Call run() as a non-owner:
   
      - DeleteChartCommand.validate enforces ownership via
      security_manager.raise_for_ownership
   
        (superset/commands/chart/delete.py lines 52-71). The empty validate() here omits that
        check,
   
        allowing potential unauthorized deletion if callers invoke this command.
   
   Explanation: The no-op validate effectively omits existence, report-association, and
   ownership checks
   
   that are present for regular chart deletion and therefore is a real security/consistency
   gap.
   ```
   </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:** 84:84
   **Comment:**
   	*Security: The `validate` method is a no-op, allowing deletion to proceed without verifying the embedded chart exists, whether there are associated reports, or whether the caller has ownership; implement the same existence, report-association, and ownership checks used in `DeleteChartCommand.validate` to prevent unauthorized or invalid deletes.
   
   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>



##########
superset/config.py:
##########
@@ -559,6 +559,8 @@ class D3TimeFormat(TypedDict, total=False):
     # This feature flag is stil in beta and is not recommended for production use.
     "GLOBAL_ASYNC_QUERIES": False,
     "EMBEDDED_SUPERSET": False,
+    # Enables the "Embed code" and "Embed chart" options in the Share menu
+    "EMBEDDABLE_CHARTS": True,

Review Comment:
   **Suggestion:** Security risk: enabling embeddable charts by default exposes the feature while the guest token secret in this repository is the insecure default, which can allow unauthorized embedded access. Disable the feature by default so operators must explicitly opt-in after configuring a strong `GUEST_TOKEN_JWT_SECRET`. [security]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Embeddable chart creation endpoint exposed by default.
   - ❌ Guest tokens signed with insecure default secret.
   - ⚠️ Many deployments use shipped defaults unchanged.
   ```
   </details>
   
   ```suggestion
       "EMBEDDABLE_CHARTS": False,
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Start Superset with the repository default configuration (no superset_config override).
   The DEFAULT_FEATURE_FLAGS dict in superset/config.py includes the lines at
   `superset/config.py:562-563` showing `"EMBEDDABLE_CHARTS": True`.
   
   2. Verify embedded configuration values present in same file: the default guest secret
   `GUEST_TOKEN_JWT_SECRET` is set to the insecure default (`"test-guest-secret-change-me"`)
   in superset/config.py (Embedded config options section).
   
   3. Use the UI flow described in the PR (Dashboard → Chart dropdown → "Embed chart" → POST
   /api/v1/embedded_chart/) which will be enabled when EMBEDDABLE_CHARTS is True; the
   endpoint will generate a guest token signed with the configured `GUEST_TOKEN_JWT_SECRET`.
   
   4. Because EMBEDDABLE_CHARTS defaults to True and the guest secret is the insecure
   default, a deployment that hasn't overridden the secret will issue guest tokens signed
   with the known default, enabling unauthorized embeddable access. This reproduces the risk
   without any codepath modifications.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/config.py
   **Line:** 563:563
   **Comment:**
   	*Security: Security risk: enabling embeddable charts by default exposes the feature while the guest token secret in this repository is the insecure default, which can allow unauthorized embedded access. Disable the feature by default so operators must explicitly opt-in after configuring a strong `GUEST_TOKEN_JWT_SECRET`.
   
   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>



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "github-advanced-security[bot] (via GitHub)" <gi...@apache.org>.
github-advanced-security[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2695176693


##########
embed-demo.html:
##########
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Superset Embedded Chart Demo</title>
+  <style>
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+      max-width: 1200px;
+      margin: 0 auto;
+      padding: 40px;
+      background: #f5f5f5;
+    }
+    h1 { color: #333; }
+    .input-section {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      margin-bottom: 20px;
+    }
+    textarea {
+      width: 100%;
+      height: 150px;
+      font-family: monospace;
+      font-size: 12px;
+      padding: 10px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      resize: vertical;
+      box-sizing: border-box;
+    }
+    button {
+      background: #20a7c9;
+      color: white;
+      border: none;
+      padding: 12px 24px;
+      font-size: 16px;
+      border-radius: 4px;
+      cursor: pointer;
+      margin-top: 10px;
+    }
+    button:hover {
+      background: #1a8fa8;
+    }
+    .chart-container {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      min-height: 450px;
+    }
+    label {
+      font-weight: 600;
+      display: block;
+      margin-bottom: 8px;
+    }
+  </style>
+</head>
+<body>
+  <h1>Superset Embedded Chart Demo</h1>
+
+  <div class="input-section">
+    <label for="iframe-input">Paste iframe_html response here:</label>
+    <textarea id="iframe-input" placeholder="Paste the iframe_html value from get_embeddable_chart response..."></textarea>
+    <button onclick="embedChart()">Embed Chart</button>
+  </div>
+
+  <div class="chart-container" id="chart-container">
+    <p style="color: #999; text-align: center; margin-top: 200px;">Chart will appear here</p>
+  </div>
+
+  <script>
+    function embedChart() {
+      const input = document.getElementById('iframe-input').value.trim();
+      const container = document.getElementById('chart-container');
+
+      if (!input) {
+        alert('Please paste the iframe_html first');
+        return;
+      }
+
+      // Insert the iframe HTML
+      container.innerHTML = input;

Review Comment:
   ## DOM text reinterpreted as HTML
   
   [DOM text](1) is reinterpreted as HTML without escaping meta-characters.
   
   [Show more details](https://github.com/apache/superset/security/code-scanning/2223)



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3755831841

   CodeAnt AI Incremental review completed.


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2795374619


##########
superset-frontend/src/dashboard/components/EmbeddedChartModal/index.tsx:
##########
@@ -0,0 +1,291 @@
+/**
+ * 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, useState } from 'react';
+import { logging, makeApi, SupersetApiError, t } from '@superset-ui/core';
+import { styled, css, Alert } from '@apache-superset/core/ui';
+import {
+  Button,
+  FormItem,
+  InfoTooltip,
+  Input,
+  Modal,
+  Loading,
+  Form,
+  Space,
+} from '@superset-ui/core/components';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { Typography } from '@superset-ui/core/components/Typography';
+import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
+
+type Props = {
+  chartId: number;
+  formData: Record<string, unknown>;
+  show: boolean;
+  onHide: () => void;
+};
+
+type EmbeddedChart = {
+  uuid: string;
+  allowed_domains: string[];
+  chart_id: number;
+  changed_on: string;
+};
+
+type EmbeddedApiPayload = { allowed_domains: string[] };
+
+const stringToList = (stringyList: string): string[] =>
+  stringyList.split(/(?:\s|,)+/).filter(x => x);
+
+const ButtonRow = styled.div`
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+`;
+
+export const ChartEmbedControls = ({ chartId, onHide }: Props) => {
+  const { addInfoToast, addDangerToast } = useToasts();
+  const [ready, setReady] = useState(true);

Review Comment:
   Fixed in commit 1e47e54 by changing the initial state from `useState(true)` to `useState(false)`. The Loading spinner now displays correctly until the GET request completes.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3887741633

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3886264374

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3886952455

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2932173774


##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,321 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.exceptions import CommandException
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+from superset.key_value.exceptions import KeyValueParseKeyError
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except (ValueError, KeyError, KeyValueParseKeyError) as ex:
+            logger.warning("Error fetching embedded chart: %s", ex)
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")

Review Comment:
   The i18n pattern in this codebase is not consistently applied to API error messages. Looking at existing endpoints:
   
   - `superset/sqllab/api.py`: `response_400(message="client_id is required")` (no i18n)
   - `superset/databases/api.py`: `response_400(message="Unexpected Invalid file type")` (no i18n)
   - `superset/themes/api.py`: `response_400(message="Request body is required")` (no i18n)
   - `superset/cachekeys/api.py`: `response_400(message="Request is incorrect")` (no i18n)
   - `superset/views/users/api.py`: `response_400(message="At least one field must be provided.")` (no i18n)
   
   While `charts/data/api.py` does use `_("Request is not JSON")`, the majority of API error messages in the codebase use plain strings. These are developer/API-facing messages returned as JSON responses (not rendered in the UI), so i18n is generally less critical for them. Happy to add `_()` wrapping if there's a push to standardize this across the codebase, but the current code is consistent with the prevailing pattern.



##########
superset/charts/api.py:
##########
@@ -1184,3 +1196,253 @@ def import_(self) -> Response:
         )
         command.run()
         return self.response(200, message="OK")
+
+    @expose("/<id_or_uuid>/embedded", methods=("GET",))
+    @protect()
+    @safe
+    @permission_name("read")
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_embedded",
+        log_to_statsd=False,
+    )
+    def get_embedded(self, id_or_uuid: str) -> Response:
+        """Get the chart's embedded configuration.
+        ---
+        get:
+          summary: Get the chart's embedded configuration
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: id_or_uuid
+            description: The chart id or uuid
+          responses:
+            200:
+              description: Result contains the embedded chart config
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        $ref: '#/components/schemas/EmbeddedChartResponseSchema'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            chart = ChartDAO.get_by_id_or_uuid(id_or_uuid)
+        except ChartNotFoundError:
+            return self.response_404()
+
+        if not chart.embedded:
+            return self.response(404)
+        embedded: EmbeddedChart = chart.embedded[0]
+        result = self.embedded_response_schema.dump(embedded)
+        return self.response(200, result=result)
+
+    @expose("/<id_or_uuid>/embedded", methods=["POST", "PUT"])
+    @protect()
+    @safe
+    @permission_name("set_embedded")
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.set_embedded",
+        log_to_statsd=False,
+    )
+    def set_embedded(self, id_or_uuid: str) -> Response:
+        """Set a chart's embedded configuration.
+        ---
+        post:
+          summary: Set a chart's embedded configuration
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: id_or_uuid
+            description: The chart id or uuid
+          requestBody:
+            description: The embedded configuration to set
+            required: true
+            content:
+              application/json:
+                schema: EmbeddedChartConfigSchema
+          responses:
+            200:
+              description: Successfully set the configuration
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        $ref: '#/components/schemas/EmbeddedChartResponseSchema'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        put:
+          description: >-
+            Sets a chart's embedded configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: id_or_uuid
+            description: The chart id or uuid
+          requestBody:
+            description: The embedded configuration to set
+            required: true
+            content:
+              application/json:
+                schema: EmbeddedChartConfigSchema
+          responses:
+            200:
+              description: Successfully set the configuration
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        $ref: '#/components/schemas/EmbeddedChartResponseSchema'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            chart = ChartDAO.get_by_id_or_uuid(id_or_uuid)
+        except ChartNotFoundError:
+            return self.response_404()
+
+        try:
+            body = self.embedded_config_schema.load(request.json)
+
+            embedded = EmbeddedChartDAO.upsert(
+                chart,
+                body["allowed_domains"],
+            )
+            db.session.commit()  # pylint: disable=consider-using-transaction
+
+            result = self.embedded_response_schema.dump(embedded)
+            return self.response(200, result=result)
+        except ValidationError as error:
+            db.session.rollback()  # pylint: disable=consider-using-transaction
+            return self.response_400(message=error.messages)
+
+    @expose("/<id_or_uuid>/embedded", methods=("DELETE",))
+    @protect()
+    @safe
+    @permission_name("set_embedded")
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self,
+        *args,
+        **kwargs: f"{self.__class__.__name__}.delete_embedded",
+        log_to_statsd=False,
+    )
+    def delete_embedded(self, id_or_uuid: str) -> Response:
+        """Delete a chart's embedded configuration.
+        ---
+        delete:
+          summary: Delete a chart's embedded configuration
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: id_or_uuid
+            description: The chart id or uuid
+          responses:
+            200:
+              description: Successfully removed the configuration
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      message:
+                        type: string
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            chart = ChartDAO.get_by_id_or_uuid(id_or_uuid)
+        except ChartNotFoundError:
+            return self.response_404()
+
+        DeleteEmbeddedChartCommand(chart).run()
+        return self.response(200, message="OK")
+
+    @expose("/embedded", methods=("GET",))
+    @protect()
+    @safe
+    @permission_name("read")
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.list_embedded",
+        log_to_statsd=False,
+    )
+    def list_embedded(self) -> Response:
+        """List all embedded chart configurations.
+        ---
+        get:
+          summary: List all embedded chart configurations
+          responses:
+            200:
+              description: List of embedded charts
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        type: array
+                        items:
+                          type: object
+                          properties:
+                            uuid:
+                              type: string
+                            chart_id:
+                              type: integer
+                            chart_name:
+                              type: string
+                            allowed_domains:
+                              type: array
+                              items:
+                                type: string
+                            changed_on:
+                              type: string
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        embedded_charts = (
+            db.session.query(EmbeddedChart)
+            .join(Slice, EmbeddedChart.chart_id == Slice.id)
+            .all()
+        )
+        result = [
+            {
+                "uuid": str(ec.uuid),
+                "chart_id": ec.chart_id,
+                "chart_name": ec.chart.slice_name if ec.chart else None,
+                "allowed_domains": ec.allowed_domains,
+                "changed_on": ec.changed_on.isoformat() if ec.changed_on else None,
+            }
+            for ec in embedded_charts
+        ]

Review Comment:
   The `list_embedded` endpoint does apply a user permission filter. Looking at lines 1433-1438:
   
   ```python
   # Filter to only charts the current user owns or has access to
   embedded_charts = (
       db.session.query(EmbeddedChart)
       .join(Slice, EmbeddedChart.chart_id == Slice.id)
       .filter(Slice.created_by_fk == g.user.get_id())
       .all()
   )
   ```
   
   The query joins `EmbeddedChart` with `Slice` (the chart model) and filters by `Slice.created_by_fk == g.user.get_id()`, so users only see embedded configurations for charts they created. Additionally, the endpoint has `@protect()` and `@permission_name("read")` decorators (lines 1390-1392), which enforce FAB RBAC permissions before the handler even executes.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
codeant-ai-for-open-source[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4054187895

   ## **Sequence Diagram**
   
   This PR introduces a new embeddable chart flow that creates ephemeral chart permalinks and issues guest tokens scoped to that permalink. The diagram shows how UI or MCP requests produce embed credentials and how the embedded page securely loads and renders the chart.
   
   ```mermaid
   sequenceDiagram
       participant Client
       participant SupersetAPI
       participant PermalinkStore
       participant TokenService
       participant EmbeddedPage
   
       Client->>SupersetAPI: Request embeddable chart with form data
       SupersetAPI->>PermalinkStore: Store chart state as permalink with ttl
       SupersetAPI->>TokenService: Create guest token for chart permalink
       SupersetAPI-->>Client: Return iframe url and guest token
       Client->>EmbeddedPage: Load iframe and send guest token
       EmbeddedPage->>SupersetAPI: Request permalink state for rendering
       SupersetAPI->>SupersetAPI: Validate token includes chart permalink access
       SupersetAPI-->>EmbeddedPage: Return chart state
       EmbeddedPage->>EmbeddedPage: Render chart from permalink form data
   ```
   
   ---
   *Generated by [CodeAnt AI](https://codeant.ai)*
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-3756819027

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2790733531


##########
superset-frontend/webpack.config.js:
##########
@@ -300,6 +300,7 @@ const config = {
     menu: addPreamble('src/views/menu.tsx'),
     spa: addPreamble('/src/views/index.tsx'),
     embedded: addPreamble('/src/embedded/index.tsx'),
+    embeddedChart: addPreamble('/src/embeddedChart/index.tsx'),
   },

Review Comment:
   <!-- Bito Reply -->
   The embeddedChart entry path is already set to 'src/embeddedChart/index.tsx' without a leading slash in the PR, matching the suggestion. No further changes needed.



##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,298 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:

Review Comment:
   <!-- Bito Reply -->
   Yes, that's correct. The POST method in `EmbeddedChartRestApi` validates both `form_data` presence and the `datasource` field before processing, returning a 400 error if either is missing.
   
   **superset/embedded_chart/api.py**
   ```
   if not form_data:
                   return self.response_400(message="form_data is required")
   
               # Validate required form_data structure
               if not form_data.get("datasource"):
                   return self.response_400(
                       message="form_data must include 'datasource' field"
                   )
   ```



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2790546303


##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,548 @@
+# 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 len(request.allowed_domains) == 2
+        assert "https://example.com" in request.allowed_domains
+
+    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:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Wrong Exception in Validation Test</b></div>
   <div id="fix">
   
   The test expects ValueError for invalid ttl_minutes, but pydantic v2 raises ValidationError instead, causing the test to pass incorrectly or fail unexpectedly.
   </div>
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #a08dbb</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,548 @@
+# 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 len(request.allowed_domains) == 2
+        assert "https://example.com" in request.allowed_domains
+
+    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:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Wrong Exception in Height Validation Test</b></div>
   <div id="fix">
   
   Similar to ttl validation, this test expects ValueError but pydantic raises ValidationError for height constraints.
   </div>
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #a08dbb</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4027277417

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "codeant-ai-for-open-source[bot] (via GitHub)" <gi...@apache.org>.
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi closed pull request #36933: feat(mcp): add embeddable charts with guest token authentication
URL: https://github.com/apache/superset/pull/36933


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894978551


##########
superset/embedded_chart/api.py:
##########
@@ -0,0 +1,304 @@
+# 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 datetime import datetime, timedelta, timezone
+from typing import Any
+
+from flask import g, request, Response
+from flask_appbuilder.api import expose, protect, safe
+
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.daos.key_value import KeyValueDAO
+from superset.embedded_chart.exceptions import (
+    EmbeddedChartAccessDeniedError,
+    EmbeddedChartPermalinkNotFoundError,
+)
+from superset.explore.permalink.schemas import ExplorePermalinkSchema
+from superset.extensions import event_logger, security_manager
+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.security.guest_token import (
+    GuestTokenResource,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
+from superset.views.base_api import BaseSupersetApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedChartRestApi(BaseSupersetApi):
+    """REST API for embedded chart data retrieval."""
+
+    resource_name = "embedded_chart"
+    allow_browser_login = True
+    openapi_spec_tag = "Embedded Chart"
+
+    def _validate_guest_token_access(self, permalink_key: str) -> bool:
+        """
+        Validate that the guest token grants access to this permalink.
+
+        Guest tokens contain a list of resources the user can access.
+        For embedded charts, we check that the permalink_key is in that list.
+        """
+        user = g.user
+        if not isinstance(user, GuestUser):
+            return False
+
+        for resource in user.resources:
+            if (
+                resource.get("type") == GuestTokenResourceType.CHART_PERMALINK.value
+                and str(resource.get("id")) == permalink_key
+            ):
+                return True
+        return False
+
+    def _get_permalink_value(self, permalink_key: str) -> dict[str, Any] | None:
+        """
+        Get permalink value without access checks.
+
+        For embedded charts, access is controlled via guest token validation,
+        so we skip the normal dataset/chart access checks.
+        """
+        # Use the same salt, resource, and codec as the explore permalink command
+        salt = get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
+        codec = MarshmallowKeyValueCodec(ExplorePermalinkSchema())
+        key = decode_permalink_id(permalink_key, salt=salt)
+        return KeyValueDAO.get_value(
+            KeyValueResource.EXPLORE_PERMALINK,
+            key,
+            codec,
+        )
+
+    @expose("/<permalink_key>", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+        log_to_statsd=False,
+    )
+    def get(self, permalink_key: str) -> Response:
+        """Get chart form_data from permalink key.
+        ---
+        get:
+          summary: Get embedded chart configuration
+          description: >-
+            Retrieves the form_data for rendering an embedded chart.
+            This endpoint is used by the embedded chart iframe to load
+            the chart configuration.
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: permalink_key
+            description: The chart permalink key
+            required: true
+          responses:
+            200:
+              description: Chart permalink state
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      state:
+                        type: object
+                        properties:
+                          formData:
+                            type: object
+                            description: The chart configuration formData
+                          allowedDomains:
+                            type: array
+                            items:
+                              type: string
+                            description: Domains allowed to embed this chart
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            # Validate guest token grants access to this permalink
+            if not self._validate_guest_token_access(permalink_key):
+                raise EmbeddedChartAccessDeniedError()
+
+            # Get permalink value without access checks (guest token already validated)
+            permalink_value = self._get_permalink_value(permalink_key)
+            if not permalink_value:
+                raise EmbeddedChartPermalinkNotFoundError()
+
+            # Return state in the format expected by the frontend:
+            # { state: { formData: {...}, allowedDomains: [...] } }
+            state = permalink_value.get("state", {})
+
+            return self.response(
+                200,
+                state=state,
+            )
+        except EmbeddedChartAccessDeniedError:
+            return self.response_401()
+        except EmbeddedChartPermalinkNotFoundError:
+            return self.response_404()
+        except Exception:
+            logger.exception("Error fetching embedded chart")
+            return self.response_500()
+
+    @expose("/", methods=("POST",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self) -> Response:
+        """Create an embeddable chart with guest token.
+        ---
+        post:
+          summary: Create embeddable chart
+          description: >-
+            Creates an embeddable chart configuration with a guest token.
+            The returned iframe_url and guest_token can be used to embed
+            the chart in external applications.
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  type: object
+                  required:
+                    - form_data
+                  properties:
+                    form_data:
+                      type: object
+                      description: Chart form_data configuration
+                    allowed_domains:
+                      type: array
+                      items:
+                        type: string
+                      description: Domains allowed to embed this chart
+                    ttl_minutes:
+                      type: integer
+                      default: 60
+                      description: Time-to-live for the embed in minutes
+          responses:
+            200:
+              description: Embeddable chart created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      iframe_url:
+                        type: string
+                        description: URL to use in iframe src
+                      guest_token:
+                        type: string
+                        description: Guest token for authentication
+                      permalink_key:
+                        type: string
+                        description: Permalink key for the chart
+                      expires_at:
+                        type: string
+                        format: date-time
+                        description: When the embed expires
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = request.json or {}
+            form_data = body.get("form_data", {})
+            allowed_domains: list[str] = body.get("allowed_domains", [])
+            ttl_minutes: int = body.get("ttl_minutes", 60)
+
+            if not form_data:
+                return self.response_400(message="form_data is required")
+
+            # Validate required form_data structure
+            if not form_data.get("datasource"):
+                return self.response_400(
+                    message="form_data must include 'datasource' field"
+                )
+
+            # Create permalink with the form_data
+            state = {
+                "formData": form_data,
+                "allowedDomains": allowed_domains,
+            }
+            permalink_key = CreateExplorePermalinkCommand(state).run()
+
+            # Calculate expiration
+            expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes)
+
+            # Generate guest token
+            username = g.user.username if hasattr(g, "user") and g.user else "anonymous"
+            guest_user: GuestTokenUser = {
+                "username": f"embed_{username}",
+                "first_name": "Embed",
+                "last_name": "User",
+            }
+
+            resources: list[GuestTokenResource] = [
+                {
+                    "type": GuestTokenResourceType.CHART_PERMALINK,
+                    "id": permalink_key,
+                }
+            ]
+
+            rls_rules: list[GuestTokenRlsRule] = []
+
+            guest_token_result = security_manager.create_guest_access_token(
+                user=guest_user,
+                resources=resources,
+                rls=rls_rules,
+            )
+
+            # Handle both bytes (older PyJWT) and string (PyJWT 2.0+)
+            guest_token = (
+                guest_token_result.decode("utf-8")
+                if isinstance(guest_token_result, bytes)
+                else guest_token_result
+            )
+
+            # Build iframe URL using request host
+            base_url = request.host_url.rstrip("/")
+            iframe_url = f"{base_url}/embedded/chart/?permalink_key={permalink_key}"

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



##########
superset/charts/schemas.py:
##########
@@ -345,6 +345,17 @@ class ChartGetCachedScreenshotResponseSchema(Schema):
     )
 
 
+class EmbeddedChartConfigSchema(Schema):
+    allowed_domains = fields.List(fields.String(), required=True)
+
+
+class EmbeddedChartResponseSchema(Schema):
+    uuid = fields.String()
+    allowed_domains = fields.List(fields.String())
+    chart_id = fields.Integer()
+    changed_on = fields.DateTime()

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



##########
superset/views/embedded_charts.py:
##########
@@ -0,0 +1,34 @@
+# 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.
+from flask_appbuilder import expose, has_access
+
+from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP
+from superset.superset_typing import FlaskResponse
+from superset.views.base import BaseSupersetView
+
+
+class EmbeddedChartsView(BaseSupersetView):
+    """View for managing embedded charts list."""
+
+    route_base = "/embeddedcharts"
+    class_permission_name = "Chart"
+    method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP
+
+    @expose("/list/")
+    @has_access
+    def list(self) -> FlaskResponse:
+        return super().render_app_template()

Review Comment:
   <!-- Bito Reply -->
   This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4006581297

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894972810


##########
superset/security/manager.py:
##########
@@ -2810,6 +2821,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except (ExplorePermalinkGetFailedError, ValueError) as ex:
+                    raise EmbeddedChartPermalinkNotFoundError() from ex

Review Comment:
   Good point about ChartAccessDeniedError potentially bubbling up through GetExplorePermalinkCommand.run(). I'll review the call chain and add it to the catch list if it can reach this point. Thanks for the thorough analysis.



##########
embed-demo.html:
##########
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Superset Embedded Chart Demo</title>
+  <style>
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+      max-width: 1200px;
+      margin: 0 auto;
+      padding: 40px;
+      background: #f5f5f5;
+    }
+    h1 { color: #333; }
+    .input-section {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      margin-bottom: 20px;
+    }
+    textarea {
+      width: 100%;
+      height: 150px;
+      font-family: monospace;
+      font-size: 12px;
+      padding: 10px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      resize: vertical;
+      box-sizing: border-box;
+    }
+    button {
+      background: #20a7c9;
+      color: white;
+      border: none;
+      padding: 12px 24px;
+      font-size: 16px;
+      border-radius: 4px;
+      cursor: pointer;
+      margin-top: 10px;
+    }
+    button:hover {
+      background: #1a8fa8;
+    }
+    .chart-container {
+      background: white;
+      border-radius: 8px;
+      padding: 20px;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+      min-height: 450px;
+    }
+    label {
+      font-weight: 600;
+      display: block;
+      margin-bottom: 8px;
+    }
+  </style>
+</head>
+<body>
+  <h1>Superset Embedded Chart Demo</h1>
+
+  <div class="input-section">
+    <label for="iframe-input">Paste iframe_html response here:</label>
+    <textarea id="iframe-input" placeholder="Paste the iframe_html value from get_embeddable_chart response..."></textarea>
+    <button onclick="embedChart()">Embed Chart</button>
+  </div>
+
+  <div class="chart-container" id="chart-container">
+    <p style="color: #999; text-align: center; margin-top: 200px;">Chart will appear here</p>
+  </div>
+
+  <script>
+    function embedChart() {
+      const input = document.getElementById('iframe-input').value.trim();
+      const container = document.getElementById('chart-container');
+
+      if (!input) {
+        alert('Please paste the iframe_html first');
+        return;
+      }
+
+      // Safely parse input and only allow iframe elements (prevent XSS)
+      const parser = new DOMParser();
+      const doc = parser.parseFromString(input, 'text/html');

Review Comment:
   Thanks for the alert. The embed-demo.html is a developer-facing demo page for local testing only — it's not served in production or bundled into the application. That said, sanitizing the postMessage data in the demo would be good hygiene and I'll add that.



##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,548 @@
+# 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 len(request.allowed_domains) == 2
+        assert "https://example.com" in request.allowed_domains
+
+    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:
   Exactly right — thanks for verifying the fix.



##########
superset/commands/chart/delete.py:
##########
@@ -68,3 +69,16 @@ 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)
+
+    def validate(self) -> None:
+        pass

Review Comment:
   Great security observation. The embedded chart delete currently relies on chart-level ownership checks (the chart owner controls embedded configs), but adding an explicit ownership verification in the validate method would be more robust defense-in-depth. I'll look into adding that. Thanks for catching this.



##########
superset/security/manager.py:
##########
@@ -2834,6 +2838,17 @@ def validate_guest_token_resources(resources: GuestTokenResources) -> None:
                     embedded = EmbeddedDashboardDAO.find_by_id(str(resource["id"]))
                     if not embedded:
                         raise EmbeddedDashboardNotFoundError()
+            elif resource["type"] == GuestTokenResourceType.CHART_PERMALINK.value:
+                # Validate that the chart permalink exists
+                permalink_key = str(resource["id"])
+                try:
+                    permalink_value = GetExplorePermalinkCommand(permalink_key).run()
+                    if not permalink_value:
+                        raise EmbeddedChartPermalinkNotFoundError()
+                except EmbeddedChartPermalinkNotFoundError:
+                    raise
+                except Exception:
+                    raise EmbeddedChartPermalinkNotFoundError()

Review Comment:
   Thanks for confirming. The 'from None' suppression is intentional to avoid leaking internal details in tracebacks.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on code in PR #36933:
URL: https://github.com/apache/superset/pull/36933#discussion_r2894970214


##########
tests/unit_tests/mcp_service/embedded_chart/tool/test_get_embeddable_chart.py:
##########
@@ -0,0 +1,548 @@
+# 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 len(request.allowed_domains) == 2
+        assert "https://example.com" in request.allowed_domains

Review Comment:
   Good catch on the URL sanitization alert. This is in a test fixture file using example.com as a mock domain for unit tests, not production code. No real risk here, but I appreciate the automated security scanning.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Re: [PR] feat(mcp): add embeddable charts with guest token authentication [superset]

Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #36933:
URL: https://github.com/apache/superset/pull/36933#issuecomment-4010684178

   AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
   
   <details>
   <summary>
   Bito Usage Guide
   </summary>
   
   **Commands**
         
   Type the following command in the pull request comment and save the comment.
         
   - `/review` - Manually triggers a full AI review.
   
   - `/pause` - Pauses automatic reviews on this pull request.
   - `/resume` - Resumes automatic reviews.
   - `/resolve` - Marks all Bito-posted review comments as resolved.
   - `/abort` - Cancels all in-progress reviews.
   
         
   Refer to the <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/available-commands" target="_blank" rel="noopener noreferrer">documentation</a> for additional commands.
         
   **Configuration**
   
   This repository uses `Superset` You can customize the agent settings <a href="https://alpha.bito.ai/home/ai-agents/code-review-agent" target="_blank" rel="noopener noreferrer">here</a> or contact your Bito workspace admin at evan@preset.io.
         
   **Documentation & Help**
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/getting-started/install-run-using-bito-cloud/create-or-customize-an-agent-instance" target="_blank" rel="noopener noreferrer">Customize agent settings</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/implementing-custom-code-review-rules" target="_blank" rel="noopener noreferrer">Review rules</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent" target="_blank" rel="noopener noreferrer">General documentation</a>
   - <a href="https://docs.bito.ai/bito-dev-agents/ai-code-review-agent/faqs" target="_blank" rel="noopener noreferrer">FAQ</a>
   
   </details>
   


-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org