Skip to content
Open
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
159 changes: 159 additions & 0 deletions openspec/changes/migrate-role-routing-to-or-rbac/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Design: migrate-role-routing-to-or-rbac

## Context

Procest's current step routing flow (client-side, display-only):

```
User opens case detail / task list
→ workflow.js fetchAvailableTransitions()
→ loads all workflowStep / workflowTemplate objects (no server-side filter)
→ client resolves user's roles: case.roles[] → roleType UUID → roleType.name
→ filters transitions.allowedRoles contains user's roleType UUID
→ filters steps.assigneeRole === user's roleType UUID
→ result: filtered list displayed in UI
```

After migration, the flow MUST include server-side enforcement:

```
User requests step / transition data
→ ObjectService::getObjects(register, 'workflowStep', filters)
→ MagicRbacHandler::applyRbacFilters(queryBuilder, userId, groups)
→ evaluates schema.authorization block against user's NC group memberships
→ returns only steps the user is authorized to read
→ client-side display filtering may still run for UX purposes
but server already guarantees the result set is authorization-correct
```

## The roleType → NC Group ID Bridge

OR's RBAC evaluates Nextcloud group IDs. Procest's workflow definitions reference OR
`roleType` UUIDs. The bridge is the `ncGroupId` property on `roleType`:

```json
// roleType object in OR (procest register)
{
"name": "Vergunningverlener",
"caseType": "uuid-of-omgevingsvergunning",
"genericRole": "handler",
"ncGroupId": "vergunningverleners" // ← new required property
}
```

The `ncGroupId` is set by the procest admin when configuring role types in the admin settings
panel. It maps a domain-level role ("Vergunningverlener") to the NC group that holds that role
in the organization ("vergunningverleners"). One-to-one mapping is the common case; a roleType
with `ncGroupId: null` means "unassigned to a group" and is treated as accessible to all
authenticated users (matching the existing behaviour for roles with no assigneeRole configured).

## File-by-File Migration Plan

### lib/Settings/procest_register.json — ADD ncGroupId to roleType, ADD authorization to step schemas

**roleType schema** — add `ncGroupId` property:

```json
"ncGroupId": {
"type": "string",
"nullable": true,
"description": "Nextcloud group ID that holds this role. Used by OR RBAC to enforce step-level access. Must be a valid NC group ID (IGroupManager::groupExists returns true). Null = role not yet mapped to a group."
}
```

**workflowStep schema** — add an `authorization` block referencing the resolved group. The
authorization block is dynamic — it references a named role defined at the register level
that OR expands at query time:

```json
"x-authorization": {
"read": [{ "role": "step.assigneeGroup" }],
"update": [{ "role": "step.assigneeGroup" }],
"delete": [{ "role": "admin" }]
}
```

The named role `step.assigneeGroup` is resolved by OR's RBAC from the step's
`assigneeRole` → `roleType.ncGroupId` chain. If `ncGroupId` is null (role not yet mapped),
OR RBAC falls back to "accessible to all authenticated users in the register."

Note: if OR's register-level named role expansion is not yet implemented in `rbac-scopes`,
an interim approach is to add the `authorization` block as a static list of group IDs
populated when a workflow is published. The design.md will track which approach is used
during the apply phase.

### lib/Settings/procest_register.json — procest_register authorization fallback

As an interim that works with the current `rbac-scopes` implementation:

Add a static `authorization` block to the `workflowStep` schema with a fallback group (e.g.
all users in the procest tenant group). This establishes the OR RBAC path. The dynamic
group-per-step expansion becomes a follow-on OR feature when `rbac-scopes` supports
register-level named role definitions.

### lib/Listener/KpiCacheInvalidationListener.php — VERIFY ONLY (no code change expected)

**Current**: listens on `ObjectCreatedEvent`, `ObjectUpdatedEvent`, `ObjectDeletedEvent`.
Calls KPI cache invalidation logic.

**Compliance check:**

1. Does the listener body call `$groupManager->isInGroup()` or any access decision? → Expected: NO.
2. Does the listener call `$objectService->getObject()` or similar OR reads on the event's
object before deciding to act? → If yes: verify these calls respect OR RBAC (i.e. the
listener runs as a system user with read access to the relevant register, not as the
triggering user).
3. Does the listener emit to any parallel audit store? → Expected: NO (covered by
`consume-or-audit-trail-fleet-wide`).

If all three checks pass: document compliance in spec. If any fails: file a corrective task.

### src/views/settings/components/StepConfigPanel.vue — ADD ncGroupId display

The admin settings panel for step configuration (`StepConfigPanel.vue`) shows step properties.
Add a read-only display field "NC Group" that shows the `ncGroupId` of the step's resolved
roleType. No edit in this panel — group mapping is configured on the roleType object's own
settings page.

### openspec/specs/role-based-step-routing/spec.md — NO CHANGES

The existing spec body is not modified. This migration changes the enforcement mechanism;
it does not change the observable requirements. The spec's scenarios remain valid as-is
(Vergunningverlener sees their steps; Behandelaar does not).

## Backwards Compatibility

- The `assigneeRole` and `allowedRoles` UUID references in stored workflow definitions are
NOT changed. They remain roleType UUIDs for import/export portability.
- The client-side role filter in workflow.js continues to run for UX purposes (instant filter
without a round-trip). OR RBAC is the enforcement layer; client filtering is convenience.
- roleTypes that do not yet have an `ncGroupId` set behave as before: accessible to all
authenticated users on the case (no group restriction applied by OR RBAC).

## OR RBAC Authorization Block Format Reference

From `openregister/openspec/specs/rbac-scopes/spec.md`:

The `authorization` JSON block in a schema definition follows the four-level hierarchy:
register > schema > object > property. Schema-level scopes control CRUD per schema.
Object-level scopes use `match` conditions for row-level refinement.

Group IDs in the block are evaluated by `PermissionHandler::hasGroupPermission()` which calls
`OCP\IGroupManager::isInGroup($userId, $groupId)` — the single trusted NC group membership
check.

## Seed Data

No new register definitions are added. Changes to `procest_register.json`:
- `roleType` schema: adds `ncGroupId` (nullable string, no migration needed — existing rows
simply have the field absent/null)
- `workflowStep` schema: adds `authorization` block (applied to new queries only; existing
data rows unaffected until an admin configures the `ncGroupId` mappings)

## Related ADRs

- **ADR-022** (primary) — mandate for this migration; "app-local RBAC on OR objects" anti-pattern.
- **ADR-023** — action RBAC vs data RBAC boundary; step routing computation (app-side) vs
step access enforcement (OR-side).
- **ADR-005** (security) — per-object authorization; all data fetches through OR's ObjectService.
- **ADR-008** (testing) — PHPUnit + integration tests required for the new enforcement path.
94 changes: 94 additions & 0 deletions openspec/changes/migrate-role-routing-to-or-rbac/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Proposal: migrate-role-routing-to-or-rbac

## Why

ADR-022 (Apps Consume OpenRegister Abstractions) explicitly prohibits "app-local RBAC on OR
objects — an app defining its own role/permission scheme for objects that live in OR's register."

Procest's role-based step routing currently has the correct consumer contract for computing
which group should handle a step (the existing `role-based-step-routing` spec), but the
access enforcement layer is missing:

- **`assigneeRole` and `allowedRoles` in workflow step / transition objects** carry OR
`roleType` UUIDs. When the frontend (workflow.js) filters transitions and tasks, it does
so entirely client-side by comparing the user's resolved roles against these UUIDs. No
OR-side `authorization` block on the step schema gates server-side access.
- **`KpiCacheInvalidationListener`** listens on `ObjectCreatedEvent` / `ObjectUpdatedEvent` /
`ObjectDeletedEvent` and correctly invalidates caches. This listener is compliant; it makes
no access decisions. It is documented here only for completeness.
- **No parallel permission table or service exists** — the violation is the absence of OR
RBAC enforcement, not the presence of a parallel implementation. Client-side role filtering
is display-only and provides no security boundary.

