fix: add missing access checks to semantic-layer and theme endpoints (#43389)

Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-08-24 08:52:45 -07:00
committed by GitHub
co-authored by Superset Dev Claude Sonnet 5
parent 8a32324184
commit 7f1b41444c
24 changed files with 1266 additions and 76 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,
@@ -729,6 +758,30 @@ def test_put_semantic_layer_not_found(
assert response.status_code == 404
@SEMANTIC_LAYERS_APP
def test_put_semantic_layer_forbidden(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test PUT /<uuid> returns 403 when the caller is not an editor."""
from superset.commands.semantic_layer.exceptions import (
SemanticLayerForbiddenError,
)
mock_command = mocker.patch(
"superset.semantic_layers.api.UpdateSemanticLayerCommand",
)
mock_command.return_value.run.side_effect = SemanticLayerForbiddenError()
response = client.put(
f"/api/v1/semantic_layer/{uuid_lib.uuid4()}",
json={"name": "New"},
)
assert response.status_code == 403
@SEMANTIC_LAYERS_APP
def test_put_semantic_layer_invalid(
client: Any,
@@ -821,6 +874,27 @@ def test_delete_semantic_layer_not_found(
assert response.status_code == 404
@SEMANTIC_LAYERS_APP
def test_delete_semantic_layer_forbidden(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test DELETE /<uuid> returns 403 when the caller is not an editor."""
from superset.commands.semantic_layer.exceptions import (
SemanticLayerForbiddenError,
)
mock_command = mocker.patch(
"superset.semantic_layers.api.DeleteSemanticLayerCommand",
)
mock_command.return_value.run.side_effect = SemanticLayerForbiddenError()
response = client.delete(f"/api/v1/semantic_layer/{uuid_lib.uuid4()}")
assert response.status_code == 403
@SEMANTIC_LAYERS_APP
def test_delete_semantic_layer_failed(
client: Any,
@@ -939,6 +1013,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,
@@ -1645,6 +1746,35 @@ def test_post_semantic_view_layer_not_found(
assert result["errors"][0]["error"] == "Semantic layer not found"
@SEMANTIC_LAYERS_APP
def test_post_semantic_view_forbidden(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test POST / collects forbidden errors instead of aborting the batch."""
mock_command = mocker.patch(
"superset.semantic_layers.api.CreateSemanticViewCommand",
)
mock_command.return_value.run.side_effect = SemanticViewForbiddenError()
payload = {
"views": [
{
"name": "View 1",
"semantic_layer_uuid": str(uuid_lib.uuid4()),
"configuration": {},
},
],
}
response = client.post("/api/v1/semantic_view/", json=payload)
assert response.status_code == 422
result = response.json["result"]
assert len(result["errors"]) == 1
assert not result["created"]
@SEMANTIC_LAYERS_APP
def test_post_semantic_view_create_failed(
client: Any,
@@ -1956,6 +2086,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,