Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ phpqa_output.log
**/update*Settings*
**/rebase*
**/setup*
!tests/vitest/setup.js
!tests/e2e/global-setup.ts

# Temporary test files that shouldn't be committed
simple-solr-test.php
Expand Down
24 changes: 24 additions & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,28 @@ Vrij en open source onder de EUPL-1.2-licentie.
<php min-version="8.1"/>
</dependencies>

<!--
openbuilt.use navigation-entry gate (REQ-OBRBAC-006).

SPEC INTENT — REQ-OBRBAC-006 calls for a <permission>openbuilt.use</permission>
child on the <navigation> element so admins can restrict the top-bar
entry to selected groups. We tried shipping that element and verified
2026-05-11 with `occ app:enable openbuilt --force` on Nextcloud 32 that
the upstream apps/info.xsd schema rejects the `<permission>` child
("appinfo file cannot be read"). The schema does not yet expose this
sub-element; an upstream issue is filed at:
https://github.com/nextcloud/server/issues/60310

FALLBACK — per design.md Decision 4 we rely on Nextcloud's standard
app-level group restriction:
occ app:enable openbuilt --groups team-alpha
which configures group visibility outside info.xml. Per-Application
RBAC enforced server-side by ApplicationsController::getManifest +
::listMine (REQ-OBRBAC-002 / REQ-OBR-006) remains the load-bearing
security boundary; this gate is coarse top-bar visibility only.

See docs/openbuilt-rbac.md for the operator guide.
-->
<navigations>
<navigation>
<id>openbuilt</id>
Expand All @@ -62,10 +84,12 @@ Vrij en open source onder de EUPL-1.2-licentie.
<install>
<step>OCA\OpenBuilt\Repair\InitializeSettings</step>
<step>OCA\OpenBuilt\Repair\SeedHelloWorld</step>
<step>OCA\OpenBuilt\Repair\PopulateApplicationPermissions</step>
</install>
<post-migration>
<step>OCA\OpenBuilt\Repair\InitializeSettings</step>
<step>OCA\OpenBuilt\Repair\SeedHelloWorld</step>
<step>OCA\OpenBuilt\Repair\PopulateApplicationPermissions</step>
</post-migration>
</repair-steps>
</info>
9 changes: 9 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
// Health check endpoint.
['name' => 'health#index', 'url' => '/api/health', 'verb' => 'GET'],

// RBAC-filtered Application list (openbuilt-rbac REQ-OBRBAC-002 / REQ-OBR-007).
// OR's schema-level read rule is a coarse group ACL — not a row-level filter on the
// Application's `permissions` block — so the editor list MUST go through this
// endpoint, NOT directly through `/apps/openregister/api/objects/openbuilt/application`,
// which would leak every Application + permissions to every authed user (IDOR).
// Listed BEFORE the {slug} route so the wildcard does not shadow it (Symfony router
// is order-sensitive when prefix overlaps).
['name' => 'applications#listMine', 'url' => '/api/applications', 'verb' => 'GET'],

// Manifest endpoint — returns the stored manifest JSON blob for a given virtual-app slug.
// Per ADR-016 routes.php is the only registration path; #[NoAdminRequired] is set on the
// controller method so auth-required-but-non-admin users can hit it (per design.md Decision 6).
Expand Down
166 changes: 166 additions & 0 deletions docs/openbuilt-rbac.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# OpenBuilt RBAC — Per-Virtual-App Permissions

OpenBuilt's per-virtual-app role-based access control (RBAC) layers on
top of OpenRegister's organisation-scoping (ADR-022). Within an
organisation, three roles partition who can do what with each
Application: `owner`, `editor`, `viewer`.

## Roles

| Action | viewer | editor | owner |
| ------------------------------------- | :----: | :----: | :---: |
| Read manifest / browse Application | yes | yes | yes |
| Save manifest draft | no | yes | yes |
| Publish (`draft → published`) | no | no | yes |
| Archive (`published → archived`) | no | no | yes |
| Re-open (`archived → draft`) | no | no | yes |
| Edit `permissions` | no | no | yes |
| Transfer ownership | no | no | yes |
| Delete Application | no | no | yes |

