rusackas commented on code in PR #44857: URL: https://github.com/apache/superset/pull/44857#discussion_r4171447127
########## superset/migrations/versions/2026-09-30_00-00_d623a0cb6bb0_sweep_legacy_password_view_perms_from_all_roles.py: ########## @@ -0,0 +1,114 @@ +# 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. +"""sweep legacy password-view permissions from all roles (#44626 follow-up) + +#44626 removed the legacy FAB server-rendered password reset views +(``ResetPasswordView``, ``ResetMyPasswordView``) and hid the +``UserDBModelView`` actions that redirected to them (``resetpasswords``, +``resetmypassword``). ``SupersetSecurityManager.sync_role_definitions`` was +updated to stop handing their permissions to the built-in Admin/Alpha/Gamma/ +sql_lab roles on ``superset init``, but that exclusion only applies to the +pvms list those four roles are resynced from -- it never touches a role that +already held one of these permissions before upgrading, since Superset never +re-syncs operator-defined (custom) roles. An upgraded install's custom role +that was explicitly granted self-service or admin password reset therefore +keeps the dead permission forever, contradicting UPDATING.md's "no role" +claim for that release. + +The permissions are already fully inert on every upgraded install regardless +of which role holds them: the views are never registered (so the routes +404/anything routing there raises ``BuildError``), and the ``UserDBModelView`` +actions are hidden and wired to a handler that also answers 404. This +migration is therefore pure metadata cleanup with no behavior change -- it +deletes the stale (view, permission) pairs from every role that holds them +(not just the four synced built-ins), plus the underlying ``ab_permission``/ +``ab_view_menu`` rows once orphaned. + +Revision ID: d623a0cb6bb0 +Revises: 95d8a99c822e +Create Date: 2026-09-30 00:00:00.000000 + +""" + +# revision identifiers, used by Alembic. +revision = "d623a0cb6bb0" +down_revision = "95d8a99c822e" Review Comment: Good catch. Rebased onto master and re-chained this revision after `884a2115ebd3` (the annotation UUID migration), so there is a single head again. Pushed in 124b72f32a. ########## superset/security/password_change.py: ########## @@ -206,30 +236,27 @@ def _enforce_password_change() -> Any: # pylint: disable=unused-variable return None flash(__("You must change your password before continuing."), "warning") - # Resolve the SPA profile page. If that endpoint can't be resolved - # (e.g. a deployment that does not register ``UserInfoView``), fall - # back to logout, which is always exempt from this enforcement. The - # logout endpoint is derived from the *registered* auth view so the - # fallback works for non-DB auth backends (LDAP, OAuth, remote-user) - # too, with ``AuthDBView.logout`` as a last resort. We must NOT fall - # back to "/" or any other non-exempt route: the index re-runs this - # same hook and would trap the user in an infinite 302 loop. If no - # exempt target can be resolved at all, return an error response rather - # than redirect, so a flagged user can never get stuck looping. - candidates = [_PROFILE_PAGE_ENDPOINT] - auth_view = getattr( - getattr(getattr(current_app, "appbuilder", None), "sm", None), - "auth_view", - None, - ) - # Only redirect to the registered auth view's logout if that view is - # itself exempt from this hook; otherwise the redirect would loop. - if ( - auth_view is not None - and getattr(auth_view, "endpoint", None) in _EXEMPT_VIEW_CLASSES - ): - candidates.append(f"{auth_view.endpoint}.logout") - candidates.append("AuthDBView.logout") + # Resolve the SPA profile page, if the user can actually reach it, with + # logout as the fallback -- which is always exempt from this + # enforcement. We must NOT fall back to "/" or any other non-exempt + # route: the index re-runs this same hook and would trap the user in + # an infinite 302 loop. If no exempt target can be resolved at all, + # return an error response rather than redirect, so a flagged user can + # never get stuck looping. + security_manager = getattr(getattr(current_app, "appbuilder", None), "sm", None) + candidates = [] + if _profile_page_reachable(security_manager): + candidates.append(_PROFILE_PAGE_ENDPOINT) + else: + flash( + __( + "Your role does not have access to the profile page " + "needed to change your password. Contact an " + "administrator." + ), + "danger", + ) + candidates.extend(_logout_fallback_candidates(security_manager)) Review Comment: Fair point, logging out cannot help under `AUTH_REMOTE_USER` since the header just re-authenticates. When the user lacks profile access and `AUTH_TYPE` is `AUTH_REMOTE_USER`, the hook now returns a terminal 403 with the "contact an administrator" message instead of redirecting to logout. Added a unit test for it, pushed in 124b72f32a. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
