feat: include authenticated user identity in HTTP access log (#9991)

Set an X-Remote-User response header containing the authenticated username
on every request when the LOG_AUTHENTICATED_USER config option is enabled
(disabled by default). This allows the HTTP 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 authentication behaviour. The
default gunicorn access log format is updated to surface the header.

The username is sanitised to a header-safe value: it is transliterated to
Latin-1 (HTTP headers are Latin-1 only) and any non-printable characters,
including CR/LF, are stripped, so unusual usernames cannot cause a 500 on
every response.
This commit is contained in:
Jeremy Schneider
2026-06-19 09:26:32 +01:00
committed by Dave Page
parent 888a053231
commit 0c6740f146
3 changed files with 25 additions and 0 deletions
+8
View File
@@ -5,6 +5,14 @@ from config import JSON_LOGGER, CONSOLE_LOG_LEVEL, CONSOLE_LOG_FORMAT_JSON
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,
+3
View File
@@ -301,6 +301,9 @@ else:
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
##########################################################################
+14
View File
@@ -873,6 +873,20 @@ def create_app(app_name=None):
@app.after_request
def after_request(response):
if config.LOG_AUTHENTICATED_USER:
if current_user.is_authenticated and current_user.username:
# HTTP headers are latin-1 only, so transliterate anything
# outside that range to avoid gunicorn 500s for unicode names.
safe = current_user.username.encode(
'latin-1', 'replace').decode('latin-1')
# CR/LF and other control chars are valid latin-1 but Werkzeug
# rejects them in header values (would 500 every request for
# that user), so drop any non-printable characters too.
safe = ''.join(c for c in safe if c.isprintable())
response.headers['X-Remote-User'] = safe
else:
response.headers.pop('X-Remote-User', None)
if 'key' in request.args:
domain = dict()
if config.COOKIE_DEFAULT_DOMAIN and \