feat: Implement UI-based system theme administration (#34560)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Maxime Beauchemin
2025-08-12 16:27:48 -04:00
committed by GitHub
parent f50cbd7958
commit 5309edf3a5
31 changed files with 2383 additions and 543 deletions

View File

@@ -92,13 +92,14 @@ class TestThemeApi(SupersetTestCase):
"created_on",
"id",
"is_system",
"is_system_default",
"is_system_dark",
"json_data",
"theme_name",
"uuid",
]
result_columns = list(data["result"][0].keys())
result_columns.sort()
assert expected_columns == result_columns
assert set(expected_columns) == set(result_columns)
@pytest.mark.usefixtures("create_themes")
def test_get_list_sort_theme(self):
@@ -207,6 +208,8 @@ class TestThemeApi(SupersetTestCase):
"theme_name": "theme_name1",
"json_data": '{"color": "theme1"}',
"is_system": False,
"is_system_default": False,
"is_system_dark": False,
"uuid": str(theme.uuid),
"changed_by": {
"first_name": theme.created_by.first_name,

View File

@@ -0,0 +1,260 @@
# 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.
"""Integration tests for theme API permissions"""
import uuid
from superset import db
from superset.models.core import Theme
from superset.utils import json
from tests.conftest import with_config
from tests.integration_tests.base_tests import SupersetTestCase
from tests.integration_tests.constants import ADMIN_USERNAME, GAMMA_USERNAME
class TestThemeAPIPermissions(SupersetTestCase):
"""Test theme API permissions and system theme functionality"""
def setUp(self):
"""Set up test fixtures"""
super().setUp()
# Generate unique identifier for this test
self.test_id = str(uuid.uuid4())[:8]
# Create test themes
self.regular_theme = Theme(
theme_name=f"Regular Theme {self.test_id}",
json_data=json.dumps({"colors": {"primary": "#1890ff"}}),
is_system=False,
created_by=self.get_user("admin"),
changed_by=self.get_user("admin"),
)
self.system_theme = Theme(
theme_name=f"System Theme {self.test_id}",
json_data=json.dumps({"colors": {"primary": "#000000"}}),
is_system=True,
created_by=self.get_user("admin"),
changed_by=self.get_user("admin"),
)
db.session.add_all([self.regular_theme, self.system_theme])
db.session.commit()
def tearDown(self):
"""Clean up test fixtures"""
# Clean up themes
for theme in [self.regular_theme, self.system_theme]:
if theme:
try:
db.session.delete(theme)
except Exception:
db.session.rollback()
try:
db.session.commit()
except Exception:
db.session.rollback()
super().tearDown()
def test_admin_can_set_system_default(self):
"""Test that admin can set a theme as system default"""
# Login as admin
self.login(ADMIN_USERNAME)
# Set theme as system default
response = self.client.put(
f"/api/v1/theme/{self.regular_theme.id}/set_system_default"
)
# Should succeed
assert response.status_code == 200
# Verify theme is now system default
theme = db.session.query(Theme).filter_by(id=self.regular_theme.id).first()
assert theme.is_system_default is True
@with_config({"ENABLE_UI_THEME_ADMINISTRATION": True})
def test_non_admin_cannot_set_system_default(self):
"""Test that non-admin users cannot set system themes"""
# Login as gamma user
self.login(GAMMA_USERNAME)
# Try to set theme as system default
response = self.client.put(
f"/api/v1/theme/{self.regular_theme.id}/set_system_default"
)
# Should be forbidden
assert response.status_code == 403
data = response.get_json()
assert "Only administrators can set system themes" in data["message"]
# Verify theme is not system default
theme = db.session.query(Theme).filter_by(id=self.regular_theme.id).first()
assert theme.is_system_default is False
@with_config({"ENABLE_UI_THEME_ADMINISTRATION": False})
def test_system_theme_requires_config_enabled(self):
"""Test that system theme APIs require configuration to be enabled"""
# Login as admin
self.login(ADMIN_USERNAME)
# Try to set theme as system default
response = self.client.put(
f"/api/v1/theme/{self.regular_theme.id}/set_system_default"
)
# Should be forbidden
assert response.status_code == 403
data = response.get_json()
assert "UI theme administration is not enabled" in data["message"]
@with_config({"ENABLE_UI_THEME_ADMINISTRATION": True})
def test_admin_can_set_system_dark(self):
"""Test that admin can set a theme as system dark"""
# Login as admin
self.login(ADMIN_USERNAME)
# Set theme as system dark
response = self.client.put(
f"/api/v1/theme/{self.regular_theme.id}/set_system_dark"
)
# Should succeed
assert response.status_code == 200
# Verify theme is now system dark
theme = db.session.query(Theme).filter_by(id=self.regular_theme.id).first()
assert theme.is_system_dark is True
@with_config({"ENABLE_UI_THEME_ADMINISTRATION": True})
def test_admin_can_unset_system_default(self):
"""Test that admin can unset system default theme"""
# First set a theme as system default
self.regular_theme.is_system_default = True
db.session.commit()
# Login as admin
self.login(ADMIN_USERNAME)
# Unset system default
response = self.client.delete("/api/v1/theme/unset_system_default")
# Should succeed
assert response.status_code == 200
# Verify no theme is system default
theme = db.session.query(Theme).filter_by(id=self.regular_theme.id).first()
assert theme.is_system_default is False
@with_config({"ENABLE_UI_THEME_ADMINISTRATION": True})
def test_admin_can_unset_system_dark(self):
"""Test that admin can unset system dark theme"""
# First set a theme as system dark
self.regular_theme.is_system_dark = True
db.session.commit()
# Login as admin
self.login(ADMIN_USERNAME)
# Unset system dark
response = self.client.delete("/api/v1/theme/unset_system_dark")
# Should succeed
assert response.status_code == 200
# Verify no theme is system dark
theme = db.session.query(Theme).filter_by(id=self.regular_theme.id).first()
assert theme.is_system_dark is False
@with_config({"ENABLE_UI_THEME_ADMINISTRATION": True})
def test_only_one_system_default_allowed(self):
"""Test that only one theme can be system default at a time"""
# Create another theme
theme2 = Theme(
theme_name=f"Another Theme {self.test_id}",
json_data=json.dumps({"colors": {"primary": "#ff0000"}}),
created_by=self.get_user("admin"),
changed_by=self.get_user("admin"),
)
db.session.add(theme2)
db.session.commit()
try:
# Login as admin
self.login(ADMIN_USERNAME)
# Set first theme as system default
response = self.client.put(
f"/api/v1/theme/{self.regular_theme.id}/set_system_default"
)
assert response.status_code == 200
# Set second theme as system default
response = self.client.put(f"/api/v1/theme/{theme2.id}/set_system_default")
assert response.status_code == 200
# Verify only the second theme is system default
theme1 = db.session.query(Theme).filter_by(id=self.regular_theme.id).first()
theme2_check = db.session.query(Theme).filter_by(id=theme2.id).first()
assert theme1.is_system_default is False
assert theme2_check.is_system_default is True
finally:
# Clean up
db.session.delete(theme2)
db.session.commit()
def test_system_theme_cannot_be_deleted(self):
"""Test that themes set as system themes cannot be deleted"""
# Set theme as system default
self.regular_theme.is_system_default = True
db.session.commit()
# Login as admin
self.login(ADMIN_USERNAME)
# Try to delete the theme
response = self.client.delete(f"/api/v1/theme/{self.regular_theme.id}")
# Should fail with appropriate error
assert response.status_code == 422
data = response.get_json()
assert (
"Cannot delete theme" in data["message"]
or "system" in data["message"].lower()
)
def test_gamma_user_can_read_themes(self):
"""Test that gamma users can read themes"""
# Login as gamma user
self.login(GAMMA_USERNAME)
# Should be able to read themes
response = self.client.get("/api/v1/theme/")
assert response.status_code == 200
# Should be able to read individual theme
response = self.client.get(f"/api/v1/theme/{self.regular_theme.id}")
assert response.status_code == 200
# Note: Gamma users' ability to create/update/delete themes
# depends on the specific permissions configuration