harden(gateway): API keys default-deny on control-plane routes (Stage 1 收口)
Service principals (Bearer dfk_...) may only reach the data plane (threads / runs / assistants). All control-plane routes — mcp, models, skills, channels, agents, memory, auth, management — now return 403 insufficient_scope. The check sits after the NULL/401 guard so an invalid key on a control-plane path still returns 401, not 403. Human cookie requests bypass the bearer branch entirely and are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,8 +73,14 @@ _DATAPLANE_PREFIXES: tuple[str, ...] = (
|
||||
|
||||
def _is_dataplane_path(path: str) -> bool:
|
||||
"""True if an API key request may reach this path. Reusable by a future
|
||||
Pattern B service-token branch."""
|
||||
return any(path.startswith(prefix) for prefix in _DATAPLANE_PREFIXES)
|
||||
Pattern B service-token branch.
|
||||
|
||||
Matches a prefix only at a path-segment boundary (exact match, or the
|
||||
prefix immediately followed by ``/``), so the allowlist can't be silently
|
||||
widened by a similarly-named route — e.g. ``/api/threads-export`` shares
|
||||
the ``/api/threads`` prefix but crosses no segment boundary, so it stays
|
||||
denied."""
|
||||
return any(path == prefix or path.startswith(prefix + "/") for prefix in _DATAPLANE_PREFIXES)
|
||||
|
||||
|
||||
class AuthMiddleware(BaseHTTPMiddleware):
|
||||
@@ -122,6 +128,17 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
||||
status_code=401,
|
||||
content={"detail": AuthErrorResponse(code=AuthErrorCode.TOKEN_INVALID, message="Invalid API key").model_dump()},
|
||||
)
|
||||
# Default-deny: a service principal may only reach the data plane
|
||||
# (threads/runs/assistants). Control-plane routes (mcp/skills/
|
||||
# channels/models/agents/memory + management/auth) are global,
|
||||
# un-partitioned config — never reachable by an API key. New
|
||||
# control-plane routes are denied automatically (allowlist, not
|
||||
# blocklist). Humans (cookie path) never enter this branch.
|
||||
if not _is_dataplane_path(request.url.path):
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
content={"detail": AuthErrorResponse(code=AuthErrorCode.INSUFFICIENT_SCOPE, message="API keys cannot access this endpoint").model_dump()},
|
||||
)
|
||||
request.state.user = result.principal
|
||||
request.state.auth = AuthContext(user=result.principal, permissions=result.permissions)
|
||||
user_token = set_current_user(result.principal)
|
||||
|
||||
Reference in New Issue
Block a user