Repository navigation
fix(lint): judge an object's defaultRowScope in the RLS predicate and org-axis rules - #24511
Conversation
… org-axis rules Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…fault-row-scope-lint
…fault-row-scope-lint
…cope predicate Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…scribe Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
📓 Docs Drift CheckThis PR changes 2 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 143 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 45b7a613c3225fe1f2820e5b8e38d63d10b3ad52 && git checkout 45b7a613c3225fe1f2820e5b8e38d63d10b3ad52
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6b42b6bc8a11b58cfaa5054e9edbb86b1898f2e7 0db86b5479f439ce40a92e630bb16892308aee83 && git checkout -B drift-repro 6b42b6bc8a11b58cfaa5054e9edbb86b1898f2e7 && git merge --no-ff 0db86b5479f439ce40a92e630bb16892308aee83
node scripts/docs-audit/affected-docs.mjs --json 6b42b6bc8a11b58cfaa5054e9edbb86b1898f2e7
|
Review: ACCEPT · PR #24511 @
|
Fixes #24452
Clause-②: no
What changed
@objectstack/lintnow judges an object's default row scope (ObjectSchema.defaultRowScope) the way it judges a permission set's row policy.validateRlsPredicateEnforceability(packages/lint/src/validate-rls-predicate-enforceability.ts). The per-policy body of the walker is moved into one function,judgePolicy(graph, policy, site, options). The walker calls it for eachpermissions[].rowLevelSecurity[]entry, as before, and for eachobjects[].defaultRowScope, with the declaring object as the target. There is no second lowering implementation: the default goes through the sameisSupportedRlsExpressionverdict, the same shape explanation (rls-predicate-unenforceable/-unparseable/-over-budget), the same reference pass (-unknown-field,-unknown-user-variable, the type and comparand faults, the list-holding and cross-class arms) and the same engine read-scope pass. Severity iserrorfor both sources.objects[N].defaultRowScope.using/.check.where:default row scope "NAME" of object "OBJECT". No part of a default's finding mentions a permission set (pinned).where,path, message and hint (every existing pin passes unedited).validateOrgAxisRedLinesrule ① (validate-org-axis-red-lines.ts) also judges a default whoseusing/checkreadsparent_organization_id, atobjects[N].defaultRowScope.CLAUSE, with the same message and hint. The reason is the rule's own: once the security plugin applies the default, it is a Layer-1 policy like any other, so walking the org parent there builds the same second hierarchy. The file's## Scopetable and its "object-level RLS" note now name the one object-level policy key that is declared.os explain(rule-explanations.ts). The sharedRLS_DROPPED_USINGparagraph (used by all fiverls-predicate-*entries) now covers both sources. For a permission set's policy, the object disappears for every holder of the set that carries it. For an object'sdefaultRowScope, which is applied only when the caller's sets carry no row policy for the object and operation, the default is always the only applicable policy. So a dropped default makes the object disappear for every caller it applies to, and a caller whose set carries its own row policy for the object is not affected. Therls-predicate-unenforceableentry's first paragraph also says it judges both places a row policy is declared. No other entry of that file is touched..changeset/24452-default-row-scope-lint.md,@objectstack/lintpatch: a new authoring-time finding on a shape that was silent before.Why the severity is the same (measured on the reader, not assumed)
PR #24451 (the runtime half, merged as
94e60c00fand merged into this branch) makescollectRLSPolicieshand the default to the same admission, the samegetApplicablePoliciesfilter and the sameRLSCompileran authored policy reaches. A non-loweringusingis dropped with the same WARN and fails closed toRLS_DENY_FILTER, and #24451 pins that on the same predicate in both forms. The one runtime difference is that a default is never OR-ed with a sibling, because it is applied only to an empty collection. That makes the "this policy vanishes from the OR" branch impossible for a default, and the explanation says so. It is no reason for a different severity: the lint's bar forerroris that no reading of the metadata behaves as written, and that holds for a default whose predicate cannot lower.Ordering. #24451 landed while this PR was in progress, and this branch has it merged, so the explanation's account of a dropped default is what
maindoes now.Pins
New file
packages/lint/src/validate-rls-predicate-enforceability.default-row-scope.test.ts. Every object fixture passesObjectSchema.safeParsefirst.usingdoes not lower is reported (rls-predicate-unenforceable,error,objects[0].defaultRowScope.using, thewhereabove), and no part of the finding mentions a permission set.checkthat does not lower is reported at.check, with the write consequence ("admits no write").objectsis read too.selectandall), and an object with no default, report nothing.current_userkey, a list-holding key under==). The same predicate as a permission-set policy and as a default earns the same rule, severity, message and hint. Onlypathdiffers.selectdefault'susingwith the declaring object (lead,{ operation: 'find' }), and its refusal is reported at the default. CONTROL: anupdatedefault is not handed to it.runAuthoringRules('validate', …)(the CLI registry door) reports the default underrls-predicate-unenforceable.os explainfor-unenforceable,-unknown-fieldand-unknown-user-variablecarries the both-source wording.validate-org-axis-red-lines.test.ts:usingthat walks the org parent is flagged (array form;whereandpathasserted; no "permission set" in it), a defaultcheckis flagged (map form), and CONTROL: a membership-scoped default is silent;objectto readdefaultRowScope/name/systemFields/tenancy(all onObjectSchema.shape) and adds ascopereceiver checked againstDefaultRowScopeSchema.shape. The clause-list guard checks both clause loops against their schemas. The reachability guard expects fourfindings.pushsites and has a spec-valid default-row-scope fixture reaching the new one.Pin sweep. I searched the whole repo for
rls-predicate-unenforceable/RLS_PREDICATE_UNENFORCEABLEand for the reworded sentence (disappears for every holder of the permission set,vanishes from the OR). The two lint pins on the old sentence (validate-rls-predicate-enforceability.test.ts, the unknown-field and unknown-user-variable cases) still match, because the new text keeps that clause for the permission-set source. The new both-source assertions are in pin 9 above. The consumer pins (packages/cli/test/rls-policy-authoring-admission.test.ts,packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts) assert permission-set paths only, and they pass unedited. No object in the tree declaresdefaultRowScopeoutside tests (git grepoverexamples/and every non-test source), so no linted example changes verdict.Ablation. Run once on
ed2d826c13, the first commit, after it was committed and before any merge (the line it mutates is unchanged since), then reverted, throughnode scripts/ablation-replace.mjsunder atraprestore:const scope = obj.defaultRowScope;becameconst scope = undefined as unknown;;bbb07af6453fto7dbfe87353be;bbb07af6453f, equal toHEAD, withgit diff HEADempty;./validate-rls-predicate-enforceability.js, noexportshop), so nodistrebuild was involved.Tests and gates
Every reading below is on the final head
0db86b5479. That head carriesorigin/main45bec43cdmerged in (with #24451 and #24474), and every run went throughscripts/pm/os-verify-lock.sh. Each exit code below is the command's own, captured before any pipe.pnpm --filter @objectstack/lint test: 146 files, 6896 passed, exit 0.pnpm --filter @objectstack/lint typecheck: exit 0. That run includescheck:test-typecheck, so the new test file compiles undertsconfig.test.jsonwith the debt ledger unchanged.@objectstack/lintdist(the closure was rebuilt by turbo on this head):packages/cli/test/rls-policy-authoring-admission.test.ts: 39 passed;packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts: 145 passed.defaultRowScope, so no linted example changes verdict.@objectstack/spec:test:repo: 70 files, 1162 passed, exit 0;src/data/object-default-row-scope.test.tsandsrc/system/metadata-form-zod-reconciliation.test.ts: 86 passed;check:generated: the only stale artifact wascontent/docs/references/**, from the describe edit, regenerated with--fix(only that one) and committed in0db86b5479.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 110 commands for this diff (10 paths, merge base45bec43cd). All 110 were run on this head and every one exited 0.--ranreconciles: 110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN.check-changeset-fixed,check:authz-resolver,check:error-code-casingandcheck:filter-alias-parity.pnpm lintand the workspace type-check lanes.Surface beyond the claim (and why)
The claim's file surface is
packages/lintplus the changeset. Measurement put one more file in this PR's way.ObjectSchema.defaultRowScope's describe (packages/spec/src/data/object.zod.ts, written by #24451) told authors: "Authoring checks do not judge this predicate, so keep it inside the lowerable subset". This PR makes that sentence false. The edit is two sentences. The describe now saysos validate/os build/os lintjudge the predicate, and that saving the object through the server does not run that check. The TSDoc says the same. With it come the three reference pagesgen:docsregenerates and a@objectstack/specpatch line in the changeset. No schema shape, export or authorable key moved.Acceptance notes
objectwrite.validateRlsPredicateEnforceabilityis registeredruntimeTypes: ['permission'](packages/lint/src/authoring-rules.ts), andvalidateOrgAxisRedLinesisCLI_ONLY. So a Studio, REST/metaor MCPobjectsave with a non-loweringdefaultRowScopeis not refused at that door by this PR. The CLI commands (os validate/build/lint) do judge it. The rule's positionalobjects[N]path is what the gate's per-write locating keys on (isLocatedOnAnotherEntry), so apermissionwrite is not blamed for an object's default. WideningruntimeTypesis outside this card's claim surface (authoring-rules.ts). It needs a measured reading of the door, and the documentation table incontent/docs/deployment/validating-metadata.mdxthat says this rule "readspermissions[]" would change with it. Not measured here, so not filed. Carrier: none.validate-org-axis-red-lines.test.ts(title and docblock) merged into this branch without conflict.picklists.<name>translations are never checked — an option key that is not in the list, or a list no package declares, passesos lint --strictand is dropped silently at runtime #23566), which edits other entries ofrule-explanations.ts, has landed (4bd7215a1), and this branch mergesmainafter it. The merge was clean.Generated by Claude Code