Skip to content

fix(rbac): namespace user-UID vs group-GID principals in permissions (closes #37)#52

Merged
rubenvdlinde merged 1 commit into
developmentfrom
fix/rbac-namespace-clash
May 13, 2026
Merged

fix(rbac): namespace user-UID vs group-GID principals in permissions (closes #37)#52
rubenvdlinde merged 1 commit into
developmentfrom
fix/rbac-namespace-clash

Conversation

@rubenvdlinde
Copy link
Copy Markdown
Contributor

Why

ApplicationsController::collectAuthorisedGroups() flattened permissions.{owners,editors,viewers} into a single deduplicated string list, which requirePermission() intersected with the caller's group-GID list (getUserGroupIds()). Both lived in the same string namespace, so an Application whose permissions.owners contained the literal username "admin" was — by coincidence — also satisfied by anyone in the admin group GID. The audited admin-bypass branch (REQ-OBRBAC-006) was never reached for that app, and the openbuilt-rbac Newman suite's rbac.admin_bypass audit row never got written.

What changed

Principal entries now encode the namespace in the string itself:

Form Matches
user:<uid> when the caller's UID equals <uid>
group:<gid> when one of the caller's group GIDs equals <gid>
<value> back-compat: still treated as a group GID. Seeded owners: ["admin"] keeps matching the admin group (zero behaviour change for existing data)
  • collectAuthorisedGroups() returns { users: [...], groups: [...] } (was: single array<string>).
  • requirePermission() checks the caller's UID against users first, then the caller's GIDs against groups. Either match grants access.
  • listMine() got the same two-step check so the listing filter stays consistent with the per-app authz gate.
  • Schema description in lib/Settings/openbuilt_register.json updated to document the three forms.

Backwards compat

Existing manifest Old behaviour New behaviour
owners: ["admin"] (seeded) matched admin group GID (coincidentally also matched user "admin") still matches the admin group GID; user-UID match only happens with explicit user:admin
owners: ["team-alpha"] (typical) matched team-alpha group GID matches team-alpha group GID — unchanged
owners: ["user:alice"] (new) matched the GID user:alice (nonsense) matches user alice (new capability)

The coincidental-match path is gone. An admin-group member trying to access an app whose permissions.owners is ["admin"] still gets in via the admin-group match — they ALSO go through the audited admin-bypass branch only when neither bucket matches, exactly as the spec intended.

Quality gates

  • vendor/bin/phpcs --standard=phpcs.xml lib/Controller/ApplicationsController.php
  • vendor/bin/phpstan analyse lib/Controller/ApplicationsController.php
  • vendor/bin/psalm lib/Controller/ApplicationsController.php ✓ (no errors)

🤖 Generated with Claude Code

…37)

Closes #37.

Before: `ApplicationsController::collectAuthorisedGroups()` flattened
`permissions.{owners,editors,viewers}` into a single deduplicated
string list, which `requirePermission()` intersected with the caller's
group-GID list (`getUserGroupIds()`). Both lived in the same string
namespace, so an Application whose `permissions.owners` contained the
literal username `"admin"` was — by coincidence — also satisfied by
anyone in the `admin` group GID. The audited admin-bypass branch
(REQ-OBRBAC-006) was never reached for that app, and the
`openbuilt-rbac` Newman suite's `rbac.admin_bypass` audit row never
got written.

After: principal entries now encode the namespace in the string itself.

  `user:<uid>`     — matches when the caller's UID equals `<uid>`.
  `group:<gid>`    — matches when one of the caller's group GIDs equals `<gid>`.
  `<value>`        — back-compat: still treated as a group GID. Seeded
                     `owners: ["admin"]` keeps matching the admin group
                     (no behaviour change for existing data).

`collectAuthorisedGroups()` returns `{ users: [...], groups: [...] }`;
`requirePermission()` checks the caller's UID against `users` first,
then the caller's GIDs against `groups`. Either match grants access;
both buckets are now independent so a username and a same-named group
no longer clash.

Schema description in `lib/Settings/openbuilt_register.json` updated
to document the three forms (user:uid, group:gid, bare-gid).

`listMine()` got the same two-step check at line ~480 so the listing
filter stays consistent with the per-app authz gate.

PHPCS strict + PHPStan + Psalm all clean on the changed file.
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openbuilt @ 6fbd013

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

Coverage: 0% (0/19 statements)


Quality workflow — 2026-05-13 08:50 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde rubenvdlinde merged commit 71e6d50 into development May 13, 2026
26 checks passed
@rubenvdlinde rubenvdlinde deleted the fix/rbac-namespace-clash branch May 13, 2026 08:51
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