mirror of
https://github.com/apache/superset.git
synced 2026-05-19 23:05:14 +00:00
Compare commits
3 Commits
docs/dashb
...
tdd/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90e86ae3a4 | ||
|
|
94083497a6 | ||
|
|
ce6e9f4a02 |
@@ -185,6 +185,16 @@ class ExportDashboardsCommand(ExportModelsCommand):
|
||||
# Add theme UUID for proper cross-system imports
|
||||
payload["theme_uuid"] = str(model.theme.uuid) if model.theme else None
|
||||
|
||||
# Include role assignments (DASHBOARD_RBAC). Role IDs are
|
||||
# environment-local, so emit names — the import side resolves them
|
||||
# back to roles in the destination environment. The key is omitted
|
||||
# entirely when there are no role restrictions; older import code
|
||||
# treats "missing" as "no restriction" and an empty list could
|
||||
# confuse importers that distinguish the two states.
|
||||
role_names = sorted(role.name for role in (model.roles or []))
|
||||
if role_names:
|
||||
payload["roles"] = role_names
|
||||
|
||||
payload["version"] = EXPORT_VERSION
|
||||
|
||||
# Check if the TAGGING_SYSTEM feature is enabled
|
||||
|
||||
@@ -256,6 +256,11 @@ def import_dashboard( # noqa: C901
|
||||
|
||||
# Note: theme_id handling moved to higher level import logic
|
||||
|
||||
# Pop roles before handing config to import_from_dict — it's a
|
||||
# relationship, not a column, and the standard SQLAlchemy import path
|
||||
# doesn't resolve role *names* into role objects. We re-attach below.
|
||||
role_names = config.pop("roles", None)
|
||||
|
||||
for key, new_name in JSON_KEYS.items():
|
||||
if config.get(key) is not None:
|
||||
value = config.pop(key)
|
||||
@@ -271,4 +276,25 @@ def import_dashboard( # noqa: C901
|
||||
if (user := get_user()) and user not in dashboard.owners:
|
||||
dashboard.owners.append(user)
|
||||
|
||||
# Re-attach DASHBOARD_RBAC role assignments by name. Role IDs are
|
||||
# environment-local; names are how exports cross environments. Roles
|
||||
# that don't exist in the destination are skipped with a warning
|
||||
# rather than failing the import — admins may need to create them
|
||||
# before the access restriction takes effect.
|
||||
if isinstance(role_names, list) and role_names:
|
||||
resolved_roles = []
|
||||
for name in role_names:
|
||||
role = security_manager.find_role(name)
|
||||
if role is not None:
|
||||
resolved_roles.append(role)
|
||||
else:
|
||||
logger.warning(
|
||||
"Dashboard '%s': role %r referenced in export does not "
|
||||
"exist in this environment; access restriction will not "
|
||||
"be applied for that role",
|
||||
dashboard.dashboard_title,
|
||||
name,
|
||||
)
|
||||
dashboard.roles = resolved_roles
|
||||
|
||||
return dashboard
|
||||
|
||||
@@ -188,6 +188,74 @@ def test_file_content_null_chart_customization_config_does_not_raise():
|
||||
assert result["metadata"]["chart_customization_config"] is None
|
||||
|
||||
|
||||
def test_file_content_includes_roles_for_dashboard_with_role_restrictions():
|
||||
"""
|
||||
Regression guard for #21000: dashboards restricted via DASHBOARD_RBAC must
|
||||
have their role assignments included in the exported YAML. Without this,
|
||||
importing the bundle into another environment recreates the dashboard with
|
||||
no role restriction — silently turning a restricted dashboard into a
|
||||
publicly accessible one.
|
||||
|
||||
The export bundle is the canonical source of truth for migrating
|
||||
dashboards across environments; dropping roles silently is a security
|
||||
regression (a "least privilege" dashboard becomes "all privileges" on
|
||||
import). The user, not the export pipeline, should decide whether to
|
||||
strip roles before sharing a bundle.
|
||||
|
||||
We assert against role *names* rather than IDs because role IDs are
|
||||
environment-local; the import side resolves names back to the destination
|
||||
environment's roles.
|
||||
"""
|
||||
from superset.commands.dashboard.export import ExportDashboardsCommand
|
||||
|
||||
role_alpha = MagicMock()
|
||||
role_alpha.name = "Finance"
|
||||
role_beta = MagicMock()
|
||||
role_beta.name = "Executives"
|
||||
|
||||
mock_dashboard = _make_mock_dashboard({"native_filter_configuration": []})
|
||||
mock_dashboard.roles = [role_alpha, role_beta]
|
||||
|
||||
with patch(
|
||||
"superset.commands.dashboard.export.feature_flag_manager.is_feature_enabled",
|
||||
return_value=False,
|
||||
):
|
||||
content = ExportDashboardsCommand._file_content(mock_dashboard)
|
||||
|
||||
result = yaml.safe_load(content)
|
||||
assert "roles" in result, (
|
||||
"Dashboard export must include role names; without them, importing "
|
||||
"into a fresh environment loses the role-based access restriction "
|
||||
"and the dashboard becomes accessible to all roles by default."
|
||||
)
|
||||
assert sorted(result["roles"]) == ["Executives", "Finance"]
|
||||
|
||||
|
||||
def test_file_content_omits_roles_field_when_dashboard_has_no_roles():
|
||||
"""
|
||||
A dashboard with no role restrictions must not emit an empty ``roles: []``
|
||||
key. Older bundles in the wild were written without the key at all, and
|
||||
the import side treats "missing" as "no restriction"; emitting an empty
|
||||
list could trip importers that distinguish the two states.
|
||||
"""
|
||||
from superset.commands.dashboard.export import ExportDashboardsCommand
|
||||
|
||||
mock_dashboard = _make_mock_dashboard({"native_filter_configuration": []})
|
||||
mock_dashboard.roles = []
|
||||
|
||||
with patch(
|
||||
"superset.commands.dashboard.export.feature_flag_manager.is_feature_enabled",
|
||||
return_value=False,
|
||||
):
|
||||
content = ExportDashboardsCommand._file_content(mock_dashboard)
|
||||
|
||||
result = yaml.safe_load(content)
|
||||
# Strict: the key must be absent (not an empty list). The import side
|
||||
# treats "missing" as "no restriction"; emitting an empty list could
|
||||
# trip importers that distinguish the two states.
|
||||
assert "roles" not in result
|
||||
|
||||
|
||||
def test_file_content_missing_dataset_preserves_dataset_id():
|
||||
"""
|
||||
When DatasetDAO.find_by_id returns None for a display control target,
|
||||
|
||||
Reference in New Issue
Block a user