pwalkow/mongodb-acl-bundle
A Symfony bundle that adds ACL (access control list) support for MongoDB-backed applications, helping manage and enforce per-document permissions. It integrates MongoDB with Symfony security ACL concepts for role- and user-based access checks.
Know the stack before you install. Despite the "Bundle" naming and the broad "MongoDB ACL" description, this is a Symfony bundle, not a Laravel package. It plugs into Symfony's symfony/security-acl component to replace the default Doctrine DBAL ACL provider with a MongoDB-backed one. If you are on Laravel, you cannot composer require this and expect it to work without writing a service-provider adapter around the underlying provider class — treat it as a library of ACL provider code rather than a drop-in package.
Minimal steps in a Symfony app:
Install the dependencies the bundle expects — typically symfony/security-acl (or symfony/security-bundle with ACL enabled) and a MongoDB ODM/ODM-bundle if the provider persists documents:
composer require pwalkow/mongodb-acl-bundle
Inspect vendor/pwalkow/mongodb-acl-bundle/composer.json first — it tells you which Symfony version and which Mongo driver/ODM package it actually supports. With 3 stars and 0 dependents, expect it to be pinned to an older Symfony line.
Register the bundle in config/bundles.php:
return [
// ...
PWalkow\MongoDBAclBundle\MongoDBAclBundle::class => ['all' => true],
];
(Verify the exact FQCN from vendor/.../src — namespaces in small bundles drift.)
Enable ACL in config/packages/security.yaml:
security:
acl:
provider: mongodb # the service alias the bundle registers
Wire Mongo — if the bundle uses Doctrine MongoDB ODM, configure doctrine_mongodb with a database and document manager; if it uses the raw ext-mongodb client, set its connection DSN under the bundle's own config key (read the README in vendor/pwalkow/mongodb-acl-bundle/README.md — that is the only source of truth here).
Confirm the provider swap worked. The bundle's whole job is to override the security.acl.provider service. Verify with:
php bin/console debug:container security.acl.provider
The class should be from the PWalkow namespace, not Symfony\Component\Security\Acl\Dbal\MutableAclProvider.
First use case — grant and enforce:
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;
// Grant
$acl = $aclProvider->createAcl(ObjectIdentity::fromDomainObject($document));
$acl->insertObjectAce(
UserSecurityIdentity::fromAccount($user),
MaskBuilder::MASK_OWNER
);
$aclProvider->updateAcl($acl);
// Enforce (controller, voter, or Twig)
$this->denyAccessUnlessGranted('EDIT', $document);
If that round-trips — ACL document written to Mongo, isGranted('EDIT', $doc) returns true — the bundle is working and you can build on it.
Create ACLs on persist, not on demand. The cleanest pattern is a Doctrine ODM event listener (or a domain-event subscriber) that fires on postPersist and creates the ACL + owner ACE:
class AclOnPersistListener
{
public function postPersist(LifecycleEventArgs $args): void
{
$doc = $args->getObject();
if (!$doc instanceof AclSecuredInterface) {
return;
}
$acl = $this->aclProvider->createAcl(ObjectIdentity::fromDomainObject($doc));
$acl->insertObjectAce(
UserSecurityIdentity::fromToken($this->tokenStorage->getToken()),
MaskBuilder::MASK_OWNER
);
$this->aclProvider->updateAcl($acl);
}
}
This is exactly why postPersist (not prePersist) matters — the ACL needs a stable ObjectIdentity id, which for MongoDB means the document must already have its _id.
Wrap ACL mutations in a service. createAcl + insertObjectAce + updateAcl is a three-call ceremony that leaks Symfony internals into controllers. Hide it behind an AclManager with methods like grant($object, $user, $mask) and revoke(...) so business code stays readable:
final class AclManager
{
public function grant(object $domainObject, UserInterface $user, int $mask): void
{
$acl = $this->aclProvider->createAcl(ObjectIdentity::fromDomainObject($domainObject));
$acl->insertObjectAce(UserSecurityIdentity::fromAccount($user), $mask);
$this->aclProvider->updateAcl($acl);
}
}
Use the filter / acl_filter option for listings. When you need "all documents this user can view," do not loop over documents calling isGranted. Use Symfony's filtering form:
$qb = $documentRepository->createQueryBuilder();
$qb->addFilteredAclExpression(...);
or, more commonly, rely on the built-in listener that adds acl_filter to the query — but check the bundle's event subscribers, because filtering is exactly the piece that custom ACL providers often fail to implement. If there is no filtered subscriber, plan to fetch candidate ids first via security.acl.provider.findObjectIdentitiesWithParent() (or similar) and intersect in your repository.
Batch operations for imports and migrations. MutableAclProviderInterface exposes createAcls() and updateAcls() that take arrays and flush once. Use these in seeders, fixtures, and data-migration commands — never call updateAcl() in a loop for bulk grants.
Custom masks for domain permissions. The standard masks stop at OWNER / MASTER. For real apps you often need MASK_APPROVE, MASK_PUBLISH, etc. Register them in security.acl.permissions config or via a custom PermissionMap, then build a voter that maps your domain verbs to those bits.
Enforce at the boundary, not inside services. Prefer denyAccessUnlessGranted('EDIT', $doc) in controllers and a Voter for composite decisions. The MongoDB ACL layer should decide whether an object is editable; whether the edit is valid belongs to the domain service.
Laravel is not a target. The Symfony ACL component has no Laravel integration, and this bundle assumes Symfony's service container, security tokens, and voter machinery. If you are serious about reusing it in Laravel, plan to port the provider class into a Laravel service provider and drop the symfony/security-acl dependency chain — at which point you may as well implement Mongo ACL storage yourself. Evaluate honestly before adopting.
Symfony's ACL component is effectively deprecated. symfony/security-acl was moved out of the main tree and receives no new features. The long-term Symfony direction is voters + attribute-based checks over objects with plain isGranted() and custom queries. Any investment in a MongoDB ACL provider is a bet on a dormant subsystem — which explains this package's 3 stars and 0 dependents. Pin your Symfony version and treat it as legacy infrastructure.
The README is the only documentation. With 0 dependents there is no community knowledge, no Stack Overflow answers, and no blog posts. Budget a day to read vendor/pwalkow/mongodb-acl-bundle/src end-to-end — particularly the Resources/config/services.xml file, which tells you exactly which service ids get overridden and what config keys exist. Do not guess config keys from memory.
Object identity is fragile across MongoDB's late _id generation. ObjectIdentity::fromDomainObject() uses Class + id. If the id is null (unpersisted document), every ACL you create will collide. Always create ACLs in postPersist, or assign the _id yourself (a ULID/UUID) before insert.
Stale ACLs on delete are your responsibility. The bundle almost certainly does not cascade-delete ACL documents when a domain document is removed. Add a postRemove listener calling $aclProvider->deleteAcl(ObjectIdentity::fromDomainObject($doc)), or your ACL collections will grow indefinitely and leak information about deleted objects.
N+1 on permission checks. Each isGranted('EDIT', $doc) triggers a Mongo query unless the provider caches. Check whether the bundle registers a cache wrapper (Symfony had security.acl.cache). If not, wrap your AclProvider or enable the ODM's result cache — and definitely use list-filtering rather than per-document checks in views.
Bitmask semantics are unintuitive. MASK_OWNER (128) implies all permissions on that object, and masks are cumulative (EDIT includes VIEW via the permission map). If a user "can edit but not view" — a bug in your grant logic, or in your PermissionMap. Test grants with debug:container --tag=security.acl and by dumping the raw ACL documents in Mongo (db.acl_entries.find() or whatever collection the bundle uses).
ACL document schema is provider-specific. Unlike Doctrine DBAL, where the schema is documented in Symfony, this bundle's Mongo schema is whatever the author chose. Before writing your own queries (e.g. "all objects user X can VIEW"), open one ACL document in Mongo Compass and confirm the field names (identifier, class, entries, object_identity, etc.). Do not assume the DBAL field names carry over.
Extension points to know. The provider is injectable and replaceable: subclass it to add caching, custom collections, or read-only ACLs. The security.acl.object_identity_retrieval_strategy service is the seam for "how do I map my object to an identity" — customise it if you need composite ids (e.g. tenantId + docId) so multi-tenant ACLs stay isolated.
Test with a real Mongo instance, not an in-memory fake. The whole value of this bundle is the storage layer; mocking the provider tests nothing meaningful. Use mongodb-memory-server via testcontainers or a dedicated test DB, and assert on the raw documents in Mongo, not just on isGranted results — that catches schema drift when you upgrade Symfony.
Pin the bundle and its transitive ACL dep. With one maintainer and few users, an unconstrained composer update will eventually pull a symfony/security-acl version the bundle's provider no longer compiles against. Commit composer.lock and add a note in your README explaining that this dependency is frozen-by-design.
How can I help you explore Laravel packages today?