Repository navigation
fix(lint): the controlledByParent owner arm prescribes ownerMayEdit only for an owner_id-keyed policy - #26658
Conversation
…for an owner_id-keyed policy (#26591) The opt-in admits only the stored owner_id, on a detail that carries it. A policy keyed on another user field (or on an owner_id the detail lacks) is flagged with or without the opt-in, with the owner_id remedy and never the opt-in. The runtime gate is unchanged. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5oX5j1RjbyiKQpdeiF
📓 Docs Drift CheckThis PR changes 2 package(s): 30 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 147 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 e0f48cb25abc3942950f1c981d910305d2c6dbb8 && git checkout e0f48cb25abc3942950f1c981d910305d2c6dbb8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a76f811e0eb29aaab055e52d541c7203394ccf5f 816501243d5805cb3dc933d5052cf926a8f793ca && git checkout -B drift-repro a76f811e0eb29aaab055e52d541c7203394ccf5f && git merge --no-ff 816501243d5805cb3dc933d5052cf926a8f793ca
node scripts/docs-audit/affected-docs.mjs --json a76f811e0eb29aaab055e52d541c7203394ccf5f
|
席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
Requested by Leehom · project thread
Fixes #26591
Clause-②: no (lint advice is not a contract face; no accepted metadata shape moves)
Metadata-protocol: none
维护者速读
os lint的security-controlled-by-parent-owner-policy-without-opt-in只在策略按owner_id == current_user.id写、且明细表确有owner_id列时,才建议加ownerMayEdit: true;按别的人员字段(如 hotclm 的owner)写的策略,不论加没加开关都会提示,并改为建议「把负责人放进owner_id」,不再建议这个开关。owner_id。以前 lint 对任何人员字段都建议加开关,hotclm 照做后 lint 不响了,但负责人照样被 403 拒绝,等于 lint 在误导。packages/lint/src/validate-security-posture.ts(新函数carriesCbpOwnerColumn、pushCbpCallerPolicyOffOwnerColumn和规则循环里的admittable一行);packages/lint/src/validate-security-posture-owner-may-edit.test.ts(新的 controlledByParent.ownerMayEdit admits only owner_id, but its lint arm prescribes it for any FIELD == current_user.id policy — an app whose accountable person is its own user field stays refused #26591 用例组);packages/lint/src/rule-explanations.ts。pnpm --filter @objectstack/lint exec vitest run src/validate-security-posture-owner-may-edit.test.ts,预期全部通过,其中「an other-field policy with the opt-in: still flagged」一条证明加了开关的owner策略仍会被提示。What changes
sharing.controlledByParent.ownerMayEdit: trueadmits one caller past acontrolled_by_parentdetail's master-write gate. That caller is the user in the stored row'sowner_id(assertControlledByParentWrite), and only on a detail whose registered schema carries that column (resolveCbpRelationresolves the opt-infalseotherwise). The lint arm prescribed the opt-in for anyFIELD == current_user.idupdate policy. This PR narrows the advice to the shape the runtime honours. The runtime gate does not change.owner_id, no opt-inowner_id(alone or beside another field)owner_id, opted inowner_id(alone or beside another field)owner_id, no opt-inowner)owner_id, and the hint says to key the accountable person onowner_id. The hint never names the opt-inowner_id, opted inowner)owner_id(ownership: 'business_unit' | 'org' | 'none', no ownowner_idfield)owner_idincludedowner_idcolumn. The hint never names the opt-inpackages/lint/src/validate-security-posture.ts.carriesCbpOwnerColumn(obj)answers whether the detail carriesowner_idthe way the runtime does. It reads the spec's injection plan (injectedColumnsFor,resolveInjectedSystemColumns) or a field the author declared under that name. The arm keeps{ optedIn, ownerColumn }per resolving detail instead of a set of details without the opt-in. A predicate whose keyed fields includeowner_idon a detail that carries it keeps today's behaviour. Any other caller-keyed predicate goes to the new push site,pushCbpCallerPolicyOffOwnerColumn, under the same rule id and the samewarningtier.callerIdentityKeyedFieldskeeps its shape: it already returns field names, and this arm is its only caller.packages/lint/src/rule-explanations.ts. Theos explainentry now says that the opt-in reads only the storedowner_id, only on a detail that carries it. It gives a separate remedy for each case.packages/spec/liveness/object.json. TheownerMayEditevidence string described the arm as flagging "an owner-keyed update policy on a detail that has not declaredownerMayEdit". After the narrowing that undersells it, so it now describes the narrowed arm, and itsverifiedAtmoves to 2026-10-27. This is an evidence string, not a contract change. Theobject.zod.tsdocblock near :3089 ("so the gate and the lint ask one question one way") is still true, because both read the opt-in throughcontrolledByParentOwnerMayEdit, so it is unchanged.@objectstack/lintpatch, plus@objectstack/specpatch becauseliveness/ships in spec'sfiles[].Tests
validate-security-posture-owner-may-edit.test.tsadds a#26591group on hotclm's shape (acontrolled_by_parentdetail with its ownowneruser field andusing: owner == current_user.id):owner_idpolicy without the opt-in: flagged, opt-in prescribed.owner_idpolicy with it: silent.ownerpolicy without the opt-in: flagged with theowner_idremedy. The hint does not containownerMayEdit.ownerpolicy with the opt-in: still flagged.owner || owner_idpolicy: prescribed without the opt-in, silent with it.ownership: org | none | business_unit, with and without the opt-in: flagged with the owner-column remedy and no opt-in.owner_idfield onownership: 'none': prescribed, then silent.ObjectStackSchema.validate-security-posture.test.tsmeta-tests:detail(the arm's own fold) andsharing(the opt-in's spelling inside the verdict string).cbpDetailsreplacescbpDetailsWithoutOptInin the plumbing list.findings.pushcount goes from 18 to 19.Acceptance notes
update/allpolicy that keys a field oncurrent_user.id, on a detail that has not opted in, "is flagged with the opt-in as the fix". After this PR that holds only for a policy keyed onowner_id. The order keepsdocs/adr/**out of this PR. The report names the line for the seat.validate-security-posture.ts, has merged since this branch was cut. It edits the OWD-alias docblocks near :101–:170, andgit merge-tree HEAD origin/mainis clean. The open feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 and feat(spec): a report declares its period (dateRange) and the export of its table (exportOptions) #26628 edit other entries ofrule-explanations.ts. No hunk is shared.Verification
All readings below are on head
816501243.Lint package: tests, typecheck, scoped eslint
pnpm --filter @objectstack/lint test:Test Files 166 passed (166)·Tests 7395 passed | 5 skipped (7400). This run was on the tree before the corpus row and thedetailNametypecheck fix. The two touched suites re-ran on the final tree:Test Files 2 passed (2)·Tests 210 passed (210).pnpm --filter @objectstack/lint typecheck(tsc --noEmit && pnpm check:test-typecheck): exit 0.eslint --no-inline-configon the four touched lint files: exit 0.pnpm --filter @objectstack/spec exec vitest run(for the liveness string):Test Files 810 passed (810)·Tests 23353 passed | 1 todo (23354)·VERDICT command-exit 0.Ablation (one, of the narrowing)
node scripts/ablation-replace.mjsreplacedconst admittable = detail.ownerColumn && fields.includes(CBP_OWNER_COLUMN);withconst admittable = true || (…); // ABL26591. That restores the old behaviour: every caller-keyed predicate takes the prescriptive path, and an opted-in detail is silent.f52007d93fafto3b0a2d4cd9cb.Tests 5 failed | 205 passed (210). The 5 failures are both other-field cases (without and with the opt-in) and the three no-owner_idcases (org / none / business_unit). Everyowner_idcase stayed green.f52007d93faf), andgit diff HEADis empty../validate-security-posture.jsfromsrc/, so no build sat between the mutation and the run.Derived gates: 65 / 65, reconciled
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 65 commands. All 65 ran, every exit code was captured before any pipe, and all 65 exited 0.--ran:✓ dispatch-gates --ran: 65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED.check-plugin-teardown-shape.mjs --self-test, aftergit fetch origin 621a4876(the self-test's positive control).check:docs-transcript-drift,check:lean-entry-closureandcheck:dual-build-cjs-loads, afterpnpm --filter '@objectstack/lint...' --filter '@objectstack/objectql...' build.!, andClause-②has no(narrowing)arm.🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5oX5j1RjbyiKQpdeiF
Generated by Claude Code