Single-database multi-tenancy in Symfony: a 31-line Doctrine filter, and the five places it never runs
Single-database multi-tenancy is the cheapest kind: one schema, one connection, an organization_id column on every tenant-owned table. The whole design rests on one promise, and it is a promise about forgetting : no developer on the team will ever have to remember to write WHERE organization_id = ? , because forgetting it once leaks another customer's data. Doctrine has had the tool for this for…
Single-database multi-tenancy in Symfony relies on a thirty-line Doctrine filter that automatically adds a WHERE clause to query results. This filter, named OrganizationFilter, is implemented as a SQLFilter and targets entities that implement the OrganizationOwnedInterface. The filter is declared in the doctrine.yaml configuration file and is disabled by default. It is enabled through a Symfony event listener that runs after the firewall, ensuring that the user is available and not in the admin section of the application.
There are five places where the filter does not run, which could potentially lead to sensitive data exposure. The first place is the console and every Messenger worker, where there is no kernel.request and thus the filter is disabled. This means that commands, cron jobs, and Messenger consumers will see all tenant rows. The second place is the back office, where an admin panel is exempted from the filter by path.
This allows admins to view data across all tenants. Thirdly, when the entity is already in the identity map, the filter is not consulted. The entity is returned from memory before any SQL is generated, and the filter only comes into play when a query is actually executed.
The fourth place is in DBAL (Database Abstraction Layer) when using native SQL queries. The filter is a DQL (Doctrine Query Language) concern and is not applied to DBAL queries. Many reporting and export screens use native SQL, which could lead to data leakage. Lastly, joined inheritance is a case where the filter is not applied if the organization_id column is not on the root table of the inheritance hierarchy.
This is a silent place where the filter does not run, and it highlights the importance of understanding the underlying ORM implementation rather than solely relying on the documentation. In summary, while the OrganizationFilter provides a valuable safety net for tenancy in Symfony applications, it is crucial to be aware of its limitations and potential blind spots to ensure comprehensive data protection.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.