- 三层正交模型:系统角色能力矩阵 / 项目成员角色 / 文档三权(author-reviewer-approver) - 四类核心动作权限矩阵:发起对话、执行任务、补充信息、文档编制与修改(deny_only_submit) - 认证与路由分层、管理台两类 gate、前端 capabilities fail-closed 消费方式 - 如实标注已知缺口:member/viewer 服务端不区分写、visibility 键仅前端消费、system:* 未拆分 - 更新 system-design/README.md 索引与目录清单 task: member-permissions-doc-2026-09-24
21 KiB
项目成员权限总览(对话 / 执行任务 / 补充信息 / 文档编制与修改)
文档定位:本文是对"项目成员在各协作动作中拥有什么权限"的只读梳理与整合,权威实现以代码为准(每节标注
文件:行号)。 口径来源:系统角色与能力矩阵见 authz/fallback.go;三权与阶段判定见 deny-only-submit 经验 与 非编制成员访问整改;路由负例见 authorization-matrix-v3-full;能力成熟度见 FEATURE-MATRIXiam-authz/workflow-gov/docgov行。 本文不新增第二套门禁,也不改变上述权威结论;仅把它们按"成员能做什么"重新组织,供协作与验收对照。 状态:research/overview(可审产物,不含实施授权)。
一、三层权限模型(先厘清概念,再看动作)
系统对"谁能做什么"由三个正交维度共同决定,任何一个单独看都会误判:
| 层 | 取值 | 权威来源 | 决定什么 |
|---|---|---|---|
| 系统角色 | role-admin / role-director / role-tech-lead / role-engineer / role-guest |
users.role_id + DB user_role_bindings → system_role_permissions;无 DB 时回落 FallbackCapabilitiesByRoleID |
系统级能力(建项目、指派负责人、管理台读写、成员管理权限键) |
| 项目成员角色 | owner / member / viewer |
project_members.project_role(+ 创建人 projects.creator_user_id);authorizer.go |
能否进入某项目、能否管成员、能否终止他人任务 |
| 文档职责(三权) | author(编制)/ reviewer(审核)/ approver(批准) |
project_members.document_duty 为权威指派,落到 document_records 三槽位;docduty/resolve.go |
能否对该文档送审 / 审核 / 批准 |
★ Insight ─────────────────────────────────────
三层是分离而非叠加的。Phase 0 术语冻结明确:author/reviewer/approver 是"文档职责",绝不能写进 project_role(authz/service_test.go 有回归拒绝 project_role="author")。这意味着"是 owner"和"是该文档的 reviewer"是两回事——owner 若未被指派为某文档 reviewer,也不能替 reviewer 点"审核通过"。
─────────────────────────────────────
1.1 系统角色的能力矩阵(服务端 fallback,与 IAM seed 对齐)
| capability 键 | admin | director | tech-lead | engineer | guest |
|---|---|---|---|---|---|
project:create 建项目 |
✓ | ✓ | ✓ | ||
project:assign_lead 指派文档负责人 |
✓ | ✓ | ✓ | ||
project:manage_members 管理项目成员 |
✓(含 perm:manage) |
||||
perm:manage 系统权限管理(全量) |
✓ | ||||
system:overview/kb:read/features:read/settings:read/audit:read |
✓ | ||||
system:settings:write 改系统设置 |
✓ | ||||
workflow:config_visibility / doc:override_visibility(仅登记,见 §6.2) |
✓ | ✓ | ✓ | ||
doc:export 导出 |
✓ | ✓ | ✓ | ✓ | ✓ |
workflow:view_all |
✓ | ✓ | ✓ | ✓ |
数据源:fallback.go、roles.go defaultSystemRoles。
role-guest只有doc:export,是"最小权限查阅"角色。
1.2 项目成员角色:owner 与非 owner 的真实差异
关键事实:服务端读取项目内绝大多数业务对象时,只判定"是不是 active 成员",不区分 member 与 viewer——RequireProjectAccess → IsProjectMember 的 SQL 条件只有 status='active' 或"是创建人"。
真正区分的是 owner/creator 这一档:
| 能力 | 判定函数 | 谁满足 |
|---|---|---|
| 进入项目 / 读项目对象 | IsProjectMember |
owner、member、viewer、creator 全部 |
| 管理成员(增删改/移出) | CanManageProjectMembers | owner 或 creator、系统管理员、持 project:manage_members |
| 终止 / 续跑他人任务 run | assertTaskRunAccess 的 cancel/resume | run 发起人本人 或 project owner(member/viewer 不行) |
| 改派文档职责(PATCH duties) | duty_reassign_handlers.go | creator 或持 project:assign_lead |
| 工作流规划/激活/推进/终止 | 各 projectinstance 服务 !isCreator → ErrForbiddenNotCreator |
仅项目创建人 |
也就是说:
viewer在服务端能读、能进对话、能发起自由查询;把"查阅者"降为只读主要靠前端隐藏写按钮(见 §5.4)。文档若要严格区分 member/viewer 的服务端写门禁,属尚未实施的缺口(如实标注,见 §6.1)。
二、请求进入系统的第一道闸:认证与路由分层
★ Insight ─────────────────────────────────────
路由不是按"权限组"分组挂载的,而是全部 /api/v1/* 统一过 authMiddleware 要求登录,成员/管理员/职责判定下沉到每个 handler。这样"公开面"极小且可枚举,代价是每个 handler 必须自觉调门禁函数——所以本仓用负例测试(authorization-matrix-v3)逐路由兜底。
─────────────────────────────────────
- 中间件链:
cors → accessLog → bodyLimit → authMiddleware → mux(server.go:371)。 - 匿名可访问(auth_middleware.go
isPublicRoute):/health、/ready、/api/v1/vault/status、POST /auth/login、POST /auth/register、GET /auth/public-config,以及所有非/api/(SPA 静态页)。 - principal 唯一可信:登录主体只来自
Bearer/X-Session-Token→principalFromContext;禁止X-User-ID头或 bodyuser_id覆盖(Phase 0 红线)。handler 一律req.UserID = user.ID,不信请求体。 - fail-closed:生产/DB 模式下授权依赖异常默认拒绝(failClosedAuthz);dev 内存模式才允许 legacy fallback。
三级门禁函数:requireAuthenticatedUser(401)→ requireProjectAccess(非成员 403 / 不存在 404,管理员直通)→ requireAdmin / requireSystemPermission(管理员判定 IsSystemAdmin)。
三、四类核心协作动作的权限矩阵
这是用户最关心的部分,逐动作给出"谁可以 + 代码位置"。
3.1 发起对话 / 进入项目会话
| 情形 | 判定 | 结论 |
|---|---|---|
| 账号名下无任何项目 | enforceChatRequiresProject | 禁止对话,返回 ERR_NO_PROJECT |
| 会话绑定了项目 | enforceChatProjectAccess → RequireProjectAccess |
必须是该项目 active 成员(owner/member/viewer 均可,管理员放行);SSE 在 Flush 前拒绝 |
| 成员进入后的自由对话 | allowFreeLaneChatInitiation | 成员确认后总是放行发起("自由车道",见 §3.2) |
| 读他人会话/消息 | assertSessionOwnedByUser、chatlog authorize | owner_id != 当前用户 → 403,读不到别人的私有会话 |
| 看同项目他人阶段消息 | MessageRecordVisible | 按阶段作用域过滤,跨阶段/私有消息对同项目其他成员不可见 |
要点:项目成员(含 viewer)都能进项目对话并发消息;会话的所有权隔离保证看不到别人的个人会话;阶段消息可见性另有一层作用域收窄。
3.2 自由协作通道(chat / 上传 / 总结 / 查询 / 影响分析 / 补充)
来自 free-collab-claim-notify-design §3 + free_lane.go:
- 准入 = 项目成员(
RequireProjectAccess),不查阶段 / duty / 实例状态。 - 适用动作:
chat、help、各类query、impact.analyze、supplement、材料上传、总结——共 14 个成员级自由意图键(freeLaneIntentKeys)。 - 红线:自由车道只放开"发起与查询",受控文档的写动作仍走 duty + 阶段 + 认领(见 §3.4)。
3.3 执行任务 / 阶段动作
| 动作 | 端点 | 权限 |
|---|---|---|
| 创建/运行 task run | POST /api/v1/tasks/run*、/projects/{id}/workflows/{key}/run |
需 RequireProjectAccess(成员);run 走 CheckDocTaskAccess(run) → 成员级放行;幂等键归属校验 |
| 查看 run / 事件流 | GET /tasks/runs/{id}、/events |
admin ∥ run 发起人 ∥ project owner ∥ 任意项目成员(view) |
| 取消 / 续跑 run | POST /tasks/runs/{id}/cancel、/resume |
仅 run 发起人 ∥ project owner(cancel/resume),普通 member/viewer 不可终止他人任务 |
| 阶段推进 / 激活 / 终止实例 | /workflow-instances/{id}/stages/{stage}/advance、/terminate、activate |
仅项目创建人(isCreator);推进还需门禁就绪(CanAdvance,未就绪 409) |
3.4 文档的发起编制、修改与三权审批
这是权限最细的部分。受控文档动作统一走 CheckDocTaskAccess → classifyAccess,遵循 deny_only_submit 原则:
| 动作 | 谁能做 | 拒绝码 |
|---|---|---|
| view 读 | 任意项目成员(view 豁免阶段收窄,TaskKeyVisible);正文读还要 authorizeControlledRead |
无项目归属文档仅管理员 |
| edit / run 编辑·跑 | 任意项目成员(deny_only_submit:run/edit 降为成员级) | 实例未激活 → 202-blocked |
| submit 提交送审 | 仅该文档 author | DOC_DUTY_FORBIDDEN |
| review 审核 | 仅该文档 reviewer | DOC_DUTY_FORBIDDEN |
| approve 批准 | 仅该文档 approver | DOC_DUTY_FORBIDDEN |
| DutyMode=partial(职责未配全) | 除 view 外全部拒绝 | DOC_DUTY_FORBIDDEN |
| DutyMode=none(未纳管审批) | run/edit 成员可,但 submit/review/approve 全拒 | DOC_DUTY_NOT_MANAGED |
★ Insight ─────────────────────────────────────
deny_only_submit 是 2026-09-24 整改的核心(lesson):过去把 run/edit 也绑到 author,导致"非编制成员连草稿/上传/总结都 403"。修正后放写、只deny审批链——编辑成员级,但 submit/review/approve 三者仍严格绑定各自单一职责,创建人也不隐式越权。角色一律 resolveDocGovRole 服务端按三槽位解析,绝不信客户端自报。
─────────────────────────────────────
认领与提案(非作者如何"发起修改"):
- 非模板文档被 author 查看/编辑 →
author_claimed=true(free_lane maybeAutoClaimOnView 开关);已认领文档,非作者不能直改,须走变更提案 handleDocGovProposeModification(成员即可提,triggeredBy 强制user:{id})。 - 提案由谁拍板:仅该文档 author(engine_proposal.go 校验
AuthorID,否则ErrUnauthorizedActor→403)。采纳后以作者身份 auto-apply。 - 前端"修改"意图只在
isClaimed && currentDocRole==='author'才路由到 propose(useSendMessage)。
3.5 补充信息 → 影响 → 确认卡
来自 supplement_impact_handlers.go + supplementimpact 服务:
| 环节 | 权限 |
|---|---|
| 查看影响批次 | userCanAccessSupplementBatch = 项目成员或创建人 |
| 确认 / 取消批次 | 仅发起该补充的 initiator 本人(service.go ActorUserID != InitiatorUserID → "only initiator" 403),且需 nonce + digest + expected_revision CAS;过期 410 |
| 已认领文档落地 | 以 initiator 身份走 ProposeModification(回到 §3.4 作者拍板);未认领 → 通知作者信息卡 |
3.6 自由私密文档(个人 → 项目共享)
来自 freeform_doc_api.go + internal/freedoc:
- 创建:需登录;建到
my_private空间必须带project_id且过RequireProjectAccessOrAdmin——非成员不能向该项目挂私密文档。 - 读:
my_private仅 owner;project_shared经项目共享后对成员可读。 - 改 / 共享 / 撤回 / 删除:仅
owner_user_id == 当前用户(service_share.go),越权 403"仅文档所有者可共享"。
四、管理台权限(/api/v1/admin/*)
两类 gate(admin_system_handlers.go、principal_context.go):
- 粗粒度 requireAdmin(=
role-admin∥perm:manage):用户 CRUD、改角色/状态、重置密码(一次性临时密码、不落日志)、角色/工作流/模板管理、workspace 运维。 - 细粒度 system:*(
/admin/system/{overview,kb,features,settings,audits}):各自requireSystemPermission(PermSystem*)。Phase-2 现状:系统管理员拥有全部system:*,非管理员对system:*fail-closed 一律 false(HasSystemPermission)——细粒度键当前是"管理员 all-or-nothing",拆分留待后续。
两个"任何登录用户可读"的例外(立项选人/权限目录展示用):GET /admin/permissions、GET /users/directory(轻量 active 目录)。
五、前端如何消费权限(能力 ≠ 唯一权威,服务端才兜底)
★ Insight ─────────────────────────────────────
前端权限是体验层,不是安全边界。Phase 0 术语冻结已声明 hasPermission/localStorage 属 UI 层;真正的对象级授权永远在服务端 middleware。所以前端"隐藏按钮"被绕过也没关系——后端各 handler 仍会独立 403。
─────────────────────────────────────
- capabilities 获取与缓存:登录成功后
fetchMe()取/auth/me的capabilities,存 localStorage 会话;启动时先用快照渲染再被/auth/me覆盖,拉取失败即清会话登出(useAuthenticatedWorkspaceSync)。 - fail-closed:rbac.ts hasPermission 在
capabilities === undefined时直接返回 false;perm:manage视为全权。 - 按职责显示审批按钮:docGovActionVisibility.ts holdsDocDuty 直接比对 userId 与 author/reviewer/approver;三槽全空(unmanaged)则不显示任何审批动作(DocGovActionBar)。
- 成员/角色 UI:ProjectMembersModal 添加下拉只给
member/viewer,owner/creator 归一不可降级(normalizeEditableRole);改职责下拉也只"项目成员/查阅者"。文档职责(author/reviewer/approver)通过 UserProjectRightsModal 的resolveCurrentDocRole匹配,未知身份 fail-closed 为 viewer。 - 对话/任务:filterProjectSessionsForUser 让非成员完全看不到项目会话,exited 成员只读留痕(输入框禁用);task run 无独立前端权限 gate,靠"进不去会话/退出即禁用"间接限制。
六、边界与已知缺口(如实标注,勿当已实施)
- member vs viewer 服务端不区分写动作:§1.2 所述,viewer 的"只读"目前主要由前端隐藏写按钮保证;服务端
IsProjectMember对 member/viewer 一视同仁。若需服务端级"viewer 禁写",是待实施的授权收窄。 doc:override_visibility/workflow:config_visibility仅前端消费:这两个 capability 在服务端只做登记(fallback + catalog),没有 handler 用HasSystemPermission检查它们;服务端实际文档可见性走ResolveStageAccess的TaskKeyVisible+ 实例快照。配置结果落入 plan/实例后由CheckDocTaskAccess执行,但"覆盖规则写鉴权"未在服务端单独 gate(见子代理调研结论)。system:*细粒度未拆分绑定:Phase-2 现状 admin all-or-nothing,非 admin fail-closed(§4)。- 无 DB 时 authz 降级:
Authorizer.enabled()==false(stub/dev 内存)时IsProjectMember返回userID>0即放行——仅 dev/stub,生产 fail-closed(Phase 0 已知 gap)。
七、权限决策函数清单(速查)
| 函数 | 文件 | 语义 |
|---|---|---|
isPublicRoute |
auth_middleware.go | 匿名白名单 |
requireAuthenticatedUser / requirePrincipal |
auth_helpers.go | 需登录(401) |
requireProjectAccess |
auth_helpers.go:115 | 项目成员可访问(管理员直通) |
requireDocGovDocAccess |
auth_helpers.go:140 | 受控文档 = 所属项目成员 |
resolveDocGovRole |
auth_helpers.go:182 | 服务端按三槽解析 author/reviewer/approver |
IsSystemAdmin / HasSystemPermission / CanManageProjectMembers |
authz/service.go | 系统权限核心判定 |
IsProjectMember / IsProjectOwner / RequireProjectAccess(OrAdmin) |
authz/authorizer.go | 项目成员/owner 判定 |
UpsertProjectMemberSpec / SetProjectMemberStatus |
authz/authorizer.go:314 | 成员 + 独占 duty 写入(advisory lock + 清槽);退出清槽 |
CheckDocTaskAccess / classifyAccess |
document_access.go / duty_action.go | deny_only_submit 六动作矩阵 |
Resolve / HealBinding |
docduty/resolve.go | 以 document_duty 为权威解析三权 |
ResolveStageAccess / TaskKeyVisible |
stage_access.go | 阶段级可见/可运行集 |
assertTaskRunAccess |
task_run_auth.go | run view/cancel/resume 门禁 |
authorizeDocRevision |
document_revision_access.go | 版本账本读=成员;写=drafting 期 +(受控)author 本人或管理员 |
userCanAccessSupplementBatch + Decide initiator |
supplement_impact_handlers.go | 补充批次查看=成员,确认=仅 initiator |
freedoc.Share/Unshare/Delete |
freedoc/service_share.go | 私密文档 owner 独占 |
附:相关权威与证据
- Phase 0 API 授权矩阵 · Phase 1 IAM 实施计划
- authorization-matrix-v3 全路由盘点
- deny-only-submit 经验 · 非编制成员访问整改 · 重验报告
- 自由协作与认领设计
- 能力成熟度:FEATURE-MATRIX
iam-authz/workflow-gov/docgov/web-sysadmin行