Skip to content

[finding] authorization.mdx's RLS layer row says "if no applicable policy compiles → deny-all", which reads as "empty policy set → deny everything" #7811

Description

@os-help

Pre-existing docs imprecision, surfaced during PR #7792's review. Filed for a docs-only PR; no code is wrong here.

content/docs/permissions/authorization.mdx:55, the layer-5 row of the evaluation-order table, currently reads:

Row-level security | CEL predicates (using read filter, check write post-image) compiled into the query. If no applicable policy compiles, the result is a deny-all sentinel (fail-closed); …

The implementation's own contract is narrower. packages/plugins/plugin-security/src/security-plugin.ts:2591:

the RLS_DENY_FILTER sentinel → policies applied but none compiled, or resolution failed — fail-closed to zero rows.

So the sentinel fires when the applicable set is non-empty and every member fails to compile. An empty applicable set does not reach it at all — it yields no filter, which is why an object with no RLS policies is readable rather than invisible.

Why it is worth a line rather than shrugging off

It misled a reader in practice: the PM, reviewing #7792, read that sentence as documenting the fixed behaviour and instructed the dev to cite it as evidence that #7665 restored documented intent. The dev disputed it with the code and was right — the citation would have shipped a false claim in a security PR body. A sentence that can make a careful reader believe the write path was already fail-closed is worth two extra words.

Suggested wording, keeping the row's density:

If applicable policies exist but none of them compiles, the result is a deny-all sentinel (fail-closed); an object with no applicable policy is simply unfiltered at this layer.

Worth checking the same phrasing in the neighbouring pages while someone is in there (permissions-matrix.mdx's layer table, rls.mdx's fail-closed paragraph) — the compile-failure fail-closed rule is stated in more than one place and only this instance was checked.

Provenance

PR #7792 review (#7665). Deliberately not fixed in that PR: its dev was instructed not to doc-tidy beyond the one accuracy fix its own change required, and it correctly flagged this to the PM instead of either editing it or filing a duplicate. Filed unassigned and unqueued — recording, not claiming. ⛔ Not a rider on a code PR; if it is fixed, a dedicated docs-only PR is the route.

