Skip to content

feat(security)!: permission attributes instead of ROLE_ADMIN (0.6.0) - #12

Merged
schmunk42 merged 5 commits into
masterfrom
feature/apiconfig-permissions
Oct 28, 2026
Merged

schmunk42 merged 5 commits into
masterfrom
feature/apiconfig-permissions

Conversation

@schmunk42

Copy link
Copy Markdown
Member

Summary

The bundle no longer checks a role. Every operation of ApiConfiguration checks a bundle-owned permission attribute, and an application decides through its own permission voter who holds it.

Permission Operations
apiconfig:read Get, GetCollection, health (also the resource-level fallback)
apiconfig:write Post, Put, Patch, Delete
apiconfig:authorize authorize, JSON and browser path
  • The constants live in Dmstr\ApiConfiguration\Security\ApiConfigurationPermissions and are tagged za7_core.permission in config/services.yaml. The bundle has no dependency on za7-core, no map and no config node.
  • AuthorizeRedirectListener now checks apiconfig:authorize before looking up the configuration. Before, any authenticated caller got the 303 redirect. Denied callers get 403, anonymous callers 401, with no 404 or 409 first.
  • symfony/security-bundle and symfony/expression-language move to require, because API Platform does not evaluate security expressions without them.
  • PHP >= 8.5.

Breaking changes

Without a voter granting apiconfig:*, every operation answers 403. The README has a migration section with a minimal voter. The CHANGELOG [0.6.0] lists all breaking changes.

Tests

A new HTTP test kernel under tests/ runs requests through the full API Platform pipeline. It uses SQLite in memory and header authentication via X-Test-Roles, X-Test-User and X-Test-Auth: anonymous.

  • A matrix of allowed and denied for all 8 operations, Accept variants on authorize, read masking and hydra:operation filtering.
  • A kernel without a voter, where everything is denied.
  • A registration test: the tagged permissions match ^[a-z][a-z0-9]*:[a-z][a-z0-9_]*$ and equal the attributes used on the operations.
  • Locally green on PHP 8.5.11: 114 tests, 241 assertions.

Commits are split: test infrastructure, permissions, browser-path hardening, PHP 8.5, docs.

Known gap upstream

AddHydraOperationsSubscriber of dmstr/api-platform-utils-bundle adds no hydra:operation to collection responses. It matches @type hydra:Collection, while API Platform 4 emits Collection. Item responses are filtered correctly.

🤖 Generated with Claude Code

schmunk42 and others added 5 commits October 28, 2026 07:50
…voter [*]

Boot the bundle in a minimal Symfony kernel (SQLite in-memory, stateless
firewall) so tests send real requests through API Platform:

- TestHeaderAuthenticator reads X-Test-Roles, X-Test-User and
  X-Test-Auth: anonymous; anonymous requests get 401
- TestPermissionVoter answers apiconfig:* from an example role map
- a kernel variant without the voter for deny-by-default tests
- AuthorizableDemoExtension for the 303 browser path of authorize
- smoke tests pinning the current behaviour

No behaviour change; require-dev only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every operation of ApiConfiguration now carries an explicit security
expression on a bundle-owned permission attribute instead of a role:

- Get, GetCollection, health: apiconfig:read
- Post, Put, Patch, Delete: apiconfig:write
- authorize: apiconfig:authorize
- resource-level fallback: apiconfig:read

The attributes live in ApiConfigurationPermissions and are tagged
za7_core.permission in config/services.yaml, so an application can
collect them. symfony/security-bundle and symfony/expression-language
move to require, because security expressions are not evaluated
without them.

BREAKING CHANGE: the bundle no longer checks ROLE_ADMIN. Without a
permission voter that grants apiconfig:* every operation answers 403.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orize [*]

AuthorizeRedirectListener answered GET .../authorize with
Accept: text/html by a 303 redirect before API Platform's security
ran, so any authenticated caller could start the OAuth flow. It now
checks apiconfig:authorize right after the Accept check and before
looking up the configuration: denied callers get 403 (401 when
anonymous) for existing and unknown ids alike.

BREAKING CHANGE: the browser path of authorize requires the
apiconfig:authorize permission.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
composer.json, the CI matrix, the compose test runner and CLAUDE.md
move from PHP 8.4 to 8.5.

BREAKING CHANGE: PHP >= 8.5 is required.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README: Requirements, the permission table, the za7_core.permission
tags and a migration section with a minimal voter. CHANGELOG: [0.6.0]
with the breaking changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@schmunk42
schmunk42 merged commit 6b6f216 into master Oct 28, 2026
2 checks passed
@schmunk42
schmunk42 deleted the feature/apiconfig-permissions branch October 28, 2026 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant