60 lines
2 KiB
Python
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",
|
|
]
|