Skip to content

fix(rbac): correct the read-visibility diagnosis and delete the dead gate - #2252

Merged
rubenvdlinde merged 1 commit into
developmentfrom
wip/correct-read-visibility-diagnosis
Aug 1, 2026
Merged

fix(rbac): correct the read-visibility diagnosis and delete the dead gate#2252
rubenvdlinde merged 1 commit into
developmentfrom
wip/correct-read-visibility-diagnosis

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Follow-up to #2251, which this corrects.

The short version

#2251 claimed the object read path gates on create, and that OpenRegister had no way to express "any authenticated user". Phase 0 disproved both.

Claim in #2251 Reality
The read path gates on create Listing is filtered in SQL by MagicRbacHandler::buildRbacConditionsSql(action: 'read'). Correct already.
filterObjectsForPermissions() is the list filter It has no production caller. The only repo-wide reference invokes it with _rbac: false, so the check never ran.
filterUuidsForPermissions() has the same bug (delete) Its only caller is deleteObjects(). delete is correct. Withdrawn.
No sentinel for "any authenticated user" authenticated already exists in all three handlers and is specified in openspec/specs/rbac-scopes/spec.md:178.

What actually caused the openbuild blackout

A non-empty authorization block that omits read fails closed to owner-only rows — deliberately, and commented as such at MagicRbacHandler:1031. Every openbuild schema carries {create,update,delete: ["admin"]} with no read, so every non-admin saw zero.

That is an openbuild-side data fix (read: ["authenticated"]), not a change to OpenRegister.

It also explains the anomaly #2251 filed as an unresolved open question: adding read: ['rbac-editors'] flipped 0 → 21 because it supplied the missing rule to the SQL gate. Nothing consulted the create gate.

What this PR changes

No behavioural change.

  • Deletes filterObjectsForPermissions() and its test, leaving a note at the site so it is not reintroduced.
  • Repoints docs/features/organisation-roles.md Read + List at the SQL gate. That doc line is part of why the dead function looked live.
  • Rewrites the OpenSpec change; adds design.md recording the anomaly's answer, the cross-handler contract table, and the reasoning error — a code path was assumed live because it was public, documented, @spec-annotated and unit-tested. One grep for callers would have caught it.
  • Fixes 3 pre-existing @spec warnings in the touched file.

Verification

  • phpcs clean at gate settings.
  • phpmd compared at identical paths: 3 findings → 2. The removal drops TooManyMethods (26 → 25); none added.
  • Live measurement on the disposable instance after the openbuild-side fix — anonymous 0 rows, authenticated 21 at the OR layer, and openbuild's own endpoint filtering that to 1/1/0 by app role.

🤖 Generated with Claude Code

…gate

The proposal merged in #2251 was wrong on both counts, and Phase 0 disproved
it. Recording the correction rather than quietly dropping it.

What it claimed:
  - the object read path gates on `create`
  - OpenRegister cannot express "any authenticated user"

What is actually true:
  - Listing is filtered in SQL by MagicRbacHandler::buildRbacConditionsSql(),
    which asks for `read`. Correct already.
  - filterObjectsForPermissions() does gate on `create` — and has NO production
    caller. Repo-wide the only reference invokes it with _rbac:false, so the
    check never ran. Dead code that reads as the live gate.
  - The `authenticated` pseudo-group already exists in all three handlers and
    is specified in openspec/specs/rbac-scopes/spec.md:178. No new sentinel.

Also withdrawn: the concern that filterUuidsForPermissions() gates on `delete`.
Its only caller is deleteObjects(), so `delete` is right.

The real cause of the openbuild blackout is a non-empty authorization block
with no `read` key, which fails closed to owner-only rows by design
(MagicRbacHandler:1031). That is an openbuild-side data fix, not an OR change.

This commit therefore makes no behavioural change:
  - deletes filterObjectsForPermissions() and its test, leaving a note at the
    site so it is not reintroduced
  - repoints docs/features/organisation-roles.md Read+List at the SQL gate;
    that doc line is part of why the dead function looked live
  - rewrites the OpenSpec change, and adds design.md recording both the
    anomaly's answer and the reasoning error that made it necessary

Gates: phpcs clean; phpmd at identical paths goes 3 findings -> 2 (the removal
drops TooManyMethods 26->25), none added.
@rubenvdlinde
rubenvdlinde merged commit c8e6685 into development Aug 1, 2026
18 checks passed
@rubenvdlinde
rubenvdlinde deleted the wip/correct-read-visibility-diagnosis branch August 1, 2026 21:21
rubenvdlinde added a commit to ConductionNL/openbuild that referenced this pull request Aug 1, 2026
)

Every openbuild schema declared

    "authorization": { "create": ["admin"], "update": ["admin"], "delete": ["admin"] }

— non-empty, and with no `read` key. OpenRegister treats that as fail-closed:
`MagicRbacHandler::buildRbacConditionsSql()` bypasses filtering only for an
EMPTY block; a populated block with no `read` rule falls through to the owner
condition alone, so every non-admin caller saw zero rows. Not a bug in OR —
deliberate, and commented as such at MagicRbacHandler:1031.

That is the whole of #76. An owner could grant a colleague editor or viewer on
an app and they still saw an empty list, because OR filtered the objects out
one layer below openbuild's own permission check.

