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)