# ADR-0022: 鉴权强制 + 租户作用域 provider 解析 -- REST 面 OIDC 双轨过渡 + provider_instance 语义升级 (R1+R2) - **Status**: Proposed (方向 PM 已拍 2026-07-12 "越晚弄发布越麻烦", 本文范围待拍) - **Date**: 2026-07-12 - **Consumers**: flytoCall (第一个外部消费者, 对接说明已预埋 "Authorization header 做成可配置"); 后续所有 vertical platform; 对账/转写/flow 全部 REST 调用方 - **关联**: ADR-0017 (engine_config/engine_provider_instances 的 tenant_id 列预留 + §6 触发条件 "真要 per-tenant 配置" 本文正式触发) · ADR-0020 v2 (provider 来源纪律, provider_instance 必填 -- 本文升级其语义) · ADR-0002 (REST 业务 / gRPC 观测 bifurcation, 鉴权基建同源) ## 1. 背景 / Context ### 1.1 R1: prod REST 裸奔, 消费者一多再补 = 破坏所有调用方 prod common 未设 `--oidc-issuer` -> Verifier 为 nil -> REST 业务面 (:8080) 的 `authMiddleware` 根本不挂载 (`internal/server/server.go:375`), `/api/v1/config/*` 含 provider key 写入无 token 可调, 只靠 Caddy/网络边界. OIDC 基建其实齐全: `internal/auth` 有 JWT 验签 (`verifier.go:112`) + tenant claim 解码 + gRPC interceptor + `HTTPMiddleware` (`middleware.go:33`), gRPC 观测面与 admin 面都在用. 缺的不是代码基建, 是 (a) REST 面的**双轨过渡语义** (现状是全有或全无: 无 verifier 全放行, 有 verifier 全强制, 没有中间态让存量消费者平滑迁移), (b) prod 配置与消费者发 token 的流程. ### 1.2 R2: provider 实例真身份 = 租户 + 类型, 全局命名是单租户代偿 PM 2026-07-12 拍 (memory project_tenancy_model "Provider 实例归属模型修正"): `flytocall-deepseek` 这类全局前缀命名是单租户现实下的代偿. store 层其实早已租户参数化 (所有查询 `WHERE tenant_id = $1`, ADR-0017 预留), 缺口精确地只有一处: `engineconfig.Manager` 构造时把 tenantID 焊死 (`manager.go:55`, 唯一构造点 `cmd/common/main.go:409` 硬传 `t_default`), 导致 `resolveFlowProvider` (`flows_run.go:401`) 与转写代理 (`transcriptions.go:97`) 拿到的 snapshot 永远是 t_default 的, 不读请求 ctx 里的租户. **越晚做, 全局命名被越多消费者写死进代码** -- 这也是 R1+R2 必须同包的原因: R2 的请求租户来自 R1 的 token 解析. ## 2. 决策 / Decision ### 2.1 R1: REST 面 OIDC 双轨过渡 (soft -> enforced 两阶段) - **soft 模式** (本包交付): verifier 构造出来后 REST 面新增双轨行为 -- 带 `Authorization: Bearer` 则验签, 验签失败返 401 (带了就必须对, 不静默降级); 不带 token 则回落 `t_default` 租户 + 打 deprecation 日志 (含 path + 远端标识, 供限期通知消费者时点名). 模式经启动 flag `--auth-mode=off|soft|enforced` 显式声明, 缺省 off 保持现状语义 (dev 便利), prod 配 soft. - **enforced 模式** (通知消费者限期后切换, 只改 flag 不改代码): 无 token 一律 401. - gRPC 观测面 / admin 面行为不变 (已强制). - prod 配置改动 (加 `--oidc-issuer` + `--auth-mode=soft`) 是独立运维步骤, 动手前单独 ⏸️. ### 2.2 R2: Manager 从单租户单 snapshot 变按租户取 snapshot - `engineconfig.Manager` 去掉构造期固定 tenantID, 改为 `SnapshotFor(tenantID)` 按请求租户取 (per-tenant snapshot cache + 原子热换语义不变, ConfigSnapshot 本身仍不可变). 读路径之外, Manager 全部写方法 (SetInstance/SetConfig/Delete/Seed*) 同步加 tenantID 参数 -- 现在它们都写死 `m.tenantID` (`manager.go:156/220/246` 等), 否则 config 写端点在多租户下会互写. - 请求租户来源: R1 中间件写进 ctx 的 tenant (无 token 时即 t_default), 消费点 `resolveFlowProvider` / 转写代理 / `/api/v1/config/*` 全部改读 ctx 租户. - **provider_instance 语义升级**: 从 "全局名" 变 "本租户作用域内的名**或类型**" -- 解析顺序: 先按名精确匹配; 未命中且该值恰是 provider 类型名 (anthropic/deepseek/openai...) 且本租户该类型**恰好只有一条**实例时按类型命中; 同类型多条且只写类型返 400 要求点名 (fail-loud, 不猜). 全局前缀命名从此消亡为纯风格. 实现前提: ConfigSnapshot 现只存 name->provider (`snapshot.go:63`), 需额外携带 name->type 映射 (rebuild 时从 store 元数据顺带填, 不加查询). - 迁移期兼容: t_default 下现有条目 (anthropic / deepseek / flytocall-deepseek / m5max) 原样保留, 现有调用 (无 token -> t_default + 按名精确匹配) 行为逐字节不变. ### 2.3 交付节奏 ADR (本文) -> 实现 -> core 与 platform/common 两 module 全量 `go test -race -count=1 -timeout 300s ./...` -> fastpush (`REMOTE=hk133`) -> prod 真调验证 (soft 模式下带 token / 不带 token 两轨都打真请求). ## 3. 替代方案 / Alternatives - **一步到位强制 OIDC (无 soft 过渡)**: 直接打断 flytoCall 现有联调; 双轨是给消费者的迁移跑道, PM 计划本就是三步 (双轨 -> 限期通知 -> 强制). 否决. - **auth-mode 用 env 而非 flag**: 与既有 `--oidc-issuer` flag 家族不一致; 且模式是部署拓扑决策, 该在启动参数里显式可见. 否决. - **R2 造 per-tenant Manager 实例池 (每租户一个 Manager)**: Manager 持有 factory/cipher 等无租户态, 复制整个 Manager 只为一个 tenantID 字段是过度设计; per-tenant 的只有 snapshot cache. 否决. - **provider_instance 保持全局名, 另加 tenant 前缀约定**: 就是现状代偿的固化, 把命名纪律外包给消费者, 与 PM 拍的归属模型相反. 否决. - **同类型多条时按 "最新创建" 自动选**: 静默猜测违反 fail-loud 纪律 (ADR-0020 v2 同款取舍). 否决. ## 4. 影响 / Consequences - **正面**: 发布前补上鉴权与租户两块契约地基, 消费者从第一天就按终态语义接入; provider 命名回归 "租户内短名", 配置界面/文档不再教全局前缀; config 写端点纳入鉴权覆盖. - **负面 / 债务**: soft 模式窗口期内无 token 调用仍可写 config (与现状等危, 不更差, 靠限期收口); per-tenant snapshot cache 需要失效策略 (config 写路径按租户失效); 消费者 token 签发流程 (IdP 侧) 不在本 ADR, 是运维/流程活. ## 5. 验证 / Validation - 单测: soft/enforced/off 三模式矩阵 (带对 token / 带错 token / 不带 token x 各模式预期码) + deprecation 日志断言; SnapshotFor 多租户隔离 (t_a 实例对 t_b 不可见) + 类型解析三分支 (单条命中 / 多条 400 / 名字优先于类型) + t_default 兼容回归. - prod 真调: soft 模式下不带 token 走通现有 flow (行为不变证明) + 带真 token 解析出租户 (日志证明) + enforced 预演在 staging (m2max) 做. ## 6. 触发重新评估的条件 / Trigger conditions - 真第二租户上线 -> per-tenant 配置 UI (ADR-0017 §6 同款) + 租户管理面. - 消费者需要机器对机器 token (非人类 OIDC 流) -> 评估 client credentials / API key 方案. - soft 窗口拖过限期 -> 直接切 enforced, 不再加过渡档. ## 7. 工程量 / Engineering footprint platform/common: 改 `internal/server/server.go` (authMiddleware 双轨) + `cmd/common/main.go` (auth-mode flag + Manager 构造) + `internal/engineconfig/manager.go` (SnapshotFor + per-tenant cache) + `internal/engineconfig/snapshot.go` (类型解析) + `flows_run.go`/`transcriptions.go`/`server_config.go` (读 ctx 租户) + 对应 test. core: 预期零改动 (纯平台层). 零新依赖. ## 8. 修订记录 / Revision history - 2026-07-12 v0 Proposed: R1+R2 同包方案成文, 待 PM 拍范围.