kematjaya / access-control-bundle
Manifest-driven RBAC: grantable menu/action permissions on top of Symfony's role hierarchy, plus a filtered navigation-menu endpoint for API-first frontends.
Package info
github.com/kematjaya0/access-control-bundle
Type:symfony-bundle
pkg:composer/kematjaya/access-control-bundle
Requires
- php: ^8.4
- doctrine/doctrine-bundle: ^2.11|^3.0
- doctrine/orm: ^2.15|^3.0
- symfony/config: ^7.0|^8.0
- symfony/dependency-injection: ^7.0|^8.0
- symfony/framework-bundle: ^7.0|^8.0
- symfony/http-foundation: ^7.0|^8.0
- symfony/http-kernel: ^7.0|^8.0
- symfony/routing: ^7.0|^8.0
- symfony/security-core: ^7.0|^8.0
- symfony/uid: ^7.0|^8.0
- symfony/yaml: ^7.0|^8.0
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^11.0|^13.0
- symfony/browser-kit: ^7.0|^8.0
- symfony/security-bundle: ^7.0|^8.0
- symfony/yaml: ^7.0|^8.0
README
Manifest-driven RBAC for Symfony: a single YAML file declares your app's
navigation menu (sections, items, icons, hrefs) and which of those items are
gated behind grantable permissions on top of Symfony's own role_hierarchy.
Ships a filtered, per-user navigation-menu endpoint for API-first frontends,
an admin permission-matrix endpoint, and a PermissionVoter for fine-grained
is_granted('notes.delete')-style checks in your own controllers.
No UI. Frontend integration (Next.js) is a separate package,
@kematjaya/access-control-ui.
Pairs naturally with kematjaya/auth-bundle
(any Symfony\Component\Security\Core\User\UserInterface implementation
works — no custom contract needed here) but does not require it.
What it owns vs. what stays in your app
| Owned by the bundle | Stays in your app |
|---|---|
Permission/RolePermission entities + tables |
The manifest YAML (your actual menus/pages/actions) |
GET /api/permissions (admin matrix catalog) |
role_hierarchy in security.yaml |
PUT /api/permissions/grants |
Enforcing specific actions: is_granted('notes.delete') in your controllers/API Platform operations |
POST /api/permissions/export |
|
GET /api/me/permissions, GET /api/me/menu |
|
PermissionVoter, PermissionResolver |
|
kematjaya:access-control:sync, kematjaya:access-control:export |
Installation
composer require kematjaya/access-control-bundle
Flex copies a starter config/permissions/default.yaml and registers the
bundle. Then:
php bin/console doctrine:schema:update --force # or a migration in prod — # Permission/RolePermission # are picked up automatically, # no entity files to write php bin/console kematjaya:access-control:sync
Configuration
config/packages/kematjaya_access_control.yaml (all keys optional, shown
with their defaults):
kematjaya_access_control: superuser_role: ROLE_ADMIN manifest_profile: default manifest_dir: '%kernel.project_dir%/config/permissions' export_dir: '%kernel.project_dir%/var/permissions-export'
superuser_role— this role implicitly has every permission and cannot be granted explicitly viaPUT /api/permissions/grants(returns 422). Must also be yoursecurity.yamlrole_hierarchy's top role forGET /api/permissionsto list the right set of grantable roles.manifest_profile/manifest_dir— selects<manifest_dir>/<manifest_profile>.yaml. Multiple profiles let you swap the whole menu/permission set per environment or white-label build.
The manifest
sections: - name: Overview items: - key: dashboard label: Dashboard href: /dashboard icon: dashboard # no `roles`, no `gated` -> visible to every authenticated user - name: Management items: - key: notes label: Notes href: /notes icon: notes gated: true # grantable per-role via the matrix defaultRoles: [ROLE_USER] # seeded on the very first sync actions: create: Create Note delete: Delete Note - name: Administration items: - key: access_control label: Access Control href: /admin/access-control icon: shield roles: [ROLE_ADMIN] # role gate, checked before the permission gate gated: true
GET /api/me/menu returns this tree already filtered for the current user
(role check, then permission-grant check, superuser sees everything) —
your frontend renders it as-is, no client-side filtering logic needed.
key/key.action strings become permission attributes usable anywhere
Symfony checks authorization: #[IsGranted('notes.delete')],
is_granted('notes.delete') in a Twig/expression, or
$authorizationChecker->isGranted('notes.delete') — PermissionVoter
resolves these against the grants table (with the superuser bypass).
Run kematjaya:access-control:sync after editing the manifest to write new
keys/labels into the database (existing rows never disappear automatically —
it only warns about orphans, so you can decide whether to keep or manually
prune them).
Endpoints
| Method | Path | Access |
|---|---|---|
| GET | /api/permissions |
superuser only — full catalog + current grants, for the admin matrix UI |
| PUT | /api/permissions/grants |
superuser only — { roleCode, permissionKeys }, full replace for that role |
| POST | /api/permissions/export |
superuser only — dumps the live grants to <export_dir> |
| GET | /api/me/permissions |
any authenticated user — { keys: string[] } granted to their roles |
| GET | /api/me/menu |
any authenticated user — filtered nav tree |
None of these are wrapped in #[IsGranted]/access_control by the bundle
itself (an app-level role_hierarchy config or a custom superuser_role
wouldn't be visible to a hardcoded attribute) — wire your own
security.yaml access_control rule for ^/api as usual; the bundle's
controllers additionally self-check superuser_role where relevant.