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
This commit is contained in:
parent
59dda1fa05
commit
224769bd45
|
|
@ -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()
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue