mirror of
https://github.com/apache/superset.git
synced 2026-09-01 13:01:33 +00:00
fix(semantic_layers): check access before returning a layer's configuration
Add SemanticLayer.raise_for_access(), mirroring the existing SemanticView.raise_for_access(), and call it from the layer GET, runtime schema, and views endpoints before returning or exercising a layer's decrypted configuration. Previously these endpoints resolved the layer by UUID with no per-object permission check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f7d505e1fd
commit
176257e42a
@@ -672,6 +672,11 @@ class SemanticLayerRestApi(BaseSupersetApi):
|
||||
if not layer:
|
||||
return self.response_404()
|
||||
|
||||
try:
|
||||
layer.raise_for_access()
|
||||
except SupersetSecurityException as ex:
|
||||
return self.response(403, message=ex.message)
|
||||
|
||||
body = request.get_json(silent=True) or {}
|
||||
runtime_data = body.get("runtime_data")
|
||||
|
||||
@@ -726,6 +731,11 @@ class SemanticLayerRestApi(BaseSupersetApi):
|
||||
if not layer:
|
||||
return self.response_404()
|
||||
|
||||
try:
|
||||
layer.raise_for_access()
|
||||
except SupersetSecurityException as ex:
|
||||
return self.response(403, message=ex.message)
|
||||
|
||||
body = request.get_json(silent=True) or {}
|
||||
runtime_data = body.get("runtime_data", {})
|
||||
|
||||
@@ -1158,4 +1168,10 @@ class SemanticLayerRestApi(BaseSupersetApi):
|
||||
layer = SemanticLayerDAO.find_by_uuid(uuid)
|
||||
if not layer:
|
||||
return self.response_404()
|
||||
|
||||
try:
|
||||
layer.raise_for_access()
|
||||
except SupersetSecurityException as ex:
|
||||
return self.response(403, message=ex.message)
|
||||
|
||||
return self.response(200, result=_serialize_layer(layer))
|
||||
|
||||
@@ -148,6 +148,26 @@ class SemanticLayer(AuditMixinNullable, Model):
|
||||
"""Compute the permission string for this semantic layer."""
|
||||
return f"[{self.name}](id:{self.uuid.hex})"
|
||||
|
||||
def raise_for_access(self) -> None:
|
||||
"""Check that the user has access to this semantic layer."""
|
||||
from superset import security_manager
|
||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||
from superset.exceptions import SupersetSecurityException
|
||||
|
||||
if security_manager.can_access_all_datasources():
|
||||
return
|
||||
|
||||
if self.perm and security_manager.can_access("datasource_access", self.perm):
|
||||
return
|
||||
|
||||
raise SupersetSecurityException(
|
||||
SupersetError(
|
||||
error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
|
||||
message=str(_("You don't have access to this semantic layer.")),
|
||||
level=ErrorLevel.ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def after_insert(
|
||||
mapper: Mapper,
|
||||
|
||||
@@ -36,6 +36,7 @@ from superset.commands.semantic_layer.exceptions import (
|
||||
SemanticViewNotFoundError,
|
||||
SemanticViewUpdateFailedError,
|
||||
)
|
||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||
from superset.exceptions import SupersetSecurityException
|
||||
from superset.semantic_layers.api import SemanticLayerRestApi, SemanticViewRestApi
|
||||
|
||||
@@ -529,6 +530,34 @@ def test_runtime_schema_not_found(
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@SEMANTIC_LAYERS_APP
|
||||
def test_runtime_schema_forbidden(
|
||||
client: Any,
|
||||
full_api_access: None,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""Test POST /<uuid>/schema/runtime returns 403 when access is denied."""
|
||||
test_uuid = str(uuid_lib.uuid4())
|
||||
mock_layer = MagicMock()
|
||||
mock_layer.raise_for_access.side_effect = SupersetSecurityException(
|
||||
SupersetError(
|
||||
error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
|
||||
message="You don't have access to this semantic layer.",
|
||||
level=ErrorLevel.ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
|
||||
mock_dao.find_by_uuid.return_value = mock_layer
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1/semantic_layer/{test_uuid}/schema/runtime",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
mock_layer.raise_for_access.assert_called_once()
|
||||
|
||||
|
||||
@SEMANTIC_LAYERS_APP
|
||||
def test_runtime_schema_unknown_type(
|
||||
client: Any,
|
||||
@@ -939,6 +968,33 @@ def test_get_semantic_layer_not_found(
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@SEMANTIC_LAYERS_APP
|
||||
def test_get_semantic_layer_forbidden(
|
||||
client: Any,
|
||||
full_api_access: None,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""Test GET /<uuid> returns 403 when user lacks access to the layer."""
|
||||
test_uuid = uuid_lib.uuid4()
|
||||
layer = MagicMock()
|
||||
layer.uuid = test_uuid
|
||||
layer.raise_for_access.side_effect = SupersetSecurityException(
|
||||
SupersetError(
|
||||
error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
|
||||
message="You don't have access to this semantic layer.",
|
||||
level=ErrorLevel.ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
|
||||
mock_dao.find_by_uuid.return_value = layer
|
||||
|
||||
response = client.get(f"/api/v1/semantic_layer/{test_uuid}")
|
||||
|
||||
assert response.status_code == 403
|
||||
layer.raise_for_access.assert_called_once()
|
||||
|
||||
|
||||
@SEMANTIC_LAYERS_APP
|
||||
def test_serialize_layer_string_config(
|
||||
client: Any,
|
||||
@@ -1956,6 +2012,37 @@ def test_get_views(
|
||||
assert result[1]["name"] == "View B"
|
||||
|
||||
|
||||
@SEMANTIC_LAYERS_APP
|
||||
def test_get_views_forbidden(
|
||||
client: Any,
|
||||
full_api_access: None,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""Test POST /<uuid>/views returns 403 when access is denied."""
|
||||
test_uuid = str(uuid_lib.uuid4())
|
||||
mock_layer = MagicMock()
|
||||
mock_layer.uuid = uuid_lib.uuid4()
|
||||
mock_layer.raise_for_access.side_effect = SupersetSecurityException(
|
||||
SupersetError(
|
||||
error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
|
||||
message="You don't have access to this semantic layer.",
|
||||
level=ErrorLevel.ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
|
||||
mock_dao.find_by_uuid.return_value = mock_layer
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1/semantic_layer/{test_uuid}/views",
|
||||
json={"runtime_data": {"database": "mydb"}},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
mock_layer.raise_for_access.assert_called_once()
|
||||
mock_layer.implementation.get_semantic_views.assert_not_called()
|
||||
|
||||
|
||||
@SEMANTIC_LAYERS_APP
|
||||
def test_get_views_with_existing(
|
||||
client: Any,
|
||||
|
||||
Reference in New Issue
Block a user