Roles are keyed by Nextcloud group ID, stored declaratively on the
Application schema:

```json
{
"permissions": {
"owners": ["team-alpha"],
"editors": ["team-alpha", "qa-shared"],
"viewers": ["everyone"]
}
}
```

A caller's effective role is computed by intersecting their Nextcloud
group membership with the three buckets, taking the highest-privilege
match.

## Default on creation

New Applications default `permissions.owners` to the **creator's
primary Nextcloud group**; `editors` and `viewers` start empty. If the
creator has no group memberships, `owners` falls back to `['admin']`
so the Application is never orphaned (REQ-OBRBAC-001).

## Manifest endpoint enforcement

`GET /index.php/apps/openbuilt/api/applications/{slug}/manifest`
deny-by-defaults to `403 Forbidden` when the caller has no group in
the union of the three buckets (REQ-OBR-006, REQ-OBRBAC-002). The
check runs before the manifest payload is emitted — the 403 body
never leaks Application metadata.

Error envelope:

```json
{ "error": "forbidden", "code": "openbuilt.rbac.no_role" }
```

## Admin bypass (audited)

A user in the Nextcloud `admin` group can read any Application's
manifest as an incident-response escape hatch. Every exercise of the
bypass writes an audit-trail event with shape:

```
event: rbac.admin_bypass
actor: <admin uid>
slug: <application slug>
ts: <ISO 8601 timestamp>
```

The bypass:

- Runs **only** in `ApplicationsController::getManifest`. The frontend
list filter does **not** include admins automatically.
- Is logged at `info` level on the OpenBuilt PSR logger (where OR's
audit-trail will pick it up via Nextcloud's logging pipeline).
- Is narrow by design — sustained bypass volume from one admin is a
signal to grant them an explicit role on the affected Applications.

## List filter

The OpenBuilt shell's Application list (`ApplicationEditor.vue`)
filters out Applications on which the caller has no role
(REQ-OBR-007). The filter runs client-side using
`loadState('openbuilt', 'currentUserGroups')` (no DOM data-attribute
reads — ADR-004 hard rule `gate-initial-state`).

A future enhancement (DQ-1, see below) will move the filter to
OR-side once `x-openregister-authorization` supports group-membership
predicates.

## Permissions modal

Owner-only modal at `src/modals/PermissionsModal.vue` (per ADR-004
`gate-modal-isolation`). Three NcSelect group pickers, all carrying
the required `input-label` prop (ADR-004 `gate-nc-input-labels`).
Frontend rejects an `owners = []` save before sending — orphan-check
guard per REQ-OBRBAC-005.

## Transfer ownership

A transfer is a single PUT to the Application's `permissions.owners`
array. No dedicated endpoint, no `TransferOwnershipService`. OR's
per-object audit trail records the before / after values
automatically.

## openbuilt.use navigation gate

Nextcloud's per-app group restriction (Apps → OpenBuilt → Restrict to
groups) gates visibility of the OpenBuilt top-bar entry. Default is
no restriction (all authenticated users see it); admins can narrow it
via the standard Apps panel or OCC:

```bash
occ app:enable openbuilt --groups digital-team
```

This is coarse on/off visibility; the load-bearing security boundary
is the per-Application `permissions` enforced server-side.

## Operational caveats

### Group renames

If a Nextcloud admin renames a group, every Application whose
`permissions` array references the old `gid` loses or gains rows
without a permission-history audit event scoped to OpenBuilt. We do
not (currently) ship a group-rename listener. If a rename breaks
access:

1. Edit the affected Application's `permissions` via the Permissions
modal (as owner).
2. Replace the stale group references with the new GID.

Tracked as design.md OQ-2; revisit if customers report breakage.

### Post-deploy "ACTION REQUIRED: re-grant access"

After upgrading to this version every pre-existing Application is
patched to `permissions.owners = ['admin']` so it is only readable by
the `admin` group. Operators MUST re-grant access for non-admin teams
via the Permissions modal:

1. Sign in as an admin user.
2. Navigate to OpenBuilt → Applications.
3. For each Application that should be broadly accessible, open the
Permissions modal and add the relevant Nextcloud groups to
`owners`, `editors`, or `viewers`.

