Gate external auth providers behind SERVER_MODE in desktop mode (#10240)

Desktop mode has no use for the kerberos, ldap, mfa, oauth2 or webserver
authentication providers: AUTHENTICATION_SOURCES defaults to internal, MFA is
inert because mfa_enabled() requires SERVER_MODE, and get_logout_url() already
guards its kerberos and oauth2 branches on SERVER_MODE. So skip importing and
initialising them unless SERVER_MODE is set, leaving desktop mode with
internal authentication alone.

The accompanying test needed wiring into our own runner before it would ever
execute: web/pgadmin/authenticate/tests/ had no __init__.py, so pkgutil did
not report it and find_modules() never reached the module, and the test class
derived from unittest.TestCase rather than BaseTestGenerator, so the
TestsGeneratorRegistry metaclass never registered it and get_suite() could not
pick it up. Both are fixed here, and the two scenarios now run and pass under
regression/runtests.py.

The test no longer asserts that pgadmin.authenticate.mfa is absent from
sys.modules in desktop mode, because it is not: both pgadmin/browser/__init__
and pgadmin/user_login_check import pgadmin.authenticate.mfa.utils
unconditionally. The original assertion only held because the test itself
popped the module first.
This commit is contained in:
dev-hari-prasad
2026-08-17 13:39:59 +01:00
committed by Dave Page
parent c2398d5b08
commit 2de30f25c0
3 changed files with 139 additions and 10 deletions
+12 -10
View File
@@ -10,6 +10,7 @@
"""External Authentication Registry.""" """External Authentication Registry."""
import config
from pgadmin.utils.dynamic_registry import create_registry_metaclass from pgadmin.utils.dynamic_registry import create_registry_metaclass
@@ -19,20 +20,21 @@ def load_modules(cls, app=None):
from . import internal as module from . import internal as module
submodules.append(module) submodules.append(module)
from . import kerberos as module if config.SERVER_MODE:
submodules.append(module) from . import kerberos as module
submodules.append(module)
from . import ldap as module from . import ldap as module
submodules.append(module) submodules.append(module)
from . import mfa as module from . import mfa as module
submodules.append(module) submodules.append(module)
from . import oauth2 as module from . import oauth2 as module
submodules.append(module) submodules.append(module)
from . import webserver as module from . import webserver as module
submodules.append(module) submodules.append(module)
for module in submodules: for module in submodules:
if "init_app" in module.__dict__.keys(): if "init_app" in module.__dict__.keys():
@@ -0,0 +1,8 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
@@ -0,0 +1,119 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Verify that external authentication providers are only loaded in server
mode, and that desktop mode is left with internal authentication alone.
"""
import sys
import config
from pgadmin.authenticate.registry import AuthSourceRegistry
from pgadmin.utils.route import BaseTestGenerator
# Providers registered as authentication sources. MFA is deliberately absent:
# it is loaded by the same registry but registers no auth source of its own.
EXTERNAL_SOURCES = ['kerberos', 'ldap', 'oauth2', 'webserver']
# Modules whose import can be asserted on. mfa is excluded because
# pgadmin/browser/__init__.py and pgadmin/user_login_check.py both import
# pgadmin.authenticate.mfa.utils unconditionally, so the mfa package is
# present in sys.modules in desktop mode whatever this registry does.
EXTERNAL_MODULES = [
'pgadmin.authenticate.{0}'.format(name) for name in EXTERNAL_SOURCES
]
class AuthSourceGatingTestCase(BaseTestGenerator):
"""Checks AuthSourceRegistry.load_modules() honours config.SERVER_MODE."""
scenarios = [
('desktop mode registers internal authentication only',
dict(server_mode=False)),
('server mode registers every external provider',
dict(server_mode=True)),
]
# Pure registry-contract test - no Postgres server interaction needed, so
# skip BaseTestGenerator.setUp's connect_server().
def setUp(self):
self._orig_server_mode = config.SERVER_MODE
self._orig_registry = dict(AuthSourceRegistry._registry or {})
self._orig_objects = dict(AuthSourceRegistry._objects or {})
def tearDown(self):
config.SERVER_MODE = self._orig_server_mode
AuthSourceRegistry._registry = self._orig_registry
AuthSourceRegistry._objects = self._orig_objects
def _reset_registry(self):
"""Empty the registry and drop the external provider modules, so that
load_modules() is observed importing them (or not) from scratch.
"""
for module in EXTERNAL_MODULES:
sys.modules.pop(module, None)
AuthSourceRegistry._registry = dict()
AuthSourceRegistry._objects = dict()
internal = sys.modules.get('pgadmin.authenticate.internal')
if internal is not None and \
hasattr(internal, 'InternalAuthentication'):
AuthSourceRegistry._registry['internal'] = \
internal.InternalAuthentication
def runTest(self):
config.SERVER_MODE = self.server_mode
self._reset_registry()
AuthSourceRegistry.load_modules(_MockApp() if self.server_mode
else None)
registered = list(AuthSourceRegistry._registry.keys())
self.assertIn(
'internal', registered,
"Internal authentication must be registered in both modes")
for source, module in zip(EXTERNAL_SOURCES, EXTERNAL_MODULES):
if self.server_mode:
self.assertIn(
source, registered,
"{0} must be registered in server mode".format(source))
self.assertIn(
module, sys.modules,
"{0} must be imported in server mode".format(module))
else:
self.assertNotIn(
source, registered,
"{0} must not be registered in desktop mode".format(
source))
self.assertNotIn(
module, sys.modules,
"{0} must not be imported in desktop mode".format(module))
class _MockApp:
"""The minimum Flask-like surface the providers' init_app() touches."""
class _LoginManager:
logout_view = None
class _Logger:
def warning(self, *args, **kwargs):
pass
def __init__(self):
self.login_manager = self._LoginManager()
self.logger = self._Logger()
def register_blueprint(self, *args, **kwargs):
pass
def register_logout_hook(self, *args, **kwargs):
pass