Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

Commit 713c581

Browse files
authored
Merge pull request #6 from ConductionNL/feature/spec-openbuilt-rbac
spec(openbuilt-rbac): per-virtual-app RBAC change artifacts
2 parents 83f7862 + 3921843 commit 713c581

33 files changed

Lines changed: 4586 additions & 189 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ phpqa_output.log
5454
**/update*Settings*
5555
**/rebase*
5656
**/setup*
57+
!tests/vitest/setup.js
58+
!tests/e2e/global-setup.ts
5759

5860
# Temporary test files that shouldn't be committed
5961
simple-solr-test.php

appinfo/info.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,28 @@ Vrij en open source onder de EUPL-1.2-licentie.
4444
<php min-version="8.1"/>
4545
</dependencies>
4646

47+
<!--
48+
openbuilt.use navigation-entry gate (REQ-OBRBAC-006).
49+
50+
SPEC INTENT — REQ-OBRBAC-006 calls for a <permission>openbuilt.use</permission>
51+
child on the <navigation> element so admins can restrict the top-bar
52+
entry to selected groups. We tried shipping that element and verified
53+
2026-05-11 with `occ app:enable openbuilt --force` on Nextcloud 32 that
54+
the upstream apps/info.xsd schema rejects the `<permission>` child
55+
("appinfo file cannot be read"). The schema does not yet expose this
56+
sub-element; an upstream issue is filed at:
57+
https://github.com/nextcloud/server/issues/60310
58+
59+
FALLBACK — per design.md Decision 4 we rely on Nextcloud's standard
60+
app-level group restriction:
61+
occ app:enable openbuilt --groups team-alpha
62+
which configures group visibility outside info.xml. Per-Application
63+
RBAC enforced server-side by ApplicationsController::getManifest +
64+
::listMine (REQ-OBRBAC-002 / REQ-OBR-006) remains the load-bearing
65+
security boundary; this gate is coarse top-bar visibility only.
66+
67+
See docs/openbuilt-rbac.md for the operator guide.
68+
-->
4769
<navigations>
4870
<navigation>
4971
<id>openbuilt</id>
@@ -62,10 +84,12 @@ Vrij en open source onder de EUPL-1.2-licentie.
6284
<install>
6385
<step>OCA\OpenBuilt\Repair\InitializeSettings</step>
6486
<step>OCA\OpenBuilt\Repair\SeedHelloWorld</step>
87+
<step>OCA\OpenBuilt\Repair\PopulateApplicationPermissions</step>
6588
</install>
6689
<post-migration>
6790
<step>OCA\OpenBuilt\Repair\InitializeSettings</step>
6891
<step>OCA\OpenBuilt\Repair\SeedHelloWorld</step>
92+
<step>OCA\OpenBuilt\Repair\PopulateApplicationPermissions</step>
6993
</post-migration>
7094
</repair-steps>
7195
</info>

appinfo/routes.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@
1616
// Health check endpoint.
1717
['name' => 'health#index', 'url' => '/api/health', 'verb' => 'GET'],
1818

19+
// RBAC-filtered Application list (openbuilt-rbac REQ-OBRBAC-002 / REQ-OBR-007).
20+
// OR's schema-level read rule is a coarse group ACL — not a row-level filter on the
21+
// Application's `permissions` block — so the editor list MUST go through this
22+
// endpoint, NOT directly through `/apps/openregister/api/objects/openbuilt/application`,
23+
// which would leak every Application + permissions to every authed user (IDOR).
24+
// Listed BEFORE the {slug} route so the wildcard does not shadow it (Symfony router
25+
// is order-sensitive when prefix overlaps).
26+
['name' => 'applications#listMine', 'url' => '/api/applications', 'verb' => 'GET'],
27+
1928
// Manifest endpoint — returns the stored manifest JSON blob for a given virtual-app slug.
2029
// Per ADR-016 routes.php is the only registration path; #[NoAdminRequired] is set on the
2130
// controller method so auth-required-but-non-admin users can hit it (per design.md Decision 6).

