This is an automated email from the ASF dual-hosted git repository.

lilykuang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 224769bd45 feat(embedded): API get embedded dashboard config by uuid 
(#19650)
224769bd45 is described below

commit 224769bd452b831ae4ab4d7fc658b61805970b62
Author: Lily Kuang <[email protected]>
AuthorDate: Tue Apr 12 15:14:08 2022 -0700

    feat(embedded): API get embedded dashboard config by uuid (#19650)
    
    * feat(embedded): get embedded dashboard config by uuid
    
    * add tests and validation
    
    * remove accidentally commit
    
    * fix tests
---
 superset/embedded/api.py                           | 105 +++++++++++++++++++++
 superset/embedded_dashboard/commands/exceptions.py |  34 +++++++
 superset/initialization/__init__.py                |   2 +
 superset/security/api.py                           |   8 +-
 superset/security/manager.py                       |  18 ++++
 tests/integration_tests/embedded/api_tests.py      |  53 +++++++++++
 tests/integration_tests/security/api_tests.py      |  29 +++++-
 7 files changed, 246 insertions(+), 3 deletions(-)

diff --git a/superset/embedded/api.py b/superset/embedded/api.py
new file mode 100644
index 0000000000..f7278d910a
--- /dev/null
+++ b/superset/embedded/api.py
@@ -0,0 +1,105 @@
+# 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 Optional
+
+from flask import Response
+from flask_appbuilder.api import expose, protect, safe
+from flask_appbuilder.hooks import before_request
+from flask_appbuilder.models.sqla.interface import SQLAInterface
+
+from superset import is_feature_enabled
+from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
+from superset.dashboards.schemas import EmbeddedDashboardResponseSchema
+from superset.embedded.dao import EmbeddedDAO
+from superset.embedded_dashboard.commands.exceptions import (
+    EmbeddedDashboardNotFoundError,
+)
+from superset.extensions import event_logger
+from superset.models.embedded_dashboard import EmbeddedDashboard
+from superset.reports.logs.schemas import openapi_spec_methods_override
+from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class EmbeddedDashboardRestApi(BaseSupersetModelRestApi):
+    datamodel = SQLAInterface(EmbeddedDashboard)
+
+    @before_request
+    def ensure_embedded_enabled(self) -> Optional[Response]:
+        if not is_feature_enabled("EMBEDDED_SUPERSET"):
+            return self.response_404()
+        return None
+
+    include_route_methods = RouteMethod.GET
+    class_permission_name = "EmbeddedDashboard"
+    method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
+
+    resource_name = "embedded_dashboard"
+    allow_browser_login = True
+
+    openapi_spec_tag = "Embedded Dashboard"
+    openapi_spec_methods = openapi_spec_methods_override
+
+    embedded_response_schema = EmbeddedDashboardResponseSchema()
+
+    @expose("/<uuid>", methods=["GET"])
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: 
f"{self.__class__.__name__}.get_embedded",
+        log_to_statsd=False,
+    )
+    # pylint: disable=arguments-differ, arguments-renamed)
+    def get(self, uuid: str) -> Response:
+        """Response
+        Returns the dashboard's embedded configuration
+        ---
+        get:
+          description: >-
+            Returns the dashboard's embedded configuration
+          parameters:
+          - in: path
+            schema:
+              type: string
+            name: uuid
+            description: The embedded configuration uuid
+          responses:
+            200:
+              description: Result contains the embedded dashboard configuration
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        $ref: 
'#/components/schemas/EmbeddedDashboardResponseSchema'
+            401:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            embedded = EmbeddedDAO.find_by_id(uuid)
+            if not embedded:
+                raise EmbeddedDashboardNotFoundError()
+            result = self.embedded_response_schema.dump(embedded)
+            return self.response(200, result=result)
+        except EmbeddedDashboardNotFoundError:
+            return self.response_404()
diff --git a/superset/embedded_dashboard/commands/exceptions.py 
b/superset/embedded_dashboard/commands/exceptions.py
new file mode 100644
index 0000000000..e99dfa807c
--- /dev/null
+++ b/superset/embedded_dashboard/commands/exceptions.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 typing import Optional
+
+from flask_babel import lazy_gettext as _
+
+from superset.commands.exceptions import ForbiddenError, ObjectNotFoundError
+
+
+class EmbeddedDashboardNotFoundError(ObjectNotFoundError):
+    def __init__(
+        self,
+        embedded_dashboard_uuid: Optional[str] = None,
+        exception: Optional[Exception] = None,
+    ) -> None:
+        super().__init__("EmbeddedDashboard", embedded_dashboard_uuid, 
exception)
+
+
+class EmbeddedDashboardAccessDeniedError(ForbiddenError):
+    message = _("You don't have access to this embedded dashboard config.")
diff --git a/superset/initialization/__init__.py 
b/superset/initialization/__init__.py
index 74b05e1688..1bc6b0b824 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -141,6 +141,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         from superset.datasets.api import DatasetRestApi
         from superset.datasets.columns.api import DatasetColumnsRestApi
         from superset.datasets.metrics.api import DatasetMetricRestApi
+        from superset.embedded.api import EmbeddedDashboardRestApi
         from superset.embedded.view import EmbeddedView
         from superset.explore.form_data.api import ExploreFormDataRestApi
         from superset.explore.permalink.api import ExplorePermalinkRestApi
@@ -208,6 +209,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         appbuilder.add_api(DatasetRestApi)
         appbuilder.add_api(DatasetColumnsRestApi)
         appbuilder.add_api(DatasetMetricRestApi)
+        appbuilder.add_api(EmbeddedDashboardRestApi)
         appbuilder.add_api(ExploreFormDataRestApi)
         appbuilder.add_api(ExplorePermalinkRestApi)
         appbuilder.add_api(FilterSetRestApi)
diff --git a/superset/security/api.py b/superset/security/api.py
index b919e29f78..6411ccf7be 100644
--- a/superset/security/api.py
+++ b/superset/security/api.py
@@ -25,6 +25,9 @@ from flask_wtf.csrf import generate_csrf
 from marshmallow import EXCLUDE, fields, post_load, Schema, ValidationError
 from marshmallow_enum import EnumField
 
+from superset.embedded_dashboard.commands.exceptions import (
+    EmbeddedDashboardNotFoundError,
+)
 from superset.extensions import event_logger
 from superset.security.guest_token import GuestTokenResourceType
 
@@ -142,13 +145,16 @@ class SecurityRestApi(BaseApi):
         """
         try:
             body = guest_token_create_schema.load(request.json)
+            
self.appbuilder.sm.validate_guest_token_resources(body["resources"])
+
             # todo validate stuff:
-            # make sure the resource ids are valid
             # make sure username doesn't reference an existing user
             # check rls rules for validity?
             token = self.appbuilder.sm.create_guest_access_token(
                 body["user"], body["resources"], body["rls"]
             )
             return self.response(200, token=token)
+        except EmbeddedDashboardNotFoundError as error:
+            return self.response_400(message=error.message)
         except ValidationError as error:
             return self.response_400(message=error.messages)
diff --git a/superset/security/manager.py b/superset/security/manager.py
index f57f1166ce..48d43d01d0 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -1313,6 +1313,24 @@ class SupersetSecurityManager(  # pylint: 
disable=too-many-public-methods
             audience = audience()
         return audience
 
+    @staticmethod
+    def validate_guest_token_resources(resources: GuestTokenResources) -> None:
+        # pylint: disable=import-outside-toplevel
+        from superset.embedded.dao import EmbeddedDAO
+        from superset.embedded_dashboard.commands.exceptions import (
+            EmbeddedDashboardNotFoundError,
+        )
+        from superset.models.dashboard import Dashboard
+
+        for resource in resources:
+            if resource["type"] == GuestTokenResourceType.DASHBOARD.value:
+                # TODO (embedded): remove this check once uuids are rolled out
+                dashboard = Dashboard.get(str(resource["id"]))
+                if not dashboard:
+                    embedded = EmbeddedDAO.find_by_id(str(resource["id"]))
+                    if not embedded:
+                        raise EmbeddedDashboardNotFoundError()
+
     def create_guest_access_token(
         self,
         user: GuestTokenUser,
diff --git a/tests/integration_tests/embedded/api_tests.py 
b/tests/integration_tests/embedded/api_tests.py
new file mode 100644
index 0000000000..8f3950fcf5
--- /dev/null
+++ b/tests/integration_tests/embedded/api_tests.py
@@ -0,0 +1,53 @@
+# 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.
+# isort:skip_file
+"""Tests for security api methods"""
+from unittest import mock
+
+import pytest
+
+from superset import db
+from superset.embedded.dao import EmbeddedDAO
+from superset.models.dashboard import Dashboard
+from tests.integration_tests.base_tests import SupersetTestCase
+from tests.integration_tests.fixtures.birth_names_dashboard import (
+    load_birth_names_dashboard_with_slices,
+    load_birth_names_data,
+)
+
+
+class TestEmbeddedDashboardApi(SupersetTestCase):
+    resource_name = "embedded_dashboard"
+
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    @mock.patch.dict(
+        "superset.extensions.feature_flag_manager._feature_flags",
+        EMBEDDED_SUPERSET=True,
+    )
+    def test_get_embedded_dashboard(self):
+        self.login("admin")
+        self.dash = 
db.session.query(Dashboard).filter_by(slug="births").first()
+        self.embedded = EmbeddedDAO.upsert(self.dash, [])
+        uri = f"api/v1/{self.resource_name}/{self.embedded.uuid}"
+        response = self.client.get(uri)
+        self.assert200(response)
+
+    def test_get_embedded_dashboard_non_found(self):
+        self.login("admin")
+        uri = f"api/v1/{self.resource_name}/bad-uuid"
+        response = self.client.get(uri)
+        self.assert404(response)
diff --git a/tests/integration_tests/security/api_tests.py 
b/tests/integration_tests/security/api_tests.py
index f936219971..9a5a085c81 100644
--- a/tests/integration_tests/security/api_tests.py
+++ b/tests/integration_tests/security/api_tests.py
@@ -19,10 +19,18 @@
 import json
 
 import jwt
+import pytest
 
-from tests.integration_tests.base_tests import SupersetTestCase
 from flask_wtf.csrf import generate_csrf
+from superset import db
+from superset.embedded.dao import EmbeddedDAO
+from superset.models.dashboard import Dashboard
 from superset.utils.urls import get_url_host
+from tests.integration_tests.base_tests import SupersetTestCase
+from tests.integration_tests.fixtures.birth_names_dashboard import (
+    load_birth_names_dashboard_with_slices,
+    load_birth_names_data,
+)
 
 
 class TestSecurityCsrfApi(SupersetTestCase):
@@ -78,10 +86,13 @@ class TestSecurityGuestTokenApi(SupersetTestCase):
         response = self.client.post(self.uri)
         self.assert403(response)
 
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
     def test_post_guest_token_authorized(self):
+        self.dash = 
db.session.query(Dashboard).filter_by(slug="births").first()
+        self.embedded = EmbeddedDAO.upsert(self.dash, [])
         self.login(username="admin")
         user = {"username": "bob", "first_name": "Bob", "last_name": "Also 
Bob"}
-        resource = {"type": "dashboard", "id": "blah"}
+        resource = {"type": "dashboard", "id": str(self.embedded.uuid)}
         rls_rule = {"dataset": 1, "clause": "1=1"}
         params = {"user": user, "resources": [resource], "rls": [rls_rule]}
 
@@ -99,3 +110,17 @@ class TestSecurityGuestTokenApi(SupersetTestCase):
         )
         self.assertEqual(user, decoded_token["user"])
         self.assertEqual(resource, decoded_token["resources"][0])
+
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    def test_post_guest_token_bad_resources(self):
+        self.login(username="admin")
+        user = {"username": "bob", "first_name": "Bob", "last_name": "Also 
Bob"}
+        resource = {"type": "dashboard", "id": "bad-id"}
+        rls_rule = {"dataset": 1, "clause": "1=1"}
+        params = {"user": user, "resources": [resource], "rls": [rls_rule]}
+
+        response = self.client.post(
+            self.uri, data=json.dumps(params), content_type="application/json"
+        )
+
+        self.assert400(response)

Reply via email to