Skip to content

security/explain 与写入路径对同一条记录给出相反答案:VAMA 持有者对无主 private 记录 explain 答 allowed:true,PATCH 回 403 #4647

Description

@os-zhuang

在 hotcrm 侧排查 objectstack-ai/hotcrm#622 时实测确认的平台侧矛盾。按 hotcrm 的 PM 分工,本仓库不由应用侧改动,只在此记录证据。未认领。

环境:@objectstack/* 17.0.0-rc.1,objectstack dev --seed-admin,sqlite(better-sqlite3) driver,capabilities 含 sharing

复现条件

  • 一个 sharingModel: 'private' 的用户所有对象(复现用 crm_contract)。
  • 一行记录的平台所有权列 owner_id 为 NULL(无主)。种子数据很容易落到这个状态:SeedLoaderService.SEED_OPTIONS{ isSystem: true },其文档明说这会关闭 organization_id / owner_id 的自动注入。
  • 当前 principal 持有 VAMA(本例是 admin_full_access 权限集的平台管理员)。

两个答案

同一个 principal、同一条记录、同一个 operation:

POST /api/v1/security/explain
  { "object": "crm_contract", "operation": "update", "recordId": "…" }
→ 200
  {
    "allowed": true,
    "layers": [
      { "layer": "owd_baseline", "verdict": "narrows",
        "record": { "outcome": "excluded",
                    "detail": "Private baseline admits only the owner; …" } },
      { "layer": "sharing", "verdict": "widens",
        "record": { "outcome": "excluded",
                    "rowFilter": { "owner_id": "…" },
                    "detail": "No ownership and no edit/full share grants write on this record." } },
      { "layer": "vama_bypass", "verdict": "widens",
        "detail": "View/Modify All Data bypass held via [admin_full_access]
                   — ownership and sharing checks are skipped" }
    ],
    "record": { "recordId": "…", "visible": false, "decidedBy": "sharing" }
  }
PATCH /api/v1/data/crm_contract/…   { "signed_by": "x" }
→ 403
  { "error": "FORBIDDEN: insufficient privileges to update crm_contract …",
    "code": "FORBIDDEN" }

同一条记录把 owner_id 填成该管理员之后,同样的 PATCH 回 200 —— 所以引擎的写入路径确实在执行记录级的 ownership 检查,而 vama_bypass 那一层写着这个检查被跳过了。

附带(同一原因、同一条记录):POST /api/v1/data/sys_attachment 因为门禁是 canEdit(parent),回 403 ATTACHMENT_PARENT_ACCESS,与 PATCH 一致、与 explain 不一致。

为什么值得单独修

  1. explain 正是管理员用来排查"为什么这条记录改不动"的工具。现在它对这个问题给出的答案,恰好是错的那一个:先说 allowed: true,再说"ownership and sharing checks are skipped",而实际写入被拒。
  2. payload 内部自己也不自洽:顶层 allowed: truerecord.visible: false / decidedBy: "sharing" 并存。消费方(Console、CLI os explain、任何 AI agent)读顶层还是读 record,会得到相反结论,而两者都没有被标注成"只回答对象级"或"只回答记录级"。
  3. 哪一侧是对的是一个产品决定,两种都说得通,所以更需要定下来而不是各自实现:
    • 若按 Salesforce 语义 Modify All Data 应当允许管理员编辑任意记录而不论归属,则写入路径漏掉了 VAMA 旁路,explain 是对的;
    • 若 private OWD 基线确实把无主行排除给所有人(含 VAMA 持有者),则**explain 的顶层 allowed** 不该在记录级判定为 excluded 时仍报 true,且 vama_bypass 那句 detail 是错的。

期望

  • 顶层 allowed 与引擎写入路径在给定 recordId 时必须同源,或明确 allowed 只是对象级判定、记录级以 record 为准,并让 vama_bypass 的 detail 反映记录级实际行为。
  • 无论选哪条,explaindata 写入对同一 (principal, record, operation) 三元组不得给出相反结论;建议补一个跨两条路径的一致性测试,用"VAMA 持有者 + private + 无主记录"这个组合作为用例。

参考:hotcrm 侧的对应修复是让种子记录不再无主(objectstack-ai/hotcrm#632),与本 issue 的判定无关 —— 无论哪一侧对,那些记录都应当有真实 owner。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions