symfony/acl-bundle
Symfony ACL Bundle enables resource-based authorization using Access Control Lists. Define and check permissions on domain objects/resources to manage fine-grained access rules. Includes documentation and a PHPUnit-based test suite for verification.
Installation:
composer require symfony/acl-bundle
Ensure Symfony\Bundle\SecurityBundle\SecurityBundle is enabled in config/bundles.php.
Basic Configuration:
Add ACL configuration to config/packages/security.yaml:
security:
acl:
connection: default
cache: app.cache.acl
object_identity_class: App\Security\Acl\ObjectIdentity
Define Custom ObjectIdentity:
Create a class to map your domain objects (e.g., Post, User) to ACL entries:
namespace App\Security\Acl;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
class ObjectIdentity extends ObjectIdentity {}
First ACL Check: Secure a controller action:
use Symfony\Component\Security\Acl\Permission\MaskBuilder;
public function edit(Post $post)
{
$aclProvider = $this->get('security.acl.provider');
$oid = ObjectIdentity::fromDomainObject($post);
$mask = MaskBuilder::MASK_EDIT;
if (!$aclProvider->isGranted($mask, $oid, $this->getUser())) {
throw $this->createAccessDeniedException();
}
// Proceed with edit logic
}
Database Setup: Run migrations to create ACL tables (default schema provided by the bundle). Example using Doctrine Migrations:
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
vendor/symfony/security-acl/Resources/doc/schema.sqlSymfony\Component\Security\Acl\Permission\MaskBuilder for permission constantsPost, Comment) to ACL entries via ObjectIdentity.
// Example: Custom ObjectIdentity for a Post
class PostObjectIdentity extends ObjectIdentity
{
public static function fromDomainObject(Post $post): self
{
return new self('post', $post->getId());
}
}
MaskBuilder to define granular permissions:
$mask = MaskBuilder::MASK_EDIT | MaskBuilder::MASK_DELETE;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;
public function loadAclRules(AclProvider $aclProvider, User $user)
{
$postOid = ObjectIdentity::fromDomainObject($post);
$acl = $aclProvider->createAcl($postOid);
$acl->insertObjectAce($postOid, MaskBuilder::MASK_EDIT, $user);
$aclProvider->updateAcl($acl);
}
security.acl.post_load to dynamically adjust permissions:
$eventDispatcher->addListener('security.acl.post_load', function (AclEvent $event) {
if ($event->getObjectIdentity()->getType() === 'post') {
$event->setAcl($this->customAclForPost($event->getObjectIdentity()));
}
});
public function update(Post $post)
{
$aclProvider = $this->get('security.acl.provider');
$oid = ObjectIdentity::fromDomainObject($post);
$mask = MaskBuilder::MASK_EDIT;
if (!$aclProvider->isGranted($mask, $oid, $this->getUser())) {
throw $this->createAccessDeniedException();
}
// Proceed
}
{% if is_granted('ROLE_USER') and app.acl.isGranted('EDIT', post) %}
<a href="{{ path('post_edit', {'id': post.id}) }}">Edit</a>
{% endif %}
(Note: Requires custom Twig extension for ACL checks.)Combine ACL with Symfony’s voter system for hybrid checks:
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class PostVoter extends Voter
{
public function vote(AuthenticationToken $token, $object, array $attributes)
{
if (!$object instanceof Post) {
return false;
}
if (in_array('EDIT', $attributes)) {
$aclProvider = $this->aclProvider;
$oid = ObjectIdentity::fromDomainObject($object);
return $aclProvider->isGranted(MaskBuilder::MASK_EDIT, $oid, $token->getUser());
}
return false;
}
}
ObjectIdentity for entities:
use Doctrine\ORM\Event\LifecycleEventArgs;
class PostListener
{
public function postLoad(Post $post, LifecycleEventArgs $args)
{
$post->setObjectIdentity(ObjectIdentity::fromDomainObject($post));
}
}
PsrAclCache for performance:
# config/packages/security.yaml
security:
acl:
cache: app.cache.acl
Clear cache when ACL rules change:
$this->get('security.acl.cache')->clear();
$oid = new ObjectIdentity('post', $post->getId(), $tenantId);
AclProvider to batch-update ACLs:
$aclProvider = $this->get('security.acl.provider');
$batch = $aclProvider->findAclsByObjectIdentity(ObjectIdentity::fromDomainObject($post));
foreach ($batch as $acl) {
$acl->insertObjectAce($oid, $mask, $user);
$aclProvider->updateAcl($acl);
}
ObjectIdentity (e.g., wrong type or ID).ObjectIdentity matches the resource exactly:
// Wrong: Different type or ID
$oid = new ObjectIdentity('post', 123); // But post ID is 456
// Correct: Use fromDomainObject()
$oid = ObjectIdentity::fromDomainObject($post);
MaskBuilder constants.MaskBuilder for clarity:
// Bad: Magic numbers
$mask = 4; // What does 4 mean?
// Good:
$mask = MaskBuilder::MASK_EDIT;
$entityManager->beginTransaction();
try {
$aclProvider->updateAcl($acl);
$entityManager->commit();
} catch (\Exception $e) {
$entityManager->rollback();
throw $e;
}
$this->get('security.acl.cache')->clear();
access_control or voters.# config/packages/security.yaml
security:
access_control:
- { path: ^/admin, roles: ROLE_ADMIN } # Coarse-grained
# ACLs handle fine-grained checks in code
Use the security.acl.dump command to debug ACL entries:
php bin/console security:acl:dump
Enable debug logging for ACL
How can I help you explore Laravel packages today?