Adds `"read": ["authenticated"]` to all 15 schemas — 6 in the monolith and 9
across the register.d fragments, which were missed by the first pass and would
have left business rules, automations, component blocks and the agent
workspace owner-only.

`authenticated` requires $userId !== null (MagicRbacHandler:414), so anonymous
callers are NOT granted. This is intentionally the coarse layer: appinfo/routes.php
already documents that OR's schema read rule is a group ACL, not a row filter,
and that the per-app `permissions` block is enforced by /api/applications. Both
layers verified live.

Measured on the disposable instance after a FORCED re-import:

  caller          OR object API   openbuild /api/applications
  admin                      21   21
  rbac-editor          0 -> 21    1  (granted editor on pw-verchain)
  rbac-viewer          0 -> 21    1  (granted viewer on pw-verchain)
  rbac-outsider        0 -> 21    0  (no grant)
  anonymous                   0   401

Diagnosis trail: ConductionNL/openregister#2252.

Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ 699e51a

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 174/174
npm ✅ 616/616
PHPUnit
Newman
Playwright ⏭️

Quality workflow — 2026-08-01 21:28 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde added a commit to ConductionNL/openbuild that referenced this pull request Aug 1, 2026
…n the RBAC suite (#84)

* fix(rbac): grant authenticated read on every openbuild schema (#76)

Every openbuild schema declared

    "authorization": { "create": ["admin"], "update": ["admin"], "delete": ["admin"] }

— non-empty, and with no `read` key. OpenRegister treats that as fail-closed:
`MagicRbacHandler::buildRbacConditionsSql()` bypasses filtering only for an
EMPTY block; a populated block with no `read` rule falls through to the owner
condition alone, so every non-admin caller saw zero rows. Not a bug in OR —
deliberate, and commented as such at MagicRbacHandler:1031.

That is the whole of #76. An owner could grant a colleague editor or viewer on
an app and they still saw an empty list, because OR filtered the objects out
one layer below openbuild's own permission check.

Adds `"read": ["authenticated"]` to all 15 schemas — 6 in the monolith and 9
across the register.d fragments, which were missed by the first pass and would
have left business rules, automations, component blocks and the agent
workspace owner-only.

`authenticated` requires $userId !== null (MagicRbacHandler:414), so anonymous
callers are NOT granted. This is intentionally the coarse layer: appinfo/routes.php
already documents that OR's schema read rule is a group ACL, not a row filter,
and that the per-app `permissions` block is enforced by /api/applications. Both
layers verified live.

Measured on the disposable instance after a FORCED re-import:

  caller          OR object API   openbuild /api/applications
  admin                      21   21
  rbac-editor          0 -> 21    1  (granted editor on pw-verchain)
  rbac-viewer          0 -> 21    1  (granted viewer on pw-verchain)
  rbac-outsider        0 -> 21    0  (no grant)
  anonymous                   0   401

Diagnosis trail: ConductionNL/openregister#2252.

* test(e2e): un-skip versionRouting 9.2 and record the measured blockers on the RBAC suite

9.2 — ENABLED, four scenarios replacing one that asserted almost nothing.

Two things unblocked it. globalSetup now provisions the rbac-* fixture users
and mints one storageState each, so no spec form-logs-in (four consecutive
logins is exactly what trips Nextcloud's brute-force throttle) — `loginAs` is
gone from this file. And openbuild#76: until every schema carried a `read`
rule, non-admins saw ZERO objects, so "viewer gets 404" passed for the wrong
reason and no 200 assertion was reachable at all.

The old body could not fail meaningfully. It located the schema list with
`.ob-schema-list` / `[data-testid="schema-list"]`, neither of which exists in
src/, so "must NOT be visible" held on any page including a correct one; and it
downgraded a missing not-found UI to a console.warn. Now asserted:

  - viewer + staging -> 404 with the body pinned EXACTLY, plus a regex check
    that the envelope names no authorisation reason
  - non-member + staging -> byte-identical to the viewer's, and identical again
    to an unknown version slug (that indistinguishability IS REQ-OBVR-003)
  - editor + staging -> 200 with a manifest. The positive control: without it a
    broken fixture, a missing chain or a blanket denial all look like a pass
  - viewer UI -> `.openbuild-schema-list` (the REAL selector) absent, no stack trace

Setup grants the roles via grantAppRoles(), which had been merged with no
caller — so the editor control now actually exercises an editor.

schema-access-scopes-rbac — STAYS SKIPPED, with honest blockers.

Its three recorded blockers are all resolved (fixture users exist; the version
chain is seeded; the feature and its copy are real — the warning is an
NcNoteCard sibling of .openbuild-access-editor, and `.note-stub` never existed).
A fourth, found by driving it rather than reading it, is not: the schema
designer is unreachable for a non-admin. rbac-editor lands on the first-time
setup wizard with `.openbuild-schema-list` count 0, because /api/setup/status is
admin-only and useSetupStatus read its 403 as "nothing done". Fixed upstream in
ConductionNL/nextcloud-vue#574; unblocks on a published bump.

The comment also records a defect found while measuring, filed as #83:
availableGroups feeds the dropdown `group:`-prefixed values while
authorLockedOut compares bare gids, so the lock-out warning fires for members
too — exactly the REQ-OBDSA-004 scenario. Noted so this suite is not simply
un-skipped and declared green once the wizard blocker lifts.

---------

Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant