feat(embedded): add hook to allow superset admins to validate guest token parameters (#30132)
Co-authored-by: David Markey <markey@rapidraitngs.com>
This commit is contained in:
parent
7bb6a14944
commit
a31a4eebdd
|
|
@ -1723,6 +1723,15 @@ GUEST_TOKEN_JWT_EXP_SECONDS = 300 # 5 minutes
|
|||
# Guest token audience for the embedded superset, either string or callable
|
||||
GUEST_TOKEN_JWT_AUDIENCE: Callable[[], str] | str | None = None
|
||||
|
||||
# A callable that can be supplied to do extra validation of guest token configuration
|
||||
# for example certain RLS parameters:
|
||||
# lambda x: len(x['rls']) == 1 and "tenant_id=" in x['rls'][0]['clause']
|
||||
#
|
||||
# Takes the GuestTokenUser dict as an argument
|
||||
# Return False from the callable to return a HTTP 400 to the user.
|
||||
|
||||
GUEST_TOKEN_VALIDATOR_HOOK = None
|
||||
|
||||
# A SQL dataset health check. Note if enabled it is strongly advised that the callable
|
||||
# be memoized to aid with performance, i.e.,
|
||||
#
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
|
||||
from flask import request, Response
|
||||
from flask import current_app, request, Response
|
||||
from flask_appbuilder import expose
|
||||
from flask_appbuilder.api import safe
|
||||
from flask_appbuilder.security.decorators import permission_name, protect
|
||||
|
|
@ -27,6 +27,7 @@ from marshmallow import EXCLUDE, fields, post_load, Schema, ValidationError
|
|||
from superset.commands.dashboard.embedded.exceptions import (
|
||||
EmbeddedDashboardNotFoundError,
|
||||
)
|
||||
from superset.exceptions import SupersetGenericErrorException
|
||||
from superset.extensions import event_logger
|
||||
from superset.security.guest_token import GuestTokenResourceType
|
||||
from superset.views.base_api import BaseSupersetApi, statsd_metrics
|
||||
|
|
@ -148,8 +149,19 @@ class SecurityRestApi(BaseSupersetApi):
|
|||
try:
|
||||
body = guest_token_create_schema.load(request.json)
|
||||
self.appbuilder.sm.validate_guest_token_resources(body["resources"])
|
||||
|
||||
# todo validate stuff:
|
||||
guest_token_validator_hook = current_app.config.get(
|
||||
"GUEST_TOKEN_VALIDATOR_HOOK"
|
||||
)
|
||||
# Run validator to ensure the token parameters are OK.
|
||||
if guest_token_validator_hook is not None:
|
||||
if callable(guest_token_validator_hook):
|
||||
if not guest_token_validator_hook(body):
|
||||
raise ValidationError(message="Guest token validation failed")
|
||||
else:
|
||||
raise SupersetGenericErrorException(
|
||||
message="Guest token validator hook not callable"
|
||||
)
|
||||
# TODO: Add generic validation:
|
||||
# make sure username doesn't reference an existing user
|
||||
# check rls rules for validity?
|
||||
token = self.appbuilder.sm.create_guest_access_token(
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class TestSecurityCsrfApi(SupersetTestCase):
|
|||
|
||||
|
||||
class TestSecurityGuestTokenApi(SupersetTestCase):
|
||||
uri = "api/v1/security/guest_token/" # noqa: F541
|
||||
uri = "api/v1/security/guest_token/"
|
||||
|
||||
def test_post_guest_token_unauthenticated(self):
|
||||
"""
|
||||
|
|
@ -138,6 +138,85 @@ class TestSecurityGuestTokenApi(SupersetTestCase):
|
|||
self.assert400(response)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices", scope="class")
|
||||
class TestSecurityGuestTokenApiTokenValidator(SupersetTestCase):
|
||||
uri = "api/v1/security/guest_token/"
|
||||
|
||||
def _get_guest_token_with_rls(self, rls_rule):
|
||||
dash = db.session.query(Dashboard).filter_by(slug="births").first()
|
||||
self.embedded = EmbeddedDashboardDAO.upsert(dash, [])
|
||||
self.login(ADMIN_USERNAME)
|
||||
user = {"username": "bob", "first_name": "Bob", "last_name": "Also Bob"}
|
||||
resource = {"type": "dashboard", "id": str(self.embedded.uuid)}
|
||||
params = {"user": user, "resources": [resource], "rls": [rls_rule]}
|
||||
return self.client.post(
|
||||
self.uri, data=json.dumps(params), content_type="application/json"
|
||||
)
|
||||
|
||||
@with_config({"GUEST_TOKEN_VALIDATOR_HOOK": lambda x: False})
|
||||
def test_guest_token_validator_hook_denied(self):
|
||||
"""
|
||||
Security API: Test False case from validator - should be 400
|
||||
"""
|
||||
rls_rule = {"dataset": 1, "clause": "tenant_id=123"}
|
||||
self.assert400(self._get_guest_token_with_rls(rls_rule))
|
||||
|
||||
@with_config({"GUEST_TOKEN_VALIDATOR_HOOK": lambda x: True})
|
||||
def test_guest_token_validator_hook_denied_allowed(self):
|
||||
"""
|
||||
Security API: Test True case from validator - should be 200
|
||||
"""
|
||||
rls_rule = {"dataset": 1, "clause": "tenant_id=123"}
|
||||
self.assert200(self._get_guest_token_with_rls(rls_rule))
|
||||
|
||||
@with_config({"GUEST_TOKEN_VALIDATOR_HOOK": 123})
|
||||
def test_guest_validator_hook_not_callable(self):
|
||||
"""
|
||||
Security API: Test validator throws exception when isn't callable
|
||||
should be 500
|
||||
"""
|
||||
rls_rule = {"dataset": 1, "clause": "tenant_id=123"}
|
||||
self.assert500(self._get_guest_token_with_rls(rls_rule))
|
||||
|
||||
@with_config({"GUEST_TOKEN_VALIDATOR_HOOK": lambda x: [][0]})
|
||||
def test_guest_validator_hook_throws_exception(self):
|
||||
"""
|
||||
Security API: Test validator throws exception - should be 500
|
||||
"""
|
||||
rls_rule = {"dataset": 1, "clause": "tenant_id=123"}
|
||||
self.assert500(self._get_guest_token_with_rls(rls_rule))
|
||||
|
||||
@with_config(
|
||||
{
|
||||
"GUEST_TOKEN_VALIDATOR_HOOK": lambda x: len(x["rls"]) == 1
|
||||
and "tenant_id=" in x["rls"][0]["clause"]
|
||||
}
|
||||
)
|
||||
def test_guest_validator_hook_real_world_example_positive(self):
|
||||
"""
|
||||
Security API: Test validator real world example, check tenant_id is in clause
|
||||
Positive case
|
||||
"""
|
||||
# Test validator real world example, check tenant_id is in clause
|
||||
# Should be 200.
|
||||
rls_rule = {"dataset": 1, "clause": "tenant_id=123"}
|
||||
self.assert200(self._get_guest_token_with_rls(rls_rule))
|
||||
|
||||
@with_config(
|
||||
{
|
||||
"GUEST_TOKEN_VALIDATOR_HOOK": lambda x: len(x["rls"]) == 1
|
||||
and "tenant_id=" in x["rls"][0]["clause"]
|
||||
}
|
||||
)
|
||||
def test_guest_validator_hook_real_world_example_negative(self):
|
||||
"""
|
||||
Security API: Test validator real world example, check tenant_id is in clause
|
||||
Negative case
|
||||
"""
|
||||
rls_rule = {}
|
||||
self.assert400(self._get_guest_token_with_rls(rls_rule))
|
||||
|
||||
|
||||
class TestSecurityRolesApi(SupersetTestCase):
|
||||
uri = "api/v1/security/roles/" # noqa: F541
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue