feat: DEBUG_AUTH_TOKEN Bypass fuer Diagnose-Sessions auf dev

Wenn ENV `DEBUG_AUTH_TOKEN` gesetzt ist, akzeptieren require_auth +
require_admin einen Header `X-Debug-Token: <secret>` oder einen
Query-Param `?__debug_token=<secret>` und liefern einen Admin-Mock-
User. Jeder Use wird mit logger.warning protokolliert.

Default: leer = inaktiv (auch in prod, weil prod-compose das nicht
durchreicht).

Damit kann ein Diagnose-Tool (Playwright, curl) ohne Keycloak-Login
auf admin-only-Endpoints zugreifen — fuer Browser-Console-Auswertung
bei UI-Bugs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-05-06 22:26:39 +02:00
parent 60db39d5b3
commit f8cfa42d9f
2 changed files with 45 additions and 0 deletions

View File

@ -22,6 +22,7 @@ Usage in main.py:
""" """
import logging import logging
import os
import time import time
from typing import Optional from typing import Optional
@ -97,6 +98,37 @@ def _extract_token(request: Request) -> Optional[str]:
return request.cookies.get("access_token") return request.cookies.get("access_token")
def _check_debug_token(request: Request) -> Optional[dict]:
"""Dev-Bypass: wenn ENV `DEBUG_AUTH_TOKEN` gesetzt ist und der Request
den passenden Header `X-Debug-Token` trägt, liefert die Funktion einen
Admin-Mock-User. Sonst None.
Default: leeres ENV kein Bypass möglich, in prod inaktiv. Auf dev
erlaubt es Diagnose-Sessions ohne Keycloak-Login. Jeder Use wird
geloggt (warning), damit Bypass-Aktivität sichtbar bleibt.
"""
expected = (os.environ.get("DEBUG_AUTH_TOKEN") or "").strip()
if not expected:
return None
presented = (request.headers.get("x-debug-token") or "").strip()
# Auch via Query-Param erlauben — für einmaligen Zugriff im Browser
if not presented:
presented = (request.query_params.get("__debug_token") or "").strip()
if presented and presented == expected:
client = request.client.host if request.client else "?"
logger.warning(
"DEBUG_AUTH_TOKEN bypass active — request from %s to %s",
client, request.url.path,
)
return {
"sub": "debug-user",
"email": "debug@local",
"name": "Debug-User",
"roles": ["admin", "gwoe-admin"],
}
return None
async def _validate_token(token: str) -> Optional[dict]: async def _validate_token(token: str) -> Optional[dict]:
"""Validiere JWT gegen Keycloak-JWKS. Returns Payload oder None.""" """Validiere JWT gegen Keycloak-JWKS. Returns Payload oder None."""
try: try:
@ -170,6 +202,10 @@ async def get_current_user(request: Request) -> Optional[dict]:
if not _is_auth_enabled(): if not _is_auth_enabled():
return None return None
debug_user = _check_debug_token(request)
if debug_user:
return debug_user
token = _extract_token(request) token = _extract_token(request)
if not token: if not token:
return None return None
@ -186,6 +222,10 @@ async def require_auth(request: Request) -> dict:
if not _is_auth_enabled(): if not _is_auth_enabled():
return {"sub": "anonymous", "email": "", "name": "Dev-Modus", "roles": []} return {"sub": "anonymous", "email": "", "name": "Dev-Modus", "roles": []}
debug_user = _check_debug_token(request)
if debug_user:
return debug_user
token = _extract_token(request) token = _extract_token(request)
if not token: if not token:
raise HTTPException( raise HTTPException(

View File

@ -26,6 +26,11 @@ services:
- GITEA_REPO_NAME=${GITEA_REPO_NAME:-gwoe-antragspruefer} - GITEA_REPO_NAME=${GITEA_REPO_NAME:-gwoe-antragspruefer}
- GITEA_FEEDBACK_LABELS=${GITEA_FEEDBACK_LABELS:-feedback,dev} - GITEA_FEEDBACK_LABELS=${GITEA_FEEDBACK_LABELS:-feedback,dev}
- APP_ENV=dev - APP_ENV=dev
# Dev-Bypass für Diagnose-Sessions: wenn gesetzt, akzeptiert
# require_auth/require_admin einen Header `X-Debug-Token: <secret>`
# oder Query-Param `?__debug_token=<secret>` und liefert einen
# Admin-Mock-User. NUR auf dev. Default leer = inaktiv.
- DEBUG_AUTH_TOKEN=${DEBUG_AUTH_TOKEN:-}
volumes: volumes:
- ./data:/app/data - ./data:/app/data
- ./reports:/app/reports - ./reports:/app/reports