Compare commits

...

5 Commits

Author SHA1 Message Date
Evan
5e0b430a03 fix(migration): chain down_revision to current master head (31dae2559c05)
Master's single head advanced to 31dae2559c05; this migration still
pointed at 33d7e0e21daa, making it a sibling and producing multiple
alembic heads. Re-point so the chain is linear.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 17:39:16 -07:00
Evan Rusackas
b725e11199 Merge branch 'master' into fix/security-menu-case-mysql 2026-06-10 16:19:57 -07:00
Evan
95b46bcf02 fix(migration): point down_revision to actual master head (33d7e0e21daa)
Previous commits used a1b2c3d4e5f6 then ce6bd21901ab, both of which are
mid-chain on master. The true head is 33d7e0e21daa (add_semantic_layers).
Branching off mid-chain caused 'Multiple head revisions' in CI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 13:42:00 -07:00
Evan
7f804262ed fix(migration): chain to correct head and transfer role bindings on merge
- Point down_revision to ce6bd21901ab (the actual current chain head,
  not a1b2c3d4e5f6 which already had two successors)
- In the "both rows exist" merge path, transfer ab_permission_view_role
  bindings from the lowercase PVM to the surviving uppercase PVM before
  deleting the duplicate, so no role silently loses a Security permission

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:29:57 -07:00
Evan
2063484ef4 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 <noreply@anthropic.com>
2026-05-29 10:17:16 -07:00
6 changed files with 127 additions and 5 deletions

View File

@@ -0,0 +1,122 @@
# 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: 31dae2559c05
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 = "31dae2559c05"
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:
upper_pvm = (
session.query(PermissionView)
.filter(
PermissionView.view_menu_id == security_upper.id,
PermissionView.permission_id == pvm.permission_id,
)
.one_or_none()
)
if upper_pvm:
# Transfer role bindings from the duplicate row to the
# surviving row before discarding the duplicate, so no
# role silently loses a permission.
for role in list(pvm.role):
if upper_pvm not in role.permissions:
role.permissions.append(upper_pvm)
session.flush()
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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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