forked from ChaiHao/BPOS-MVP
基线:鉴权会话+工作组+工作台三区修复+编排工作区+产品设计文档
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# ADR-010 鉴权会话方案
|
||||
|
||||
状态:已接受(2026-07-27)
|
||||
背景出处:编码规范 §4「鉴权定调:单组织 cookie session —— 登录、会话与岗位权限校验中间件在
|
||||
apps/api 的 app 层实现,权限判定读 `org` 出口;不引 OAuth/OIDC 与多租户机制」;
|
||||
§12 留白清单「鉴权会话与凭证加密方案 —— 实现期再定,各建 ADR 后方可动工」。
|
||||
|
||||
## 问题
|
||||
|
||||
在此之前系统**没有登录、也没有登出**。中间件只做一件事:
|
||||
|
||||
```ts
|
||||
const match = /(?:^|;\s*)jcai_account=([^;]+)/.exec(cookie);
|
||||
c.set('account', match?.[1] ?? DEFAULT_LOGIN_ACCOUNT);
|
||||
```
|
||||
|
||||
三个后果,前两个是缺功能,第三个是安全缺陷:
|
||||
|
||||
1. 没有登录口 —— 谁打开页面都是种子登录者冯兵,无法验多人协作
|
||||
(A1 冯兵 → A3 林佩萱 → AP 丁子乔 的流转根本走不通)
|
||||
2. 没有登出口 —— cookie 从未被写入,也就无从清除
|
||||
3. **身份可任意伪造** —— cookie 是明文账号,浏览器里改一个字符即可冒充任何人,
|
||||
包括审批人。「驳回必附意见」「完成只出自放行通过」这类规矩全部形同虚设:
|
||||
规矩管的是**这个人**做了什么,而这个人是谁可以随手改写
|
||||
|
||||
## 决策
|
||||
|
||||
### 一、凭据:口令 + scrypt
|
||||
|
||||
`staff` 增列 `password_hash`。散列用 Node 内置 `crypto.scrypt`(N=16384, r=8, p=1),
|
||||
格式 `scrypt$N$r$p$<salt-b64>$<hash-b64>`,比对走 `timingSafeEqual`。
|
||||
|
||||
**不引第三方散列库**:scrypt 是 Node 标准库自带的内存硬散列,参数与格式自描述,
|
||||
零依赖即可满足 ≤50 人单组织的口令存储要求。
|
||||
|
||||
种子口令取自环境变量 `JCAI_SEED_PASSWORD`,缺省 `jcai-dev-2026`,播种时在 stdout 明示。
|
||||
**源码里不写任何真实口令**。
|
||||
|
||||
### 二、会话:服务端会话表 + 不透明 token
|
||||
|
||||
新表 `sys.session`:`id`(即 token,32 字节随机 base64url)、`staff_id`、
|
||||
`issued_at`、`expires_at`、`last_seen_at`。cookie `jcai_session`,
|
||||
`HttpOnly; SameSite=Lax; Path=/`,有效期 12 小时,每次命中滑动续期。
|
||||
|
||||
**为什么不用签名 cookie(HMAC 自包含)**:登出必须**立刻**失效。签名 cookie 在到期前
|
||||
无法作废,除非另建吊销表 —— 那还不如直接用会话表。会话表还顺带解决另一件事:
|
||||
`org` 停用员工时可以一并踢掉他的在途会话,否则被停用的人凭旧 cookie 能继续办事。
|
||||
|
||||
### 三、归属:会话归 `org`,中间件归 app
|
||||
|
||||
会话是**身份**的一部分,而 `org` 已经拥有 staff / position / grant 与 `loadSession`。
|
||||
所以 `login` / `logout` / `resolveSession` / `setPassword` 落 `org` 出口;
|
||||
app 层只放中间件与两条路由 —— 与 §4「中间件在 app 层实现,权限判定读 `org` 出口」一致。
|
||||
|
||||
### 四、免鉴权白名单
|
||||
|
||||
只三条,其余一律 401:
|
||||
|
||||
| 路由 | 理由 |
|
||||
|---|---|
|
||||
| `GET /api/health` | 存活探针 |
|
||||
| `POST /api/login` | 登录本身 |
|
||||
| `POST /api/ingest/mail/:channelId` | **外界边界**:代替真实 IMAP 拉件,不是人的请求 |
|
||||
|
||||
`/api/stream`(SSE)此前完全无鉴权,现纳入鉴权 —— 它推的是「哪个目标动了」,
|
||||
虽不含业务载荷,但目标 ID 本身即信息。
|
||||
|
||||
### 五、明文 `jcai_account` cookie 立即废止,不走三步弃用
|
||||
|
||||
三步弃用是给**契约变更**留的缓冲期。这一条是安全缺陷修复:保留兼容期
|
||||
等于保留一条「改 cookie 即可冒充审批人」的旁路。同理,`DEFAULT_LOGIN_ACCOUNT`
|
||||
不再作为请求身份的兜底,只保留其种子语义(播种时谁是第一个员工)。
|
||||
|
||||
## 后果
|
||||
|
||||
- 前端必须有登录页:`/api/session` 返回 401 即落登录页,登录成功后进原台面
|
||||
- 开发期用 curl 打接口需要先 `POST /api/login` 拿 cookie,不能再靠缺省身份
|
||||
- 口令重置目前只在种子期给出;治理台的「员工」面后续可加重置动作(未做,见 debt)
|
||||
- 未做:失败次数限制 / 锁定、口令强度校验、首登强制改密。单组织 ≤50 人、
|
||||
部署在内网的前提下先不做,列入 `docs/debt.md`
|
||||
@@ -0,0 +1,66 @@
|
||||
# ADR-011 工作组:起单时的岗位绑定
|
||||
|
||||
状态:已接受(2026-07-27)
|
||||
背景出处:OS 03「编排声明岗位,**起单时确定性解析**。策略封闭四选;
|
||||
默认:岗位唯一人;岗上多人时按轮值。解析发生在节点起单时,结果与依据留痕」。
|
||||
|
||||
## 问题
|
||||
|
||||
流程包声明的是**岗位**(`owner.position`),谁来做由四策略在节点起单时算。
|
||||
这对生产是对的,对两类场景却不够:
|
||||
|
||||
1. **一单一队**:同一条工作流的各节点,业务上常常要固定给同一拨人
|
||||
(客户 A 的报价,前后都归这个商务、这个采购)。`follow-source` 只能固定
|
||||
*同一岗位*的人,跨岗位的搭档关系表达不了。
|
||||
2. **测试与演练**:要让一条流的所有节点都落到同一个人身上,好一个人从头走到尾。
|
||||
现状做不到 —— 每个岗位都会解析到该岗位上的人。
|
||||
|
||||
## 决策
|
||||
|
||||
引入**工作组**:一组「岗位 → 员工」的绑定,起单时装载,随这条工作流走。
|
||||
|
||||
### 一、绑定先于策略,但不是第五种策略
|
||||
|
||||
OS 03 说「策略封闭四选」,这条不动。工作组不是策略,是**岗位的人选已经定了** ——
|
||||
四策略回答的是「这个岗位该派给谁」,工作组直接给出了答案,于是策略无从发问。
|
||||
解析顺序因此是:
|
||||
|
||||
```
|
||||
工作组给了这个岗位的人 且 此人在岗 → 用他(by: 'work-group')
|
||||
否则 → 四策略照常(by: 四选之一)
|
||||
```
|
||||
|
||||
工作组给的人**已离岗**时不硬用,退回四策略 —— 绑定不该把单派给一个不在岗的人。
|
||||
|
||||
### 二、快照装载,不引用
|
||||
|
||||
起单时把成员表**拷进工作流记录**,之后改组不影响已起的单。
|
||||
理由与版本信封一致(AM 9.1):留痕要能解释「当时为什么派给他」,
|
||||
而不是被今天的组织调整改写。
|
||||
|
||||
### 3、按来源人装载
|
||||
|
||||
「凡单必有来源」(OS 3.1)。起单时取 `source.by`(登记人 / 触发来源人)的工作组:
|
||||
|
||||
| 来源 | 装载 |
|
||||
|---|---|
|
||||
| 登记触发(人工起单) | 登记人的工作组 |
|
||||
| 事件 / 周期触发,带来源人 | 该人的工作组 |
|
||||
| 无来源人 | **不装载** —— 全走四策略 |
|
||||
|
||||
没有工作组的员工起单也不装载。**向后兼容**:此前起的单、以及所有没建组的人,
|
||||
行为与从前完全一致。
|
||||
|
||||
### 四、归属 `org`
|
||||
|
||||
工作组是「人和岗位」的事,`org`(L1)已经拥有 staff / position / grant。
|
||||
`process` 经既有的 `OrgGateway` 端口多问一句 `workGroupOf(staffId)`,不新增依赖方向。
|
||||
|
||||
## 后果
|
||||
|
||||
- `WorkflowRecord` 增 `workGroup?: Record<岗位 code, 员工 id>`(快照,可空)
|
||||
- `Resolution.by` 从 `OwnerStrategy` 放宽到 `OwnerStrategy | 'work-group'` ——
|
||||
留痕里能一眼看出这单是绑定派的还是策略派的
|
||||
- 一人一组即可造出「整条流都归我」的测试组:所有岗位都指向同一人
|
||||
- 未做:工作组的治理台管理界面(建组 / 改组 / 指派默认组)。
|
||||
目前只能经播种或直接调用例创建,列入 `docs/debt.md`
|
||||
Reference in New Issue
Block a user