docs/openbuilt-rbac.md

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# OpenBuilt RBAC — Per-Virtual-App Permissions
2+
3+
OpenBuilt's per-virtual-app role-based access control (RBAC) layers on
4+
top of OpenRegister's organisation-scoping (ADR-022). Within an
5+
organisation, three roles partition who can do what with each
6+
Application: `owner`, `editor`, `viewer`.
7+
8+
## Roles
9+
10+
| Action | viewer | editor | owner |
11+
| ------------------------------------- | :----: | :----: | :---: |
12+
| Read manifest / browse Application | yes | yes | yes |
13+
| Save manifest draft | no | yes | yes |
14+
| Publish (`draft → published`) | no | no | yes |
15+
| Archive (`published → archived`) | no | no | yes |
16+
| Re-open (`archived → draft`) | no | no | yes |
17+
| Edit `permissions` | no | no | yes |
18+
| Transfer ownership | no | no | yes |
19+
| Delete Application | no | no | yes |
20+
21+
Roles are keyed by Nextcloud group ID, stored declaratively on the
22+
Application schema:
23+
24+
```json
25+
{
26+
"permissions": {
27+
"owners": ["team-alpha"],
28+
"editors": ["team-alpha", "qa-shared"],
29+
"viewers": ["everyone"]
30+
}
31+
}
32+
```
33+
34+
A caller's effective role is computed by intersecting their Nextcloud
35+
group membership with the three buckets, taking the highest-privilege
36+
match.
37+
38+
## Default on creation
39+
40+
New Applications default `permissions.owners` to the **creator's
41+
primary Nextcloud group**; `editors` and `viewers` start empty. If the
42+
creator has no group memberships, `owners` falls back to `['admin']`
43+
so the Application is never orphaned (REQ-OBRBAC-001).
44+
45+
## Manifest endpoint enforcement
46+
47+
`GET /index.php/apps/openbuilt/api/applications/{slug}/manifest`
48+
deny-by-defaults to `403 Forbidden` when the caller has no group in
49+
the union of the three buckets (REQ-OBR-006, REQ-OBRBAC-002). The
50+
check runs before the manifest payload is emitted — the 403 body
51+
never leaks Application metadata.
52+
53+
Error envelope:
54+
55+
```json
56+
{ "error": "forbidden", "code": "openbuilt.rbac.no_role" }
57+
```
58+
59+
## Admin bypass (audited)
60+
61+
A user in the Nextcloud `admin` group can read any Application's
62+
manifest as an incident-response escape hatch. Every exercise of the
63+
bypass writes an audit-trail event with shape:
64+
65+
```
66+
event: rbac.admin_bypass
67+
actor: <admin uid>
68+
slug: <application slug>
69+
ts: <ISO 8601 timestamp>
70+
```
71+
72+
The bypass:
73+
74+
- Runs **only** in `ApplicationsController::getManifest`. The frontend
75+
list filter does **not** include admins automatically.
76+
- Is logged at `info` level on the OpenBuilt PSR logger (where OR's
77+
audit-trail will pick it up via Nextcloud's logging pipeline).
78+
- Is narrow by design — sustained bypass volume from one admin is a
79+
signal to grant them an explicit role on the affected Applications.
80+
81+
## List filter
82+
83+
The OpenBuilt shell's Application list (`ApplicationEditor.vue`)
84+
filters out Applications on which the caller has no role
85+
(REQ-OBR-007). The filter runs client-side using
86+
`loadState('openbuilt', 'currentUserGroups')` (no DOM data-attribute
87+
reads — ADR-004 hard rule `gate-initial-state`).
88+
89+
A future enhancement (DQ-1, see below) will move the filter to
90+
OR-side once `x-openregister-authorization` supports group-membership
91+
predicates.
92+
93+
## Permissions modal
94+
95+
Owner-only modal at `src/modals/PermissionsModal.vue` (per ADR-004
96+
`gate-modal-isolation`). Three NcSelect group pickers, all carrying
97+
the required `input-label` prop (ADR-004 `gate-nc-input-labels`).
98+
Frontend rejects an `owners = []` save before sending — orphan-check
99+
guard per REQ-OBRBAC-005.
100+
101+
## Transfer ownership
102+
103+
A transfer is a single PUT to the Application's `permissions.owners`
104+
array. No dedicated endpoint, no `TransferOwnershipService`. OR's
105+
per-object audit trail records the before / after values
106+
automatically.
107+
108+
## openbuilt.use navigation gate
109+
110+
Nextcloud's per-app group restriction (Apps → OpenBuilt → Restrict to
111+
groups) gates visibility of the OpenBuilt top-bar entry. Default is
112+
no restriction (all authenticated users see it); admins can narrow it
113+
via the standard Apps panel or OCC:
114+
115+
```bash
116+
occ app:enable openbuilt --groups digital-team
117+
```
118+
119+
This is coarse on/off visibility; the load-bearing security boundary
120+
is the per-Application `permissions` enforced server-side.
121+
122+
## Operational caveats
123+
124+
### Group renames
125+
126+
If a Nextcloud admin renames a group, every Application whose
127+
`permissions` array references the old `gid` loses or gains rows
128+
without a permission-history audit event scoped to OpenBuilt. We do
129+
not (currently) ship a group-rename listener. If a rename breaks
130+
access:
131+
132+
1. Edit the affected Application's `permissions` via the Permissions
133+
modal (as owner).
134+
2. Replace the stale group references with the new GID.
135+
136+
Tracked as design.md OQ-2; revisit if customers report breakage.
137+
138+
### Post-deploy "ACTION REQUIRED: re-grant access"
139+
140+
After upgrading to this version every pre-existing Application is
141+
patched to `permissions.owners = ['admin']` so it is only readable by
142+
the `admin` group. Operators MUST re-grant access for non-admin teams
143+
via the Permissions modal:
144+
145+
1. Sign in as an admin user.
146+
2. Navigate to OpenBuilt → Applications.
147+
3. For each Application that should be broadly accessible, open the
148+
Permissions modal and add the relevant Nextcloud groups to
149+
`owners`, `editors`, or `viewers`.
150+
151+
The `hello-world` demo follows the same default (admin-only). Grant
152+
`viewers = ['users']` to restore broad visibility for the demo.
153+
154+
## Deferred Questions
155+
156+
- **DQ-1 — OR `x-openregister-authorization` group-membership predicate.**
157+
Investigated 2026-05-11; the current OpenRegister REST API does
158+
**not** expose a `groupIn` / `groupMember` predicate that can be
159+
parameterised by a pointer into the object's own `permissions`
160+
block. Until OR ships it, the frontend list filter remains the
161+
fallback path; the controller's 403 check is the load-bearing
162+
enforcement. Tracked as design.md OQ-1.
163+
- **DQ-2 — Group rename listener.** Punted; documented above.
164+
- **DQ-3 — Permission-history retention.** Defer to OR-register-level
165+
retention configuration (Conduction's compliance baseline pins this
166+
at the OR-register level).

