feat: /api/platform 豁免租户解析(认证守护)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-13 08:32:55 +08:00
parent c5e8dd6ded
commit 386b5eb90a
2 changed files with 64 additions and 1 deletions
+24 -1
View File
@@ -10,6 +10,8 @@ scope type,两条路都先过租户解析。
- scope 非 http/websocket(lifespan 等)原样放行;
- path 精确等于 ``/health`` 豁免(部署健康检查 ``curl 127.0.0.1:8018``
/health,Host 是 IP:端口必然未知;精确匹配,``/healthxxx`` 不豁免);
- path 前缀 ``/api/platform/`` 豁免(Phase 2 Task 3,平台后台不依赖租户
域名解析;安全模型详见 ``__call__`` 内注释——豁免≠放行);
- Host 头 → ``registry.resolve_site``: 未知 → http 404 / ws close(1008);
租户停用 → http 503 中性"服务暂停"页(不暴露租户信息)/ ws close(1013);
- 解析成功把 tenant/site 写进 ``scope["state"]``——Request/WebSocket.state
@@ -73,7 +75,28 @@ class TenantHostMiddleware:
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] not in ("http", "websocket"):
return await self.app(scope, receive, send)
if scope.get("path") == "/health":
# 平台级豁免(先于租户解析;判断在 scope type 分发之后,http 与
# websocket 两类 scope 同构生效——当前 /api/platform 前缀没有 WS
# 端点,保持与 /health 同构以防将来有):
# - /health 精确匹配(见模块 docstring);
# - /api/platform/ 前缀(Phase 2 Task 3)。
#
# 安全模型——豁免≠放行:本豁免只跳过"租户解析"这一层,不打开
# 任何受保护端点:/api/platform/* 的认证由路由依赖守护
# (platform_user = get_current_user + enforce_password_changed,
# 未带有效 token 一律 401/409)。未知 Host 下 admin API 可达是
# **有意设计**:生产的 admin 域名与租户域名在反代层区分(见
# spec §4 边界规则与部署文档),应用层不按 Host 收紧 admin 面。
# 前缀精确到斜杠:startswith("/api/platform/"),/api/platform-evil
# 不豁免(仍走租户解析,测试 test_prefix_match_precise 钉住)。
#
# 已知观察项(本任务仅注记不改实现):反代部署形态下登录限流键
# 取 request.client.host 不解析 XFF——所有请求同源(反代 IP),
# 键退化为按用户名(fail-safe:只更严不会更松,单账号爆破面
# 不变);可信代理头解析归部署批次统一处理。
path = scope.get("path") or ""
if path == "/health" or path.startswith("/api/platform/"):
return await self.app(scope, receive, send)
registry = getattr(scope["app"].state, "registry", None)