Repository navigation
feat(spec,plugin-security,lint): sharing.controlledByParent.ownerMayEdit — a detail row's owner updates it with read on the master - #26005
Conversation
…dit — a detail row's owner updates it with read on the master WIP: spec key with parse refusal off a detail, the owner leg of the controlled-by-parent write gate, the lint arm for an owner-keyed update policy on a detail that has not opted in, the ADR-0055 §3 amendment and the liveness ledger rows. Tests, generated spec artefacts and the changeset follow. Claude-Session: https://claude.ai/code/session_01XT1qCAHDq5w7zUftxENNeE Co-authored-by: Claude <noreply@anthropic.com>
…MayEdit — the opt-in parse, the owner leg of the write gate, the lint arm; regenerated spec artefacts; changeset - spec: the parse pins (the opt-in only on a controlled_by_parent detail, the reader fails closed); the two compose-stacks pins gain `sharing` as the config object it is; api-surface, export-origins, docs, strictness-ledger counts and liveness counts regenerated from the built dist. - plugin-security: the owner-may-edit rig over the real SecurityPlugin and ObjectQL (SqlDriver, in memory): the owner who reads the master is admitted and the row changes; a non-owner, an unowned row, an owner who cannot read the master, a delete, an insert, a predicate update, a delegated update and a body re-pointing the master are refused by the master gate; a detail without the opt-in refuses its owner as before; an opt-in on a detail with no owner column is reported once; checkControlledByParentWrite answers allow for the owner and deny otherwise. - lint: the owner-policy arm's cases; the #5017 meta-guard table gains the arm's reads (ps.rowLevelSecurity, a policy receiver, the CEL AST nodes); the rule id constant on one line for the barrel pin. Claude-Session: https://claude.ai/code/session_01XT1qCAHDq5w7zUftxENNeE Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 37 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 149 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 19fb3477970d361f173e746076da7e1191e77fb1 && git checkout 19fb3477970d361f173e746076da7e1191e77fb1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0a6948210cc96a95cc568b30193895e157968c01 07f1f087aabd8c394f99dc1b7197bcc167bab2ee && git checkout -B drift-repro 0a6948210cc96a95cc568b30193895e157968c01 && git merge --no-ff 07f1f087aabd8c394f99dc1b7197bcc167bab2ee
node scripts/docs-audit/affected-docs.mjs --json 0a6948210cc96a95cc568b30193895e157968c01
|
…nd author its translation leaves CI on dece711 was red in three places, every one a consequence of the new `ObjectSchema.sharing` block (#25760) that the first round did not carry: - metadata-core's OBJECT_REFERENCE_POSITIONS did not classify `sharing`, so its "classify each new ObjectSchema key" guard went red (Test Core 8/8). Classified as `keep`: a block of booleans that names no field. - The platform-objects translation bundles had drifted from the object form (`pnpm check:i18n`, Type Check · consumer gates). Regenerated, and the six new leaves (`sharing`, `sharing.controlledByParent` and `sharing.controlledByParent.ownerMayEdit`, a label and a help text each) authored in zh-CN, ja-JP and es-ES, so their provenance rows drop as the #19403 regime asks of every metadata-form leaf. The two leaf-count pins of that regime move with the form: 107 → 113 collapsed-section leaves (`advanced` 96 → 102) and 679 → 682 row labels authored in all three locales. Claude-Session: https://claude.ai/code/session_01XT1qCAHDq5w7zUftxENNeE Co-authored-by: Claude <noreply@anthropic.com>
Resolves the one conflict: the lifecycle echo-decisions positive-control pin reads 683 (main's 680 for the dataset duration measure, plus the three sharing row labels of #25760). Claude-Session: https://claude.ai/code/session_01XT1qCAHDq5w7zUftxENNeE Co-authored-by: Claude <noreply@anthropic.com>
Discharges the regeneration the merge commit deferred: content/docs/references/data/object.mdx, regenerated with gen:schema and gen:docs from the merged tree (main's dataset-form additions beside the sharing opt-in of #25760). check:generated and check:docs exit 0; the translation bundles re-extracted byte-identical. Claude-Session: https://claude.ai/code/session_01XT1qCAHDq5w7zUftxENNeE Co-authored-by: Claude <noreply@anthropic.com>
…vel properties The /meta/types control pins the served property count per type. `object` moved 47 → 48 when it gained `sharing` (#25760, the per-object sharing opt-ins of the ADR-0055 §3 amendment); the pin and its history line move with it. Test Core (7/8) on e0883a0: 1 failed of 21557 before the move, 266 files green after. Claude-Session: https://claude.ai/code/session_01XT1qCAHDq5w7zUftxENNeE Co-authored-by: Claude <noreply@anthropic.com>
The BOUNDARY test in auth-gate.test.ts walks about 133k paths × 6 methods through the allowlist predicate. It takes about 1.3 s on an idle box and 5.4 s on a Test Core runner that carries core beside spec's test and test:repo tasks (PR #26005, shard 5, three runs in a row), where vitest's 5 s default refused a healthy enumeration. The explicit timeout is a ceiling on load, not a budget: no skip, no quarantine, no change to what the test asserts. The file is outside #25760's claim surface; the breach is stated in the os-dev report. Claude-Session: https://claude.ai/code/session_01XT1qCAHDq5w7zUftxENNeE Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #25760 (body and all seven comments, ruling 6398566164 letter 1, the premise check 6409904490, the amended claim 6410093517, os-dev-reports 6411992806 / 6414737027 / 6417667323), PR #26005 (body, 35-file list, net diff against main), and the 48 check-runs on the head: every non-skipped run is ① Derived judgmentsEach accept-set or public-surface change the diff implies, with its verdict.
Nothing in the diff widens beyond the ruling, and nothing narrows an existing accept set. ② Semver levelClause-②: yes (widening)
③ Boundary flagsThe dev's current os-dev-report is 6417667323;
No flag is left open. The PR stays a draft awaiting the Tier H approval the ADR amendment owes; this record is the contract-tier review of record for the head above and merges nothing. Implemented-by: VERDICT: PASS |
维护者速读改了什么这个 PR 给"访问随主记录"(
为什么改依据裁决 6398566164 第 1 条。这对应 Salesforce 里"活动随父记录控制,但所有者能改自己的活动"的形态:销售能看客户、不能改客户,但要能改自己名下挂在客户上的任务。原来平台表达不了这一点;作者自己写行级策略也无效,因为行级策略只会收窄,不会放宽主记录门。 风险与代价(含回滚)
席位意见建议批准。契约审查 PASS(6417851199),席位核对见下方。这是纯放宽:没有任何现有写法被收窄,新键从第一版起就是封闭结构,CI 48 项 0 失败。 只有一个不阻塞的小瑕疵:表单上 你要做的审阅 席位核对ACCEPT · domain:spec seat 2 · 2026-10-26T14:00Z · head
|
|
Seat 2 · landing after an authorized approval: os-zhuang APPROVED (review 5654114202) on head |
…a direct grant (#24757) controlled-by-parent-owner-may-edit.test.ts landed on main (#26005) after this branch's base. Its delegator reached omd_requester through a sys_user_position row named after the set, the fold this PR retires, so the delegated-update pin refused at the on-behalf-of check instead of the master gate. The row is now a sys_user_permission_set grant, the same rebinding the chain-middle-master suite received; every expectation is unchanged. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8LYP8UCJURxbFWZqnywyD
Requested by Steedos · project thread
Fixes #25760
Clause-②: yes (widening) — a detail row's owner now passes the master-detail write gate with READ on the master, on an object that declares the opt-in; everyone else, and every other shape of write, still needs master EDIT. The default is unchanged.
Metadata-protocol: added (
ObjectSchema.sharing.controlledByParent.ownerMayEdit, a per-object opt-in read only undersharingModel: 'controlled_by_parent')Ruling-ref: 6398566164 (letter 1)
What changes
ADR-0055 §3 had one rule for every write of a
controlled_by_parentdetail: EDIT access to the master record. Salesforce's Activities under OWD = Controlled by Parent let the owner of an activity edit it while merely reading its account; the platform could not express that, and an authoredowner_id == current_user.idupdate policy on the detail could not either, because the detail's own policies compose AND with the master gate (step 2.7 then 2.8), so such a policy only narrowed the master's editors.packages/spec/src/data/object.zod.ts,object.form.ts, liveness ledger, generated authorable surface).ObjectSchemagainssharing: { controlledByParent: { ownerMayEdit: boolean } }, both blocks closed (strictObject, with guidance for the near-miss spellings).ownerMayEdit: trueon an object that is not asharingModel: 'controlled_by_parent'detail is refused at parse, onecustomissue atsharing.controlledByParent.ownerMayEdit, with the prescription. The readercontrolledByParentOwnerMayEdit(schema)(@objectstack/spec/data) answerstrueonly for the parsed literal and fails closed on every other shape.packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts,security-plugin-cbp-relation.ts,security-plugin.ts).CbpRelationcarriesownerMayEdit, resolved on the same registered schema the relation came from, and only when that schema carries theowner_idcolumn (the registry's injected per-record owner underownership: 'user', the default). InassertControlledByParentWrite, before the §3 chain walk, a by-idupdateon an opted-in detail by the caller whoseuserIdequals the stored row'sowner_idis admitted when the caller can READ the master row, asked of the read door under the caller's own context (readRowByIdthroughreadUnlessRefused, the same probe the by-id pre-image gate uses; a hidden row and a declared refusal are one answer, a store fault propagates). A body that re-points the master reference still asks EDIT on the master it names.ISecurityService.checkControlledByParentWriteanswers the same way, since it runs the same check.packages/lint/src/validate-security-posture.ts,rule-explanations.ts,index.ts). New advisory armsecurity-controlled-by-parent-owner-policy-without-opt-in(warning): anupdateorallrow-level policy whoseusingorcheckkeys a field on the caller (FIELD == current_user.id, either operand order, the SQL=spelling the compiler bridges,record.FIELDtoo) on acontrolled_by_parentdetail that has not declared the opt-in. Judged by shape on the canonical CEL parse; a predicate that does not parse is the parse rule's.os explaincarries the entry.warning, noterror, becausevalidateSecurityPosturealso runs at the runtime publish doors, where anerrorrefuses the write, and a narrowing can be meant.Scope:
updateonly — stated as the ruling asksThe ruling names
update. This PR admits the owner on a by-id update and nothing else:multi: true): judges every master of the rows it selects and has no single row whose owner could be asked; unchanged.owner_idcolumn (ownershipother than'user') is inert and reported once per object atwarnby the relation resolver (the parse cannot refuse that half: the injected-column plan lives in a module that importsobject.zod.ts).Insert and delete can be added later under the same key if the maintainer rules so; nothing here forecloses it.
维护者速读(草稿)
改了什么
给
controlled_by_parent(访问随主记录)的从对象加了一个按对象声明的开关sharing.controlledByParent.ownerMayEdit: true。打开后,从记录的所有者(owner_id是自己)只要能读主记录,就能按 id 修改自己这条从记录;其他人、以及新增/删除/按条件批量改/代办写入,仍然要求对主记录有编辑权。不开这个开关,一切照旧。os lint新增一条提示:在没开开关的从对象上写了owner_id == current_user.id的更新策略,会提醒作者这条策略永远放不进所有者。ADR-0055 §3 同步修订。为什么改
裁决 6398566164(第 1 条)。对应 Salesforce 的"活动随父记录控制,但活动所有者始终能编辑自己的活动"这一常见业务形态:销售能看客户、不能改客户,但要能改自己名下挂在客户上的任务/义务。原来平台表达不了,作者自己写行级策略也无效(策略只会收窄,不会放宽主记录门)。
风险与代价(含回滚)
owner_id、调用者能读主记录。默认值不变,存量对象不受影响。sys_-级读探针(读主记录)只在上述三件事成立时才发生。席位意见
(留空)
你要做的
docs/adr/0055-master-detail-controlled-by-parent.md的修订段落(受管面,Tier H),确认"只放宽按 id 更新、不含新增/删除"的范围与裁决一致。Acceptance notes
owner_id(the sharing service'sOWNER_FIELD, the "My records" scope), notcreated_by: the ruling says "the detail row's own owner", and the platform has one owner concept.updategrant and its own row-level policies (step 2.7) run before the leg, unchanged; an authoredowner_id == current_user.idpolicy on an opted-in detail now decides as authored.??别名读法(#5009 建议 3 的核对结果) #5017) gained the new reads (ps.rowLevelSecurity, apolicyreceiver againstPermissionSetSchema.rowLevelSecurity[], the CEL AST receivers excused by name) and the push-site count moved 17 → 18 with a corpus fixture.content/docs/permissions/*.mdxdo not yet mention the opt-in (the generated spec docs do, through the key's describe); the owner-column half of the opt-in is reported at runtime rather than refused at parse (documented in the ADR amendment and the gate).Verification
Head
dece7114. Local verification, all under the shared verify lock where it is a build or a suite (the three affected packages, their dependency closure built first withpnpm --filter '@objectstack/plugin-security^...' --filter '@objectstack/lint^...' build, exit 0):pnpm --filter @objectstack/spec test— Test Files 722 passed | 1 skipped (723), Tests 21899 passed | 1 skipped | 1 todo.typecheckexit 0.pnpm --filter @objectstack/plugin-security test— Test Files 224 passed (224), Tests 4700 passed | 45 skipped.typecheckexit 0.pnpm --filter @objectstack/lint test— Test Files 164 passed (164), Tests 7363 passed.typecheckexit 0.rel.ownerMayEditshort-circuited to false, then the file restored from git): 4 of 17 rig pins red — the owner's admitted update, the delegated case's control, the body naming the row's own contract, andcheckControlledByParentWriteansweringallow; the 13 refusal pins stayed green, as they must (the leg adds no refusal of its own).pnpm --filter @objectstack/spec check:generatedexit 0 after regenerating the five artefacts it proved stale (api-surface, export-origins, docs, strictness-ledger counts, liveness counts);check:liveness,check:api-surface,check:authorable-surface,check:docsexit 0.node scripts/pm/dispatch-gates.mjs --commands, 124 families for this change set): 122 run, every one exit 0, each exit code captured before any pipe;--ranreconciliation exit 0 with 0 UNRUN. 2 NOT MEASURED locally, both a recorded exit 3 (prerequisite: the whole farm'sdist/, which this seat did not build):pnpm check:dual-build-cjs-loads,pnpm check:i18n— CI's check-runs are the verdict for those two.Four pins outside the new suites moved with this diff, each a one-line or typed change, none a behaviour change: the two compose-stacks pins list
sharingas the config object it is; the lint barrel-export pin wants the rule id on one line; the #5017 meta-guard table gained the arm's reads.Generated by Claude Code