Skip to content
Merged
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
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ COPY images/assets/pulp-api /usr/bin/pulp-api
COPY images/assets/pulp-content /usr/bin/pulp-content
COPY images/assets/pulp-resource-manager /usr/bin/pulp-resource-manager
COPY images/assets/pulp-worker /usr/bin/pulp-worker
COPY images/assets/log_middleware.py /usr/bin/log_middleware.py

USER pulp:pulp
RUN PULP_STATIC_ROOT=/var/lib/operator/static/ PULP_CONTENT_ORIGIN=localhost \
Expand Down
4 changes: 3 additions & 1 deletion deploy/clowdapp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ objects:
annotations:
"kubectl.kubernetes.io/default-container": pulp-api
image: ${IMAGE}:${IMAGE_TAG}
command: ['pulpcore-api', '-b', '0.0.0.0:8000', '--timeout', '${PULP_API_GUNICORN_TIMEOUT}', '--max-requests', '${PULP_API_GUNICORN_MAX_REQUESTS}', '--max-requests-jitter', '${PULP_API_GUNICORN_MAX_REQUESTS_JITTER}', '--workers', '${PULP_API_GUNICORN_WORKERS}', '--access-logfile', '-', '--access-logformat', '(pulp [%({correlation-id}o)s]: %(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(M)s)']
command: ['pulpcore-api', '-b', '0.0.0.0:8000', '--timeout', '${PULP_API_GUNICORN_TIMEOUT}', '--max-requests', '${PULP_API_GUNICORN_MAX_REQUESTS}', '--max-requests-jitter', '${PULP_API_GUNICORN_MAX_REQUESTS_JITTER}', '--workers', '${PULP_API_GUNICORN_WORKERS}', '--access-logfile', '-', '--access-logformat', '(pulp [%({correlation-id}o)s]: %(h)s %(l)s %({REMOTE_USER}e)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(M)s)']
volumeMounts:
- name: secret-volume
mountPath: "/etc/pulp/keys"
Expand Down Expand Up @@ -345,6 +345,8 @@ objects:
cpu: ${{PULP_API_CPU_LIMIT}}
memory: ${{PULP_API_MEMORY_LIMIT}}
env:
- name: GUNICORN_CMD_ARGS
value: "--config /usr/bin/log_middleware.py"
- name: PULP_OTEL_ENABLED
value: ${TELEMETRY_ENABLED}
- name: OTEL_EXPORTER_OTLP_PROTOCOL
Expand Down
66 changes: 66 additions & 0 deletions images/assets/log_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import base64
import json
import logging
import sys

logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(process)d] [%(levelname)s] %(message)s',
stream=sys.stderr
)
log = logging.getLogger(__name__)


class UserExtractionMiddleware:
"""
WSGI middleware to extract user from X-RH-IDENTITY header and set REMOTE_USER.
This runs before gunicorn logs, so the user will be available in access logs.
"""

def __init__(self, app):
self.app = app

def __call__(self, environ, start_response):
# Extract user from X-RH-IDENTITY header if present
rh_identity = environ.get('HTTP_X_RH_IDENTITY')
username = None

if rh_identity:
try:
decoded = base64.b64decode(rh_identity)
identity_data = json.loads(decoded)

if 'identity' in identity_data:
identity = identity_data['identity']
# User details (highest priority - most specific)
if 'user' in identity and 'username' in identity['user']:
username = identity['user']['username']
# Service account (x509 certificate)
elif 'x509' in identity and 'subject_dn' in identity['x509']:
username = identity['x509']['subject_dn']
# SAML user
elif 'associate' in identity and 'email' in identity['associate']:
username = identity['associate']['email']
# Org ID (fallback)
elif 'org_id' in identity:
username = f"org:{identity['org_id']}"

if not username:
log.warning(f"X-RH-IDENTITY present but no username found.")
except Exception as e:
log.error(f"Failed to extract user from RH Identity header: {e}", exc_info=True)

# Set REMOTE_USER in environ if we found a username
if username:
environ['REMOTE_USER'] = username

return self.app(environ, start_response)


def post_worker_init(worker):
"""
Gunicorn hook to wrap the WSGI application after worker initialization.
This is called after the worker has been initialized but before it starts serving requests.
"""
log.info("Wrapping WSGI application with UserExtractionMiddleware")
worker.wsgi = UserExtractionMiddleware(worker.wsgi)