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/08/10 01:00:48 UTC
(superset) 01/01: fix(security_manager): stop SupersetAuthView from shadowing AUTH_REMOTE_USER
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch fix/auth-remote-user-superset-auth-view-shadow-36117
in repository https://gitbox.apache.org/repos/asf/superset.git
commit cdbe37587f29deae3ea6bbb128a88f192daffb95
Author: rusackas <ev...@rusackas.com>
AuthorDate: Sun Aug 9 18:00:31 2026 -0700
fix(security_manager): stop SupersetAuthView from shadowing AUTH_REMOTE_USER
SupersetSecurityManager.register_views() unconditionally registers
SupersetAuthView at "/login/" before calling super().register_views(),
which is where FlaskAppBuilder picks the auth_type-specific view (e.g.
AuthRemoteUserView for AUTH_REMOTE_USER) and registers it at the same
route. Both views add distinct Flask endpoints for the same URL rule, and
SupersetAuthView always wins the dispatch since it's registered first, so
the SPA login shell renders instead of AuthRemoteUserView ever checking
the remote-user header/env var. AUTH_REMOTE_USER is silently a no-op as a
result: plain AUTH_TYPE = AUTH_REMOTE_USER config (unchanged since it
worked in 5.0.0) stops logging anyone in once SupersetAuthView exists.
This is a regression from the frontend login migration (#33255, part of
the 6.0 theming overhaul). #39098 added an opt-out flag
(register_superset_auth_view = False) for custom SecurityManager
subclasses, but that requires writing and wiring up a subclass just to
get the out-of-the-box config working again -- not something a deployer
setting plain AUTH_TYPE = AUTH_REMOTE_USER would know to do, and not
documented anywhere. Confirmed still broken on 6.1.0 in the issue thread.
Skip registering SupersetAuthView specifically for AUTH_REMOTE_USER, whose
whole point is a non-interactive, header-driven login -- there's no
legitimate use for the SPA shell there. Every other auth type (DB, OAuth,
LDAP, SAML) is unaffected.
Fixes #36117
---
superset/security/manager.py | 15 +++++-
tests/unit_tests/security/manager_test.py | 76 +++++++++++++++++++++++++++++++
2 files changed, 90 insertions(+), 1 deletion(-)
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 5dc044d4a89..410eeace8cd 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -38,6 +38,7 @@ from flask import current_app, Flask, g, has_app_context, Request, Response
from flask_appbuilder import Model
from flask_appbuilder.api import expose, protect, safe
from flask_appbuilder.models.filters import BaseFilter
+from flask_appbuilder.security.manager import AUTH_REMOTE_USER
from flask_appbuilder.security.sqla.apis import GroupApi, RoleApi, UserApi
from flask_appbuilder.security.sqla.apis.permission_view_menu.api import (
PermissionViewMenuApi,
@@ -5048,7 +5049,19 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
def register_views(self) -> None:
from superset.views.auth import SupersetAuthView, SupersetRegisterUserView
- if self.register_superset_auth_view:
+ # AUTH_REMOTE_USER has no interactive login form to render: the whole
+ # point is that an upstream proxy already authenticated the request and
+ # passes the identity via a header/env var, so FlaskAppBuilder's
+ # AuthRemoteUserView performs a silent GET-time login with no UI. That
+ # view registers at the same "/login/" route as SupersetAuthView, and
+ # since both add distinct Flask endpoints for the same URL rule,
+ # whichever gets registered first wins the dispatch -- SupersetAuthView
+ # always wins because it's added before super().register_views() runs
+ # FlaskAppBuilder's own auth_type dispatch. That silently shadows
+ # AUTH_REMOTE_USER: the SPA login shell renders instead of the header
+ # ever being checked. Skip registering it for this auth type so
+ # FlaskAppBuilder's AuthRemoteUserView actually claims the route.
+ if self.register_superset_auth_view and self.auth_type != AUTH_REMOTE_USER:
self.auth_view = self.appbuilder.add_view_no_menu(SupersetAuthView)
if self.register_superset_registeruser_view:
self.registeruser_view = self.appbuilder.add_view_no_menu(
diff --git a/tests/unit_tests/security/manager_test.py b/tests/unit_tests/security/manager_test.py
index b940225c6cb..2f06397725a 100644
--- a/tests/unit_tests/security/manager_test.py
+++ b/tests/unit_tests/security/manager_test.py
@@ -24,6 +24,7 @@ from typing import Any, Optional
from unittest.mock import MagicMock
import pytest
+from flask_appbuilder.const import AUTH_DB, AUTH_REMOTE_USER
from flask_appbuilder.security.sqla.models import Role, User
from pytest_mock import MockerFixture
@@ -51,6 +52,81 @@ def test_security_manager(app_context: None) -> None:
assert sm
+def _register_views_with_mock_appbuilder(
+ mocker: MockerFixture, auth_type: int
+) -> MagicMock:
+ """
+ Build a SupersetSecurityManager bound to a fresh mock appbuilder and call
+ register_views() on it, with FlaskAppBuilder's own register_views (the
+ super() call, which does its own large auth_type dispatch and permission
+ registration) stubbed out so the test stays scoped to just the override.
+ Returns the mock appbuilder so the caller can inspect what got registered.
+ """
+ from flask import current_app
+
+ current_app.config["AUTH_TYPE"] = auth_type
+ current_app.config["AUTH_USER_REGISTRATION"] = False
+ current_app.config["AUTH_RATE_LIMITED"] = False
+
+ mock_appbuilder = mocker.MagicMock()
+ mock_appbuilder.baseviews = []
+ mock_appbuilder.menu.get_list.return_value = []
+
+ sm = SupersetSecurityManager.__new__(SupersetSecurityManager)
+ sm.appbuilder = mock_appbuilder
+ sm.register_superset_auth_view = True
+ sm.register_superset_registeruser_view = False
+ sm.userstatschartview = None
+
+ mocker.patch(
+ "flask_appbuilder.security.sqla.manager.SecurityManager.register_views",
+ autospec=True,
+ )
+
+ sm.register_views()
+ return mock_appbuilder
+
+
+def test_register_views_does_not_shadow_auth_remote_user(
+ app_context: None, mocker: MockerFixture
+) -> None:
+ """
+ AUTH_REMOTE_USER must not register SupersetAuthView at "/login/".
+
+ AuthRemoteUserView performs a silent, header-driven login with no
+ interactive UI. SupersetAuthView (the SPA login shell) previously
+ registered at the same route unconditionally and always won the routing
+ dispatch, so the remote-user header was never even checked -- reported in
+ apache/superset#36117 as a regression from the frontend login migration
+ (#31590). See also the related opt-out added in #39098.
+ """
+ from superset.views.auth import SupersetAuthView
+
+ mock_appbuilder = _register_views_with_mock_appbuilder(mocker, AUTH_REMOTE_USER)
+
+ registered = [
+ call.args[0] for call in mock_appbuilder.add_view_no_menu.call_args_list
+ ]
+ assert SupersetAuthView not in registered
+
+
+def test_register_views_still_registers_superset_auth_view_for_db_auth(
+ app_context: None, mocker: MockerFixture
+) -> None:
+ """
+ Control case: AUTH_DB (the common, interactive case) is unaffected --
+ SupersetAuthView still registers at "/login/" as before.
+ """
+ from superset.views.auth import SupersetAuthView
+
+ mock_appbuilder = _register_views_with_mock_appbuilder(mocker, AUTH_DB)
+
+ registered = [
+ call.args[0] for call in mock_appbuilder.add_view_no_menu.call_args_list
+ ]
+ assert SupersetAuthView in registered
+
+
@pytest.fixture
def stored_metrics() -> list[AdhocMetric]:
"""