feat(authz): data-plane allowlist helper + INSUFFICIENT_SCOPE code (Stage 1 收口)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ class AuthErrorCode(StrEnum):
|
||||
NOT_AUTHENTICATED = "not_authenticated"
|
||||
SYSTEM_ALREADY_INITIALIZED = "system_already_initialized"
|
||||
WORKSPACE_REQUIRED = "workspace_required"
|
||||
INSUFFICIENT_SCOPE = "insufficient_scope"
|
||||
|
||||
|
||||
class TokenError(StrEnum):
|
||||
|
||||
@@ -55,6 +55,28 @@ def _is_public(path: str) -> bool:
|
||||
return any(path.startswith(prefix) for prefix in _PUBLIC_PATH_PREFIXES)
|
||||
|
||||
|
||||
# Data-plane / SDK route prefixes a service principal (API key) may reach.
|
||||
# Everything else (global control plane: models/mcp/memory/skills/channels/
|
||||
# agents, plus management/auth endpoints) is denied by default for API keys.
|
||||
# NOTE: nginx rewrites /api/langgraph/(.*) -> /api/$1 before the gateway, so
|
||||
# AuthMiddleware never sees /api/langgraph; the SDK surface arrives as
|
||||
# /api/threads, /api/runs, /api/assistants. assistants.search()/get() is
|
||||
# required for langgraph-sdk client init, so /api/assistants is allowed.
|
||||
_DATAPLANE_PREFIXES: tuple[str, ...] = (
|
||||
"/api/threads",
|
||||
"/api/v1/threads",
|
||||
"/api/runs",
|
||||
"/api/v1/runs",
|
||||
"/api/assistants",
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class AuthMiddleware(BaseHTTPMiddleware):
|
||||
"""Strict auth gate: reject requests without a valid session.
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""API key control-plane default-deny tests (Stage 1 收口).
|
||||
|
||||
service principal (API key) 只能访问数据平面 (threads/runs/assistants);
|
||||
控制平面 (models/mcp/memory/skills/channels/agents 与管理/auth) 一律 403。
|
||||
真人 cookie 路径不受影响。设计见 spec
|
||||
docs/superpowers/specs/2026-06-28-api-key-control-plane-default-deny-design.md。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.gateway.auth_middleware import _is_dataplane_path
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/api/threads",
|
||||
"/api/threads/abc",
|
||||
"/api/v1/threads",
|
||||
"/api/v1/threads/abc/runs/xyz/feedback",
|
||||
"/api/runs",
|
||||
"/api/runs/stream",
|
||||
"/api/v1/runs/stream",
|
||||
"/api/assistants",
|
||||
"/api/assistants/search",
|
||||
],
|
||||
)
|
||||
def test_dataplane_paths_allowed(path):
|
||||
assert _is_dataplane_path(path) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/api/models",
|
||||
"/api/v1/models",
|
||||
"/api/mcp/config",
|
||||
"/api/v1/mcp/config",
|
||||
"/api/v1/memory",
|
||||
"/api/v1/skills/install",
|
||||
"/api/v1/channels/restart",
|
||||
"/api/v1/agents",
|
||||
"/api/v1/service-accounts",
|
||||
"/api/v1/api-keys",
|
||||
"/api/v1/auth/me",
|
||||
"/api/v1/assistants", # assistants 是 LangGraph 兼容 shim,无 /api/v1 孪生:只放行 /api/assistants,缺 v1 变体是有意为之
|
||||
"/api/langgraph/threads", # nginx 死代码:中间件本看不到,真混进来也应 deny
|
||||
],
|
||||
)
|
||||
def test_control_plane_paths_denied(path):
|
||||
assert _is_dataplane_path(path) is False
|
||||
Reference in New Issue
Block a user