From 2de30f25c073fcfaf2e3b5df6ea93fce88a041ee Mon Sep 17 00:00:00 2001 From: dev-hari-prasad Date: Mon, 17 Aug 2026 13:39:59 +0100 Subject: [PATCH] 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. --- web/pgadmin/authenticate/registry.py | 22 ++-- web/pgadmin/authenticate/tests/__init__.py | 8 ++ .../authenticate/tests/test_auth_gating.py | 119 ++++++++++++++++++ 3 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 web/pgadmin/authenticate/tests/__init__.py create mode 100644 web/pgadmin/authenticate/tests/test_auth_gating.py diff --git a/web/pgadmin/authenticate/registry.py b/web/pgadmin/authenticate/registry.py index 7b3d607b6..99487a5dc 100644 --- a/web/pgadmin/authenticate/registry.py +++ b/web/pgadmin/authenticate/registry.py @@ -10,6 +10,7 @@ """External Authentication Registry.""" +import config from pgadmin.utils.dynamic_registry import create_registry_metaclass @@ -19,20 +20,21 @@ def load_modules(cls, app=None): from . import internal as module submodules.append(module) - from . import kerberos as module - submodules.append(module) + if config.SERVER_MODE: + from . import kerberos as module + submodules.append(module) - from . import ldap as module - submodules.append(module) + from . import ldap as module + submodules.append(module) - from . import mfa as module - submodules.append(module) + from . import mfa as module + submodules.append(module) - from . import oauth2 as module - submodules.append(module) + from . import oauth2 as module + submodules.append(module) - from . import webserver as module - submodules.append(module) + from . import webserver as module + submodules.append(module) for module in submodules: if "init_app" in module.__dict__.keys(): diff --git a/web/pgadmin/authenticate/tests/__init__.py b/web/pgadmin/authenticate/tests/__init__.py new file mode 100644 index 000000000..c69ae19d4 --- /dev/null +++ b/web/pgadmin/authenticate/tests/__init__.py @@ -0,0 +1,8 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2026, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## diff --git a/web/pgadmin/authenticate/tests/test_auth_gating.py b/web/pgadmin/authenticate/tests/test_auth_gating.py new file mode 100644 index 000000000..7c1d5adb1 --- /dev/null +++ b/web/pgadmin/authenticate/tests/test_auth_gating.py @@ -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