Repository navigation
fix(plugin-sharing)!: a sharing rule on an object without record sharing is refused at save - #30953
Conversation
…rd sharing A sys_sharing_rule on an object no sharing gate consults saved 201 and then shared nothing. Both save doors (the sys_sharing_rule hook guard and defineRule) now refuse it with SHARING_NOT_ENABLED 422, reading the reconcile's own D7 inert-grant verdict. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyFB75QwwCDu3S5GNZ
…out record sharing, and its changeset Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyFB75QwwCDu3S5GNZ
… object-enforcement guard Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyFB75QwwCDu3S5GNZ
📓 Docs Drift CheckThis PR changes 1 package(s): 38 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 11 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 0aa98ca78948af94ead17e73c136b10142c939b2 && git checkout 0aa98ca78948af94ead17e73c136b10142c939b2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 256a4b725116859f891160ad71b21c2e092d1fb0 dc804fbb17e230449f01be794255263eea9ffd06 && git checkout -B drift-repro 256a4b725116859f891160ad71b21c2e092d1fb0 && git merge --no-ff dc804fbb17e230449f01be794255263eea9ffd06
node scripts/docs-audit/affected-docs.mjs --json 256a4b725116859f891160ad71b21c2e092d1fb0
|
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 Contract review for card #30621 · narrowing face ( Contract reviewServed-tier: Inputs: card #30621 (body, triage 6685173622, claim 6685311068 and its re-post 6685434298, os-dev-report 6686849997), PR #30953 (body, file list, net diff against Check-runs on the head (read 2026-11-06, every run concluded): 38 ① Derived judgmentsAccept-set changes the diff implies, each judged against the cited code:
② Semver level
Clause-②: no (narrowing) — adopted. ③ Boundary flags
④ Design judgmentThe narrowing against the published spec on Probes:
Findings:
No must-fix. Implemented-by: VERDICT: PASS |
…l, and reconcile the sharing-rules page with D7's real owner_id Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyFB75QwwCDu3S5GNZ
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 Delta contract review for card #30621 · narrowing face · 6a24dc2..dc804fb · read-only reviewer at the contract-review tier · 2026-11-06T15:10Z Delta contract review for card #30621 · PR #30953 · narrowing face ( Contract reviewServed-tier: This is a delta review: it re-judges the one commit pushed since the prior PASS record (dc804fb, "name the federated-anchor remedy before the model, and reconcile the sharing-rules page with D7's real owner_id"), read through Check-runs on the head (read 2026-11-06, every run concluded after the ① Derived judgments
② Semver levelUnchanged from the prior record: Clause-②: no (narrowing) — adopted. ③ Boundary flags
④ Design judgmentThe narrowing against the published spec on Probes:
Findings:
No should-fix, no must-fix. The prior record's one should-fix is closed. Implemented-by: VERDICT: PASS |
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 Maintainer's per-PR word (BREAKING) · 2026-11-06T15:10Z Elon chose 「同意落地」 on the seat's decision card in the server seat 4 thread, 2026-11-06T14:58Z, for this PR (card #30621: a sharing rule on an object without record sharing is refused at save with |
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 维护者速读
席位意见:ACCEPT(#30621)。 席位核对Head-sha:
|
Requested by Elon · project thread
domain:server#4 · os-elon-musk · session_016WV4cyFB75QwwCDu3S5GNZ
Fixes #30621
Clause-②: no (narrowing)
Metadata-protocol: none
维护者速读
sys_session、sys_account、sys_api_key)保存共享规则,现在两个保存入口(Setup 表单走的/data门,和POST /api/v1/sharing/rules)都会拒绝,答422 SHARING_NOT_ENABLED,句子点名对象并说明怎样为它开启记录共享;改对象的编辑同样拒绝。201)、在 Setup 里显示为启用,但调和时每条授权都被拒、一条sys_record_share也不写,管理员以为生效了,其实什么都没共享。packages/plugins/plugin-sharing/src/sharing-service.ts的sharingRuleObjectRefusal(判定与assertNotInertGrant同源);②rule-hooks.ts的bindRuleObjectEnforcementGuard(更新只在换对象时检查);③sharing-rule-service.ts的defineRule新增检查。pnpm --filter @objectstack/plugin-sharing exec vitest run src/sharing-rule-object-enforcement.test.ts,预期 9 个测试全绿。What changes
Before, an administrator could save a sharing rule (
sys_sharing_rule) on an object no sharing gate consults. The save answered201, the rule showed as active in Setup, and its reconcile then met the ADR-0111 D7 inert-grant guard (SharingService.assertNotInertGrant,SHARING_NOT_ENABLED) and wrote nosys_record_share, so the recipients saw nothing more than before.Now both save doors refuse such a rule and save nothing:
SharingService.sharingRuleObjectRefusal(object)returns the refusal sentence, ornull. It reads the same caller-independent verdict the grant pre-flight throws on (the new privateobjectInertReason, whichassertNotInertGrantnow calls too), so the save door and the reconcile cannot disagree about which objects are under enforcement. The sentence names the object, gives the D7 reason, and adds the remedy for that arm: a private or read-onlysharingModelplus anowner_idfield; the master object for acontrolled_by_parentdetail; a real remoteowner_idfor a federated phantom-anchor object; or "cannot be turned on" for a bypass-listed platform object. A public object that has an owner column stays admitted, because the grant service there still grants an external member of the record's organization (sharing: a share or sharing rule to an external member is refused as inert on a public_read_write or model-less system object, though the external OWD floor would consult it #25549)./datadoor (Setup's New and edit forms, scripts, imports):bindRuleObjectEnforcementGuardbindsbeforeInsert/beforeUpdateonsys_sharing_rule, bound insharing-plugin.tsbeside the criteria guard. An insert is checked; an update only when it moves the rule onto another object (object_namesupplied and different fromctx.previous), so an administrator can still switch off a rule saved before this change. System writes pass, as with the declared-name guard.POST /api/v1/sharing/rules(SharingRuleService.defineRule): the same check for non-system callers, before the insert or upsert; an upsert that keeps a stored rule on its object is not a move.The refusal is
SharingRuleObjectNotEnforcedError(rule-criteria.ts) withcode: 'SHARING_NOT_ENABLED'andstatus: 422: an existing code in the@objectstack/plugin-sharingledger row, and the pair the per-record share routes andPOST .../evaluatealready answer for the same verdict. No new code, nopackages/specchange. Both doors answer a declaredcode+statusat its own status, the same mechanismDeclaredSharingRuleNameConflictError(409) uses on these doors.Docs:
content/docs/permissions/sharing-rules.mdxandcontent/docs/kernel/runtime-services/sharing-service.mdxsay the rule is refused at save. One changeset,@objectstack/plugin-sharing: patch, with the BREAKING banner and one ADR-0087 marker.Tests
packages/plugins/plugin-sharing/src/sharing-rule-object-enforcement.test.ts, realObjectQL+SqlDriver(better-sqlite3:memory:), so the hook runs on the engine's own write path:/datadoor: an insert on an owner-less object (thesys_sessionshape) is refused withSHARING_NOT_ENABLED/422, the sentence names the object,sharingModelandowner_id, and nothing is saved; CONTROL: an insert on an enforced object saves; CONTROL: a public object with an owner column is admitted; an update that moves a rule onto a non-enforced object is refused and the row keeps its object; an update that leaves a legacy rule on its object passes.defineRule: a rule on an owner-less object is refused (code+status) and nothing is saved; CONTROL: a rule on an enforced object saves; an upsert that moves an existing rule onto a non-enforced object is refused.public_read_writefederated object (external: { remoteName }) whoseowner_idis the registry's injected anchor is refused at the/datadoor with422 SHARING_NOT_ENABLED, nothing is saved, and the sentence says to declare a real remoteowner_idcolumn, not thesharingModel: 'private'remedy. Before the fix:1 failed | 8 passed (9); after:9 passed (9).Readings (final head dc804fb: plugin-sharing `Test Files 68 passed`, `Tests 1445 passed`, typecheck exit 0, derived gates 96/96 exit 0 with `--ran` 0 NOT-MEASURED; full dogfood not re-run, as the delta is one remedy sentence, its pin and docs. Earlier readings below are on head 6a24dc2; the code is unchanged since 1390d36, the last commit is the census row on a docs page)
Tests 8 failed (8)(bindRuleObjectEnforcementGuard is not a function). After:Tests 8 passed (8), lockVERDICT command-exit 0.scripts/ablation-replace.mjs(if (!reason) return null;→if (reason || !reason) return null;insharingRuleObjectRefusal):Tests 4 failed | 4 passed (8). The 4 red are exactly the 4 refusal pins; the 4 controls stay green. Restore proven: blob after restore == HEAD blob2afbcaad0cda,git diff HEADempty. The suite importssrc/by relative path, so no dist rebuild is on the path.@objectstack/plugin-sharingpnpm test:Test Files 68 passed (68),Tests 1444 passed (1444);pnpm typecheck(incl.check:test-typecheck) exit 0. One lock run,VERDICT command-exit 0.pnpm --filter @objectstack/dogfood test, no shard, after a whole-treeturbo run build --concurrency=2, 119/119 tasks):Test Files 425 passed | 3 skipped (428),Tests 3543 passed | 41 skipped (3584),VERDICT command-exit 0. Measured on 1390d36; 6a24dc2 adds only the census row incontent/docs/permissions/system-context.mdx.Acceptance notes
@objectstack/platform-objectsexports into anObjectQLregistry and askingsharingRuleObjectRefusal: 58 of 58 are refused, among themsys_session,sys_accountandsys_api_key(noowner_id, no declared model), the three the card names. A custom object with anowner_idand a private model is admitted (the CONTROL pins).SHARING_NOT_ENABLEDis already in the@objectstack/plugin-sharingrow ofERROR_CODE_LEDGER; 422 is its published status on the share routes andPOST .../evaluate. A scratch probe (not committed) of@objectstack/rest'smapDataErrorandclassifiedRefusalAnswerwith this error answered422/SHARING_NOT_ENABLEDwith the sentence aserroron both the/datadoor and the sharing-rule routes.sys_sharing_rulerow (fixture-18.0,fixture-18.0.0:upgrade_harness_red_projectsonshowcase_project, enforced;fixture-18.0.0-next.5: none). DeclaredsharingRulesare not stored rows and are already held byvalidate-sharing-rule-enforceability's anchor findings. Stored rules are not migrated; one already saved keeps sharing nothing and can still be switched off.assertNotInertGrantwould throw: the privateobjectInertReasonis now the one reading both call. A public object with an owner column is admitted on purpose (sharing: a share or sharing rule to an external member is refused as inert on a public_read_write or model-less system object, though the external OWD floor would consult it #25549, external recipients still receive grants). An object the engine has no schema for is admitted, as the grant pre-flight's inertness half admits it.bindDeclaredRuleNameGuard: the platform's own writers (the ownership ceremony, migrations) and the existing system-caller pins (system-caller-inert-grant.test.ts, which defines such rules to pin the reconcile refusal) are unchanged. The system-context census row 40 now namesbindRuleObjectEnforcementGuardanddefineRule(register regenerated with--write).Verification
Derived gates (head 6a24dc2)
node scripts/pm/dispatch-gates.mjs --commands: 96 families over 10 changed paths. All 96 ran on 6a24dc2, each:: exit 0.node scripts/pm/dispatch-gates.mjs --ran FILE:✓ dispatch-gates --ran: 96 derived famil(ies) accounted for — 96 run, 0 NOT-MEASURED.pnpm check:system-context-censusred ([read-without-a-row]forbindRuleObjectEnforcementGuardanddefineRule); fixed in 6a24dc2 by keying row 40, then--write; re-run green.Test Core, type-check lanes) are CI's; this PR cites them from its check-runs.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cyFB75QwwCDu3S5GNZ