Archived
后端:`KeycloakDirectory` 端口(users / groups / roles 读写;写直接穿透 Keycloak —— 这里建的人立刻能登录、 删的人立刻消失,停用与收权之后 logout 踢掉会话)、`keycloak-directory.ts` + `keycloak-admin-http.ts` 适配器、 org-directory 契约与 /api/gov 路由、`sync-roster` CLI(ops 路由够不着时整册照过来), 容器按 provisioning 服务账号有无决定注不注入(没配就如实说不可用)。 `personName` 姓在前:汉字直拼「柴浩」、否则留一个空格「Admin Amy」,token 与名册同步两条路同一把尺; `staffKindOf` 按 Keycloak 固定前缀 service-account- 认服务账号,员工那一屏不摆它(`Staff.kind` 不落库,按账号名现判)。 前端:员工那一屏重做成成员 / 组织 / 权限三页签(member-page 下钻),页签栏右端一句话说清这一格管什么。 契约变更:contracts org.ts / api/http.ts / api/org-directory.ts;api-report 已刷新;org CONTRACT.md 与 ADR-012 同步。 2026-08-26 做的,一直留在工作树没入库。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
81 lines
6.4 KiB
Markdown
81 lines
6.4 KiB
Markdown
# ADR-012 统一鉴权中心(Keycloak):身份、名册与「角色 → 端」
|
||
|
||
状态:已接受(2026-08-25)
|
||
背景出处:`docs/keycloak-backend-guide.md`(Keycloak 26.7.1 / jose,核实日期 2026-08-12);
|
||
设计稿 `docs/superpowers/specs/2026-08-25-keycloak-auth-design.md`。
|
||
取代:ADR-010(口令 + 服务端会话)与编码规范 §4 原「不引 OAuth/OIDC」的定调。
|
||
|
||
## 问题
|
||
|
||
公司有了统一鉴权中心,本系统还在自建账号口令、服务端会话表与岗位授权(客体三类 × 动作三种)。
|
||
人要在两处维护,权限模型也比产品实际需要的细太多 —— 用户只要「不同角色可以动态配置能进哪几个端」。
|
||
|
||
## 决策
|
||
|
||
1. **资源服务器,不做 BFF**:前端(`web-portal` / `web-console`,PKCE)自取 token;后端只本地验签
|
||
Bearer JWT(`jose`:RS256 写死、校 iss 与 aud、30s 容差、远程 JWKS 缓存 10 分钟 / 冷却 30 秒),
|
||
不回源、不 introspect、**不配 client secret**。
|
||
2. **身份在 token 里**:`Staff.id` = `sub`;`account` = `preferred_username` 只作显示;
|
||
首次见到即建档(JIT),账号 / 姓名 / 邮箱 / 角色变了才写,`last_seen_at` 60 秒内不重写。
|
||
3. **权限只到端**:三个端 design / ops / workbench;`sys.role_access` 一行一个 realm 角色,
|
||
运营中心可改;一个人的端 = 其角色命中行的并集。缺省 `admin → 三端`,`user → workbench`。
|
||
**锁死不变量**:改完至少一名在册活跃员工仍能进运营中心,否则 409。
|
||
4. **路由归属进契约**:`RouteContract.access` 必填(public / any / 端 / 端数组);app 层一处中间件
|
||
按注册表判 401 / 403;治理路由里 59 处逐条守卫整体删除(governance-ops 45 + governance 14);覆盖测试 fail-closed。
|
||
5. **名册**:岗位仍是本系统的表(哨兵岗 `NONE / 未分配`,运营中心指派);名册经 Admin API 同步
|
||
(master realm 的 provisioning 服务账号,只要 `view-users`)+ 首登即建。`state` 只由同步置
|
||
`disabled`(Keycloak 停用或删除),有效 token 不改它 —— 撤权在同步之后即时 403。
|
||
6. **票据**:`EventSource` 与 `<a href download>` 带不了 Authorization 头 —— `GET /api/auth/ticket`
|
||
换一枚 60 秒 HMAC 票据(`HKDF(JCAI_SECRET_KEY, 'jcai-ticket')`),只在 GET 的 `?ticket=` 上认;
|
||
票据里没有权限,中间件拿 sub 回库现算。无状态、不单次:60 秒重放窗换零状态表。
|
||
**横向范围要说明白:票据里既然没有用途,它在这 60 秒里就等于持票人的全部读权限** ——
|
||
凭一枚为下载附件取的票据,同一分钟内照样读得了 `GET /api/gov/data/:domain/:object`(整张客户主数据)。
|
||
这**不是提权**(该用户本就有这些读权限),风险在**留痕面**:票据走在 URL 上,
|
||
浏览器历史、反向代理日志、`Referer` 都可能把它留下来。收敛做法见 `docs/debt.md` 鉴权段
|
||
(签发时把用途绑进票据,或中间件对认票据的路径设白名单)。
|
||
7. **退役**:`login` / `logout` / `session` / 建人 / 停用 / 岗位授权八条路由回 410(新错误码 `gone`),
|
||
契约标 `@deprecated` 留一轮(§12.4 三步);`sys.session` `sys.position_grant` `staff.password_hash` drop。
|
||
8. **测试账号**:`JCAI_TEST_ACCOUNTS`(Keycloak 用户名)命中者落 TEST 岗 + 独立工作组(ADR-011 原语义);
|
||
e2e 与试跑用服务账号(client_credentials),不用人的账号。
|
||
|
||
## 后果
|
||
|
||
- 撤角色的时效 ≤ 5 分钟(token 寿命);停用经同步后即时。
|
||
- **权限只到端,粒度就到此为止** —— 这是有意的收敛,但半年后有人问起时,答案得在这儿写着:
|
||
`design` 现在同时意味着装活动、跑测试台、传文件、探大模型;`design|ops` 意味着
|
||
`GET /api/gov/data/:domain/:object` 能翻整张客户主数据(**从前要 `data:read` 这类客体授权才行**)。
|
||
产品要的就是「不同角色能进哪几个端」,再细的粒度没有需求撑着;真要收窄,
|
||
该走的是「给那几条路由单列一个端」,不是把客体 × 动作那套授权请回来。
|
||
- web 需按 `docs/前端对接说明-统一鉴权中心.md` 改造(登录 / Bearer / 401 跳转 / SSE 与下载改走票据 / 入口亮灯改读 `/api/me` 的 `ends`),
|
||
改造前登不进 —— 这是「只做后端」的既定后果。
|
||
- **部署顺序是先跑迁移、再 `reset-runs`**:迁移 0032 清空 `sys.staff` 并连带清掉两张持有身份的
|
||
**配置**表(`sys.node_owner`、`sys.trigger` 的确认位);而 `sys.task.owner_id` 这类**无外键**的
|
||
运行数据列不受迁移约束,若顺序颠倒,这些运行数据会指着一个已经不存在的身份变成孤儿 ——
|
||
`reset-runs` 必须排在迁移之后跑,才能把这批指着旧身份的运行数据一并清掉。
|
||
- Keycloak 侧前置(管理员):本服务 client(建议复用 `api-gateway`)、`web-console` 补 audience mapper、
|
||
provisioning 服务账号、e2e 服务账号 client(`bpos-e2e`,realm 角色 admin,firstName「AI」)。
|
||
- 欠账见 `docs/debt.md`「鉴权」段。
|
||
|
||
## 补充(2026-08-26):成员与组织三页签
|
||
|
||
运营中心「员工」那一屏按目标件重做成三个页签,一一对着 Keycloak 的三样东西:
|
||
|
||
| 页签 | 数据来自 | 写去哪 |
|
||
|---|---|---|
|
||
| **成员** | Keycloak users(合本地的岗位与「几时来过」) | 穿透 Keycloak:新建 / 改资料 / 启停 / 重置口令 / 删除 |
|
||
| **权限** | Keycloak realm roles × 本地 `sys.role_access` | 端配置写本地;角色授收穿透 Keycloak |
|
||
| **组织** | Keycloak groups | 穿透:建组 / 改名 / 删组 / 成员进出组 |
|
||
|
||
`/api/gov/org/*` 十五条,`access` 一律 `ops`。三处口径值得记:
|
||
|
||
1. **写是穿透的,没有撤销** —— 这里建的人立刻能登录、停的人立刻登不上。界面上凡写必二次确认
|
||
(三必答:谁受影响 / 能不能撤 / 几时生效)。
|
||
2. **服务账号是身份不是人** —— 目录读一律剔除、写面一律 403。停掉它的后果不显眼却很重:
|
||
试跑与 e2e 第二天全趴下。
|
||
3. **两种 502 分开说**:`keycloak-misconfigured`(服务账号缺角色,重试无益,去补权限)与
|
||
`keycloak-unavailable`(暂时够不着,可退避重试)。**重试有没有意义**是分界 —— 混成一个 code,
|
||
界面只能说一句含糊的「出错了」。
|
||
|
||
同批拿掉的:「岗位授权」与「工作组」两个页签(用户定)。**后端数据与接口原样留着** ——
|
||
岗位仍决定活动派单的候选集,工作组仍在起单那一刻快照进工作流;拿掉的只是入口。
|