l10n/en.json

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,29 @@
6262
"Roll back to version {version}?": "Roll back to version {version}?",
6363
"Rolling back copies this snapshot's manifest onto the current draft. Existing history is preserved (append-only).": "Rolling back copies this snapshot's manifest onto the current draft. Existing history is preserved (append-only).",
6464
"Roll back": "Roll back",
65-
"Cancel": "Cancel",
6665
"Manifest diff": "Manifest diff",
6766
"Nothing to diff — publish the app first.": "Nothing to diff — publish the app first.",
6867
"From": "From",
6968
"To": "To",
7069
"Current draft": "Current draft",
71-
"Loading diff…": "Loading diff…"
70+
"Loading diff…": "Loading diff…",
71+
"Role": "Role",
72+
"Owner": "Owner",
73+
"Viewer": "Viewer",
74+
"No access": "No access",
75+
"No applications available — ask an owner to grant you access.": "No applications available — ask an owner to grant you access.",
76+
"Permissions": "Permissions",
77+
"Manage permissions": "Manage permissions",
78+
"Configure which Nextcloud groups can view, edit, or own this virtual app. Members of any of these groups will see the app in their list; only owners may publish, archive, delete, transfer ownership, or change these permissions.": "Configure which Nextcloud groups can view, edit, or own this virtual app. Members of any of these groups will see the app in their list; only owners may publish, archive, delete, transfer ownership, or change these permissions.",
79+
"Owners (full control)": "Owners (full control)",
80+
"Editors (can save drafts)": "Editors (can save drafts)",
81+
"Viewers (read-only)": "Viewers (read-only)",
82+
"Save permissions": "Save permissions",
83+
"Saving permissions…": "Saving permissions…",
84+
"Cancel": "Cancel",
85+
"At least one owner group is required — saving with no owners would orphan this application.": "At least one owner group is required — saving with no owners would orphan this application.",
86+
"Editor or owner role required to save the manifest.": "Editor or owner role required to save the manifest.",
87+
"Your access to this application was revoked. The list will refresh.": "Your access to this application was revoked. The list will refresh."
7288
},
7389
"plurals": ""
7490
}

