This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/docs/adr/ADR-012-keycloak-identity.md
xcsandClaude Fable 5 8556432dba feat(org): 成员目录 —— Keycloak 读写面穿透、治理台成员/组织/权限三页、姓在前 personName、Staff.kind
后端:`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
2026-08-27 11:44:36 +08:00

81 lines
6.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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,
界面只能说一句含糊的「出错了」。
同批拿掉的:「岗位授权」与「工作组」两个页签(用户定)。**后端数据与接口原样留着** ——
岗位仍决定活动派单的候选集,工作组仍在起单那一刻快照进工作流;拿掉的只是入口。