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.

Maintainers

Package info

github.com/kematjaya0/access-control-bundle

Type:symfony-bundle

pkg:composer/kematjaya/access-control-bundle

Transparency log

Statistics

Installs: 29

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.2 2026-08-19 10:33 UTC

This package is auto-updated.

Last update: 2026-08-19 10:35:06 UTC


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 via PUT /api/permissions/grants (returns 422). Must also be your security.yaml role_hierarchy's top role for GET /api/permissions to 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.