Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/en_US/oauth2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,25 @@ file (see the :ref:`config.py <config_py>` documentation) on the system where
pgAdmin is installed in Server mode. You can copy these settings from *config.py*
file and modify the values for the following parameters.

.. warning::
pgAdmin reads OAuth2 provider settings **only** from the ``OAUTH2_CONFIG``
list; each provider is a single dictionary entry within that list. Settings
such as ``OAUTH2_CLIENT_ID`` or ``OAUTH2_SSL_CERT_VERIFICATION`` defined at
the top level of the configuration are ignored.

This matters in container deployments. Although most options can be set with
an individual ``PGADMIN_CONFIG_<KEY>`` environment variable, OAuth2 is the
exception: individual variables such as ``PGADMIN_CONFIG_OAUTH2_CLIENT_ID``
or ``PGADMIN_CONFIG_OAUTH2_SSL_CERT_VERIFICATION`` will **not** configure a
provider. Instead, configure the whole provider list through a single
``PGADMIN_CONFIG_OAUTH2_CONFIG`` variable, for example::

PGADMIN_CONFIG_OAUTH2_CONFIG="[{'OAUTH2_NAME': 'my-provider', 'OAUTH2_DISPLAY_NAME': 'My Provider', 'OAUTH2_CLIENT_ID': '...', 'OAUTH2_CLIENT_SECRET': '...', 'OAUTH2_SERVER_METADATA_URL': 'https://provider.example.com/.well-known/openid-configuration', 'OAUTH2_SCOPE': 'openid email profile', 'OAUTH2_SSL_CERT_VERIFICATION': False}]"

If pgAdmin detects per-provider OAuth2 settings at the top level while no
provider is configured in ``OAUTH2_CONFIG``, it logs a warning at startup
to highlight the misconfiguration.

OAuth2 vs OpenID Connect (OIDC)
================================

Expand Down
54 changes: 54 additions & 0 deletions web/pgadmin/authenticate/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,58 @@
OAUTH2_LOGOUT = 'oauth2.logout'
OAUTH2_AUTHORIZE = 'oauth2.authorize'

# OAuth2 settings that legitimately live at the top level of the
# configuration. Every other OAUTH2_* setting belongs *inside* an entry
# of the OAUTH2_CONFIG list and is ignored if set at the top level.
_TOP_LEVEL_OAUTH2_SETTINGS = {'OAUTH2_CONFIG', 'OAUTH2_AUTO_CREATE_USER'}


def warn_on_misplaced_oauth2_config(app):
"""Warn when per-provider OAuth2 settings have been supplied as
top-level configuration variables instead of inside an OAUTH2_CONFIG
entry.

pgAdmin only reads provider settings from the OAUTH2_CONFIG list, so
values such as a top-level OAUTH2_CLIENT_ID or OAUTH2_SSL_CERT_VERIFICATION
are silently ignored. This is an easy trap in container deployments:
every other setting is configured as PGADMIN_CONFIG_<KEY>, so it is
natural to assume PGADMIN_CONFIG_OAUTH2_CLIENT_ID and friends will work
the same way, when in fact OAuth2 must be configured through a single
PGADMIN_CONFIG_OAUTH2_CONFIG holding the provider list. Surfacing this
loudly turns a silent misconfiguration into a diagnosable one (see
issue #10053).
"""
misplaced = sorted(
key for key in dir(config)
if key.startswith('OAUTH2_') and
key not in _TOP_LEVEL_OAUTH2_SETTINGS
)
if not misplaced:
return

# If at least one provider is actually configured in OAUTH2_CONFIG we
# assume the deployment knows what it is doing and stay quiet, even if
# some stray top-level keys are also present.
providers_configured = any(
isinstance(provider, dict) and provider.get('OAUTH2_NAME')
for provider in (getattr(config, 'OAUTH2_CONFIG', None) or [])
)
if providers_configured:
return

