vignette/apps/api/app/access_logging.py

60 lines
2 KiB
Python

"""Uvicorn access log에서 인증 콜백 비밀값을 제거한다.
Uvicorn은 ASGI scope의 path와 query string을 합쳐 ``uvicorn.access``의
세 번째 포맷 인자로 기록한다. Google OAuth 콜백에는 일회용 authorization
code와 CSRF state가 query에 있으므로, 애플리케이션 라우터가 안전하게 로깅해도
기본 access log에서 먼저 노출될 수 있다.
"""
from __future__ import annotations
import logging
from typing import Any
OAUTH_CALLBACK_PATH = "/auth/callback"
def redact_access_log_target(target: Any) -> Any:
"""OAuth callback의 query 전체를 제거하고 다른 request target은 보존한다."""
if not isinstance(target, str):
return target
path, separator, _query = target.partition("?")
if separator and path.rstrip("/") == OAUTH_CALLBACK_PATH:
return path
return target
class UvicornAccessLogRedactionFilter(logging.Filter):
"""Uvicorn의 구조화된 access-log 인자에서 request target만 정제한다."""
def filter(self, record: logging.LogRecord) -> bool:
args = record.args
# Uvicorn 0.30.x access 포맷:
# (client_addr, method, path_with_query, http_version, status_code)
if isinstance(args, tuple) and len(args) >= 3:
target = args[2]
redacted = redact_access_log_target(target)
if redacted != target:
record.args = (*args[:2], redacted, *args[3:])
return True
_ACCESS_LOG_FILTER = UvicornAccessLogRedactionFilter()
def install_uvicorn_access_log_redaction() -> None:
"""현재 프로세스의 Uvicorn access logger에 필터를 한 번만 설치한다."""
access_logger = logging.getLogger("uvicorn.access")
if not any(
isinstance(existing, UvicornAccessLogRedactionFilter)
for existing in access_logger.filters
):
access_logger.addFilter(_ACCESS_LOG_FILTER)
__all__ = [
"UvicornAccessLogRedactionFilter",
"install_uvicorn_access_log_redaction",
"redact_access_log_target",
]