"""테넌트 라우팅 미들웨어. 규약 (계획서 B): - 헤더 `X-Tenant-ID`(1순위) / 경로 `/t/{id}/...`(폴백)로 테넌트 식별. - 부재 시 400(TENANT_HEADER_MISSING), 미등록 404(TENANT_NOT_REGISTERED). - default tenant 금지 (KT 사고 방지) — allow_default_tenant=false. - 요청 본문엔 tenant 미포함(위조 방지). 식별된 tenant_id 는 request.state.tenant_id 로만 전달. NOTE(P0): 여기서는 헤더 추출 + 부재 검증까지만 한다. 미등록 404 판정(레지스트리 조회)·company_id 매핑·엔진 resolve 는 P4(TenantEngineRegistry)에서 채운다. health/docs 등 화이트리스트 경로는 검사를 건너뛴다. """ from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import JSONResponse from common.enums import ErrorType from config.server_configs import agent_config _TENANT_HEADER = "X-Tenant-ID" # 테넌트 식별이 필요 없는 경로 (헬스/문서/스키마/데모 UI + 전역 카탈로그 리로드=내부 운영). _WHITELIST_PREFIXES = ("/healthz", "/health", "/v1/health", "/docs", "/redoc", "/openapi.json", "/demo", "/v1/catalog-refresh-all") class TenantMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): # CORS preflight(OPTIONS)는 테넌트 헤더 없이 오므로 통과시킨다(CORSMiddleware 가 처리). if request.method == "OPTIONS": return await call_next(request) path = request.url.path if any(path.startswith(p) for p in _WHITELIST_PREFIXES): return await call_next(request) tenant_id = request.headers.get(_TENANT_HEADER) # 경로 폴백: /t/{tenant_id}/... if not tenant_id and path.startswith("/t/"): parts = path.split("/", 3) if len(parts) >= 3 and parts[2]: tenant_id = parts[2] if not tenant_id: if agent_config.allow_default_tenant: tenant_id = "_base" else: return JSONResponse( status_code=400, content={"result": {"success": False, "code": ErrorType.TENANT_HEADER_MISSING.value, "desc": ErrorType.TENANT_HEADER_MISSING.name}}, ) # 미등록 테넌트는 404 (default tenant 금지 — KT 사고 방지). from tenancy.registry import tenant_registry if not tenant_registry.is_registered(tenant_id): return JSONResponse( status_code=404, content={"result": {"success": False, "code": ErrorType.TENANT_NOT_REGISTERED.value, "desc": ErrorType.TENANT_NOT_REGISTERED.name}}, ) request.state.tenant_id = tenant_id return await call_next(request)