Skip to content

[OpenSpec] [openregister] rbac-disable-public-inheritance #1439

@rjzondervan

Description

@rjzondervan

Summary

OpenRegister's RBAC currently treats public as universally inclusive — every read rule that targets public is also evaluated for authenticated users. This is the explicit "logged-in users should also have at least the same rights as 'public' users" semantics in PermissionHandler::hasPermission (line 229-241) and the matching qualification logic in MagicRbacHandler::processConditionalRule (if ($group === 'public') $userQualifies = true).

That's the right default for most schemas — but for tiered visibility flows (a public catalogue with date-windowed visibility plus a separate authenticated curated view) and for privacy-strict schemas (access earned only through explicit group membership), inheritance leaks unintended access.

This change adds an opt-out: an inheritFromPublic boolean (default true) at the authorization-block level (schema-level, with register-level cascade and tenant-wide IAppConfig default). When false, authenticated users do NOT qualify for public rules — they qualify only via their own group memberships. Anonymous users see no behaviour change. Default stays true, so existing schemas are unaffected.

OpenSpec change directory: openspec/changes/rbac-disable-public-inheritance/

Specs

  • rbac-scopes (delta) — schema/register authorization gains inheritFromPublic boolean; PHP-side hasPermission and SQL-side applyRbacFilters honour the flag identically. Delta: specs/rbac-scopes/spec.md

Cross-app

No paired changes. Default true preserves current behaviour for all consumers (DocuDesk, OpenCatalogi, Softwarecatalog, Procest, Pipelinq, ZaakAfhandelApp). The OpenCatalogi PublicationsController flow that surfaced the original use case automatically benefits once a publication schema sets inheritFromPublic: false.

Tasks

1. resolveInheritFromPublic helper

  • 1.1 Add private array $cachedInheritFromPublic = []; field on PermissionHandler.php for per-request caching keyed by schema ID.
  • 1.2 Add public method resolveInheritFromPublic(Schema $schema): bool implementing the cascade: schema authorization → register authorization → IAppConfig openregister.rbac.inherit_from_public_default → hard-coded true. Treat null as "unset".
  • 1.3 Wire IAppConfig dependency.
  • 1.4 Cache resolved value per request.
  • 1.5 Unit-test the cascade (four levels + null=unset semantics).

2. PHP-side enforcement

  • 2.1 In PermissionHandler::hasPermission lines 229-241, wrap the inheritance fallback in if ($this->resolveInheritFromPublic($schema) === true).
  • 2.2 Confirm anonymous-user behaviour at lines 174-184 unchanged.
  • 2.3 Confirm owner / admin shortcuts unaffected.
  • 2.4 Unit-test the four-state matrix on hasPermission.
  • 2.5 Verify owner / admin grants regardless of flag.

3. SQL-side enforcement

  • 3.1 In MagicRbacHandler::applyRbacFilters, resolve inheritFromPublic once at the top.
  • 3.2 Pass the flag into processAuthorizationRuleprocessConditionalRule and processSimpleRule.
  • 3.3 Update processConditionalRule: when $group === 'public' AND inheritFromPublic === false AND $userId !== null, set $userQualifies = false.
  • 3.4 Update processSimpleRule: when $rule === 'public' AND inheritFromPublic === false AND $userId !== null, return false.
  • 3.5 Same updates in UNION-based path: buildRbacConditionsSql, processConditionalRuleSql.
  • 3.6 Unit-test applyRbacFilters four-state matrix.
  • 3.7 Unit-test buildRbacConditionsSql four-state matrix.

4. Schema entity / serialisation

  • 4.1 Confirm Schema::getAuthorization/setAuthorization round-trips preserve inheritFromPublic.
  • 4.2 Confirm Register::getAuthorization similarly preserves the field.
  • 4.3 No schema migration needed (additive JSON-level field).

5. Tenant default IAppConfig

  • 5.1 IAppConfig key openregister.rbac.inherit_from_public_default (read by helper).
  • 5.2 Document the key in RBAC docs.
  • 5.3 Validate boolean parsing (true/false/string variants) via getValueBool or equivalent.

6. Cross-app integration check

  • 6.1 Smoke-test against DocuDesk's existing RBAC-using flows.
  • 6.2 Smoke-test against OpenCatalogi's PublicationsController.
  • 6.3 Smoke-test against Softwarecatalog or another consuming app.

7. Unit + integration tests

  • 7.1 PermissionHandlerTest extension — four-state matrix + cascade resolution.
  • 7.2 MagicRbacHandlerTest extension — four-state matrix on both applyRbacFilters and buildRbacConditionsSql.
  • 7.3 Integration test: schema with inheritFromPublic: false + public-conditional read; verify anon allowed, auth denied without explicit group, auth allowed with explicit group.
  • 7.4 Integration test for cascade — register-level fallback honoured.
  • 7.5 Integration test for tenant default IAppConfig.

8. Documentation

  • 8.1 Extend rbac-scopes documentation with the new field, cascade, four-state matrix, authenticated-rule alternative.
  • 8.2 Add a worked example: publication-style schema with public-time-window read + inheritFromPublic: false.
  • 8.3 CHANGELOG entry under "Added".
  • 8.4 CHANGELOG entry under "Behavior changes" — flipping is deliberate opt-in.

9. Quality and verification

  • 9.1 Full unit test suite — clean.
  • 9.2 Static analysis (Psalm / PHPStan) — clean.
  • 9.3 Code style (PHPCS) — clean.
  • 9.4 Manual smoke against live stack: configure schema with inheritFromPublic: false and public-conditional read; verify the four-state matrix manually.
  • 9.5 Run openspec validate rbac-disable-public-inheritance — clean.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions