mirror of
https://github.com/apache/superset.git
synced 2026-04-20 00:24:38 +00:00
feat: Embedded dashboard configuration (#19364)
* embedded dashboard model * embedded dashboard endpoints * DRY up using the with_dashboard decorator elsewhere * wip * check feature flags and permissions * wip * sdk * urls * dao option for id column * got it working * Update superset/embedded/view.py * use the curator check * put back old endpoint, for now * allow access by either embedded.uuid or dashboard.id * keep the old endpoint around, for the time being * openapi * lint * lint * lint * test stuff * lint, test * typo * Update superset-frontend/src/embedded/index.tsx * Update superset-frontend/src/embedded/index.tsx * fix tests * bump sdk
This commit is contained in:
committed by
GitHub
parent
a4c261d72c
commit
8e29ec5a66
@@ -388,7 +388,14 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
|
||||
rv = self.get_assert_metric(uri, "info")
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
assert rv.status_code == 200
|
||||
assert set(data["permissions"]) == {"can_read", "can_write", "can_export"}
|
||||
assert set(data["permissions"]) == {
|
||||
"can_read",
|
||||
"can_write",
|
||||
"can_export",
|
||||
"can_get_embedded",
|
||||
"can_delete_embedded",
|
||||
"can_set_embedded",
|
||||
}
|
||||
|
||||
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
|
||||
def test_get_dashboard_not_found(self):
|
||||
@@ -1710,3 +1717,58 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
|
||||
|
||||
response_roles = [result["text"] for result in response["result"]]
|
||||
assert "Alpha" in response_roles
|
||||
|
||||
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
|
||||
def test_embedded_dashboards(self):
|
||||
self.login(username="admin")
|
||||
uri = "api/v1/dashboard/world_health/embedded"
|
||||
|
||||
# initial get should return 404
|
||||
resp = self.get_assert_metric(uri, "get_embedded")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
# post succeeds and returns value
|
||||
allowed_domains = ["test.example", "embedded.example"]
|
||||
resp = self.post_assert_metric(
|
||||
uri,
|
||||
{"allowed_domains": allowed_domains},
|
||||
"set_embedded",
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
result = json.loads(resp.data.decode("utf-8"))["result"]
|
||||
self.assertIsNotNone(result["uuid"])
|
||||
self.assertNotEqual(result["uuid"], "")
|
||||
self.assertEqual(result["allowed_domains"], allowed_domains)
|
||||
|
||||
# get returns value
|
||||
resp = self.get_assert_metric(uri, "get_embedded")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
result = json.loads(resp.data.decode("utf-8"))["result"]
|
||||
self.assertIsNotNone(result["uuid"])
|
||||
self.assertNotEqual(result["uuid"], "")
|
||||
self.assertEqual(result["allowed_domains"], allowed_domains)
|
||||
|
||||
# save uuid for later
|
||||
original_uuid = result["uuid"]
|
||||
|
||||
# put succeeds and returns value
|
||||
resp = self.post_assert_metric(uri, {"allowed_domains": []}, "set_embedded")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertIsNotNone(result["uuid"])
|
||||
self.assertNotEqual(result["uuid"], "")
|
||||
self.assertEqual(result["allowed_domains"], allowed_domains)
|
||||
|
||||
# get returns changed value
|
||||
resp = self.get_assert_metric(uri, "get_embedded")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
result = json.loads(resp.data.decode("utf-8"))["result"]
|
||||
self.assertEqual(result["uuid"], original_uuid)
|
||||
self.assertEqual(result["allowed_domains"], [])
|
||||
|
||||
# delete succeeds
|
||||
resp = self.delete_assert_metric(uri, "delete_embedded")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# get returns 404
|
||||
resp = self.get_assert_metric(uri, "get_embedded")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
16
tests/integration_tests/embedded/__init__.py
Normal file
16
tests/integration_tests/embedded/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
51
tests/integration_tests/embedded/dao_tests.py
Normal file
51
tests/integration_tests/embedded/dao_tests.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# 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
|
||||
import pytest
|
||||
|
||||
import tests.integration_tests.test_app # pylint: disable=unused-import
|
||||
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.world_bank_dashboard import (
|
||||
load_world_bank_dashboard_with_slices,
|
||||
load_world_bank_data,
|
||||
)
|
||||
|
||||
|
||||
class TestEmbeddedDAO(SupersetTestCase):
|
||||
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
|
||||
def test_upsert(self):
|
||||
dash = db.session.query(Dashboard).filter_by(slug="world_health").first()
|
||||
assert not dash.embedded
|
||||
EmbeddedDAO.upsert(dash, ["test.example.com"])
|
||||
assert dash.embedded
|
||||
self.assertEqual(dash.embedded[0].allowed_domains, ["test.example.com"])
|
||||
original_uuid = dash.embedded[0].uuid
|
||||
self.assertIsNotNone(original_uuid)
|
||||
EmbeddedDAO.upsert(dash, [])
|
||||
self.assertEqual(dash.embedded[0].allowed_domains, [])
|
||||
self.assertEqual(dash.embedded[0].uuid, original_uuid)
|
||||
|
||||
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
|
||||
def test_get_by_uuid(self):
|
||||
dash = db.session.query(Dashboard).filter_by(slug="world_health").first()
|
||||
uuid = str(EmbeddedDAO.upsert(dash, ["test.example.com"]).uuid)
|
||||
db.session.expire_all()
|
||||
embedded = EmbeddedDAO.find_by_id(uuid)
|
||||
self.assertIsNotNone(embedded)
|
||||
@@ -22,6 +22,7 @@ from flask import g
|
||||
|
||||
from superset import db, security_manager
|
||||
from superset.dashboards.commands.exceptions import DashboardAccessDeniedError
|
||||
from superset.embedded.dao import EmbeddedDAO
|
||||
from superset.exceptions import SupersetSecurityException
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.security.guest_token import GuestTokenResourceType
|
||||
@@ -38,14 +39,9 @@ from tests.integration_tests.fixtures.birth_names_dashboard import (
|
||||
EMBEDDED_SUPERSET=True,
|
||||
)
|
||||
class TestGuestUserSecurity(SupersetTestCase):
|
||||
# This test doesn't use a dashboard fixture, the next test does.
|
||||
# That way tests are faster.
|
||||
|
||||
resource_id = 42
|
||||
|
||||
def authorized_guest(self):
|
||||
return security_manager.get_guest_user_from_token(
|
||||
{"user": {}, "resources": [{"type": "dashboard", "id": self.resource_id}]}
|
||||
{"user": {}, "resources": [{"type": "dashboard", "id": "some-uuid"}]}
|
||||
)
|
||||
|
||||
def test_is_guest_user__regular_user(self):
|
||||
@@ -83,60 +79,6 @@ class TestGuestUserSecurity(SupersetTestCase):
|
||||
guest_user = security_manager.get_current_guest_user_if_guest()
|
||||
self.assertEqual(guest_user, g.user)
|
||||
|
||||
def test_has_guest_access__regular_user(self):
|
||||
g.user = security_manager.find_user("admin")
|
||||
has_guest_access = security_manager.has_guest_access(
|
||||
GuestTokenResourceType.DASHBOARD, self.resource_id
|
||||
)
|
||||
self.assertFalse(has_guest_access)
|
||||
|
||||
def test_has_guest_access__anonymous_user(self):
|
||||
g.user = security_manager.get_anonymous_user()
|
||||
has_guest_access = security_manager.has_guest_access(
|
||||
GuestTokenResourceType.DASHBOARD, self.resource_id
|
||||
)
|
||||
self.assertFalse(has_guest_access)
|
||||
|
||||
def test_has_guest_access__authorized_guest_user(self):
|
||||
g.user = self.authorized_guest()
|
||||
has_guest_access = security_manager.has_guest_access(
|
||||
GuestTokenResourceType.DASHBOARD, self.resource_id
|
||||
)
|
||||
self.assertTrue(has_guest_access)
|
||||
|
||||
def test_has_guest_access__authorized_guest_user__non_zero_resource_index(self):
|
||||
guest = self.authorized_guest()
|
||||
guest.resources = [
|
||||
{"type": "dashboard", "id": self.resource_id - 1}
|
||||
] + guest.resources
|
||||
g.user = guest
|
||||
|
||||
has_guest_access = security_manager.has_guest_access(
|
||||
GuestTokenResourceType.DASHBOARD, self.resource_id
|
||||
)
|
||||
self.assertTrue(has_guest_access)
|
||||
|
||||
def test_has_guest_access__unauthorized_guest_user__different_resource_id(self):
|
||||
g.user = security_manager.get_guest_user_from_token(
|
||||
{
|
||||
"user": {},
|
||||
"resources": [{"type": "dashboard", "id": self.resource_id - 1}],
|
||||
}
|
||||
)
|
||||
has_guest_access = security_manager.has_guest_access(
|
||||
GuestTokenResourceType.DASHBOARD, self.resource_id
|
||||
)
|
||||
self.assertFalse(has_guest_access)
|
||||
|
||||
def test_has_guest_access__unauthorized_guest_user__different_resource_type(self):
|
||||
g.user = security_manager.get_guest_user_from_token(
|
||||
{"user": {}, "resources": [{"type": "dirt", "id": self.resource_id}]}
|
||||
)
|
||||
has_guest_access = security_manager.has_guest_access(
|
||||
GuestTokenResourceType.DASHBOARD, self.resource_id
|
||||
)
|
||||
self.assertFalse(has_guest_access)
|
||||
|
||||
def test_get_guest_user_roles_explicit(self):
|
||||
guest = self.authorized_guest()
|
||||
roles = security_manager.get_user_roles(guest)
|
||||
@@ -158,13 +100,65 @@ class TestGuestUserSecurity(SupersetTestCase):
|
||||
class TestGuestUserDashboardAccess(SupersetTestCase):
|
||||
def setUp(self) -> None:
|
||||
self.dash = db.session.query(Dashboard).filter_by(slug="births").first()
|
||||
self.embedded = EmbeddedDAO.upsert(self.dash, [])
|
||||
self.authorized_guest = security_manager.get_guest_user_from_token(
|
||||
{"user": {}, "resources": [{"type": "dashboard", "id": self.dash.id}]}
|
||||
{
|
||||
"user": {},
|
||||
"resources": [{"type": "dashboard", "id": str(self.embedded.uuid)}],
|
||||
}
|
||||
)
|
||||
self.unauthorized_guest = security_manager.get_guest_user_from_token(
|
||||
{"user": {}, "resources": [{"type": "dashboard", "id": self.dash.id + 1}]}
|
||||
{
|
||||
"user": {},
|
||||
"resources": [
|
||||
{"type": "dashboard", "id": "06383667-3e02-4e5e-843f-44e9c5896b6c"}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
def test_has_guest_access__regular_user(self):
|
||||
g.user = security_manager.find_user("admin")
|
||||
has_guest_access = security_manager.has_guest_access(self.dash)
|
||||
self.assertFalse(has_guest_access)
|
||||
|
||||
def test_has_guest_access__anonymous_user(self):
|
||||
g.user = security_manager.get_anonymous_user()
|
||||
has_guest_access = security_manager.has_guest_access(self.dash)
|
||||
self.assertFalse(has_guest_access)
|
||||
|
||||
def test_has_guest_access__authorized_guest_user(self):
|
||||
g.user = self.authorized_guest
|
||||
has_guest_access = security_manager.has_guest_access(self.dash)
|
||||
self.assertTrue(has_guest_access)
|
||||
|
||||
def test_has_guest_access__authorized_guest_user__non_zero_resource_index(self):
|
||||
# set up a user who has authorized access, plus another resource
|
||||
guest = self.authorized_guest
|
||||
guest.resources = [
|
||||
{"type": "dashboard", "id": "not-a-real-id"}
|
||||
] + guest.resources
|
||||
g.user = guest
|
||||
|
||||
has_guest_access = security_manager.has_guest_access(self.dash)
|
||||
self.assertTrue(has_guest_access)
|
||||
|
||||
def test_has_guest_access__unauthorized_guest_user__different_resource_id(self):
|
||||
g.user = security_manager.get_guest_user_from_token(
|
||||
{
|
||||
"user": {},
|
||||
"resources": [{"type": "dashboard", "id": "not-a-real-id"}],
|
||||
}
|
||||
)
|
||||
has_guest_access = security_manager.has_guest_access(self.dash)
|
||||
self.assertFalse(has_guest_access)
|
||||
|
||||
def test_has_guest_access__unauthorized_guest_user__different_resource_type(self):
|
||||
g.user = security_manager.get_guest_user_from_token(
|
||||
{"user": {}, "resources": [{"type": "dirt", "id": self.embedded.uuid}]}
|
||||
)
|
||||
has_guest_access = security_manager.has_guest_access(self.dash)
|
||||
self.assertFalse(has_guest_access)
|
||||
|
||||
def test_chart_raise_for_access_as_guest(self):
|
||||
chart = self.dash.slices[0]
|
||||
g.user = self.authorized_guest
|
||||
|
||||
@@ -888,7 +888,9 @@ class TestRolePermission(SupersetTestCase):
|
||||
["AuthDBView", "login"],
|
||||
["AuthDBView", "logout"],
|
||||
["CurrentUserRestApi", "get_me"],
|
||||
# TODO (embedded) remove Dashboard:embedded after uuids have been shipped
|
||||
["Dashboard", "embedded"],
|
||||
["EmbeddedView", "embedded"],
|
||||
["R", "index"],
|
||||
["Superset", "log"],
|
||||
["Superset", "theme"],
|
||||
|
||||
Reference in New Issue
Block a user