The `hello-world` demo follows the same default (admin-only). Grant
`viewers = ['users']` to restore broad visibility for the demo.

## Deferred Questions

- **DQ-1 — OR `x-openregister-authorization` group-membership predicate.**
Investigated 2026-05-11; the current OpenRegister REST API does
**not** expose a `groupIn` / `groupMember` predicate that can be
parameterised by a pointer into the object's own `permissions`
block. Until OR ships it, the frontend list filter remains the
fallback path; the controller's 403 check is the load-bearing
enforcement. Tracked as design.md OQ-1.
- **DQ-2 — Group rename listener.** Punted; documented above.
- **DQ-3 — Permission-history retention.** Defer to OR-register-level
retention configuration (Conduction's compliance baseline pins this
at the OR-register level).
20 changes: 18 additions & 2 deletions l10n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,29 @@
"Roll back to version {version}?": "Roll back to version {version}?",
"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).",
"Roll back": "Roll back",
"Cancel": "Cancel",
"Manifest diff": "Manifest diff",
"Nothing to diff — publish the app first.": "Nothing to diff — publish the app first.",
"From": "From",
"To": "To",
"Current draft": "Current draft",
"Loading diff…": "Loading diff…"
"Loading diff…": "Loading diff…",
"Role": "Role",
"Owner": "Owner",
"Viewer": "Viewer",
"No access": "No access",
"No applications available — ask an owner to grant you access.": "No applications available — ask an owner to grant you access.",
"Permissions": "Permissions",
"Manage permissions": "Manage 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.": "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.",
"Owners (full control)": "Owners (full control)",
"Editors (can save drafts)": "Editors (can save drafts)",
"Viewers (read-only)": "Viewers (read-only)",
"Save permissions": "Save permissions",
"Saving permissions…": "Saving permissions…",
"Cancel": "Cancel",
"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.",
"Editor or owner role required to save the manifest.": "Editor or owner role required to save the manifest.",
"Your access to this application was revoked. The list will refresh.": "Your access to this application was revoked. The list will refresh."
},
"plurals": ""
}
22 changes: 19 additions & 3 deletions l10n/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"published": "gepubliceerd",
"archived": "gearchiveerd",
"modified since last publish": "gewijzigd sinds laatste publicatie",
"Editor": "Editor",
"Editor": "Bewerker",
"Version history": "Versiegeschiedenis",
"Diff": "Vergelijken",
"Published version {uuid}": "Versie {uuid} gepubliceerd",
Expand All @@ -62,13 +62,29 @@
"Roll back to version {version}?": "Terugzetten naar versie {version}?",
"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).",
"Roll back": "Terugzetten",
"Cancel": "Annuleren",
"Manifest diff": "Manifest-vergelijking",
"Nothing to diff — publish the app first.": "Niets te vergelijken — publiceer de app eerst.",
"From": "Van",
"To": "Naar",
"Current draft": "Huidig concept",
"Loading diff…": "Vergelijking laden…"
"Loading diff…": "Vergelijking laden…",
"Role": "Rol",
"Owner": "Eigenaar",
"Viewer": "Lezer",
"No access": "Geen toegang",
"No applications available — ask an owner to grant you access.": "Geen applicaties beschikbaar — vraag een eigenaar om toegang.",
"Permissions": "Rechten",
"Manage permissions": "Rechten beheren",
"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.",
"Owners (full control)": "Eigenaren (volledige controle)",
"Editors (can save drafts)": "Bewerkers (kunnen concepten opslaan)",
"Viewers (read-only)": "Lezers (alleen-lezen)",
"Save permissions": "Rechten opslaan",
"Saving permissions…": "Rechten opslaan…",
"Cancel": "Annuleren",
"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.",
"Editor or owner role required to save the manifest.": "Bewerker- of eigenaar-rol vereist om het manifest op te slaan.",
"Your access to this application was revoked. The list will refresh.": "Je toegang tot deze applicatie is ingetrokken. De lijst wordt vernieuwd."
},
"plurals": ""
}
Loading
Loading