The umbrella spec `consume-or-rbac-fleet-wide` (hydra) mandates that role-based access on
OR-owned objects must be enforced by OR's RBAC stack (rbac-scopes + auth-system), using
Nextcloud group IDs as the canonical role identifier.

## What

Bring procest's step routing into full OR RBAC compliance:

1. **Resolve roleType UUIDs to NC group IDs** at enforcement time. Each `roleType` OR object
SHALL carry a `ncGroupId` property (a Nextcloud group ID). The routing service reads this
field and builds the NC group ID that OR's RBAC uses for enforcement.
2. **Add OR `authorization` blocks to the `workflowStep` and `workflowTemplate` schemas** in
`procest_register.json` so that OR's `MagicRbacHandler` filters step objects at the
database level based on the requesting user's group memberships.
3. **Verify `KpiCacheInvalidationListener`** listens only on OR's published object events and
makes no access decisions. Document compliance in this spec. No code change expected.
4. **Preserve the consumer contract**: the `role-based-step-routing` spec body is NOT modified.
The enforcement mechanism changes; the observable behaviour (a Vergunningverlener sees
their steps; a Behandelaar does not) is preserved or improved (now enforced server-side).
5. **Tests**: verify that step objects filtered by OR's RBAC at the API level match the set
previously produced by client-side role filtering.

## Capabilities

### New Capabilities

- `role-routing-via-or-rbac`: Step and transition routing decisions are enforced server-side
via OR's RBAC stack. `GET /api/objects/{register}/workflowStep` returns only steps the
requesting user is authorized to access per the schema's `authorization` block.

### Modified Capabilities

- `role-based-step-routing` (existing spec) — no body changes. The underlying enforcement
mechanism changes from client-side filtering to OR server-side RBAC, but all observable
requirements remain valid.

## Affected Projects

- [x] Project: `procest` — all implementation work is in this repo
- Reference: `hydra/openspec/changes/consume-or-rbac-fleet-wide/` (umbrella policy)
- Reference: `openregister/openspec/specs/rbac-scopes/spec.md` (OR RBAC contract)
- Reference: `openregister/openspec/specs/auth-system/spec.md` (OR auth contract)

## Scope

### In Scope

- Adding `ncGroupId` property to the `roleType` schema in `procest_register.json`
- Adding `authorization` blocks to `workflowStep` and `workflowTemplate` schemas
- Verifying `KpiCacheInvalidationListener` compliance and documenting it
- Tests verifying that OR RBAC correctly restricts step access by NC group membership
- Admin UI: surfacing `ncGroupId` as a configurable field on roleType objects in admin settings

### Out of Scope

- Modifying the `role-based-step-routing` spec body (constraint from umbrella)
- Converting stored `assigneeRole` / `allowedRoles` UUID values in existing workflow
definitions (those remain UUID references; only the enforcement layer changes)
- Procest's parafering role-gating — addressed by `consume-or-approval-workflow-fleet-wide`
- Procest's `roles-decisions` domain (role assignment as participation record) — this is
correct OR consumer usage, not a parallel RBAC scheme
- Modifying OR's `rbac-scopes` or `auth-system` specs

## Success Criteria

- `openspec validate --strict migrate-role-routing-to-or-rbac` exits 0.
- `roleType` schema in `procest_register.json` includes an `ncGroupId` property.
- `workflowStep` schema includes an `authorization` block referencing NC group IDs resolved
from the step's `assigneeRole` `roleType` object.
- `GET /api/objects/{register}/workflowStep` returns HTTP 403 / empty list for a user whose
NC group is not in the step's resolved authorization set.
- `composer check:strict` passes.
Loading
Loading