PHPackages                             mhpdigital/cross-tenant-security-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. mhpdigital/cross-tenant-security-bundle

ActiveSymfony-bundle[Database &amp; ORM](/categories/database)

mhpdigital/cross-tenant-security-bundle
=======================================

Symfony bundle for role-based, row-level tenant isolation in Doctrine repositories

v1.1.3(1w ago)1489[1 PRs](https://github.com/mhpdigital/cross-tenant-security-bundle/pulls)MITPHPPHP &gt;=8.1

Since Mar 31Pushed 1w agoCompare

[ Source](https://github.com/mhpdigital/cross-tenant-security-bundle)[ Packagist](https://packagist.org/packages/mhpdigital/cross-tenant-security-bundle)[ RSS](/packages/mhpdigital-cross-tenant-security-bundle/feed)WikiDiscussions main Synced 4d ago

READMEChangelogDependencies (30)Versions (15)Used By (0)

Cross-Tenant Security Bundle
============================

[](#cross-tenant-security-bundle)

Role-based, row-level tenant isolation for Doctrine repositories in Symfony.

Pick one of three traits per repository to declare its access policy. Filtering is applied automatically in `createQueryBuilder()` (and `find()`, `findBy()`, `findOneBy()`, `findAll()`), so controllers and services never pass the current user around — the security context is read from the token.

Install &amp; register
----------------------

[](#install--register)

```
composer require mhpdigital/cross-tenant-security-bundle
```

Wire the factory as Doctrine's repository factory:

```
# config/packages/doctrine.yaml
doctrine:
    orm:
        repository_factory: 'Mhpdigital\CrossTenantSecurity\Repository\CrossTenantRepositoryFactory'
```

The factory injects `token_storage`, `role_hierarchy` and `request_stack` into every repository that uses one of the traits.

Access matrix
-------------

[](#access-matrix)

Traitconsole / workerauthenticated webtoken-less web`CrossTenantRepository`**all rows**your tenant filter (override)**none** (`1=0`)`AdminOnlyAccessRepository`**all rows**`ROLE_SUPER_ADMIN` → all, else none**none**`OpenAccessRepository`all rowsall rows**all rows** — no repository-level gate"console / worker" = any process with no HTTP request in flight (a console command, a Messenger/queue worker, a cron run). These are trusted local processes and get full access automatically — see [Console context](#console--worker-context).

### What the matrix does and does not say

[](#what-the-matrix-does-and-does-not-say)

It describes the **repository** layer only: which rows a query returns in a given context. It says nothing about *who can reach the code that runs the query* — that is your firewall and `access_control`, which this bundle does not touch. The practical difference between the traits is therefore where the backstop sits:

- `CrossTenantRepository` and `AdminOnlyAccessRepository` filter in the repository, so a route you accidentally leave unprotected still returns nothing to a token-less request.
- `OpenAccessRepository` applies no filter at all, so **the route's own protection is the only gate**. Behind a login-required route it means "all rows to every logged-in user"; on a route reachable without authentication it means "all rows to anyone".

So "token-less web → all rows" means *this trait will not stop such a request* — not that the data is necessarily exposed. Choose `OpenAccessRepository` when you are content for the repository to add no protection of its own.

Examples
--------

[](#examples)

### 1. Tenant-scoped — `CrossTenantRepository`

[](#1-tenant-scoped--crosstenantrepository)

Each user sees only their own rows; `ROLE_SUPER_ADMIN` sees all. Use the **trait-alias pattern**: call the library's secured builder first (you inherit the unauthenticated `1=0` *and* the console-context bypass), then add your own filter.

```
use Mhpdigital\CrossTenantSecurity\Repository\CrossTenantRepository;

class PostRepository extends ServiceEntityRepository
{
    use CrossTenantRepository {
        CrossTenantRepository::createQueryBuilder as secureQueryBuilder;
    }

    public function createQueryBuilder($alias, $indexBy = null): QueryBuilder
    {
        $qb = $this->secureQueryBuilder($alias, $indexBy);

        // Authenticated non-super-admins are scoped to their own rows.
        // (No current user ⇒ console context ⇒ full access, so this is skipped.)
        if ($this->getCurrentUser() !== null && $this->getHighestRole() !== 'ROLE_SUPER_ADMIN') {
            $qb->andWhere("$alias.author = :_author")
               ->setParameter('_author', $this->getUserId());
        }

        return $qb;
    }
}
```

### 2. Unfiltered lookup — `OpenAccessRepository`

[](#2-unfiltered-lookup--openaccessrepository)

Reference data the repository does not gate at all: **every request that reaches it sees all rows**, authenticated or not. Typical for `sex`, `country`, `currency`, `status`, `category`, `tag`. No override needed.

```
use Mhpdigital\CrossTenantSecurity\Repository\OpenAccessRepository;

class SexRepository extends ServiceEntityRepository
{
    use OpenAccessRepository;
}
```

> Because the trait adds no filter, whether this data is truly *public* is decided entirely by the routes that expose it. If the lookup must require login **regardless of how its routes are configured**, do not use this trait — use `CrossTenantRepository` (authenticated → all rows, token-less web → none), which enforces that at the repository.

### 3. Admin-only — `AdminOnlyAccessRepository`

[](#3-admin-only--adminonlyaccessrepository)

Only `ROLE_SUPER_ADMIN` (and trusted console/worker runs) see rows; everyone else sees none.

```
use Mhpdigital\CrossTenantSecurity\Repository\AdminOnlyAccessRepository;

class AuditLogRepository extends ServiceEntityRepository
{
    use AdminOnlyAccessRepository;
}
```

### 4. Content filter + access gate (custom `createQueryBuilder`)

[](#4-content-filter--access-gate-custom-createquerybuilder)

When a repository has an **always-on content filter** (e.g. hide soft-deleted or unpublished rows) *as well as* an access gate, reimplement `createQueryBuilder()` and place `isConsoleContext()` **between** them — the content filter must apply in every context, but the access gate is bypassed for console/worker:

```
public function createQueryBuilder($alias, $indexBy = null): QueryBuilder
{
    $qb = $em->createQueryBuilder()->select($alias)->from(/* … */);

    $qb->andWhere("$alias.deleted IS NULL");   // (1) content filter — ALWAYS applies

    if ($this->isConsoleContext()) {           // CLI/worker bypasses ONLY the gate below
        return $qb;
    }

    // (2) access gate — bypassed in console
    if (!\in_array($this->getHighestRole(), self::READER_ROLES, true)) {
        $qb->andWhere('1=0');
    }

    return $qb;
}
```

A console index build then sees all **non-deleted** rows without `createUnrestrictedQueryBuilder()`, while deleted rows stay hidden everywhere.

Console / worker context
------------------------

[](#console--worker-context)

A console command, Messenger/queue worker or cron run has **no HTTP request** and carries no security token. The bundle detects this (`isConsoleContext()` — request-presence, so functional tests that issue a sub-request keep full web semantics) and grants **full access** through the secured `createQueryBuilder()`. CLI/background code no longer needs `createUnrestrictedQueryBuilder()`.

If `request_stack` is not wired, the bundle assumes a web context and fails **closed**.

Explicit, context-independent bypass
------------------------------------

[](#explicit-context-independent-bypass)

```
$qb = $repo->createUnrestrictedQueryBuilder('e'); // raw Doctrine builder, no filtering at all
```

Use only when you must bypass filtering regardless of context (e.g. an authenticated admin maintenance action). For ordinary CLI/background work the console-context detection already grants full access, so you should rarely need this.

See also
--------

[](#see-also)

The `example/` app is a runnable Symfony project exercising every trait — `example/src/Repository/*` and `example/tests/Integration/BundleIntegrationTest.php` are the executable documentation, including the console-context tests.

###  Health Score

47

—

FairBetter than 93% of packages

Maintenance98

Actively maintained with recent releases

Popularity19

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 78.3% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~15 days

Recently: every ~27 days

Total

9

Last Release

9d ago

Major Versions

v0.0.9 → v1.0.02026-03-31

### Community

Maintainers

![](https://www.gravatar.com/avatar/6c94c72b97d2849fe5f2c58045e8818bf2fc109db9748ab5031b2529e99181b3?d=identicon)[mhpdigital](/maintainers/mhpdigital)

---

Top Contributors

[![jochendaum](https://avatars.githubusercontent.com/u/2119862?v=4)](https://github.com/jochendaum "jochendaum (18 commits)")[![mhpdigital](https://avatars.githubusercontent.com/u/272352319?v=4)](https://github.com/mhpdigital "mhpdigital (5 commits)")

---

Tags

symfonybundlesecuritydoctrinerepositorysaasmulti-tenanttenancyrow-level-security

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/mhpdigital-cross-tenant-security-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/mhpdigital-cross-tenant-security-bundle/health.svg)](https://phpackages.com/packages/mhpdigital-cross-tenant-security-bundle)
```

###  Alternatives

[rcsofttech/audit-trail-bundle

Enterprise-grade, high-performance Symfony audit trail bundle. Automatically track Doctrine entity changes with split-phase architecture, multiple transports (HTTP, Queue, Doctrine), and sensitive data masking.

12017.1k](/packages/rcsofttech-audit-trail-bundle)[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k18.3M418](/packages/easycorp-easyadmin-bundle)[2lenet/crudit-bundle

The easy like Crud'it Bundle.

1617.3k16](/packages/2lenet-crudit-bundle)[kimai/kimai

Kimai - Time Tracking

4.8k9.4k1](/packages/kimai-kimai)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k6.0M776](/packages/sylius-sylius)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M231](/packages/sulu-sulu)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
