Files
Agent-System/.doc/system-design/2026-09-24-member-permissions-overview.md
T
weichen 86881bef6e docs(system-design): 新增项目成员权限总览(对话/任务/补充/文档三权整合)
- 三层正交模型:系统角色能力矩阵 / 项目成员角色 / 文档三权(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
2026-09-25 20:20:58 +00:00

21 KiB
Raw Blame History

项目成员权限总览(对话 / 执行任务 / 补充信息 / 文档编制与修改)

文档定位:本文是对"项目成员在各协作动作中拥有什么权限"的只读梳理与整合,权威实现以代码为准(每节标注 文件:行号)。 口径来源:系统角色与能力矩阵见 authz/fallback.go;三权与阶段判定见 deny-only-submit 经验 与 非编制成员访问整改;路由负例见 authorization-matrix-v3-full;能力成熟度见 FEATURE-MATRIX iam-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 头或 body user_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):

  1. 粗粒度 requireAdmin(= role-admin ∥ perm:manage):用户 CRUD、改角色/状态、重置密码(一次性临时密码、不落日志)、角色/工作流/模板管理、workspace 运维。
  2. 细粒度 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。 ─────────────────────────────────────

  1. capabilities 获取与缓存:登录成功后 fetchMe() 取 /auth/me 的 capabilities,存 localStorage 会话;启动时先用快照渲染再被 /auth/me 覆盖,拉取失败即清会话登出(useAuthenticatedWorkspaceSync)。
  2. fail-closed:rbac.ts hasPermission 在 capabilities === undefined 时直接返回 false;perm:manage 视为全权。
  3. 按职责显示审批按钮:docGovActionVisibility.ts holdsDocDuty 直接比对 userId 与 author/reviewer/approver;三槽全空(unmanaged)则不显示任何审批动作(DocGovActionBar)。
  4. 成员/角色 UI:ProjectMembersModal 添加下拉只给 member/viewer,owner/creator 归一不可降级(normalizeEditableRole);改职责下拉也只"项目成员/查阅者"。文档职责(author/reviewer/approver)通过 UserProjectRightsModal 的 resolveCurrentDocRole 匹配,未知身份 fail-closed 为 viewer。
  5. 对话/任务:filterProjectSessionsForUser 让非成员完全看不到项目会话,exited 成员只读留痕(输入框禁用);task run 无独立前端权限 gate,靠"进不去会话/退出即禁用"间接限制。

六、边界与已知缺口(如实标注,勿当已实施)

  1. member vs viewer 服务端不区分写动作:§1.2 所述,viewer 的"只读"目前主要由前端隐藏写按钮保证;服务端 IsProjectMember 对 member/viewer 一视同仁。若需服务端级"viewer 禁写",是待实施的授权收窄。
  2. doc:override_visibility / workflow:config_visibility 仅前端消费:这两个 capability 在服务端只做登记(fallback + catalog),没有 handler 用 HasSystemPermission 检查它们;服务端实际文档可见性走 ResolveStageAccess 的 TaskKeyVisible + 实例快照。配置结果落入 plan/实例后由 CheckDocTaskAccess 执行,但"覆盖规则写鉴权"未在服务端单独 gate(见子代理调研结论)。
  3. system:* 细粒度未拆分绑定:Phase-2 现状 admin all-or-nothing,非 admin fail-closed(§4)。
  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 独占

附:相关权威与证据