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:
Superset Dev
2026-08-21 00:38:48 -07:00
co-authored by Claude Sonnet 5
parent f7d505e1fd
commit 176257e42a
3 changed files with 123 additions and 0 deletions
@@ -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,