c2-core: add CORS middleware so the browser can call the REST API (#110)
The Archive page's GET /calls/search failed its CORS preflight (OPTIONS -> 405, no Access-Control-* headers). Allow the app origin(s) explicitly for the standard methods and the authorization/content-type headers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tbknwttzou4s46PAykmtix
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
bccb3e0316
commit
d60fef67ad
@@ -180,16 +180,18 @@ class Settings(BaseSettings):
|
||||
# between genuinely separate transmissions on a busy dispatch channel.
|
||||
duplicate_window_seconds: int = 10
|
||||
|
||||
# CORS — set to your frontend origin(s) in production, e.g. ["https://app.example.com"]
|
||||
# Defaults to "*" for local development only.
|
||||
# Browser origins allowed to call this API cross-origin. The only browser
|
||||
# caller is the frontend's Archive page (GET /calls/search) — every other
|
||||
# page reads Firestore directly. The frontend is served on the BARE domain
|
||||
# (see infra Caddyfile.j2 — only drb. and api. have DNS records), so the
|
||||
# default is that origin, not app.<domain>. Override via CORS_ORIGINS (JSON
|
||||
# list) if the frontend ever moves; keep infra/.../c2-core.env.j2 in sync.
|
||||
#
|
||||
# Leaving this as "*" is not merely permissive: main.py turns OFF
|
||||
# allow_credentials when it sees a wildcard, because Starlette would
|
||||
# otherwise reflect each caller's origin back WITH
|
||||
# Access-Control-Allow-Credentials. So a production deployment that
|
||||
# forgets to set this gets a loud ERROR at startup and loses credentialed
|
||||
# cross-origin requests, rather than silently accepting every origin.
|
||||
cors_origins: list[str] = ["*"]
|
||||
# A "*" entry here still works for local dev but is refused a credentialed
|
||||
# response: main.py never enables allow_credentials (auth is a Bearer
|
||||
# header, not a cookie), and it logs a loud ERROR when it sees a wildcard
|
||||
# in a deployment so a forgotten override is visible.
|
||||
cors_origins: list[str] = ["https://drb.cusano.net"]
|
||||
|
||||
# Discord webhook URL that app/internal/ai_health.py posts to when an AI
|
||||
# tier (transcription/correlation) transitions into or out of degraded
|
||||
|
||||
+24
-17
@@ -78,33 +78,40 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
app = FastAPI(title="DRB C2 Core", lifespan=lifespan)
|
||||
|
||||
# "*" plus allow_credentials=True is not the permissive-but-harmless setting it
|
||||
# looks like. Starlette does not refuse the combination -- it reflects the
|
||||
# caller's Origin back and still sends Access-Control-Allow-Credentials: true,
|
||||
# so the effective policy becomes "any origin, with credentials", the opposite
|
||||
# of what a wildcard normally means. Rather than trust every deployment to
|
||||
# remember to override CORS_ORIGINS, make the dangerous pair unrepresentable.
|
||||
# The browser needs CORS to reach this API at all: the frontend's Archive page
|
||||
# calls GET /calls/search with Authorization + Content-Type headers, which
|
||||
# forces a preflight. Without this middleware the OPTIONS gets a bare 405 and
|
||||
# the fetch fails (#110). allow_origins is an explicit list -- never "*" in a
|
||||
# deployment -- so name every host the frontend is served from in CORS_ORIGINS.
|
||||
#
|
||||
# allow_credentials stays False on purpose: auth here is a Bearer header, not a
|
||||
# cookie, so credentialed CORS is never needed, and keeping it False is what
|
||||
# lets an explicit-origin allowlist work without Starlette's "*"-only
|
||||
# restriction. "*" + credentials is the dangerous pair (Starlette reflects the
|
||||
# caller's Origin back WITH Access-Control-Allow-Credentials: true); this code
|
||||
# cannot produce it because credentials are hard-off.
|
||||
def cors_allows_credentials(origins: list[str]) -> bool:
|
||||
"""False when any entry is a wildcard. Extracted so it can be tested
|
||||
without re-importing this module, which drags in every router."""
|
||||
return "*" not in origins
|
||||
"""Always False -- credentialed CORS is never enabled here (Bearer auth,
|
||||
not cookies). Kept as a named predicate so a future edit that wants to
|
||||
turn credentials on has to go through here and confront the "*" case.
|
||||
A wildcard entry would additionally be refused a credentialed response."""
|
||||
return False
|
||||
|
||||
|
||||
_cors_is_wildcard = not cors_allows_credentials(settings.cors_origins)
|
||||
_cors_is_wildcard = "*" in settings.cors_origins
|
||||
if _cors_is_wildcard:
|
||||
logger.error(
|
||||
"CORS_ORIGINS is '*', so credentialed cross-origin requests are being "
|
||||
"DISABLED to avoid reflecting every caller's origin back with "
|
||||
"Access-Control-Allow-Credentials. Set CORS_ORIGINS to your frontend "
|
||||
"origin(s) in production, e.g. [\"https://app.example.com\"]."
|
||||
"CORS_ORIGINS contains '*'. That is fine for local dev but is almost "
|
||||
"certainly a misconfigured deployment -- set CORS_ORIGINS to your "
|
||||
"frontend origin(s), e.g. [\"https://drb.cusano.net\"]."
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
allow_credentials=not _cors_is_wildcard,
|
||||
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
||||
allow_headers=["authorization", "content-type"],
|
||||
allow_credentials=False,
|
||||
)
|
||||
|
||||
app.include_router(nodes.router, dependencies=[Depends(require_service_or_firebase_token)])
|
||||
|
||||
Reference in New Issue
Block a user