Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Permission Bundle Laravel Package

epubli4/permission-bundle

Symfony bundle that eases permission handling for e4 microservices using API Platform. Install with Composer, configure your microservice name and permission export/aggregation routes, then enable the Doctrine self-permission filter. Works with l0wskilled/api-platform-test for unit tests.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the bundle and wire up the three config files before writing any entity code — the bundle is silent until all of them exist.

composer require epubli4/permission-bundle
  1. Register the bundle — auto via Flex, or add Epubli\PermissionBundle\EpubliPermissionBundle::class => ['all' => true] to config/bundles.php.
  2. Create config/packages/epubli_permission.yaml with your microservice_name plus permission_export_route and aggregated_permissions_route (each needs base_uri, path, and a permission key naming the upstream service permission). The microservice name is prefixed to every permission key you declare, so pick it deliberately — renaming it later breaks every downstream permission grant.
  3. Create config/packages/test/epubli_permission.yaml with is_test_environment: true. Without this your test suite will try to hit the real user service.
  4. Enable the Doctrine filter in config/packages/doctrine.yaml under doctrine.orm.filters with key epubli_permission_bundle_self_permission_filter and class Epubli\PermissionBundle\Filter\SelfPermissionFilter. This is what powers row-level scoping — forgetting it means SelfPermissionInterface silently does nothing.
  5. Install the test helper — composer require l0wskilled/api-platform-test >=0.1.21 (or epubli/api-platform-test) is effectively required for testing, since every endpoint is locked behind a JWT.

First use case: annotate one ApiResource with the security expressions from the README (is_granted(null, _api_resource_class) on collections, is_granted(null, object) on items). Then hit that endpoint with a valid token and confirm it's denied without permissions and allowed with them. That single round-trip validates config, bundle registration, and token handling at once.

Where to look first:

  • SelfPermissionFilter — understand how it manipulates the query before you build list endpoints.
  • AccessToken service — your primary day-to-day API into the token.
  • JWTMockTrait + UnitTestTrait — set these up early or your first PR will be blocked on "how do I test this?"

Implementation Patterns

Security gate per operation. Every operation on an ApiResource that should be permission-gated needs an explicit security / security_post_denormalize key. The bundle does not infer them. Collection get uses _api_resource_class; item operations use object. For post, use security_post_denormalize with object so the freshly-denormalized entity is available.

Two flavours of entity. Plain permission checks (no ownership) need only the security annotations. Ownership-aware checks need SelfPermissionInterface. Prefer the SelfPermissionTrait when your entity has a direct user_id column — it's the two-line path:

class ExampleEntity implements SelfPermissionInterface
{
    use SelfPermissionTrait;
    // ... $user_id column + getUserId()
}

Relationship-based ownership. When there's no direct user_id, implement the interface manually: hasUserIdProperty() returns false, getFieldNameOfUserIdForPermissionBundle() returns '', and getPrimaryIdsWhichBelongToUser() runs a query joining through the relation to collect IDs. Keep that query cheap — it runs on every filtered request. Return only id, use getArrayResult(), and consider caching if the set is large.

Injected AccessToken. Autowire it into controllers/services — no config needed. Use the four primitives together:

if (!$accessToken->exists()) { throw new UnauthorizedHttpException('Bearer', '...'); }
if (!$accessToken->hasPermissionKey('test.customPermission1')) { throw new AccessDeniedHttpException('...'); }

exists() is authentication, hasPermissionKey() is authorization. Don't conflate them — returning 403 for a missing token leaks that the endpoint exists.

Custom permissions via annotation. Declare @Permission(key="...", description="...") on the method (stack multiple annotations for multiple keys). The microservice name is auto-prepended. This is how you export permissions the user service will know about on the next import — the annotation is both documentation and export source.

Faking a token in tests. Use JWTMockTrait with setUpJsonWebTokenMockCreator() in setUpBeforeClass, then either set the cookie jar globally in setUp (all requests authorized — good for smoke tests) or per-test (good for negative-path tests). Never test the happy path with a token you can't also remove.

Data-driven endpoint tests. UnitTestTrait plus a UnitTestConfig and per-verb data factories (UnitTestGetItemData, UnitTestPostData, UnitTestUpdateData, UnitTestDeleteData) gives you permission-matrix coverage almost for free. Each factory takes the URI, permission key, and target user ID, so the trait can assert both the allowed and forbidden cases. Fill in getDemoEntity() with Faker data and let the trait run the matrix.

Gotchas and Tips

  • microservice_name is a permission prefix, not a label. Changing it orphans every permission already exported to the user service. Treat it as immutable once shipped; if you must change it, coordinate a re-import.
  • The Doctrine filter is opt-in and silent. If SelfPermissionFilter isn't registered, SelfPermissionInterface compiles fine and does nothing. Symptom: users see each other's rows in collection endpoints. Check doctrine.orm.filters first when scoping breaks.
  • Missing security key = open endpoint. The bundle only activates when you set the key. A resource without it is wide open and looks fine in tests using JWTMockTrait (which authorizes everything). Write at least one test that omits the cookie and asserts 401.
  • getPrimaryIdsWhichBelongToUser() runs per request. It's the query behind the filter. Badly written (e.g., findAll() then PHP filter, or missing index on the joined user column), it becomes your slowest endpoint. Profile it under realistic data volume, not with three fixtures.
  • Empty getFieldNameOfUserIdForPermissionBundle() + hasUserIdProperty() === false must agree. Returning true while returning '' from the field name will produce broken SQL in the filter. The two methods are a contract; implement both together.
  • Test env config file path matters. It's config/packages/test/epubli_permission.yaml, not config/packages/epubli_permission_test.yaml. Wrong location means the flag is false in test runs and the bundle attempts real HTTP calls to http://user.
  • Routes are hardcoded URIs, not Symfony routes. base_uri: http://user and a literal path — there's no route generation, no service discovery, no retry. If the user service moves or is down, exports fail. Wrap any manual calls in your own error handling; the bundle won't do it for you.
  • {user_id} substitution is string-based. Only that exact placeholder is replaced. Don't invent new ones.
  • Annotation permissions need the annotation reader. If you've dropped Doctrine annotations for attributes (PHP 8), the @Permission annotations on controllers will not be picked up. This bundle predates attribute support (last release July 2021) — stay on annotations or check for a newer fork before migrating.
  • Test-only dependency is not optional in practice. JWTMockTrait lives in the bundle, but OrmApiPlatformTestCase comes from api-platform-test >= 0.1.21. Pin both; older versions of the test package miss interfaces the trait expects.
  • Extension point: the bundle is small and unmaintained-looking (3 stars, one 2021 release). If you need behaviour it doesn't give you — caching aggregated permissions, custom export serialization, multiple upstream user services — fork AccessToken or the export service rather than bending config. Read the bundle source before fighting it; there's less there than the README implies.
  • Debugging permission denials: dump $accessToken->getJTI(), getUserId(), and the raw permission list first. Most "the bundle is broken" reports are a token issued for a user whose permissions were never imported, or a key with the wrong microservice prefix.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc