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
13 changes: 12 additions & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,18 @@ Vrij en open source onder de EUPL-1.2-licentie.

**Ondersteuning:** Voor ondersteuning, neem contact op via support@conduction.nl.
]]></description>
<version>0.4.6</version>
<!--
Bumped for the register authorization baseline (0.7.0 -> 0.8.0 on the
register row). The <post-migration> repair steps below, InitializeSettings
among them, run only on `occ upgrade`, and `occ upgrade` is a NO-OP when
the app version has not moved — so without this bump the security fix
would sit in the JSON on every existing instance and never be imported.
The register's OWN version bump is separately required: ImportHandler's
register path (importRegister, ~line 879) skips outright when the incoming
version is <= the stored one, and unlike the SCHEMA path it has no
content-differs escape hatch to fall back on.
-->
<version>0.4.7</version>
<licence>EUPL-1.2</licence>
<author mail="info@conduction.nl" homepage="https://www.conduction.nl/">Conduction</author>
<namespace>Decidesk</namespace>
Expand Down
24 changes: 22 additions & 2 deletions lib/Settings/decidesk_register.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"info": {
"title": "Decidesk Register",
"description": "Universal decision-making platform for governance bodies, associations, corporate boards, and operational meetings",
"version": "0.7.0"
"version": "0.8.0"
},
"x-openregister": {
"type": "application",
Expand Down Expand Up @@ -1386,7 +1386,27 @@
"slug": "decidesk",
"title": "Decidesk Register",
"description": "Universal decision-making platform for governance bodies, associations, corporate boards, and operational meetings. Holds every governance body, meeting, agenda item, decision, vote, minutes, consultation and integrity object the app reads and writes.",
"version": "0.7.0",
"version": "0.8.0",
"_authorizationNote": "REGISTER-LEVEL BASELINE, and it is a BEHAVIOUR CHANGE. Until this block existed, every decidesk schema without its own `authorization` block was read AND WRITE open to every authenticated user through OpenRegister's own /apps/openregister/api/objects/decidesk/<schema> API — the API the frontend uses directly under ADR-022, which no decidesk controller guard sits in front of. OpenRegister's PermissionHandler::hasGroupPermission() tests `empty($authorization)`, and PHP's empty() is true for null and [] alike, so an ABSENT block takes the same default-OPEN branch as an empty one; `enforce_default_closed` defaults to false and, even when enabled, closes only create/update/delete. Measured on this tree before the change: 93 schemas, 24 carrying a block (all read-only), this register row carrying none — so 69 schemas, including Decision, VotingRound, Vote, Participant and EngagementRecord, granted create/update/delete to any logged-in account. Same shape as docudesk#631, where a plain user overwrote another user's template. THE CASCADE IS WHY THIS SITS ON THE REGISTER ROW: PermissionHandler::resolveAuthorization() uses a schema's own block when it has one and falls back to the register's only when it does not, so this baseline reaches exactly the 69 unprotected schemas and changes nothing for the 24 that already declare their own (their public-read rules are untouched). WHAT EACH ACTION IS FOR: read and list name BOTH `authenticated` AND `public`, and create names `authenticated`, so no read goes dark and any member can still raise a decision, cast a vote or file a reaction — those flows are unchanged. `public` on the READ actions is not a widening: before this block existed the default-OPEN branch granted every action to every principal INCLUDING the anonymous one, so omitting `public` here would have CLOSED anonymous reads — a separate and much larger policy change than the write hole this block exists to fix, and one that would 403 every #[PublicPage] citizen-participation surface. CI proved it rather than theory: the first version of this block omitted `public` and all six PHPUnit legs failed with `User 'Anonymous' does not have permission to 'read' objects in schema 'Meeting'`. `public` appears on NO write action, so the anonymous fail-closed rule from openregister#1955 keeps denying anonymous create/update/delete exactly as it did before. update/delete are NOT granted to `authenticated`, which is the whole fix: OpenRegister bypasses the object OWNER unconditionally and SQL-side before any rule is consulted, and bypasses `admin` too, so the author of an object keeps full control of it and only OTHER users lose the ability to rewrite or destroy it. `decidesk-administrators` is the named Nextcloud group for a griffie/secretariat that must edit records it did not create; it is admin-provisioned, and on an instance where it does not exist this simply resolves to owner-plus-admin, which is fail-closed. EVERY ACTION IS WRITTEN OUT ON PURPOSE: once a block is non-empty, OpenRegister DENIES any action the block omits, so an omitted `list` or `create` here would silently break the app rather than secure it. Per-body actor authorization (chair / signatory scopes, x-decidesk-rbac-scopes above) is a separate layer and is unaffected — OpenRegister cannot template a group name per object, which is why that determination is made at the app boundary.",
"authorization": {
"read": [
"authenticated",
"public"
],
"list": [
"authenticated",
"public"
],
"create": [
"authenticated"
],
"update": [
"decidesk-administrators"
],
"delete": [
"decidesk-administrators"
]
},
"schemas": [
"action-item",
"advies",
Expand Down
34 changes: 34 additions & 0 deletions openspec/specs/authorization-via-or-rbac/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,37 @@ caller as "check skipped".

@e2e exclude fail-closed edge (unresolvable body scope) with no distinct UI flow; unit-proven in GovernanceScopeGuardTest (fails closed when the body is unresolvable / on OR error) and MeetingServiceTest (chair-only transition denied when the governanceBody cannot be resolved).


### Requirement: REQ-RBAC-006 The register declares an authorization baseline so an absent block cannot grant writes
The `decidesk` register row SHALL declare an `authorization` block naming EVERY canonical
OpenRegister action (`read`, `list`, `create`, `update`, `delete`). `read`, `list` and `create`
SHALL be granted to `authenticated`; `update` and `delete` SHALL NOT be, so that a user who is
neither the object's owner, nor a Nextcloud admin, nor a member of the named administrator group
cannot rewrite or destroy another user's decidesk object through OpenRegister's own
`/apps/openregister/api/objects/decidesk/<schema>` API. Schemas that declare their own
`authorization` block SHALL keep it — OpenRegister resolves the schema block first and falls back to
the register's only when a schema has none — and those blocks SHALL continue to name read actions
only. The register version, the configuration version and the app version SHALL all be bumped in the
same change, because the register import skips on a non-newer version with no content fallback and
the `<post-migration>` repair step that performs the import runs only on `occ upgrade`.

#### Scenario: A non-owner cannot rewrite another user's object
- **GIVEN** a Decision created by user A, and user B who is not an admin and not in the
administrator group
- **WHEN** user B issues an update or delete against that Decision through OpenRegister's object API
- **THEN** OpenRegister denies the write
- **AND WHEN** user A issues the same write on their own object
- **THEN** OpenRegister permits it via the unconditional owner bypass.

#### Scenario: Reads and creates are unchanged
- **GIVEN** any authenticated user
- **WHEN** the user lists or reads decidesk objects, or creates a new one
- **THEN** the action is permitted exactly as before the baseline was declared.

#### Scenario: The baseline names every action
- **GIVEN** the shipped register row
- **WHEN** its `authorization` block is read
- **THEN** every canonical action is named with a non-empty rule list, because OpenRegister denies
any action a non-empty block omits — an unnamed action would break the app rather than secure it.

@e2e exclude The assertion is a per-user DENIAL by OpenRegister's own permission evaluator against a declaration this repo ships, and the owner bypass is unconditional and SQL-side — so a browser test driven by a single seeded (and therefore owning, usually admin) session cannot observe it at all, and would report success over the exact hole. Pinned by `tests/Unit/RegisterAuthorizationTest.php` on the declaration side; the per-user behaviour needs a two-account probe against a live instance, recorded in the PR as verification owed rather than claimed.
Loading
Loading