From beea339b74b64ee9524f94571324c5a52d9c9fe0 Mon Sep 17 00:00:00 2001 From: Jeremy Schneider Date: Fri, 29 May 2026 09:38:29 -0700 Subject: [PATCH 1/2] feat: include authenticated user identity in HTTP access log Set an X-Remote-User response header containing the authenticated username on every request. This allows the access log to be configured to include user identity via standard log format directives (%({x-remote-user}o)s in gunicorn, %{X-Remote-User}o in Apache) without requiring any changes to pgAdmin's session or auth behaviour. Signed-off-by: Jeremy Schneider --- pkg/docker/gunicorn_config.py | 5 +++++ web/pgadmin/__init__.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/pkg/docker/gunicorn_config.py b/pkg/docker/gunicorn_config.py index ac7afe08175..699ab6b342d 100644 --- a/pkg/docker/gunicorn_config.py +++ b/pkg/docker/gunicorn_config.py @@ -5,6 +5,11 @@ gunicorn.SERVER_SOFTWARE = "Python" +# Include the authenticated user identity in the access log. +# %({x-remote-user}o)s reads the X-Remote-User response header set by pgAdmin +# for authenticated requests; unauthenticated requests log '-'. +access_log_format = '%(h)s %(l)s %({x-remote-user}o)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' + if JSON_LOGGER: logconfig_dict = { "version": 1, diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index 171f02ed53f..d8992a149bb 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -863,6 +863,9 @@ def before_request(): @app.after_request def after_request(response): + if current_user.is_authenticated: + response.headers['X-Remote-User'] = current_user.username + if 'key' in request.args: domain = dict() if config.COOKIE_DEFAULT_DOMAIN and \ From 0f6f4d5cc25cf5782cafb219bbddd73b405af0c0 Mon Sep 17 00:00:00 2001 From: Jeremy Schneider Date: Wed, 10 Jun 2026 00:22:32 -0700 Subject: [PATCH 2/2] add LOG_AUTHENTICATED_USER config flag, fix latin-1 encoding for X-Remote-User header Addresses reviewer feedback on PR pgadmin-org#9991: - Gate the X-Remote-User header behind LOG_AUTHENTICATED_USER (default False) - Encode username as latin-1 with replacement to prevent gunicorn 500s for non-ASCII usernames - Clear the header on unauthenticated requests when the feature is enabled Signed-off-by: Jeremy Schneider style: fix E501 line too long in after_request style: fix E501 line too long in gunicorn_config.py --- pkg/docker/gunicorn_config.py | 5 ++++- web/config.py | 3 +++ web/pgadmin/__init__.py | 10 ++++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pkg/docker/gunicorn_config.py b/pkg/docker/gunicorn_config.py index 699ab6b342d..dbe591cb8b0 100644 --- a/pkg/docker/gunicorn_config.py +++ b/pkg/docker/gunicorn_config.py @@ -8,7 +8,10 @@ # Include the authenticated user identity in the access log. # %({x-remote-user}o)s reads the X-Remote-User response header set by pgAdmin # for authenticated requests; unauthenticated requests log '-'. -access_log_format = '%(h)s %(l)s %({x-remote-user}o)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' +access_log_format = ( + '%(h)s %(l)s %({x-remote-user}o)s %(t)s "%(r)s" %(s)s %(b)s ' + '"%(f)s" "%(a)s"' +) if JSON_LOGGER: logconfig_dict = { diff --git a/web/config.py b/web/config.py index c8ab16233bb..29b3d028e03 100644 --- a/web/config.py +++ b/web/config.py @@ -301,6 +301,9 @@ LOG_ROTATION_SIZE = 10 # In MBs LOG_ROTATION_AGE = 1440 # In minutes LOG_ROTATION_MAX_LOG_FILES = 90 # Maximum number of backups to retain +# Include the authenticated username in the X-Remote-User response header so +# it can be captured in the HTTP access log. Disabled by default. +LOG_AUTHENTICATED_USER = False ########################################################################## # Server Connection Driver Settings ########################################################################## diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index d8992a149bb..1474445571b 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -863,8 +863,14 @@ def before_request(): @app.after_request def after_request(response): - if current_user.is_authenticated: - response.headers['X-Remote-User'] = current_user.username + if config.LOG_AUTHENTICATED_USER: + if current_user.is_authenticated and current_user.username: + # Encode as latin-1 to avoid gunicorn 500s for unicode names + safe = current_user.username.encode( + 'latin-1', 'replace').decode('latin-1') + response.headers['X-Remote-User'] = safe + else: + response.headers.pop('X-Remote-User', None) if 'key' in request.args: domain = dict()