Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4eaecb4
feat(openspec): rbac-disable-public-inheritance
rjzondervan May 7, 2026
3c05b62
feat(rbac): implement inheritFromPublic flag (#1439)
rjzondervan May 7, 2026
cd56112
feat(rbac): expose inheritFromPublicDefault in settings UI (#1439)
rjzondervan May 7, 2026
40cfc90
fix(rbac): wire inheritFromPublic through dedicated endpoint and vali…
rjzondervan May 7, 2026
4194dc8
docs(rbac): mark live-stack smoke checks done in tasks.md (#1439)
rjzondervan May 7, 2026
7d7d491
test(rbac): add SQL-side inheritFromPublic matrix tests (#1439)
rjzondervan May 7, 2026
15ffd5f
docs(rbac): document inheritFromPublic flag and tenant default (#1439)
rjzondervan May 7, 2026
cad0e3b
Fix composer vulnerability
rjzondervan May 7, 2026
21d3e6a
Updated package
remko48 May 7, 2026
6d18b50
fix(rbac): address PR #1440 review — strict bools, no fail-open, CSRF…
rjzondervan May 7, 2026
5122080
chore(rbac): phpcs auto-fix in PermissionHandler — docblock spacing (…
rjzondervan May 7, 2026
69e3edc
Merge pull request #1441 from ConductionNL/hotfix/1439/rbac-disable-p…
rjzondervan May 7, 2026
91b7466
Release: merge development into beta for the fleet-wide beta checkup
Aug 20, 2026
95d2552
Merge pull request #2636 from ConductionNL/sync/dev-to-beta-20260820
rubenvdlinde Aug 20, 2026
eff429e
fix(beta): remove the orphaned inheritFromPublicDefault RBAC control
Aug 20, 2026
f006dc5
Merge pull request #2640 from ConductionNL/fix/beta-orphaned-inheritf…
rubenvdlinde Aug 20, 2026
1597ca6
chore(release): 1.1.6-beta.20260820205738 [skip ci] (#2641)
github-actions[bot] Aug 26, 2026
55d83be
Release: merge development into beta
Aug 27, 2026
ca1dbb2
Merge pull request #2909 from ConductionNL/hotfix/beta-sync-20260827
rubenvdlinde Aug 27, 2026
215e823
merge: development into beta
Aug 28, 2026
4545b8e
release: merge development into beta (#2972)
rubenvdlinde Aug 28, 2026
7c60b24
chore(release): sync beta back into development
Aug 29, 2026
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
138 changes: 136 additions & 2 deletions docs/Features/access-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,10 +448,17 @@ sequenceDiagram
"create": ["admin", "editors"],
"read": ["admin", "editors", "viewers", "public"],
"update": ["admin", "editors"],
"delete": ["admin"]
"delete": ["admin"],
"inheritFromPublic": true
}
```

The optional `inheritFromPublic` boolean controls whether authenticated users
qualify for `public` rules on this schema. It defaults to `true` (the
pre-change behaviour). See [Disabling public-group inheritance for
authenticated users](#disabling-public-group-inheritance-for-authenticated-users-inheritfrompublic)
below.

**Object Authorization Field:**
- Stored in `oc_openregister_objects.authorization` (JSON)
- Inherits from schema but can be overridden per-object
Expand Down Expand Up @@ -600,6 +607,124 @@ All three enforcement points route conditional match evaluation through the shar

A schema authored with `{ "read": [{ "group": "public", "match": { "publishDate": { "$lte": "$now" } } }] }` returns the same object set from `GET /api/objects/{register}/{schema}` (list) and `GET /api/objects/{register}/{schema}/{id}` (find). List-vs-find drift caused by differing grammar is no longer possible.

### Disabling public-group inheritance for authenticated users (`inheritFromPublic`)

By default, authenticated users qualify for any rule that targets the `public`
group — they inherit at least the rights of an anonymous visitor. This is
convenient for most schemas, but it gets in the way of two patterns:

1. **Privacy-strict schemas** where authentication is meant to be a strict
gate, not a superset of public access. For example: a schema where the
public group can only see redacted/anonymised rows via a conditional
rule, but logged-in users should be channelled through a different
curated view rather than seeing the same redacted set.
2. **Tiered visibility flows** where a public catalogue uses a date-windowed
`match` (e.g. `publishedAt $lte $now`) and a separate authenticated
curated view uses its own group rule. With public inheritance on, the
authenticated view leaks the public catalogue rows.

The optional `inheritFromPublic` boolean on the authorization block of a
schema or register lets a tenant opt out of the inherit-from-public
behaviour. When `false`, authenticated users no longer qualify for `public`
rules on that schema/register; they must qualify via their own group
memberships. Anonymous users are unaffected — the flag does not change
what an unauthenticated visitor sees.

#### Cascade

The effective value is resolved per schema, walking the cascade until the
first explicitly-set value is found:

1. The schema's own `authorization.inheritFromPublic`.
2. The parent register's `authorization.inheritFromPublic`.
3. The tenant-wide IAppConfig key `openregister.rbac.inherit_from_public_default`
(read via `IAppConfig::getValueBool`, which accepts `true`/`false`/`"true"`/
`"false"`/`"1"`/`"0"`/`1`/`0` at the storage layer).
4. Hard-coded `true` (preserves pre-change behaviour).

**Strict-boolean check at schema and register levels.** Steps 1 and 2 require
the stored value to be a literal `true` or `false` — anything else (string
forms, integers, mistyped JSON) is rejected as "unset" and the cascade
falls through, with a warning logged. This closes a foot-gun where a seed
write or migration that bypasses the schema validator could store the
string `"false"` and silently invert the gate (`(bool) "false"` is `true`).
Always store real JSON booleans on schema/register `authorization` blocks.
The IAppConfig layer keeps its broader tolerance because the `occ` /
operator surface intentionally accepts those forms.

`null` at any level is treated as "unset" — the cascade falls through to
the next level. The first explicit boolean wins; a schema that sets the
flag overrides its register and the tenant default.

The tenant default can be flipped from the OpenRegister settings UI under
**RBAC Configuration → Authenticated users inherit `public` group rights
(default)**, or via `occ config:app:set openregister rbac.inherit_from_public_default --value=false --type=boolean`.

#### Four-state matrix

For a schema with `read: [{ "group": "public", "match": <m> }]`:

| User | `inheritFromPublic` | Result |
|-------------------|---------------------|-----------------------------------------------------------------------|
| anonymous | `true` (default) | granted when `<m>` matches (pre-change behaviour) |
| anonymous | `false` | granted when `<m>` matches — anonymous users are unaffected |
| authenticated | `true` (default) | granted when `<m>` matches (authenticated user inherits public) |
| authenticated | `false` | denied unless the user qualifies via another rule (own group / owner / admin) |

Owner-shortcut and admin-bypass paths are unaffected by the flag — an
object's owner and any user in the `admin` group always have access
regardless of `inheritFromPublic`.

The PHP-side per-object check (`PermissionHandler::hasPermission`) and the
SQL-side listing filter (`MagicRbacHandler::applyRbacFilters` /
`buildRbacConditionsSql`) both honour the flag identically; per-object
checks and listing membership cannot drift.

#### Worked example: a publication-style schema with a curated authenticated view

A `publication` schema needs to be visible to anonymous visitors only after
its `publishedAt` timestamp, but logged-in editors should see the curated
in-progress queue (which is a different rule) instead of the public-time
window. Authenticated users without `editors` membership should see
**nothing** — they should not inherit the public window.

```json
{
"authorization": {
"inheritFromPublic": false,
"read": [
{ "group": "public", "match": { "publishedAt": { "$lte": "$now" } } },
{ "group": "editors", "match": { "status": { "$in": ["draft", "review"] } } }
]
}
}
```

Behaviour:

- An anonymous visitor sees rows where `publishedAt <= now` (public match
applies; the flag does not affect anonymous users).
- An `editors` member sees rows where `status` is `draft` or `review` (their
group rule applies). They do NOT inherit the public time-window because
`inheritFromPublic` is `false`.
- A logged-in user who is not in `editors` sees nothing on this schema.
They have no qualifying rule, and the flag prevents them from falling
back to the public match.
- An owner of a row sees it via the owner shortcut, regardless of the flag.

If the same schema were authored with `inheritFromPublic: true` (or the
field omitted), the third case would change: the non-`editors` logged-in
user would inherit the public window and see the same rows the anonymous
visitor sees.

#### When to reach for `'authenticated'` instead of `'public'`

`inheritFromPublic` governs the `public` group only. The simple-string
rule `'authenticated'` is a separate construct — it grants access to any
logged-in user, independent of the flag. If you want "any logged-in user,
no group filtering, regardless of `inheritFromPublic`", use
`{ "read": ["authenticated"] }` rather than relying on public-inheritance.

### Property-Level Authorization

In addition to the schema- and object-level rules above, individual properties can carry their own `authorization` block with conditional rules. This is covered in depth in [Property Authorization](./property-authorization.md); this section is a short map into that feature.
Expand Down Expand Up @@ -651,12 +776,21 @@ RBAC can be configured in Nextcloud app settings:
```json
{
"enabled": true,
"adminOverride": true
"adminOverride": true,
"inheritFromPublicDefault": true
}
```

- **`enabled`**: Master switch for RBAC system
- **`adminOverride`**: Allow users in 'admin' group to bypass all RBAC checks
- **`inheritFromPublicDefault`**: Tenant-wide default for the schema-level
`inheritFromPublic` flag. When `true` (default), authenticated users
qualify for `public` rules on any schema that does not explicitly set
the flag. When `false`, authenticated users must qualify via their own
group memberships unless a schema or register opts back in. Persisted
to the IAppConfig key `openregister.rbac.inherit_from_public_default`.
See
[Disabling public-group inheritance for authenticated users](#disabling-public-group-inheritance-for-authenticated-users-inheritfrompublic).

### Performance Optimizations

Expand Down
2 changes: 1 addition & 1 deletion docs/Features/property-authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Each rule combines a group check with an optional condition:

| Field | Meaning |
| --- | --- |
| `group` | A Nextcloud group the current user must belong to. The literal value `"public"` matches **any authenticated user** (including when no other group matches). |
| `group` | A Nextcloud group the current user must belong to. The literal value `"public"` matches anonymous users, and — by default — authenticated users as well. The schema-level `inheritFromPublic` flag (see [Access Control → inheritFromPublic](./access-control.md#disabling-public-group-inheritance-for-authenticated-users-inheritfrompublic)) lets a tenant turn off the authenticated-user inheritance per schema, register, or globally. |
| `match` | Optional map of conditions evaluated against the object. All conditions must be true for the rule to grant access. Omit `match` for an unconditional rule. |

A rule is satisfied when **both** the group check and every `match` condition pass.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-07
Loading
Loading