fix(demo): claim the platform owner_id on seeded records, not just the app owner lookup (#622) - #632
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
os-zhuang
marked this pull request as ready for review
August 2, 2026 14:04
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #622
Description
演示组织里每一条种子合同对所有人只读——管理员也不例外。打开一条 demo 合同改一个字段,
PATCH回 403;#602 刚开的附件面板在合同上传时回 403ATTACHMENT_PARENT_ACCESS,看起来像"附件坏了",实际坏的是合同的所有权。这些对象身上有两个所有权列:
ownerlookup(sys_user)字段notify、分析数据集的 owner 维度owner_idsharingModel: 'private'下 OWD 基线只放行owner_id的持有者,share 只能从这个 owner 往外 widendemo_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
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_id即 Seeded contracts are ownerless at the platform level (owner_idnull) — nobody, admin included, can edit one #622 状态)、已有真实 owner 的记录不得被抢、重复运行幂等、首个用户出现前不动任何行。test/runtime-coverage.test.ts—demo_bootstrap因此离开PENDING_FLOWS。src/data/index.ts— 只加注释:说明平台层所有权归 sweep 管,不要在 seed 里长出owner/owner_id值来掩盖。Testing
pnpm test) —Test Files 38 passed (38) · Tests 798 passed | 1 skipped (799)pnpm lint) —1 warning(s), 13 suggestion(s),均为既有项,无新增pnpm build) +pnpm validate+pnpm typecheck+pnpm hygiene全绿新用例先做过反向验证:把 flow 临时改回只盖
owner的旧行为,三个平台所有权断言如期失败(expected null to be 'usr_first'),确认它们不是空转。真机验证(
objectstack dev --seed-admin,@objectstack/*17.0.0-rc.1,全新 DB)——按 issue 里的原话复现再修复:八个被认领对象全量:
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
.changeset/demo-bootstrap-platform-ownership.mdAdditional Notes
issue 里
security/explain与写入路径互相矛盾的那一半:判定为平台侧,不在本 PR 内改。 在同一个 principal、同一条无主记录上实测:vama_bypass这一层自己写着"ownership and sharing checks are skipped",而引擎的写入路径并没有跳过——两者对同一个问题给出相反答案,且explain正是管理员会用来排查这个问题的工具。这段逻辑完全在@objectstack/plugin-security的 explain 与引擎写入路径之间,没有任何本仓库的元数据参与,本仓库也改不动它,因此另开 upstream issue 记录,不在这里动平台代码。本 PR 的修复与它无关:无论哪一侧是对的,种子记录都应该有一个真实的 owner。Generated by Claude Code