l10n/nl.json

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"published": "gepubliceerd",
5050
"archived": "gearchiveerd",
5151
"modified since last publish": "gewijzigd sinds laatste publicatie",
52-
"Editor": "Editor",
52+
"Editor": "Bewerker",
5353
"Version history": "Versiegeschiedenis",
5454
"Diff": "Vergelijken",
5555
"Published version {uuid}": "Versie {uuid} gepubliceerd",
@@ -62,13 +62,29 @@
6262
"Roll back to version {version}?": "Terugzetten naar versie {version}?",
6363
"Rolling back copies this snapshot's manifest onto the current draft. Existing history is preserved (append-only).": "Bij terugzetten wordt het manifest van deze snapshot op het huidige concept geplaatst. Bestaande geschiedenis blijft behouden (alleen toevoegen).",
6464
"Roll back": "Terugzetten",
65-
"Cancel": "Annuleren",
6665
"Manifest diff": "Manifest-vergelijking",
6766
"Nothing to diff — publish the app first.": "Niets te vergelijken — publiceer de app eerst.",
6867
"From": "Van",
6968
"To": "Naar",
7069
"Current draft": "Huidig concept",
71-
"Loading diff…": "Vergelijking laden…"
70+
"Loading diff…": "Vergelijking laden…",
71+
"Role": "Rol",
72+
"Owner": "Eigenaar",
73+
"Viewer": "Lezer",
74+
"No access": "Geen toegang",
75+
"No applications available — ask an owner to grant you access.": "Geen applicaties beschikbaar — vraag een eigenaar om toegang.",
76+
"Permissions": "Rechten",
77+
"Manage permissions": "Rechten beheren",
78+
"Configure which Nextcloud groups can view, edit, or own this virtual app. Members of any of these groups will see the app in their list; only owners may publish, archive, delete, transfer ownership, or change these permissions.": "Stel in welke Nextcloud-groepen deze virtuele app mogen bekijken, bewerken of bezitten. Leden van deze groepen zien de app in hun lijst; alleen eigenaren mogen publiceren, archiveren, verwijderen, eigenaarschap overdragen of deze rechten wijzigen.",
79+
"Owners (full control)": "Eigenaren (volledige controle)",
80+
"Editors (can save drafts)": "Bewerkers (kunnen concepten opslaan)",
81+
"Viewers (read-only)": "Lezers (alleen-lezen)",
82+
"Save permissions": "Rechten opslaan",
83+
"Saving permissions…": "Rechten opslaan…",
84+
"Cancel": "Annuleren",
85+
"At least one owner group is required — saving with no owners would orphan this application.": "Minimaal één eigenaar-groep is vereist — opslaan zonder eigenaren zou deze applicatie wees maken.",
86+
"Editor or owner role required to save the manifest.": "Bewerker- of eigenaar-rol vereist om het manifest op te slaan.",
87+
"Your access to this application was revoked. The list will refresh.": "Je toegang tot deze applicatie is ingetrokken. De lijst wordt vernieuwd."
7288
},
7389
"plurals": ""
7490
}

0 commit comments

Comments
 (0)