You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/05/29 17:17:45 UTC
(superset) 01/01: fix(security): correct class_permission_name case for Security menu views
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch fix/security-menu-case-mysql
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 2063484ef4cb6c490a8a9929f89d2dd1751fa33b
Author: Evan <ev...@preset.io>
AuthorDate: Fri May 29 10:17:16 2026 -0700
fix(security): correct class_permission_name case for Security menu views
Five views that back the Security menu navbar items declared
`class_permission_name = "security"` (lowercase). Flask-AppBuilder uses
this name to look up or insert the corresponding `ab_view_menu` row. On
MySQL's default case-insensitive collation, the lookup finds whatever
row exists first, meaning the DB can end up storing `"security"` instead
of `"Security"`. Because Superset's Python comparisons in
`sync_role_definitions` and `SupersetSecurityManager` are case-sensitive,
`"security" != "Security"` → the Security menu disappears from the UI on
MySQL/MariaDB deployments.
Fixes:
- Change `class_permission_name` to `"Security"` in users_list.py,
roles.py, groups.py, logs.py, and user_registrations.py.
- Add migration `b4a3f2e1d0c9` that normalises any existing lowercase
`"security"` row: renames it to `"Security"` if it is the only row,
or merges its `ab_permission_view` entries into the correctly-cased
row and deletes the duplicate if both exist.
Closes #40330
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
...-00_b4a3f2e1d0c9_fix_security_view_menu_case.py | 115 +++++++++++++++++++++
superset/views/groups.py | 2 +-
superset/views/logs.py | 2 +-
superset/views/roles.py | 2 +-
superset/views/user_registrations.py | 2 +-
superset/views/users_list.py | 2 +-
6 files changed, 120 insertions(+), 5 deletions(-)
diff --git a/superset/migrations/versions/2026-05-29_00-00_b4a3f2e1d0c9_fix_security_view_menu_case.py b/superset/migrations/versions/2026-05-29_00-00_b4a3f2e1d0c9_fix_security_view_menu_case.py
new file mode 100644
index 00000000000..6d1978ca9de
--- /dev/null
+++ b/superset/migrations/versions/2026-05-29_00-00_b4a3f2e1d0c9_fix_security_view_menu_case.py
@@ -0,0 +1,115 @@
+# 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.
+"""fix Security view menu case
+
+Revision ID: b4a3f2e1d0c9
+Revises: a1b2c3d4e5f6
+Create Date: 2026-05-29 00:00:00.000000
+
+On MySQL with the default case-insensitive collation, five Superset views that
+declare ``class_permission_name = "security"`` (lowercase) cause FAB to look up
+or insert a view-menu entry named ``"security"``. MySQL's case-insensitive
+``UNIQUE`` constraint means the lookup finds an existing ``"Security"`` row and
+uses it, OR a fresh install stores ``"security"`` because that view is
+registered before the FAB built-in views register ``"Security"``.
+
+Python string comparisons in ``sync_role_definitions`` and the menu-hiding
+logic are case-sensitive, so an all-lowercase ``"security"`` row breaks the
+Security menu for MySQL (and MariaDB) deployments.
+
+This migration normalises the row:
+* If only the lowercase ``"security"`` row exists: rename it to ``"Security"``.
+* If both rows exist (SQLite / PostgreSQL fresh-install with the old code):
+ reassign every ``ab_permission_view`` from the lowercase row to the
+ correctly-cased row, then delete the duplicate.
+"""
+
+# revision identifiers, used by Alembic.
+revision = "b4a3f2e1d0c9"
+down_revision = "a1b2c3d4e5f6"
+
+import logging # noqa: E402
+
+from alembic import op # noqa: E402
+from sqlalchemy.orm import Session # noqa: E402
+
+from superset.migrations.shared.security_converge import ( # noqa: E402
+ PermissionView,
+ ViewMenu,
+)
+
+logger = logging.getLogger("alembic.env")
+
+
+def upgrade() -> None:
+ bind = op.get_bind()
+ session = Session(bind=bind)
+
+ # Fetch all view menus and compare in Python (SQL filter_by is
+ # case-insensitive on MySQL, so we cannot rely on it here).
+ all_vms: list[ViewMenu] = session.query(ViewMenu).all()
+ security_upper = next((vm for vm in all_vms if vm.name == "Security"), None)
+ security_lower = next((vm for vm in all_vms if vm.name == "security"), None)
+
+ if security_lower is None:
+ logger.info("No lowercase 'security' view-menu found; nothing to do.")
+ return
+
+ if security_upper is None:
+ # Simple rename — only the incorrect row exists.
+ logger.info(
+ "Renaming ab_view_menu 'security' -> 'Security' (id=%d)",
+ security_lower.id,
+ )
+ security_lower.name = "Security"
+ session.flush()
+ else:
+ # Both rows exist. Re-home every permission_view from the lowercase
+ # entry to the correctly-cased entry, then drop the duplicate.
+ logger.info(
+ "Both 'security' (id=%d) and 'Security' (id=%d) found; merging.",
+ security_lower.id,
+ security_upper.id,
+ )
+ pvms_lower: list[PermissionView] = (
+ session.query(PermissionView)
+ .filter(PermissionView.view_menu_id == security_lower.id)
+ .all()
+ )
+ for pvm in pvms_lower:
+ conflict = (
+ session.query(PermissionView)
+ .filter(
+ PermissionView.view_menu_id == security_upper.id,
+ PermissionView.permission_id == pvm.permission_id,
+ )
+ .one_or_none()
+ )
+ if conflict:
+ session.delete(pvm)
+ else:
+ pvm.view_menu_id = security_upper.id
+ session.flush()
+ session.delete(security_lower)
+
+ session.commit()
+
+
+def downgrade() -> None:
+ # There is no safe way to determine the original state (whether the row was
+ # lowercase or whether there were two rows), so downgrade is a no-op.
+ pass
diff --git a/superset/views/groups.py b/superset/views/groups.py
index 61dc9dc659d..4c2b78ef1fd 100644
--- a/superset/views/groups.py
+++ b/superset/views/groups.py
@@ -25,7 +25,7 @@ from .base import BaseSupersetView
class GroupsListView(BaseSupersetView):
route_base = "/"
- class_permission_name = "security"
+ class_permission_name = "Security"
@expose("/list_groups/")
@has_access
diff --git a/superset/views/logs.py b/superset/views/logs.py
index 39fe10d0a3e..2f8b91ac322 100644
--- a/superset/views/logs.py
+++ b/superset/views/logs.py
@@ -25,7 +25,7 @@ from .base import BaseSupersetView
class ActionLogView(BaseSupersetView):
route_base = "/"
- class_permission_name = "security"
+ class_permission_name = "Security"
@expose("/actionlog/list")
@has_access
diff --git a/superset/views/roles.py b/superset/views/roles.py
index 29f293f51e5..38b38972033 100644
--- a/superset/views/roles.py
+++ b/superset/views/roles.py
@@ -25,7 +25,7 @@ from .base import BaseSupersetView
class RolesListView(BaseSupersetView):
route_base = "/"
- class_permission_name = "security"
+ class_permission_name = "Security"
@expose("/roles/")
@has_access
diff --git a/superset/views/user_registrations.py b/superset/views/user_registrations.py
index 10c8fab2dcf..60ed5d42159 100644
--- a/superset/views/user_registrations.py
+++ b/superset/views/user_registrations.py
@@ -25,7 +25,7 @@ from .base import BaseSupersetView
class UserRegistrationsView(BaseSupersetView):
route_base = "/"
- class_permission_name = "security"
+ class_permission_name = "Security"
@expose("/registrations/")
@has_access
diff --git a/superset/views/users_list.py b/superset/views/users_list.py
index 42f93fce9ff..0ed5f64b02c 100644
--- a/superset/views/users_list.py
+++ b/superset/views/users_list.py
@@ -25,7 +25,7 @@ from .base import BaseSupersetView
class UsersListView(BaseSupersetView):
route_base = "/"
- class_permission_name = "security"
+ class_permission_name = "Security"
@expose("/users/")
@has_access