Multi-Tenancy for Laravel and Laravel-Doctrine
, (*1)
This library provides the necessary infra-structure for a complex multi-tenant application.
Multi-tenancy allows an application to be silo'd into protected areas by some form of tenant
identifier. This could be by sub-domain, URL parameter or some other scheme., (*2)
Terminology
Tenant
Identifies the currently loaded account that the user belongs to. There are two components:, (*3)
- Tenant Owner: tenant_owner_id
- Tenant Creator: tenant_creator_id
The tenant owner is the root account that actual "owns" the tenant-aware data., (*4)
The tenant creator is the instance that is adding or manipulating data that belongs to the tenant owner., (*5)
The tenant owner and creator may be the same entity., (*6)
The Tenant is its own object, registered in the container as: auth.tenant., (*7)
Tenant Participant
A tenant participant identifies the entity that is actually providing the tenancy reference.
This must be defined for this library to work and there can only be a single entity., (*8)
Typically this will be an Account class or User or (from laravel-doctrine/acl), an organization., (*9)
The tenant participant may be a polymorphic entity e.g.: one that uses single table inheritance., (*10)
Tenant Participant Mapping
Provides an alias to the tenant participant for easier referencing., (*11)
Note: this is not a container alias but used internally for tagging routes. e.g.:
the participant class is \App\Entity\SomeType\TheActualInstanceClass and in the routes we want
to restrict to this type. Instead of using the whole class name, it can be aliased to "short_name"., (*12)
Tenant Aware
An entity that implements the TenantAware contract (interface). This allows the data to be portioned
by the tenant owner / creator., (*13)
A tenant aware entity requires:, (*14)
- get/set TenantOwnerId
- get/set TenantCreatorId
- importTenancyFrom
Tenant Aware Repository
A specific repository that will enforce the tenant requirements ensuring that any fetch request
will be correctly bound with the tenant owner and creator, depending on the security scheme
that has been implemented on the tenant owners data., (*15)
A tenant aware repository usually wraps the standard entities repository class. This may be the
standard Doctrine EntityRepository., (*16)
Security Model
Defines how data is shared within a tenant owners account. In many situations this will be just
the tenant owner and creator only, however this library allows a hierarchy and a user to have
multiple tenants associated with them. In this instance the security level will determine what
information is available to the user depending on their current creator instance., (*17)
The provided security models are:, (*18)
- shared - all data within the tenant owner is shared to all tenant creators
- user - the user can access all data they are allowed access to within the tenant owner
- closed - only the current creator within the owner is permitted
- inherit - defer to the parent to get the security model.
Additional models can be implemented. The default configuration is closed, with no sharing., (*19)
Note: to implement your own security models, create an alternative SecurityModel class.
The enumeration object cannot be extended., (*20)
Domain Aware Tenant Participant
A domain aware tenant participant adds support for a domain name to the interface. This allows
the tenant information to be resolved from the current host name passed into the application.
This is used with the TenantSiteResolver middleware., (*21)
Domain Aware Tenant Participant Repository
The repository for the domain aware tenant participants. It is separate to the tenant
participant allowing separate instances to be used. Domain aware is used with the
TenantSiteResolver middleware., (*22)
This library provides the following tenant setups, in increasing order of complexity:, (*23)
- multi-account (single App), URI tenancy
- multi-site, domain name tenancy
- multi-site with multi-account tenancy
Multi-Account, URI Tenancy
The simplest case is a single App that has multi-account tenants. All users must be registered
and the tenancy is defined by the tenant_creator_id in the route URI. The tenancy is resolved
on User login meaning that this offers the smallest impact in your application., (*24)
If you need to serve static, non-tenant pages or your app does not need theme support, this is
the preferred tenancy model., (*25)
Multi-Site, Domain Name Tenancy
Increasing in complexity, the next level is domain-name based tenancy. Multiple sites running from
a single app folder. This is usually some form of white-labelling setup i.e. the same application
is re-skinned with different branding but the underlying app is practically the same., (*26)
Note: this is a substantial increase in difficulty from single app tenancy. You will need to
change the Application instance in /bootstrap/app.php to use:, (*27)
Somnambulist\Tenancy\Foundation\TenantAwareApplication
Note: you have to remove RouteServiceProvider and add TenantRouteResolver middleware., (*28)
Note: you must ensure that any caches you use can handle per-site caching., (*29)
In addition, this form of tenancy requires a middleware to run all the time to resolve the current
tenant information before any users login or the main app actually runs. If using a database for
the tenant source, this could increase site overhead and a high-performance cache is highly
recommended for production environments e.g.: APCu or an in memory-cache that persists between
requests to reduce the overhead of the tenant resolution., (*30)
A file-system repository can be easily created instead of using the database, or a combination of
both where a cache file is generated when the tenant sources change., (*31)
Routes can be customised per site by adding a file to your routes folder using the domain name.
Domain suffixes can be ignored by adding them to the list of ignorables in the tenancy.php config
file under: tenancy.multi_site.ignorable_domain_components
. The default are dev. and test., (*32)
Routes are searched for in several locations:, (*33)
- routes/
- app/Http/
- routes/
- app/Http/
- routes/routes
- routes/web
- routes/api
- app/Http/routes
- app/Http/web
- app/Http/api
A single set of routes can be shared with all sites. If neither app/Http or routes exists, no routes
will be loaded and an exception raised with the paths that were tried., (*34)
In multi-site, changes must be made to your app config:, (*35)
- view.paths: should have the default path changed to views/default
- view.compiled: should have the default path changed to views/default
When creating your app, you will need to create a "default" view theme and then mirror this for
each domain you serve from the app. The view folder should be named after the domain that is
bound to the tenant., (*36)
www.example.com -> resources/views/www.example.com
Your views folder will end up looking like:, (*37)
resources/views/default
resources/views/www.example.com
resources/views/store.example2.com
resources/views/store.example3.com
Once the tenant information has been resolved, several updates are made to the container
configuration:, (*38)
- app.url is replaced with the current host domain (not resolved domain name)
- template paths are re-computed as a hierarchy and the finder reset
Template path order is reset to:, (*39)
- tenant creator domain
- tenant owner domain (if different)
- default / existing paths
This way templates should be evaluated from most specific to least specific., (*40)
Note: auth.tenant is initialised with the tenant owner / creator and a NullUser., (*41)
Multi-Site with Multi-Account Tenancy
Note: this is the most complex scenario. TenantAwareApplication is required., (*42)
Note: you have to remove RouteServiceProvider and add TenantRouteResolver middleware., (*43)
Note: you must ensure that any caches you use can handle per-site caching., (*44)
This is a combination of both methods where there are multiple tenants per multi-site. In this
configuration there are limitations on the security that can be implemented unless a custom
implementation is made:, (*45)
- there is only one tenant owner per domain
- all tenant owners should have the closed security model
- all tenant creators should have the closed security model
It is possible to allow further tenanting however this would have to be a custom implementation
as your tenant creator would have to allow child tenants and implement a security model that is
appropriate in this situation. One possible example would be to cascade up through the parents
to set the tenant owner (which would be the domain tenant owner)., (*46)
This setup has the highest impact on site performance and requires users login to resolve their
tenancy. As such, this essentially results in double tenancy resolution., (*47)
This setup is not recommended as it could lead to hard to diagnose issues, but is included as it
is technically feasible with the current implementation., (*48)
Note: auth.tenant is initialised with the tenant owner / creator and a NullUser but after
User authentication will be updated with the current, authenticated user and any changes to the
creator tenant as needed. The owner tenant should still be the same as the creator must be a
child of the owner., (*49)
Requirements
- PHP 7.3+
- Laravel 7+
- laravel-doctrine/orm
Installation
Install using composer, or checkout / pull the files from github.com., (*50)
- composer install somnambulist/laravel-doctrine-tenancy
Setup / Getting Started
- add
Somnambulist\Tenancy\TenancyServiceProvider::class
to your config/app.php
- add
Somnambulist\Tenancy\EventSubscribers\TenantOwnerEventSubscriber::class
to config/doctrine.php subscribers
- create or import the config/tenancy.php file
- create your
TenantParticipant
entity / repository and add to the config file
- create your participant mappings in the config file (at least class => class)
- create your
User
with tenancy support
- create an
App\Http\Controller\TenantController
to handle the various tenant redirects
- add the basic routes
- for multi-site
- in bootstrap/app.php
- change Application instance to
Somnambulist\Tenancy\Foundation\TenantAwareApplication
-
Note: if multi-site is enabled and this changed is not made, an exception will be raised.
- in HttpKernel:
- add
TenantSiteResolver
middleware to middleware, after CheckForMaintenanceMode
- add
TenantRouteResolver
middleware to middleware, after TenantSiteResolver
- remove RouteServiceProvider from config/app.php
- for standard app tenancy and/or for tenancy within multi-site
- add
AuthenticateTenant
as auth.tenant to HttpKernel route middlewares
- add
EnsureTenantType
as auth.tenant.type to HttpKernel route middlewares
Example User
The following is an example of a tenant aware user that has a single tenant:, (*51)
tenant = $tenant;
}
public function getTenantParticipant()
{
return $this->tenant;
}
}
```
You should always set the tenancy whenever you create an entity. In previous versions there was an
event subscriber to discover it from the current request, however it has been removed as the tenant
information is a critical part of the record, and it is safer to always require it.
#### Example Tenant Participant
The following is an example of a tenant participant:
```php
'tenant', 'as' => 'tenant.', 'middleware' => ['auth']], function () {
Route::get('select', ['as' => 'select_tenant', 'uses' => 'TenantController@selectTenantAction']);
Route::get('no-tenants', ['as' => 'no_tenants', 'uses' => 'TenantController@noTenantsAvailableAction']);
Route::get('no-access', ['as' => 'access_denied', 'uses' => 'TenantController@accessDeniedAction']);
Route::get('not-supported', ['as' => 'tenant_type_not_supported', 'uses' => 'TenantController@tenantTypeNotSupportedAction']);
Route::get('invalid-request', ['as' => 'invalid_tenant_hierarchy', 'uses' => 'TenantController@invalidHierarchyAction']);
});
```
As a separate block (or within the previous section) add the areas of the application that
require tenancy support / enforcement. These routes should be prefixed with at least:
{tenant_creator_id}. {tenant_owner_id} can be used (first) which will force the auth.tenant
middleware to validate that the creator belongs to the owner as well as the current user
having access to the creator.
__Note:__ the user does not need access to the tenant owner, access to the tenant creator implies
permission to access a sub-set of the data.
```php
'account/{tenant_creator_id}', 'as' => 'tenant.', 'namespace' => 'Tenant', 'middleware' => ['auth', 'auth.tenant']], function () {
Route::get('/', ['as' => 'index', 'uses' => 'DashboardController@indexAction']);
// routes that should be limited to certain ParticipantTypes
Route::group(['prefix' => 'customer', 'as' => 'customer.', 'namespace' => 'Customer', 'middleware' => ['auth.tenant.type:crm']], function () {
Route::get('/', ['as' => 'index', 'uses' => 'CustomerController@indexAction']);
});
});
```
#### AuthController Changes
When using tenancy, the AuthController must be modified to include the redirector service
to know where to go to after a successful login. If your AuthController is the standard
Laravel provided one, simply add an authenticated method:
```php
setLastLogin(Carbon::now());
//$em = app('em');
//$em->flush($user);
// redirect to tenant uri
return app('auth.tenant.redirector')->resolve($user);
}
}
```
In addition, if you allow registration of new users you will need to now add support for the
tenancy component. This must be done by overriding the postRegister method:
```php
validator($request->all());
if ($validator->fails()) {
$this->throwValidationException(
$request,
$validator
);
}
$user = $this->create($request->all());
Auth::login($user);
// call into redirector which was previously mapped above
return $this->authenticated($request, $user);
}
}
```
It is up to the implementer to figure out what to do with new registrations or if this
should even be allowed.
#### Tenant Aware Entity
Finally you need something that is actually tenant aware! So lets create a really basic
customer:
```php
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="App\Entity\Customer" table="customers" repository-class="App\Repository\CustomerRepository">
<unique-constraints>
<unique-constraint xml:id="uniq_customers_uuid" columns="uuid" />
</unique-constraints>
<id name="id" type="integer">
<generator strategy="IDENTITY"/>
<options>
<option name="unsigned">true</option>
</options>
</id>
<field name="uuid" type="guid" />
<field name="tenantOwnerId" type="integer" />
<field name="tenantCreatorId" type="integer" />
<field name="name" type="string" length="255" />
<field name="createdBy" type="string" length="36" />
<field name="updatedBy" type="string" length="36" />
<field name="createdAt" type="datetime" />
<field name="updatedAt" type="datetime" />
</entity>
</doctrine-mapping>
Tenant Aware Repositories
Note: applies to Doctrine only., (*52)
Tenant aware repositories simply wrap an existing entity repository with the standard
repository interface. They should be defined and created as we actually want to be
able to inject these as dependency and set them up in the container., (*53)
First you will need to create an App level TenantAwareRepository that extends:, (*54)
- Somnambulist\Tenancy\Repositories\TenantAwareRepository
For example:, (*55)
<?php
namespace App\Repository;
use Somnambulist\Tenancy\Repositories\TenantAwareRepository;
class AppTenantAwareRepository extends TenantAwareRepository
{
}
Provided you don't have a custom security model, this should be good to extend again
as a namespaced "tenant" repository for our customer:, (*56)
<?php
namespace App\Repository\TenantAware;
use App\Repository\AppTenantAwareRepository;
class CustomerRepository extends AppTenantAwareRepository
{
}
Now, the config/tenancy.php can be updated to add a repository config definition so this class
will be automatically available in the container., (*57)
Note: this step presumes the standard repository is already mapped to the container using
the repository class as the key., (*58)
[
'repository' => \App\Repository\TenantAware\CustomerRepository::class,
'base' => \App\Repository\CustomerRepository::class,
//'alias' => 'app.repository.tenant_aware_customer', // optionally alias
//'tags' => ['repository', 'tenant_aware'], // optionally tag
],
Security Models
The security model defines how data within a tenant owner should be shared. The default is no
sharing at all. In fact the security model only applies when the User implements the
BelongsToTenantParticipants and there can be multiple tenants on one user., (*59)
Shared
In this situation, the tenant owner may decide that any data can be shared by all child tenants
of the owner. This model is called "shared" and means that all data in the tenant owner is
available to all tenant creators at any time., (*60)
To set the security model, simply save the TenantParticipant instance with the security model
set to: TenantSecurityModel::SHARED(), (*61)
Behind the scenes, when the TenantAwareRepository is queried, the current Tenant information is
extracted and the query builder instance modified to set the tenant owner and/or creator. For
shared data, only the owner is set., (*62)
The other pre-built models are:, (*63)
User
The User model restricts the queries to the current tenant owner and any mapped tenant. So if
a User has 4 child tenants, they will be able to access the data created only by those 4
child tenants. All other data will be excluded., (*64)
Closed
If the security model is set to closed, then all queries are created with the tenant owner and
current creator only. The user in this scheme, even with multiple tenant creators, will only
ever see data that was created by the current creator., (*65)
Inherit
Inherit allows the security model to be adopted from a parent tenant. If the parent model
is inherit, or there is no parent then the model will be set to closed automatically. This
library attempts to favour least access whenever possible., (*66)
Applying / Adding Security Models
The security model rules are applied by methods within the TenantAwareRepository. The model
name is capitalised, prefixed with "apply" and suffixed with SecurityModel so "shared" becomes
"applySharedSecurityModel"., (*67)
This is why an App level repository is strongly suggested as you can then implement your own
security models simply by extending the TenantSecurityModel, defining some new constants and
then adding the appropriate method in your App repository., (*68)
For example: say you want to have a "global" policy where all unowned data is shared all over
but you also have your own data that is private to your tenant, you could add this as a new
method:, (*69)
<?php
class AppTenantAwareRepository extends TenantAwareRepository
{
protected function applyGlobalSecurityModel(QueryBuilder $qb, $alias)
{
$qb
->where("({$alias}.tenantOwnerId IS NULL OR {$alias}.tenantOwnerId = :tenantOwnerId)")
->setParameters([
':tenantOwnerId' => $this->tenant->getTenantOwnerId(),
])
;
}
}
Additional schemes can be added as needed., (*70)
Note: while in theory you can mix security models within a tenant e.g.: some children are
closed, others shared, some user; this may result in strange results or inconsistencies.
It may lead to a large increase in duplicate records. It is up to you to manage this
accordingly., (*71)
Routing
For any route within the tenant group and provided that the placeholder name is tenant_creator_id,
any route generated for a tenant controller will automatically embed the current tenant information.
In fact both the owner and creator are automatically checked for and injected when creating
routes., (*72)
This is done by overriding the default UrlGenerator with one that adds the Tenant entity and then
checking the route information for both {tenant_owner_id} and {tenant_creator_id}. The properties
are then automatically injected., (*73)
This only occurs when using named routes and within a tenancy group., (*74)
For paths, you will have to include the tenant information yourself; similarly when creating a tenant
selection list, you must supply the tenant information as parameters when outputting the links., (*75)
The tenant parameters can of course be overridden by simply setting them when calling link_to_route., (*76)
You should use named routes for tenancy, as this makes it easier to make changes to the routing
structure., (*77)
Finally: as with repositories you should clearly label tenant based routes so that they are not
confused with standard routes., (*78)
In addition any un-authenticated routes should be excluded from the tenancy group - unless you
implement a tenant aware anonymous user (not recommended)., (*79)
Multi-Site Routing
In a multi-site setup you may want to have different routes per site. In this case you will need
to remove your RouteServiceProvider entirely and switch it for the TenantRouteResolver middleware.
Then you will need to either create per tenant domain route files (which can include() shared
routes) or symlink the files if you wish to use the exact same routes., (*80)
A middleware is provided to handle loading the routes for a multi-site setup. This must be loaded
after the TenantSiteResolver, but before any other middleware. In addition you must disable / remove
the default App/Providers/RouteServiceProvider. This provider is registered too early and must be
delayed / resolved via the TenantRouteResolver instead., (*81)
The reasons for this setup are to ensure that only the chosen tenants routes are loaded, and not
appended to any existing routing files., (*82)
Note: these are not route middleware but Kernel middleware., (*83)
Your Kernel.php will end up looking like the following:, (*84)
<?php
class Kernel extends HttpKernel
{
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
// must appear AFTER maintenance mode, but before everything else
\Somnambulist\Tenancy\Http\Middleware\TenantSiteResolver::class,
\Somnambulist\Tenancy\Http\Middleware\TenantRouteResolver::class,
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
];
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.tenant' => \Somnambulist\Tenancy\Http\Middleware\AuthenticateTenant::class,
'auth.tenant.type' => \Somnambulist\Tenancy\Http\Middleware\EnsureTenantType::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
];
}
The auth.tenant / auth.tenant.type are optional in multi-site, and should only be included if
you are using multi-account tenancy., (*85)
Again: ensure that the previous RouteServiceProvider in config/app.php has been removed., (*86)
Note: you must not use the standard route:list, route:cache in a multi-site setup. Tenant
aware versions of these commands are automatically registered if a multi-site setup is detected
in the configuration settings and are prefixed with tenant:., (*87)
Route Namespace
When using the TenantRouteResolver, you must specify the route namespace in the tenancy config
file under the multi_site configuration block:, (*88)
<?php
// config/app.php
return [
// other stuff...
'multi_site' => [
'router' => [
'namespace' => 'App\Http\Controller', // default
],
],
// more stuff...
];
If left out, the default App\Http\Controller is used. If set to an empty string, then no
namespace prefix will be set on any routes., (*89)
Route Patterns
Like the namespace, patterns can still be set by adding them to your config/tenancy.php under
multi_site.router.patterns. This is an associative array of identifier and pattern. They are
registered with the router when the routes are resolved., (*90)
<?php
// config/app.php
return [
// other stuff...
'multi_site' => [
'router' => [
'namespace' => 'App\Http\Controller', // default
'patterns' => [
'id' => '[0-9]+',
],
],
],
// more stuff...
];
Middleware
AuthenticateTenant
AuthenticateTenant ensures the currently authenticated user is permitted to access the currently
specified tenant URI. It is used as Route middleware and is required for Multi-Account tenant
systems., (*91)
TenantSiteResolver
TenantSiteResolver will determine if the requested host is a valid tenant host. This is the primary
Multi-Site tenancy middleware. It must be registered as a Kernel middleware, and run after the
maintenance mode check but before any others., (*92)
TenantRouteResolver
TenantRouteResolver is the second part of the Multi-Site middleware. It runs after the site
resolver and tries to load the hosts route information from a file located in App/Http/.php.
If the current tenant is not a DomainAwareTenantParticipant, the standard routes.php file is
checked for instead., (*93)
EnsureTenantType
EnsureTenantType is a Route middleware and is used when you have used inheritance for your tenant
participant. It allows routes to be safe-guarded from certain tenant types so for example: you
could mark a set of routes as requiring a particular membership type, or as an opportunity to
up-sell services - or purely as a security safe-guard to ensure that basic tenants do not gain
access to admin features., (*94)
This middleware should be the last to run of the tenancy middleware., (*95)
Twig Extension
A Twig extension is provided that can be added to the config/twigbridge.php extensions. This adds
the following template functions:, (*96)
- current_tenant_owner_id
- current_tenant_creator_id
- current_tenant_owner
- current_tenant_creator
- current_tenant_security_model
This allows access to the current resolved Tenant instance. To enable the Twig extension, add it to
the list of extensions in the config/twigbridge.php file., (*97)
Note: in a previous iteration, this included functions to look up tenant owner/creator from
a repository, however: as the tenant could be domain aware or standard tenant, you do not
know which repository to use so it was removed. Further: this information almost certainly
should not be being pulled in a standard view anyway., (*98)
Views
The bundled TenantController expects to find views under:, (*99)
- /resources/views/tenant
- /resources/views/tenant/error
These are not included as they require application implementation. The TenantController class
has information about file names and route mappings., (*100)
In multi-site, these will need placing in appropriate sub-folders / duplicating where
necessary., (*101)
Potential Issues
Working with multi-tenancy can be very complex. This library works on a shared database, not
individual databases, however you could setup specific databases based on the tenant if
necessary (if you are comfortable with multiple connections / definitions in Doctrine)., (*102)
When creating repositories always ensure that tenant aware / non-tenant aware are clearly
marked to avoid using the wrong type in the wrong context. Best case: you don't see anything,
worst case - you see everything unfiltered., (*103)
You will note that in this system there are no magic SQL filters pre-applied through Doctrines
DQL filters: this is deliberate. You should be able to switch the tenancy easily at any point
and this can be done by simply updating the Tenant instance, or using the non-tenant aware
repository., (*104)
Additionally: none of the tenant ids are references to other objects. Again this is very
deliberate. It allows e.g. customer data to be in a separate database to your users and makes
it a lot more portable., (*105)
Using tenancy will add an amount of overhead to your application. How much will depend on
how much data you have and the security model you apply., (*106)
Always test and have functional tests to ensure that the tenancy is applied correctly and
whenever in doubt: always deny rather than grant access., (*107)
Links
Other Multi-Tenant Projects