Skip to content

【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652

Description

@baozhoutao

背景(业务场景)

下游项目 Titanwind EHR(steedos-labs/os-project-titanwind-ehr)的「考核单」走三级审批(部门负责人→总经理→财务)。业务说明书要求:考核单台账的使用角色(设备管理员,非审批参与人)在单据详情中能查看审批流转记录(各级审批人+决策+时间+意见)。项目侧缺陷单:steedos-labs/os-project-titanwind-ehr#1327(已按平台能力受限挂起)。

现状(@objectstack/plugin-approvals@17.0.0-rc.6)

  • Console 记录页仅当 GET /api/v1/approvals/requests 返回 ≥1 条时才渲染「审批」页签;
  • 该接口经 ApprovalService.listRequests() → visibleRequestIds() 过滤,可见集 = 提交人 ∪ 当前审批人 ∪ 历史动作人;唯一旁路 isOverrideActor() 只认平台/租户管理员;
  • ApprovalsPluginOptions 无任何可见性配置开关,项目侧无 hook 点。

实测硬证据(项目侧真机):对一张走完三级审批的单据,非参与人账号(对该业务记录本身有 CRUD_ALL 读权)调 GET /api/v1/approvals/requests 返回 HTTP 200 + {"data":[]}(不是 403,即非对象权限问题);同一账号读业务记录本体 200、全字段可见。结果:该角色在 UI 上完全看不到审批页签与任何审批动态。

诉求(二选一或都做)

  1. 内置一档可见性:「对该业务记录有读权限的用户,可只读查看其审批请求/审批动态」(不下发审批动作),以 ApprovalsPluginOptions 开关或 per-object 配置启用;
  2. 或提供可见性 hook:允许宿主项目注入自定义 visibleRequestIds() 逻辑。

影响面

不止单一项目:任何「台账/主管角色需要查看审批进度但不是审批参与人」的场景(质量 QIF/NCR、考核单、零星派工审批等)都会撞到同一堵墙;项目侧唯一替代方案是自建审批日志对象并在每级审批写副本,与平台能力重复且有存量回填负担。

环境

@objectstack/plugin-approvals@17.0.0-rc.6(runtime/objectql/spec 同版本);复现细节与逐账号 API 探针记录见 steedos-labs/os-project-titanwind-ehr#1327 的开发排查报告评论。

Activity

  1. added theissue type on Aug 14, 2026
  2. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Triage: lands in packages/plugins/plugin-approvals/src/approval-service.ts (visibleRequestIds()) ⇒ domain:services. Type Feature. Escalated to needs-user-decision.

    为什么进决策箱(而非直接入队):两条人工地板同时命中 —— ①功能新增类(卡面本身就是「能力需求」);②安全/权限边界:两个方案都在放宽「谁能读到审批记录」。放宽访问控制的「修复」是伪装的产品决定,分诊席不得代裁。

    ⚠️ priority:p0 已保留,但请注意它不会让此卡离开决策箱 —— 优先级是排序,不是豁免;裁决之前它不可派发。

    现状判读与卡面一致(visibleRequestIds() 可见集 = 提交人 ∪ 当前审批人 ∪ 历史动作人,唯一旁路 isOverrideActor() 只认平台/租户管理员;ApprovalsPluginOptions 无开关)。项目侧实测 200 + {"data":[]} 而非 403,确认是可见性过滤而非对象权限 —— 这个区分是干净的,省掉了一轮误诊。

    四棱卡面

    • ① 平台长远合理性 — 方案 1。可见性是平台级的权限语义,应当被声明一次、平台强制一次。方案 2(hook)把一条权限判定导出给宿主项目,等于每个项目各自重新发明一遍访问控制规则,平台既无法约束也无法审计。
    • ② 实测业务拉力 — 有,且已具名:Titanwind EHR 考核单三级审批,台账角色(设备管理员,非审批参与人)按业务说明书需看流转记录(steedos-labs/os-project-titanwind-ehr#1327,附逐账号 API 探针)。卡面另指出同形场景族(质量 QIF/NCR、零星派工审批)。拉力真实,但已确认消费者为 1 个,「场景族」尚属推断 —— 请按 1 个已证实消费者定价。
    • ③ 防 AI 写错 — 方案 1,且这是两案差距最大的一棱。方案 2 让(常由 AI 编写的)宿主项目自己写一条安全相关的可见性谓词 —— 写错就是越权读,且平台看不见。方案 1 是闭合的、平台强制的一档。⛔ 方案 2 应视为风险最高的形状,不是「更灵活」的那个。
    • ④ 创业期范围纪律 — 两案都是永久义务:方案 1 欠一个声明档位,方案 2 欠一个公开 hook API(更难收回)。若必须做,方案 1 的义务更窄;若拉力不足,defer 优于 declare-and-maintain。

    四棱指向一致(方案 1 优于方案 2),但「做不做」在人工地板上 ⇒ 交裁。

    建议

    若确认要做:方案 1,严格只读、按「对该业务记录有读权」派生,以 ApprovalsPluginOptions 开关(默认关闭)启用 —— 默认关闭使其对现有安装非破坏性,开关是显式声明。⛔ 明确不做方案 2:导出权限判定给宿主项目,与「声明即强制」相悖。

    需要你回答的两个问题:

    1. 只读可见的粒度:审批动态全量(各级审批人 + 决策 + 时间 + 意见正文),还是只到「谁、何时、什么决策」而不含意见正文?意见正文常含敏感评价 —— 这条决定了能力面的大小。
    2. 开关粒度:插件级一档,还是 per-object 配置?(per-object 更贴合「台账可见、其它不可见」,但那是更大的一块永久义务。)

    项目侧的替代方案(自建审批日志对象 + 每级写副本)会与平台能力重复并带存量回填负担 —— 如果裁决是 defer,建议在卡上给出具名重启条件,让项目侧知道等什么。


    Generated by Claude Code

  3. os-zhuang commented on Aug 15, 2026

    @os-zhuang
    Contributor

    Maintainer ruling (delegated adjudication, batch 1/―; delegation 2026-08-15 verbatim 「决策你直接帮我做」, this batch confirmed verbatim 「同意」 in the live PM session): Option ① — built-in read-only visibility tier; Option ② (host hook) rejected.

    A user with read access to the target business record may view that record's approval requests and full action history, read-only (no approval actions delivered). Enabled by a per-object or plugin-level switch, default OFF — existing deployments' confidentiality posture is unchanged; the downstream project opts in.

    Rationale (four-prism): real p0 pull (Titanwind EHR, measured 200+empty for a reader holding record CRUD; QIF/NCR-class scenarios generalize; the downstream alternative — duplicating approval logs per app — is worse) · anchors on the EXISTING record-read permission rather than inventing a permission concept · a closed switch is auditable where a visibility hook outsources a security decision to host code — the exact surface where generated code silently over-exposes · one bounded tier vs a permanent extension-API obligation.

    needs-user-decision → pm:queue; domain:services seat dispatches.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions