From 4e6d34251e6c9d0d8c743111eecf873924a96f88 Mon Sep 17 00:00:00 2001 From: Lily Kuang Date: Thu, 7 Apr 2022 18:59:43 -0700 Subject: [PATCH 1/4] feat(embedded): get embedded dashboard config by uuid --- superset-frontend/package-lock.json | 26 ++--- superset/embedded/api.py | 105 ++++++++++++++++++ superset/embedded/dao.py | 15 +++ .../embedded_dashboard/commands/exception.py | 33 ++++++ superset/initialization/__init__.py | 2 + superset/models/embedded_dashboard.py | 11 ++ 6 files changed, 175 insertions(+), 17 deletions(-) create mode 100644 superset/embedded/api.py create mode 100644 superset/embedded_dashboard/commands/exception.py diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 2e845ffab4f53..9be7363749a6f 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -26,7 +26,6 @@ "@superset-ui/legacy-plugin-chart-chord": "file:./plugins/legacy-plugin-chart-chord", "@superset-ui/legacy-plugin-chart-country-map": "file:./plugins/legacy-plugin-chart-country-map", "@superset-ui/legacy-plugin-chart-event-flow": "file:./plugins/legacy-plugin-chart-event-flow", - "@superset-ui/legacy-plugin-chart-force-directed": "file:./plugins/legacy-plugin-chart-force-directed", "@superset-ui/legacy-plugin-chart-heatmap": "file:./plugins/legacy-plugin-chart-heatmap", "@superset-ui/legacy-plugin-chart-histogram": "file:./plugins/legacy-plugin-chart-histogram", "@superset-ui/legacy-plugin-chart-horizon": "file:./plugins/legacy-plugin-chart-horizon", @@ -21908,10 +21907,6 @@ "resolved": "plugins/legacy-plugin-chart-event-flow", "link": true }, - "node_modules/@superset-ui/legacy-plugin-chart-force-directed": { - "resolved": "plugins/legacy-plugin-chart-force-directed", - "link": true - }, "node_modules/@superset-ui/legacy-plugin-chart-heatmap": { "resolved": "plugins/legacy-plugin-chart-heatmap", "link": true @@ -58656,7 +58651,6 @@ "@superset-ui/legacy-plugin-chart-chord": "*", "@superset-ui/legacy-plugin-chart-country-map": "*", "@superset-ui/legacy-plugin-chart-event-flow": "*", - "@superset-ui/legacy-plugin-chart-force-directed": "*", "@superset-ui/legacy-plugin-chart-heatmap": "*", "@superset-ui/legacy-plugin-chart-histogram": "*", "@superset-ui/legacy-plugin-chart-horizon": "*", @@ -59071,7 +59065,8 @@ }, "peerDependencies": { "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*" + "@superset-ui/core": "*", + "react": "^16.13.1" } }, "plugins/legacy-plugin-chart-country-map/node_modules/d3-array": { @@ -59099,6 +59094,7 @@ "plugins/legacy-plugin-chart-force-directed": { "name": "@superset-ui/legacy-plugin-chart-force-directed", "version": "0.18.25", + "extraneous": true, "license": "Apache-2.0", "dependencies": { "d3": "^3.5.17", @@ -59360,7 +59356,8 @@ }, "peerDependencies": { "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*" + "@superset-ui/core": "*", + "react": "^16.13.1" } }, "plugins/legacy-plugin-chart-sunburst": { @@ -59373,7 +59370,8 @@ }, "peerDependencies": { "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*" + "@superset-ui/core": "*", + "react": "^16.13.1" } }, "plugins/legacy-plugin-chart-time-table": { @@ -59403,7 +59401,8 @@ }, "peerDependencies": { "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*" + "@superset-ui/core": "*", + "react": "^16.13.1" } }, "plugins/legacy-plugin-chart-world-map": { @@ -76645,13 +76644,6 @@ "prop-types": "^15.6.2" } }, - "@superset-ui/legacy-plugin-chart-force-directed": { - "version": "file:plugins/legacy-plugin-chart-force-directed", - "requires": { - "d3": "^3.5.17", - "prop-types": "^15.7.2" - } - }, "@superset-ui/legacy-plugin-chart-heatmap": { "version": "file:plugins/legacy-plugin-chart-heatmap", "requires": { diff --git a/superset/embedded/api.py b/superset/embedded/api.py new file mode 100644 index 0000000000000..41c3c597c2981 --- /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_dashboard.commands.exception import \ + EmbeddedDashboardAccessDeniedError, 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 + + show_columns = [ + "uuid", + ] + openapi_spec_tag = "Embedded Dashboard" + openapi_spec_methods = openapi_spec_methods_override + + embedded_response_schema = EmbeddedDashboardResponseSchema() + + @expose("/", 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, + ) + 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: id_or_slug + description: The dashboard id or slug + responses: + 200: + description: Result contains the embedded dashboard config + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/EmbeddedDashboardResponseSchema' + 401: + $ref: '#/components/responses/404' + 500: + $ref: '#/components/responses/500' + """ + try: + embedded = EmbeddedDashboard.get_by_uuid(uuid) + result = self.embedded_response_schema.dump(embedded) + return self.response(200, result=result) + except EmbeddedDashboardAccessDeniedError: + return self.response_403() + except EmbeddedDashboardNotFoundError: + return self.response_404() diff --git a/superset/embedded/dao.py b/superset/embedded/dao.py index 957a7242a77d3..4617c030d92eb 100644 --- a/superset/embedded/dao.py +++ b/superset/embedded/dao.py @@ -18,6 +18,9 @@ from typing import Any, Dict, List from superset.dao.base import BaseDAO +from superset.embedded_dashboard.commands.exception import ( + EmbeddedDashboardNotFoundError +) from superset.extensions import db from superset.models.dashboard import Dashboard from superset.models.embedded_dashboard import EmbeddedDashboard @@ -51,3 +54,15 @@ def create(cls, properties: Dict[str, Any], commit: bool = True) -> Any: At least, until we are ok with more than one embedded instance per dashboard. """ raise NotImplementedError("Use EmbeddedDAO.upsert() instead.") + + @staticmethod + def get(uuid: str) -> EmbeddedDashboard: + """ + Sets up a dashboard to be embeddable. + Upsert is used to preserve the embedded_dashboard uuid across updates. + """ + embedded = EmbeddedDashboard.get_by_uuid(embedded_uuid=uuid) + if not embedded: + raise EmbeddedDashboardNotFoundError() + + return embedded diff --git a/superset/embedded_dashboard/commands/exception.py b/superset/embedded_dashboard/commands/exception.py new file mode 100644 index 0000000000000..822c7a9da907e --- /dev/null +++ b/superset/embedded_dashboard/commands/exception.py @@ -0,0 +1,33 @@ +# 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 74b05e168804e..1bc6b0b82417d 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -141,6 +141,7 @@ def init_views(self) -> None: 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 @@ def init_views(self) -> None: 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/models/embedded_dashboard.py b/superset/models/embedded_dashboard.py index 7718bc886f97a..f0d9cb6e29de0 100644 --- a/superset/models/embedded_dashboard.py +++ b/superset/models/embedded_dashboard.py @@ -14,6 +14,8 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +from __future__ import annotations + import uuid from typing import List @@ -22,6 +24,7 @@ from sqlalchemy.orm import relationship from sqlalchemy_utils import UUIDType +from superset import db from superset.models.helpers import AuditMixinNullable @@ -55,3 +58,11 @@ def allowed_domains(self) -> List[str]: An empty list means any domain can embed. """ return self.allow_domain_list.split(",") if self.allow_domain_list else [] + + @classmethod + def get_by_uuid(cls, embedded_uuid: str) -> EmbeddedDashboard: + session = db.session() + qry = session.query(EmbeddedDashboard).filter( + EmbeddedDashboard.uuid == embedded_uuid + ) + return qry.one_or_none() From 1ebd387a7666a615839100d3c5a56bdce04f53b2 Mon Sep 17 00:00:00 2001 From: Lily Kuang Date: Fri, 8 Apr 2022 13:55:17 -0700 Subject: [PATCH 2/4] add tests and validation --- superset/embedded/api.py | 24 ++++----- superset/embedded/dao.py | 15 ------ .../commands/{exception.py => exceptions.py} | 3 +- superset/models/embedded_dashboard.py | 11 ----- superset/security/api.py | 8 ++- superset/security/manager.py | 18 +++++++ tests/integration_tests/embedded/api_tests.py | 49 +++++++++++++++++++ tests/integration_tests/security/api_tests.py | 29 ++++++++++- 8 files changed, 115 insertions(+), 42 deletions(-) rename superset/embedded_dashboard/commands/{exception.py => exceptions.py} (96%) create mode 100644 tests/integration_tests/embedded/api_tests.py diff --git a/superset/embedded/api.py b/superset/embedded/api.py index 41c3c597c2981..f7278d910a079 100644 --- a/superset/embedded/api.py +++ b/superset/embedded/api.py @@ -18,15 +18,17 @@ from typing import Optional from flask import Response -from flask_appbuilder.api import expose, protect, safe +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_dashboard.commands.exception import \ - EmbeddedDashboardAccessDeniedError, EmbeddedDashboardNotFoundError +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 @@ -51,9 +53,6 @@ def ensure_embedded_enabled(self) -> Optional[Response]: resource_name = "embedded_dashboard" allow_browser_login = True - show_columns = [ - "uuid", - ] openapi_spec_tag = "Embedded Dashboard" openapi_spec_methods = openapi_spec_methods_override @@ -67,6 +66,7 @@ def ensure_embedded_enabled(self) -> Optional[Response]: 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 @@ -78,11 +78,11 @@ def get(self, uuid: str) -> Response: - in: path schema: type: string - name: id_or_slug - description: The dashboard id or slug + name: uuid + description: The embedded configuration uuid responses: 200: - description: Result contains the embedded dashboard config + description: Result contains the embedded dashboard configuration content: application/json: schema: @@ -96,10 +96,10 @@ def get(self, uuid: str) -> Response: $ref: '#/components/responses/500' """ try: - embedded = EmbeddedDashboard.get_by_uuid(uuid) + 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 EmbeddedDashboardAccessDeniedError: - return self.response_403() except EmbeddedDashboardNotFoundError: return self.response_404() diff --git a/superset/embedded/dao.py b/superset/embedded/dao.py index 4617c030d92eb..957a7242a77d3 100644 --- a/superset/embedded/dao.py +++ b/superset/embedded/dao.py @@ -18,9 +18,6 @@ from typing import Any, Dict, List from superset.dao.base import BaseDAO -from superset.embedded_dashboard.commands.exception import ( - EmbeddedDashboardNotFoundError -) from superset.extensions import db from superset.models.dashboard import Dashboard from superset.models.embedded_dashboard import EmbeddedDashboard @@ -54,15 +51,3 @@ def create(cls, properties: Dict[str, Any], commit: bool = True) -> Any: At least, until we are ok with more than one embedded instance per dashboard. """ raise NotImplementedError("Use EmbeddedDAO.upsert() instead.") - - @staticmethod - def get(uuid: str) -> EmbeddedDashboard: - """ - Sets up a dashboard to be embeddable. - Upsert is used to preserve the embedded_dashboard uuid across updates. - """ - embedded = EmbeddedDashboard.get_by_uuid(embedded_uuid=uuid) - if not embedded: - raise EmbeddedDashboardNotFoundError() - - return embedded diff --git a/superset/embedded_dashboard/commands/exception.py b/superset/embedded_dashboard/commands/exceptions.py similarity index 96% rename from superset/embedded_dashboard/commands/exception.py rename to superset/embedded_dashboard/commands/exceptions.py index 822c7a9da907e..e99dfa807cf49 100644 --- a/superset/embedded_dashboard/commands/exception.py +++ b/superset/embedded_dashboard/commands/exceptions.py @@ -17,6 +17,7 @@ from typing import Optional from flask_babel import lazy_gettext as _ + from superset.commands.exceptions import ForbiddenError, ObjectNotFoundError @@ -24,7 +25,7 @@ class EmbeddedDashboardNotFoundError(ObjectNotFoundError): def __init__( self, embedded_dashboard_uuid: Optional[str] = None, - exception: Optional[Exception] = None + exception: Optional[Exception] = None, ) -> None: super().__init__("EmbeddedDashboard", embedded_dashboard_uuid, exception) diff --git a/superset/models/embedded_dashboard.py b/superset/models/embedded_dashboard.py index f0d9cb6e29de0..7718bc886f97a 100644 --- a/superset/models/embedded_dashboard.py +++ b/superset/models/embedded_dashboard.py @@ -14,8 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from __future__ import annotations - import uuid from typing import List @@ -24,7 +22,6 @@ from sqlalchemy.orm import relationship from sqlalchemy_utils import UUIDType -from superset import db from superset.models.helpers import AuditMixinNullable @@ -58,11 +55,3 @@ def allowed_domains(self) -> List[str]: An empty list means any domain can embed. """ return self.allow_domain_list.split(",") if self.allow_domain_list else [] - - @classmethod - def get_by_uuid(cls, embedded_uuid: str) -> EmbeddedDashboard: - session = db.session() - qry = session.query(EmbeddedDashboard).filter( - EmbeddedDashboard.uuid == embedded_uuid - ) - return qry.one_or_none() diff --git a/superset/security/api.py b/superset/security/api.py index b919e29f78ddd..6411ccf7be56b 100644 --- a/superset/security/api.py +++ b/superset/security/api.py @@ -25,6 +25,9 @@ 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 @@ def guest_token(self) -> Response: """ 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 f57f1166ce394..48d43d01d0f76 100644 --- a/superset/security/manager.py +++ b/superset/security/manager.py @@ -1313,6 +1313,24 @@ def _get_guest_token_jwt_audience() -> str: 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 0000000000000..df2d222a179ff --- /dev/null +++ b/tests/integration_tests/embedded/api_tests.py @@ -0,0 +1,49 @@ +# 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""" +import json + +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") + 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 f936219971517..9a5a085c81c34 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 @@ def test_post_guest_token_unauthorized(self): 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 @@ def test_post_guest_token_authorized(self): ) 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) From 72f0450c3d685de77c5d2ac3be16c6b11bb42955 Mon Sep 17 00:00:00 2001 From: Lily Kuang Date: Mon, 11 Apr 2022 09:37:05 -0700 Subject: [PATCH 3/4] remove accidentally commit --- superset-frontend/package-lock.json | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 9be7363749a6f..2e845ffab4f53 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -26,6 +26,7 @@ "@superset-ui/legacy-plugin-chart-chord": "file:./plugins/legacy-plugin-chart-chord", "@superset-ui/legacy-plugin-chart-country-map": "file:./plugins/legacy-plugin-chart-country-map", "@superset-ui/legacy-plugin-chart-event-flow": "file:./plugins/legacy-plugin-chart-event-flow", + "@superset-ui/legacy-plugin-chart-force-directed": "file:./plugins/legacy-plugin-chart-force-directed", "@superset-ui/legacy-plugin-chart-heatmap": "file:./plugins/legacy-plugin-chart-heatmap", "@superset-ui/legacy-plugin-chart-histogram": "file:./plugins/legacy-plugin-chart-histogram", "@superset-ui/legacy-plugin-chart-horizon": "file:./plugins/legacy-plugin-chart-horizon", @@ -21907,6 +21908,10 @@ "resolved": "plugins/legacy-plugin-chart-event-flow", "link": true }, + "node_modules/@superset-ui/legacy-plugin-chart-force-directed": { + "resolved": "plugins/legacy-plugin-chart-force-directed", + "link": true + }, "node_modules/@superset-ui/legacy-plugin-chart-heatmap": { "resolved": "plugins/legacy-plugin-chart-heatmap", "link": true @@ -58651,6 +58656,7 @@ "@superset-ui/legacy-plugin-chart-chord": "*", "@superset-ui/legacy-plugin-chart-country-map": "*", "@superset-ui/legacy-plugin-chart-event-flow": "*", + "@superset-ui/legacy-plugin-chart-force-directed": "*", "@superset-ui/legacy-plugin-chart-heatmap": "*", "@superset-ui/legacy-plugin-chart-histogram": "*", "@superset-ui/legacy-plugin-chart-horizon": "*", @@ -59065,8 +59071,7 @@ }, "peerDependencies": { "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "react": "^16.13.1" + "@superset-ui/core": "*" } }, "plugins/legacy-plugin-chart-country-map/node_modules/d3-array": { @@ -59094,7 +59099,6 @@ "plugins/legacy-plugin-chart-force-directed": { "name": "@superset-ui/legacy-plugin-chart-force-directed", "version": "0.18.25", - "extraneous": true, "license": "Apache-2.0", "dependencies": { "d3": "^3.5.17", @@ -59356,8 +59360,7 @@ }, "peerDependencies": { "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "react": "^16.13.1" + "@superset-ui/core": "*" } }, "plugins/legacy-plugin-chart-sunburst": { @@ -59370,8 +59373,7 @@ }, "peerDependencies": { "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "react": "^16.13.1" + "@superset-ui/core": "*" } }, "plugins/legacy-plugin-chart-time-table": { @@ -59401,8 +59403,7 @@ }, "peerDependencies": { "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "react": "^16.13.1" + "@superset-ui/core": "*" } }, "plugins/legacy-plugin-chart-world-map": { @@ -76644,6 +76645,13 @@ "prop-types": "^15.6.2" } }, + "@superset-ui/legacy-plugin-chart-force-directed": { + "version": "file:plugins/legacy-plugin-chart-force-directed", + "requires": { + "d3": "^3.5.17", + "prop-types": "^15.7.2" + } + }, "@superset-ui/legacy-plugin-chart-heatmap": { "version": "file:plugins/legacy-plugin-chart-heatmap", "requires": { From 5ac9f575b2f77aeb9032d43bb7a40289646fddb3 Mon Sep 17 00:00:00 2001 From: Lily Kuang Date: Mon, 11 Apr 2022 18:15:40 -0700 Subject: [PATCH 4/4] fix tests --- tests/integration_tests/embedded/api_tests.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/integration_tests/embedded/api_tests.py b/tests/integration_tests/embedded/api_tests.py index df2d222a179ff..8f3950fcf5462 100644 --- a/tests/integration_tests/embedded/api_tests.py +++ b/tests/integration_tests/embedded/api_tests.py @@ -16,7 +16,7 @@ # under the License. # isort:skip_file """Tests for security api methods""" -import json +from unittest import mock import pytest @@ -34,6 +34,10 @@ 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()