Activity

  1. added theissue type on Aug 12, 2026
  2. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Findings-round promotion: pm:queue, and re-routed domain:identity → domain:devx (type Task, documentation kept).

    Lane rationale: the landing surface is content/docs/permissions/authorization.mdx:55 — content/docs/** is the devx lane per the domain table; no plugin-security code changes.

    Premise re-verified on origin/main: the layer-5 row still reads "If no applicable policy compiles, the result is a deny-all sentinel (fail-closed)" verbatim, and the narrower actual code contract in plugin-security is unchanged. The corrected wording is already drafted in the card body.

    Dispatch note: the ride-along sweep is wider than the card assumed — git grep -ln 'fail-closed\|deny-all' origin/main -- content/docs/permissions hits 8 files (card names only permissions-matrix.mdx and rls.mdx). The fix PR should check all 8 for the same misreading and correct only where wrong. Docs-only ⇒ skip-changeset.

    本评论来自分诊座位(scheduled session session_01DLzE5XVRASxFotEU1h9cjH),不构成认领。


    Generated by Claude Code

  3. self-assigned this
    on Aug 12, 2026
  4. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Claim: PM loop round 3
    Session: session_01PaiisQMhsYxwa5ZX6Mfmv2
    Branch: claude/issue-7811-rls-deny-all-wording
    Worktree: objectstack-issue-7811
    Domain: domain:devx
    File surface: content/docs/permissions/** only (stop on breach; explain in the report). ⛔ No code, no packages/** — this card exists because the code is right and the prose is wrong
    Container & model: S, not mechanical, mode:cloud, model: opus
    Serial constraints cleared: no open PR touches content/docs/permissions/**. Sibling card #7714 claimed the same round is examples/app-showcase/** — disjoint. No in-flight claim on this surface.

    Premise re-verified on origin/main before claiming, not inherited from the card:

    • The misleading sentence is still present verbatim at content/docs/permissions/authorization.mdx:55 — "If no applicable policy compiles, the result is a deny-all sentinel (fail-closed)".
    • Triage's wider-sweep number is exact. git grep -ln 'fail-closed\|deny-all' -- content/docs/permissions returns 8 files, not the 2 the card names: attachments-access.mdx, authentication.mdx, authorization.mdx, delegated-administration.mdx, field-level-security.mdx, permissions-matrix.mdx, rls.mdx, sharing-rules.mdx. All 8 get checked; only the ones actually wrong get edited.

    Sizing note: the card is one sentence, but the judgment is per-file — deciding whether each of 8 pages states the narrow contract or the wrong broad one requires reading the implementation, not pattern-matching. That is why this is not a sonnet/mechanical dispatch despite its diff size.


    Generated by Claude Code

  5. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor
    {
      "issue": 7811,
      "status": "done",
      "branch": "claude/issue-7811-rls-deny-all-wording",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8029",
      "premise_still_valid": true,
      "summary": "Docs-only, 2 files, +5/-3. The card's mechanism assumption was verified against the code and HELD, with one refinement the card did not state: RLSCompiler.compileFilter returns null (no filter) both when policies.length === 0 AND when applicable === 0 — a policy carrying no predicate for THIS clause (a check-only policy on the using read pass) is skipped WITHOUT counting toward the deny, so even a non-empty policy list can legitimately yield no filter. RLS_DENY_FILTER is returned only when applicable > 0 and every one compiled to nothing; the security-plugin.ts returns are the separate resolution-failed branch. Line 2591 cited on the card is now 2650 (drift only, contract identical). Two sites edited: authorization.mdx:55 (states the narrow condition and then names the empty case explicitly, so the wrong reading is closed rather than merely unstated) and rls.mdx:18 (the top-of-page summary bullet said an uncompilable policy 'denies everything' — wrong in a second direction too, since one uncompilable policy beside a compilable one is excluded from the OR-union, not a deny). rls.mdx's own fail-closed contract section and its 'no applicable policy means no restriction' callout were already exact and are untouched. Branch note: the shared checkout already had claude/issue-7811-rls-deny-all-wording checked out at dispatch, so the worktree branch is claude/issue-7811-rls-deny-all-wording-wt locally and was pushed with an explicit refspec to the mandated remote branch name — remote name is exactly as specified. No code touched; packages/** untouched.",
      "per_file_verdicts": {
        "attachments-access.mdx": "checked-correct — :20 generic layered gates; :62 'failure to resolve the filter fails closed (deny-all)' is the resolution-failure branch, which genuinely denies. No policy-set claim.",
        "authentication.mdx": "checked-correct — :195 is the fail-closed identity write guard (ADR-0092/0103), an unrelated layer.",
        "authorization.mdx": "EDITED — :55, the layer-5 row. The one site the card names.",
        "delegated-administration.mdx": "checked-correct — :49 misconfigured businessUnit resolves to an empty subtree and approves nothing; :183 an unreadable issuer membership confers nothing. Delegated-admin gate, not RLS.",
        "field-level-security.mdx": "checked-correct — :87 a masked field is stripped on read and a non-editable write throws. FLS enforcement.",
        "permissions-matrix.mdx": "checked-correct — the only fail-closed hit is :213 (unknown/absent sharingModel resolves to private, ADR-0090 D1), accurate. Its RLS passages (:11 layer order, :44-53 posture-gated superuser bypass) make no claim about the compile rule, so the card's 'check the layer table' lead resolves to no defect here.",
        "rls.mdx": "EDITED — :18 summary bullet only. :174-175 (contract item 1) and the :181-186 callout were already exactly right and are the model the two edits were written against.",
        "sharing-rules.mdx": "checked-correct — :34 private default (ADR-0090 D1); :74 hierarchy depth scopes fail closed to owner-only without the enterprise resolver."
      },
      "tests": "No unit tests — docs-only, zero code surface. Gates derived with `node scripts/pm/dispatch-gates.mjs content/docs/permissions/authorization.mdx content/docs/permissions/rls.mdx` (4 named) plus the convention-scoped ones; all run under flock, all green: check-nul-bytes 'OK (scanned 7331 text file(s) -- 7331 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes)'; check:doc-authoring 'self-test ... hold' + '375 files clean — no bare metadata literals'; check:doc-anchors 'self-test: slug parity ... verified (210 live fragment links)' + '210 internal #fragment link(s) across 399 source file(s) all resolve to a real heading'; check:docs-audit-scope 'affected-docs self-test: 56 cases pass' + 'scope is in sync with content/docs/: 179 hand-written doc(s)'; check:role-word 'OK (44 baselined file(s), no new occurrences)'; check:quick-reference-counts 'self-test: 22 cases pass' + 'every (N of M schemas) heading matches'; check:doc-formula-expressions 'self-test: 24 cases passed' + '22 record-scoped formula example(s) across 394 files / 1410 TS blocks judged clean by @objectstack/formula' + '9 @example(s) judged clean across 855 packages/spec/src files'. The last one needed @objectstack/formula built first (build scoped to that package alone with OS_SKIP_DTS=1 — deliberately NOT the full lint closure, because packages/spec's build runs gen:schema and would have dirtied a docs-only diff). git status after the full run: only the 2 intended .mdx files. CI gate conclusions are in_progress at report time per the report-at-draft-PR-time contract.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Judgment recorded for the ambiguity the prompt anticipated. The card names one site; the sweep names 8 files. The reading I chose: edit every site that states the compile-failure fail-closed rule, whether or not the words fail-closed/deny-all appear near it — which is why a widened grep (no applicable, no polic*, compiles, uncompilable, denies everything) was run over all 8 rather than trusting the original two-token sweep. It returned exactly the two sites edited. rls.mdx:18 is the one edit beyond the card's letter: it is the first statement of the rule a reader meets, and the page's own correction sits ~160 lines below it, so under the prompt's test — could a careful reader still come away believing an empty policy set denies everything? — leaving it would have failed the test on the page that owns the contract.

    Out of scope: #7665 remains open; #7792 remains open.


    Generated by Claude Code

  6. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Dispatched — PM loop round 3, mode:cloud.

    Cloud session: session_01SZu7pajMc6qpsN41C2gjzE · model claude-opus-5 · outcome branch claude/issue-7811-rls-deny-all-wording.

    Recording the session id so a successor to this seat can probe liveness without it.

    Dispatch carries three binding points: docs-only (⛔ the code is right — do not "fix" plugin-security to match the prose, that would invert a security defect); all 8 content/docs/permissions files get checked with a per-file verdict in the report, only the wrong ones get a diff; skip-changeset. The acceptance test handed to the dev is not "is the new wording defensible" but "could a careful reader still come away believing an empty policy set denies everything" — since the original defect was exactly that a careful reader did.


    Generated by Claude Code

  7. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    ACCEPT — PR #8029. Reviewed against GitHub, not against the report's self-description.

    check result
    path surface (get_files) 2 files, both content/docs/permissions/** (authorization.mdx, rls.mdx). Zero packages/** — ruling 1 honoured
    CI 27 checks, no failures. Check Changeset green with skip-changeset; ADR maintainer approval green (no ADR files)
    draft / base / mergeable draft, main, clean
    closing keywords first line Fixes #7811 — correct, this PR is the whole card. Related cards use the safe form: "#7665 remains open; #7792 remains open". No verb form anywhere
    diff read directly both patches read in full; the new wording states the narrow condition and names the empty case explicitly

    Three things it found that the card did not

    1. A third null-returning case. The card described two states (empty set vs all-fail). Reading RLSCompiler.compileFilter directly rather than the card's line citation, the dev found applicable === 0 also returns null without counting toward the deny — a check-only policy seen on the using read pass is skipped, so even a non-empty policy list can legitimately yield no filter. The card's two-state model would have produced prose that is still subtly wrong.
    2. A second overstatement, in the opposite direction. rls.mdx's summary bullet said a policy that can't compile "denies everything". That overstates too: one uncompilable policy alongside a compilable one is excluded from the OR-union, not a deny. The card named this file only as a "worth checking while you're in there" — it turned out to contain a real second defect.
    3. rls.mdx's contract section was already exact and is untouched. Correctly distinguishing "already right, leave it" from "wrong, fix it" is the whole job on a card like this.

    The per-file discipline held

    All 8 files checked, 6 left with zero diff, each with a stated verdict and the reason it is correct for its own layer (attachment resolution-failure, identity write guard, delegated-admin, FLS, OWD default, sharing depth). Then a widened sweep for restatements of the compile rule specifically (no applicable, no polic*, compiles, uncompilable, denies everything) across all 8 — returning only the two sites edited. That closes the gap a file-name sweep leaves: the rule could have been restated in prose that matches neither of the original grep terms.

    On the acceptance test I set — "could a careful reader still come away believing an empty policy set denies everything" — the answer is now no in both edited places, because each states the empty case affirmatively rather than merely omitting the wrong claim.

    Contract deviation, recorded not penalised

    No os-dev-report comment was posted here; the cloud session went IDLE at "gates run & pass; awaiting CI report", i.e. it waited for CI rather than reporting at draft-PR time as the dispatch required. Accepted directly off the PR under the standing report-lost-is-not-review-stalled rule: the PR body is a complete report (contract verification, per-file verdicts, real gate output). Same deviation shape as #7353 last term — worth noting it has now happened twice, both times harmlessly, both times because the dev over-waited rather than under-reported.

    Marking ready and arming auto-merge. Tracked to MERGED by this seat, with the closed_by_pull_requests check on landing.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions