Skip to content

fix(demo): claim the platform owner_id on seeded records, not just the app owner lookup (#622) - #632

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-622-contract-seed-ownership
Aug 2, 2026
Merged

fix(demo): claim the platform owner_id on seeded records, not just the app owner lookup (#622)#632
os-zhuang merged 1 commit into
mainfrom
claude/issue-622-contract-seed-ownership

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #622

Description

演示组织里每一条种子合同对所有人只读——管理员也不例外。打开一条 demo 合同改一个字段,PATCH 回 403;#602 刚开的附件面板在合同上传时回 403 ATTACHMENT_PARENT_ACCESS,看起来像"附件坏了",实际坏的是合同的所有权。

这些对象身上有两个所有权列:

归属 作用
owner 应用自己的 lookup(sys_user) 字段 "My Leads / My Deals / My Cases" 视图、所有面向 owner 的 notify、分析数据集的 owner 维度
owner_id ObjectQL 注入的平台所有权列 共享服务唯一读取的那一个:sharingModel: 'private' 下 OWD 基线只放行 owner_id 的持有者,share 只能从这个 owner 往外 widen

demo_bootstrap 过去只盖 owner。于是一条到达数据库时平台层无主的记录,扫过之后在人能看到的每个地方都像是被认领了(owner = dev admin),而在访问控制眼里仍然属于任何人都不是;更糟的是扫描自己的过滤条件随后变成 owner != null,这条记录再也不会被看第二眼——这是一个终态,产品内没有任何恢复路径

记录为什么会那样到达数据库:seed 写入跑在 { isSystem: true } 下,按 seeder 自己的契约这会关闭 organization_id / owner_id 的自动注入("seeds either declare those fields explicitly per record"),而本仓库的 seed 无法声明它——cel\os.user.id`在 seed 阶段不解析(启动日志对这些字段正好打Unknown variable: os`),这本来就是这个 flow 存在的理由。所以平台层所有权是这个 flow 的职责,没有别人的

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Related Issues

Fixes #622
Related to #602 (附件面板是这个缺陷浮出水面的地方,它本身是对的)

Changes Made

  • src/flows/demo-bootstrap.flow.ts — 每一次认领同时盖 两个 列;每个对象扫两遍,一遍捞缺 owner 的、一遍捞缺 owner_id 的,所以已经处在半认领终态的组织会在下一次扫描时自愈,不需要重置数据库。用两个单字段过滤而不是一个 $or{ field: null } 是这些 sweep 对真实 driver 用过的唯一过滤形状,健康组织下两遍都选不出任何行。
  • test/flow-scheduled.test.ts — 新增 demo_bootstrap 的 runtime 用例,断言的是结果而不是形状:凡是 demo_bootstrap 认领的对象,出 bootstrap 后都不能在平台层无主。对象清单从 flow 自身读出,所以以后新增一个被认领的对象当天就自动被覆盖。同时覆盖三种无主形态(两列皆空 / 只缺 owner / 只缺 owner_idSeeded contracts are ownerless at the platform level (owner_id null) — nobody, admin included, can edit one #622 状态)、已有真实 owner 的记录不得被抢、重复运行幂等、首个用户出现前不动任何行。
  • test/runtime-coverage.test.tsdemo_bootstrap 因此离开 PENDING_FLOWS
  • src/data/index.ts — 只加注释:说明平台层所有权归 sweep 管,不要在 seed 里长出 owner / owner_id 值来掩盖。

Testing

  • Unit tests pass (pnpm test) — Test Files 38 passed (38) · Tests 798 passed | 1 skipped (799)
  • Linting passes (pnpm lint) — 1 warning(s), 13 suggestion(s),均为既有项,无新增
  • Build succeeds (pnpm build) + pnpm validate + pnpm typecheck + pnpm hygiene 全绿
  • Manual testing completed(见下)
  • New tests added

新用例先做过反向验证:把 flow 临时改回只盖 owner 的旧行为,三个平台所有权断言如期失败(expected null to be 'usr_first'),确认它们不是空转。

真机验证(objectstack dev --seed-admin@objectstack/* 17.0.0-rc.1,全新 DB)——按 issue 里的原话复现再修复:

# 人工造出 issue 报告的那个状态:owner 已认领、owner_id 为空
PATCH /api/v1/data/crm_contract/Hj5iX02U7wUVsttH   → 403
  {"error":"FORBIDDEN: insufficient privileges to update crm_contract Hj5iX02U7wUVsttH"}
POST  /api/v1/data/sys_attachment {parent_object: crm_contract, …} → 403 ATTACHMENT_PARENT_ACCESS

# 修复后的 demo_bootstrap 扫过一遍
crm_contract  owner_id 4/4   owner 4/4     ← 自愈,无需重置数据库

PATCH /api/v1/data/crm_contract/Hj5iX02U7wUVsttH   → 200
POST  /api/v1/data/sys_attachment {parent_object: crm_contract, …} → 400 "File is required"
                                                    ← 已过 canEdit(parent) 门禁,剩下的是缺文件体
PATCH /api/v1/data/crm_account/…                   → 200
PATCH /api/v1/data/crm_quote/…                     → 200

八个被认领对象全量:crm_account 9/9 · crm_contact 9/9 · crm_opportunity 20/20 · crm_quote 5/5 · crm_case 38/38 · crm_task 7/7 · crm_lead 21/21 · crm_contract 4/4

Checklist

  • I have added a changeset.changeset/demo-bootstrap-platform-ownership.md
  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation(flow 与 seed 的内联说明;无面向用户的行为变化需要新文档页)
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Additional Notes

issue 里 security/explain 与写入路径互相矛盾的那一半:判定为平台侧,不在本 PR 内改。 在同一个 principal、同一条无主记录上实测:

POST /api/v1/security/explain {object: crm_contract, operation: update, recordId: …}
  → allowed: true
     layers[vama_bypass].detail = "View/Modify All Data bypass held via [admin_full_access]
                                   — ownership and sharing checks are skipped"
     record = { visible: false, decidedBy: "sharing" }

PATCH /api/v1/data/crm_contract/<同一条>   → 403 FORBIDDEN

vama_bypass 这一层自己写着"ownership and sharing checks are skipped",而引擎的写入路径并没有跳过——两者对同一个问题给出相反答案,且 explain 正是管理员会用来排查这个问题的工具。这段逻辑完全在 @objectstack/plugin-security 的 explain 与引擎写入路径之间,没有任何本仓库的元数据参与,本仓库也改不动它,因此另开 upstream issue 记录,不在这里动平台代码。本 PR 的修复与它无关:无论哪一侧是对的,种子记录都应该有一个真实的 owner。


Generated by Claude Code

…e app owner lookup

Every seeded contract was read-only for every user on a demo org, admin
included: PATCH answered 403 FORBIDDEN and the Attachments panel #602 just
enabled answered 403 ATTACHMENT_PARENT_ACCESS on upload — which reads as
"attachments are broken on contracts" when it is the contract's ownership that
is broken.

These objects carry two ownership columns. `owner` is the app's own
lookup(sys_user) and drives the "My …" views and every owner-addressed notify.
`owner_id` is the platform column ObjectQL injects, and it is the only one the
sharing service reads: under sharingModel:'private' the OWD baseline admits the
owner of `owner_id`, and a share can only WIDEN from an owner that is not there.

`demo_bootstrap` stamped `owner` alone. A row that reached the database without
a platform owner therefore came out of the sweep looking claimed everywhere a
person would check, while being owned by nobody for access control — and since
the sweep then selected on `owner != null`, it never looked at that row again.
The state was terminal, with no in-product recovery.

Rows reach the database that way because seed writes run under
{ isSystem: true }, which by the seeder's documented contract disables
auto-injection of organization_id / owner_id ("seeds either declare those fields
explicitly per record"), and these seeds cannot declare it — cel`os.user.id`
does not resolve at seed time (boot logs "Unknown variable: os" for exactly
these fields), which is why this flow exists at all. Platform ownership is this
flow's job and nothing else's.

- Every claim pass now stamps BOTH columns.
- Each object is swept twice, once for rows missing `owner` and once for rows
  missing `owner_id`, so an org already left half-claimed repairs itself on the
  next pass instead of needing a database reset. Two single-field filters rather
  than one $or: `{ field: null }` is the only filter shape these sweeps have
  ever used against the real driver.
- test/flow-scheduled.test.ts gains runtime cases that assert the OUTCOME — no
  object demo_bootstrap claims comes out of bootstrap ownerless at the platform
  level — over the object list read from the flow itself, so a newly claimed
  object is covered the day it is added. demo_bootstrap accordingly leaves
  PENDING_FLOWS in test/runtime-coverage.test.ts.

Verified on a fresh stack (objectstack dev --seed-admin, @objectstack/* 17.0.0-rc.1)
by recreating the reported state (owner set, owner_id null): PATCH 403 and
attachment 403 ATTACHMENT_PARENT_ACCESS before the sweep; after it, owner_id 4/4
and the same PATCH returns 200 with the attachment POST past the parent gate.

Fixes #622

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SS7C5SXpniKeCApxgARyf
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hotcrm Ignored Ignored Aug 2, 2026 1:52pm

Request Review

@github-actions github-actions Bot added ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces backend Server-side behaviour — hooks, flows, actions labels Aug 2, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 14:04
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit ffed923 Aug 2, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Server-side behaviour — hooks, flows, actions ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Seeded contracts are ownerless at the platform level (owner_id null) — nobody, admin included, can edit one

1 participant