mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
fix(embedded): prevent double RLS application in virtual datasets (#37395)
This commit is contained in:
committed by
GitHub
parent
a9def2fc15
commit
09e9c6a522
286
tests/unit_tests/models/test_double_rls_virtual_dataset.py
Normal file
286
tests/unit_tests/models/test_double_rls_virtual_dataset.py
Normal file
@@ -0,0 +1,286 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user