mirror of
https://github.com/apache/superset.git
synced 2026-04-08 10:55:20 +00:00
287 lines
9.0 KiB
Python
287 lines
9.0 KiB
Python
# 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.
|
|
"""
|
|
Tests for double RLS application in virtual datasets (Issue #37359).
|
|
|
|
This module tests that guest user RLS filters are applied only once
|
|
when querying virtual datasets, not both in the underlying table SQL
|
|
and the outer query.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from flask import Flask
|
|
from sqlalchemy.sql.elements import TextClause
|
|
|
|
from superset.connectors.sqla.models import BaseDatasource
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_datasource() -> MagicMock:
|
|
"""Create a mock datasource for testing."""
|
|
datasource = MagicMock(spec=BaseDatasource)
|
|
datasource.get_template_processor.return_value = MagicMock()
|
|
datasource.get_template_processor.return_value.process_template = lambda x: x
|
|
datasource.text = lambda x: TextClause(x)
|
|
return datasource
|
|
|
|
|
|
def test_rls_filters_include_guest_when_enabled(
|
|
mock_datasource: MagicMock,
|
|
app: Flask,
|
|
) -> None:
|
|
"""
|
|
Test that RLS filters include guest filters when enabled.
|
|
|
|
When include_global_guest_rls=True and EMBEDDED_SUPERSET is enabled,
|
|
both regular and guest RLS filters should be returned.
|
|
"""
|
|
regular_filter = MagicMock()
|
|
regular_filter.clause = "col1 = 'value1'"
|
|
regular_filter.group_key = None
|
|
|
|
guest_rule = {"clause": "col2 = 'value2'"}
|
|
|
|
with (
|
|
patch(
|
|
"superset.connectors.sqla.models.security_manager.get_rls_filters",
|
|
return_value=[regular_filter],
|
|
),
|
|
patch(
|
|
"superset.connectors.sqla.models.security_manager.get_guest_rls_filters",
|
|
return_value=[guest_rule],
|
|
),
|
|
patch(
|
|
"superset.connectors.sqla.models.is_feature_enabled",
|
|
return_value=True,
|
|
),
|
|
):
|
|
# Call with include_global_guest_rls=True
|
|
filters = BaseDatasource.get_sqla_row_level_filters(
|
|
mock_datasource, include_global_guest_rls=True
|
|
)
|
|
|
|
# Should include both regular and guest RLS
|
|
assert len(filters) == 2
|
|
filter_strs = [str(f) for f in filters]
|
|
assert any("col1" in s for s in filter_strs)
|
|
assert any("col2" in s for s in filter_strs)
|
|
|
|
|
|
def test_rls_filters_exclude_guest_when_requested(
|
|
mock_datasource: MagicMock,
|
|
app: Flask,
|
|
) -> None:
|
|
"""
|
|
Test that RLS filters exclude guest filters when requested.
|
|
|
|
Issue #37359: When analyzing underlying tables in virtual datasets,
|
|
guest RLS should be excluded to prevent double application.
|
|
"""
|
|
regular_filter = MagicMock()
|
|
regular_filter.clause = "col1 = 'value1'"
|
|
regular_filter.group_key = None
|
|
|
|
guest_rule = {"clause": "col2 = 'value2'"}
|
|
|
|
with (
|
|
patch(
|
|
"superset.connectors.sqla.models.security_manager.get_rls_filters",
|
|
return_value=[regular_filter],
|
|
),
|
|
patch(
|
|
"superset.connectors.sqla.models.security_manager.get_guest_rls_filters",
|
|
return_value=[guest_rule],
|
|
),
|
|
patch(
|
|
"superset.connectors.sqla.models.is_feature_enabled",
|
|
return_value=True,
|
|
),
|
|
):
|
|
# Call internal API with include_global_guest_rls=False
|
|
filters = BaseDatasource.get_sqla_row_level_filters(
|
|
mock_datasource, include_global_guest_rls=False
|
|
)
|
|
|
|
# Should include only regular RLS, not guest RLS
|
|
assert len(filters) == 1
|
|
filter_strs = [str(f) for f in filters]
|
|
assert any("col1" in s for s in filter_strs)
|
|
assert not any("col2" in s for s in filter_strs)
|
|
|
|
|
|
def test_rls_filters_include_guest_by_default(
|
|
mock_datasource: MagicMock,
|
|
app: Flask,
|
|
) -> None:
|
|
"""
|
|
Test that RLS filters include guest filters by default.
|
|
|
|
The default behavior (include_global_guest_rls=True) ensures backwards
|
|
compatibility with existing code.
|
|
"""
|
|
regular_filter = MagicMock()
|
|
regular_filter.clause = "col1 = 'value1'"
|
|
regular_filter.group_key = None
|
|
|
|
guest_rule = {"clause": "col2 = 'value2'"}
|
|
|
|
with (
|
|
patch(
|
|
"superset.connectors.sqla.models.security_manager.get_rls_filters",
|
|
return_value=[regular_filter],
|
|
),
|
|
patch(
|
|
"superset.connectors.sqla.models.security_manager.get_guest_rls_filters",
|
|
return_value=[guest_rule],
|
|
),
|
|
patch(
|
|
"superset.connectors.sqla.models.is_feature_enabled",
|
|
return_value=True,
|
|
),
|
|
):
|
|
# Call internal API with default include_global_guest_rls=True
|
|
filters = BaseDatasource.get_sqla_row_level_filters(mock_datasource)
|
|
|
|
# Should include both regular and guest RLS
|
|
assert len(filters) == 2
|
|
|
|
|
|
def test_regular_rls_always_included(
|
|
mock_datasource: MagicMock,
|
|
app: Flask,
|
|
) -> None:
|
|
"""
|
|
Test that regular (non-guest) RLS is always included.
|
|
|
|
Even when include_global_guest_rls=False, regular RLS filters must still
|
|
be applied to underlying tables in virtual datasets.
|
|
"""
|
|
regular_filter = MagicMock()
|
|
regular_filter.clause = "tenant_id = 123"
|
|
regular_filter.group_key = None
|
|
|
|
with (
|
|
patch(
|
|
"superset.connectors.sqla.models.security_manager.get_rls_filters",
|
|
return_value=[regular_filter],
|
|
),
|
|
patch(
|
|
"superset.connectors.sqla.models.security_manager.get_guest_rls_filters",
|
|
return_value=[],
|
|
),
|
|
patch(
|
|
"superset.connectors.sqla.models.is_feature_enabled",
|
|
return_value=True,
|
|
),
|
|
):
|
|
# Call internal API with include_global_guest_rls=False
|
|
filters = BaseDatasource.get_sqla_row_level_filters(
|
|
mock_datasource, include_global_guest_rls=False
|
|
)
|
|
|
|
# Regular RLS should still be included
|
|
assert len(filters) == 1
|
|
assert "tenant_id" in str(filters[0])
|
|
|
|
|
|
def test_guest_rls_skipped_when_feature_disabled(
|
|
mock_datasource: MagicMock,
|
|
app: Flask,
|
|
) -> None:
|
|
"""
|
|
Test that guest RLS is skipped when EMBEDDED_SUPERSET is disabled.
|
|
|
|
This verifies that the feature flag is respected regardless of
|
|
the include_global_guest_rls parameter.
|
|
"""
|
|
regular_filter = MagicMock()
|
|
regular_filter.clause = "col1 = 'value1'"
|
|
regular_filter.group_key = None
|
|
|
|
guest_rule = {"clause": "col2 = 'value2'"}
|
|
|
|
with (
|
|
patch(
|
|
"superset.connectors.sqla.models.security_manager.get_rls_filters",
|
|
return_value=[regular_filter],
|
|
),
|
|
patch(
|
|
"superset.connectors.sqla.models.security_manager.get_guest_rls_filters",
|
|
return_value=[guest_rule],
|
|
),
|
|
patch(
|
|
"superset.connectors.sqla.models.is_feature_enabled",
|
|
return_value=False, # Feature disabled
|
|
),
|
|
):
|
|
# Even with include_global_guest_rls=True, feature flag takes precedence
|
|
filters = BaseDatasource.get_sqla_row_level_filters(
|
|
mock_datasource, include_global_guest_rls=True
|
|
)
|
|
|
|
# Should include only regular RLS
|
|
assert len(filters) == 1
|
|
assert not any("col2" in str(f) for f in filters)
|
|
|
|
|
|
def test_filter_grouping_preserved(
|
|
mock_datasource: MagicMock,
|
|
app: Flask,
|
|
) -> None:
|
|
"""
|
|
Test that filter grouping logic is preserved in internal method.
|
|
|
|
Filters with the same group_key should be ORed together, while
|
|
different groups are ANDed.
|
|
"""
|
|
filter1 = MagicMock()
|
|
filter1.clause = "region = 'US'"
|
|
filter1.group_key = "region_group"
|
|
|
|
filter2 = MagicMock()
|
|
filter2.clause = "region = 'EU'"
|
|
filter2.group_key = "region_group"
|
|
|
|
filter3 = MagicMock()
|
|
filter3.clause = "active = true"
|
|
filter3.group_key = None
|
|
|
|
with (
|
|
patch(
|
|
"superset.connectors.sqla.models.security_manager.get_rls_filters",
|
|
return_value=[filter1, filter2, filter3],
|
|
),
|
|
patch(
|
|
"superset.connectors.sqla.models.security_manager.get_guest_rls_filters",
|
|
return_value=[],
|
|
),
|
|
patch(
|
|
"superset.connectors.sqla.models.is_feature_enabled",
|
|
return_value=False,
|
|
),
|
|
):
|
|
filters = BaseDatasource.get_sqla_row_level_filters(
|
|
mock_datasource, include_global_guest_rls=False
|
|
)
|
|
|
|
# Should have 2 filters: one ungrouped, one grouped (ORed)
|
|
assert len(filters) == 2
|