diff --git a/pkg/docker/gunicorn_config.py b/pkg/docker/gunicorn_config.py index ac7afe081..dbe591cb8 100644 --- a/pkg/docker/gunicorn_config.py +++ b/pkg/docker/gunicorn_config.py @@ -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, diff --git a/web/config.py b/web/config.py index 644b8fd3f..f39c60851 100644 --- a/web/config.py +++ b/web/config.py @@ -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 ########################################################################## diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index 9af6d0305..66902dfaa 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -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 \