app.logger.warning(
"OAuth2: the following settings are defined at the top level of the "
"configuration but pgAdmin reads OAuth2 provider settings only from "
"the OAUTH2_CONFIG list, so they are being ignored: %s. If you are "
"configuring OAuth2 through individual PGADMIN_CONFIG_OAUTH2_* "
"environment variables, this will not work; configure a single "
"PGADMIN_CONFIG_OAUTH2_CONFIG holding the provider list instead, "
"e.g. PGADMIN_CONFIG_OAUTH2_CONFIG='[{\"OAUTH2_NAME\": \"...\", "
"\"OAUTH2_CLIENT_ID\": \"...\"}]'. See the OAuth2 documentation for "
"details.",
", ".join(misplaced)
)


class Oauth2Module(PgAdminModule):
def register(self, app, options):
Expand Down Expand Up @@ -114,6 +166,8 @@ def oauth_logout():
app.register_blueprint(blueprint)
app.login_manager.logout_view = OAUTH2_LOGOUT

warn_on_misplaced_oauth2_config(app)


class OAuth2Authentication(BaseAuthentication):
"""OAuth Authentication Class"""
Expand Down
91 changes: 91 additions & 0 deletions web/pgadmin/browser/tests/test_oauth2_misplaced_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################

from types import SimpleNamespace
from unittest.mock import patch, MagicMock

from pgadmin.utils.route import BaseTestGenerator
from pgadmin.authenticate import oauth2


class Oauth2MisplacedConfigTestCase(BaseTestGenerator):
"""
Verify warn_on_misplaced_oauth2_config() warns only when per-provider
OAuth2 settings are supplied as top-level configuration variables while
no provider is actually configured in OAUTH2_CONFIG (issue #10053).
"""

# The unconfigured default shipped in config.py.
_DEFAULT_OAUTH2_CONFIG = [{'OAUTH2_NAME': None}]
_CONFIGURED_OAUTH2_CONFIG = [{'OAUTH2_NAME': 'authentik'}]

scenarios = [
('Misplaced top-level keys with unconfigured OAUTH2_CONFIG warns',
dict(
config_attrs={
'OAUTH2_CONFIG': _DEFAULT_OAUTH2_CONFIG,
'OAUTH2_AUTO_CREATE_USER': True,
'OAUTH2_CLIENT_ID': 'redacted',
'OAUTH2_SSL_CERT_VERIFICATION': False,
},
expect_warning=True,
expect_keys=['OAUTH2_CLIENT_ID', 'OAUTH2_SSL_CERT_VERIFICATION'],
)),
('Misplaced top-level keys with a configured provider stays quiet',
dict(
config_attrs={
'OAUTH2_CONFIG': _CONFIGURED_OAUTH2_CONFIG,
'OAUTH2_AUTO_CREATE_USER': True,
'OAUTH2_CLIENT_ID': 'redacted',
},
expect_warning=False,
expect_keys=None,
)),
('Only legitimate top-level OAuth2 settings stays quiet',
dict(
config_attrs={
'OAUTH2_CONFIG': _DEFAULT_OAUTH2_CONFIG,
'OAUTH2_AUTO_CREATE_USER': True,
},
expect_warning=False,
expect_keys=None,
)),
('No OAuth2 configuration at all stays quiet',
dict(
config_attrs={
'OAUTH2_CONFIG': [],
'OAUTH2_AUTO_CREATE_USER': True,
},
expect_warning=False,
expect_keys=None,
)),
]

def runTest(self):
"""Run a single misplaced-config scenario and assert whether a
startup warning is emitted, and that it lists the expected keys."""
app = MagicMock()
app.logger = MagicMock()
fake_config = SimpleNamespace(**self.config_attrs)

with patch.object(oauth2, 'config', fake_config):
oauth2.warn_on_misplaced_oauth2_config(app)

if not self.expect_warning:
app.logger.warning.assert_not_called()
return

app.logger.warning.assert_called_once()
# The misplaced keys are interpolated into the message as the
# single positional argument after the format string.
args = app.logger.warning.call_args.args
self.assertEqual(len(args), 2)
listed = args[1]
for key in self.expect_keys:
self.assertIn(key, listed)
Loading