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.
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
Epubli\PermissionBundle\EpubliPermissionBundle::class => ['all' => true] to config/bundles.php.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.config/packages/test/epubli_permission.yaml with is_test_environment: true. Without this your test suite will try to hit the real user service.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.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?"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.
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.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.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.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.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.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.@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.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.AccessToken or the export service rather than bending config. Read the bundle source before fighting it; there's less there than the README implies.$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.How can I help you explore Laravel packages today?