kilix/abac-bundle
A Symfony bundle for attribute-based access control (ABAC). Define policies over user, resource, and environment attributes to make fine-grained, context-aware authorization decisions, integrated with Symfony’s security system.
Before writing a line of code, check that the package even installs against your stack — this bundle dates from the Symfony 2/3 era and has 15 stars and no dependents:
composer show kilix/abac-bundle --all # inspect symfony/framework-bundle + php constraints
composer require kilix/abac-bundle
If the constraint graph doesn't resolve on Symfony 5/6 or PHP 8, stop there: the bundle is a thin DI wrapper, and you can get the same value by wiring kilix/php-abac yourself in services.yaml (see Gotchas).
Assuming it installs, register the bundle (config/bundles.php, or automatically via Flex) and create a policy directory:
config/abac/
post_view.json
post_edit.json
Configure the path — verify the key names first in vendor/kilix/abac-bundle/DependencyInjection/Configuration.php, since docs are effectively nonexistent:
# config/packages/kilix_abac.yaml
kilix_abac:
policy_definition: '%kernel.project_dir%/config/abac'
Where to look first, in this order:
DependencyInjection/Configuration.php — the real config schema.DependencyInjection/Compiler/ — the tag name your attribute providers must use, and the service IDs you'll inject.Resources/config/services.* — the enforcer service ID (usually something like kilix_abac.policy_enforcer).kilix/php-abac README and, more importantly, its tests/ fixtures — that's the only reliable documentation of the policy JSON shape.First use case: gate one action on one resource you already have in hand. Don't attempt to model your whole permission system on day one. Pick "can this user edit this Post?" — resolve the enforcer, hand it a subject, a resource and an action string, and assert the boolean.
// Controller — inject the enforcer service, not the container
public function edit(Post $post, PolicyEnforcer $abac): Response
{
// Confirm the argument order/signature against the installed version;
// older php-abac releases use (subject, resource, environment, action).
if (!$abac->enforce($this->getUser(), $post, $this->environment(), 'edit')) {
throw $this->createAccessDeniedException();
}
// ...
}
Policies are JSON with three parts: targets (which subjects/resources/environment the policy applies to), rules (named, with an effect and a condition expression), and the attribute list each rule is allowed to read. Treat the file as code: review it, version it, and never let a controller make a decision the policy doesn't.
Do not call the enforcer directly from controllers in production code. The idiomatic Symfony integration is a Voter that delegates to the enforcer, so is_granted(), #[IsGranted], and Twig's is_granted all work:
final class AbacVoter extends Voter
{
public function __construct(private PolicyEnforcer $abac) {}
protected function supports(string $attribute, mixed $subject): bool
{
return $subject instanceof AbacResourceInterface;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof UserInterface) {
return false; // never abstain on a supported attribute
}
return $this->abac->enforce($user, $subject, $this->environment($token), $attribute);
}
}
Mark your domain entities with a small AbacResourceInterface (or an #[AsAbacResource] attribute) so supports() is unambiguous and you don't accidentally evaluate unrelated subjects.
Attribute providers are where the real work lives. The bundle collects tagged services and the enforcer calls them on every enforce(). Depending on the php-abac version you get either one AttributeProvider with retrieveUserAttributes() / retrieveObjectAttributes() / retrieveEnvironmentAttributes(), or separate subject/resource/environment interfaces. Keep each provider tiny and typed:
final class PostAttributeProvider implements ResourceAttributeProviderInterface
{
public function supports(object $resource): bool { return $resource instanceof Post; }
public function getResourceAttributes(object $resource): array
{
return [
'author_id' => $resource->getAuthorId(), // int, not string
'status' => $resource->getStatus(), // 'draft' | 'public'
'tenant_id' => $resource->getTenantId(),
];
}
}
Environment attributes should come from RequestStack, not superglobals:
final class RequestEnvironmentProvider implements EnvironmentAttributeProviderInterface
{
public function __construct(private RequestStack $requests) {}
public function getEnvironmentAttributes(): array
{
$r = $this->requests->getCurrentRequest();
return [
'ip' => $r?->getClientIp(),
'hour' => (int) (new \DateTimeImmutable())->format('G'),
'tenant' => $r?->attributes->get('_tenant'),
];
}
}
Keep policy JSON flat and attribute-driven. ABAC earns its keep when the decision depends on resource state plus relationship plus context. If a policy is just role == 'ROLE_ADMIN', that's RBAC wearing a costume — use a plain voter and save yourself the JSON.
Test policies without Symfony. This is the package's best property: policies are data, so you can unit-test the decision table by instantiating the enforcer with stub providers and asserting a matrix of (subject, resource, environment, action) → expected boolean. Do that in CI; it's cheaper and more legible than functional tests hitting controllers.
Trace in dev only. Decorate the enforcer service (or add an event listener that logs before/after enforce()) behind %kernel.debug% so you can see which policy matched and which attributes were read. Without this, every "why was I denied?" bug is a 30-minute dive.
Policy parsing on every request is the main performance risk. The bundle builds the enforcement graph from JSON at construction time. With a handful of policies this is invisible; with dozens it's a per-request tax. Warm it at deploy (a cache warmer that constructs the enforcer once and serialises the compiled definitions), or wrap the enforcer in a decorator backed by a cache.app pool keyed on a hash of the policy files.
Attribute providers trigger N+1 queries. One enforce() call may hit the DB for the subject, the resource, and every parent resource in a chain. Memoise per-request inside the provider (private array $cache = [] keyed on object ID) and eager-load relations that policies reference. Providers are called repeatedly across a page of is_granted() checks in Twig — this is where you'll notice it first.
Return false, not ABSTAIN, from your voter for supported attributes. ACCESS_ABSTAIN on a supported attribute lets a later permissive voter silently grant access. Since the enforcer is deny-by-default (no matching policy → false), make that denial authoritative in the voter chain.
Type coercion bites. JSON comparisons against DB values are strict-ish — an attribute returned as "1" from a PDO fetch won't match 1 in the policy. Cast explicitly in providers and include a test case for each comparison you rely on. Similarly, null attributes silently fail conditions; decide whether absent means deny (usually yes) and assert it.
Confirm the multi-policy semantics. Whether multiple matching policies are OR'd, AND'd, or first-match-wins determines your whole authoring style and is not documented in the bundle. Read php-abac's enforcer, then write one test that proves it. Getting this wrong produces policies that look reasonable and are quietly permissive.
Watch for shared state in the enforcer service. It's a container singleton that accretes attribute bags. Anything that mutates it across requests (or in long-running workers like RoadRunner/Swoole) can leak decisions between users. Reset or re-instantiate per request if you go beyond FPM.
Pin the version hard and expect to fork. With 15 stars and no active maintainers, treat the bundle as vendored code: "kilix/abac-bundle": "1.0.*" (exact), commit composer.lock, and read vendor/ before trusting any README. The exit path is cheap — remove the bundle, register the php-abac enforcer as a service in services.yaml, keep your providers and policies — so don't let it grow tentacles into your app via bundle-specific helper classes.
The config keys and tag names in this guide are the conventional ones, not verified against your installed version. Grep the bundle's DependencyInjection/ directory before copying any YAML from a blog post (including this one); the schema is small enough to read in five minutes and it is the only source of truth you have.
How can I help you explore Laravel packages today?