-
Notifications
You must be signed in to change notification settings - Fork 162
Creating a multi tenant app
AuthP's Multi-Tenant feature manages the Multi-Tenant structure and DataKeys for you to implement a multi-tenant application. This document lists what the AuthP library provides and then defines what you need to set up your multi-tenant DbContext.
Once you have configure the multi-tenant AuthP features (see Multi-Tenant configuration) the following features are available:
- You can create, update, delete a AuthP
Tenant
(see Single level Multi-Tenant admin or Hierarchical Multi-Tenant admin documentation). - Once you have added a AuthP
Tenant
to an AuthP user, then aDataKey
claim will be added to the ASP.NET Core user (works for Cookie or JWT Token authentication). - AuthP registers the service
IGetDataKeyFromUser
which will provide you with theDataKey
form the user (null if not logged in, or the AuthP user doesn't have a Tenant linked to it). - You need to create a tenant change service which must implement the
ITenantChangeService
- see the Multi tenant configuration -> Building a tenant change service on how to do that. - Your AuthP multi-tenant configuration will have registered your implementation of the (see Multi-Tenant configuration -> Registering your tenant change service for how to do that) which will apply any AuthP Tenant create, update, delete or hierarchical tenant move to the application's data.
From this place you are ready to set up your multi-tenant application DbContext
NOTE: Typically you won't implement the ITenantChangeService
until you have set up your application DbContext, but you need this service before you code can work with AuthP's Tenant DataKeys.
This section lists the things you need to do to create an application DbContext that can filter the EF Core classes/tables so that only data that has the same DataKey
as the logged in user is available to the user.
Before listing the steps there are two example multi-tenant applications in the AuthP repo which can be run. These provide an excellent code base to inspect and learn from.
- Example3 implements a single level multi-tenant application. You can find the tenant data application entity classes, EF Core DbContext and implemented
ITenantChangeService
service in the Example3.InvoiceCode project. - Example4 implements a hierarchical multi-tenant application. You can find the tenant data application entity classes, EF Core DbContext and implemented
ITenantChangeService
service in the Example4.ShopCode project. - Example6 implements a single level multi-tenant application using a hybrid database arrangement. You can find the tenant data application entity classes, EF Core DbContext and implemented
ITenantChangeService
service in the Example6.SingleLevelShardingproject.
You need to provide a second parameter to the constructor in your application DbContext, as shown in the example code below.
public class YourDbContext : DbContext, IDataKeyFilter
{
public string DataKey { get; }
public YourDbContext (DbContextOptions<YourDbContext> options,
IGetDataKeyFromUser dataKeyFilter)
: base(options)
{
// The DataKey is null when: no one is logged in, its a background service,
// or user hasn't got an assigned tenant.
// In these cases its best to set the data key that doesn't match any possible DataKey
DataKey = dataKeyFilter?.DataKey ?? "stop any user without a DataKey to access the data";
}
// rest of code left out.
}
The multiple-database arrangement (i.e. the AddSharding
member is added to the TenantType
in the options) you need both the DataKey and a connection string. See the ShardingSingleDbContext
for how this is done.
You need to configure a global query filter on all the entities that hold tenant's DataKey. You could do this by calling HasQueryFilter
Fluent API method in your configuration, but I recommend you automate this.
- If its a single-level multi-tenant system with one database the user's DataKey much exactly match the DataKey in the multi-tenant entities. (have a look at the code inside the
OnModelCreating
method of the InvoicesDbContext used in Example3's single level multi-tenant system. - If its a single-level multi-tenant with
AsSharding
system the user's DataKey much exactly match the DataKey in the multi-tenant entities, or if the tenant has its own database it has a special string that turns of the query filter. (have a look at the code inside theOnModelCreating
method of the ShardingSingleDbContext used in Example6's single level multi-tenant system with sharding and its call to theAddSingleTenantShardingQueryFilter
. - If its a hierarchical multi-tenant system (with or without sharding) the DataKey in the multi-tenant entities must
StartWith
the user's DataKey. (Have a look at code inside theOnModelCreating
method of the RetailDbContext used in Example4's hierarchical multi-tenant system.)
Assuming you are using the automated configuration, each entities that hold tenant data should have an interface to say they have a DataKey. AuthP provides two types of interfaces (but you can create your own)
-
IDataKeyFilterReadWrite
, which assumes you are setting the DataKey by overriding the DbContext'sSaveChanges
/SaveChangesAsync
when a new entity is add to the database (see section 3.1 below). Example3 uses this approach. -
IDataKeyFilterReadOnly
, which assumes the DataKey value is taken from a local tenant class in your application database. This local tenant will be updated by the implementedITenantChangeService
service and new entities take the DataKey from the local tenant class. Example4 uses this approach.
The two main options are:
This is the normal approach and requires you to override the DbContext's SaveChanges
/ SaveChangesAsync
and a method updates the DataKey
for every newly added entity classes, or use EF Core's SaveChanges interceptor. The example code shown comes from Example3'
public class InvoicesDbContext : DbContext, IDataKeyFilterReadOnly
{
public string DataKey { get; } //setting of the DataKey has been left out
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
this.MarkWithDataKeyIfNeeded(DataKey);
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess,
CancellationToken cancellationToken = default(CancellationToken))
{
this.MarkWithDataKeyIfNeeded(DataKey);
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
/// other code left out
Here is an example the MarkWithDataKeyIfNeeded
extension method taken from Example3.
public static void MarkWithDataKeyIfNeeded(this DbContext context, string accessKey)
{
foreach (var entityEntry in context.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added))
{
// This is a newly added entity, so see if it has a DataKey and the DataKey is null,
// then we will apply the DataKey provided by the user.
var hasDataKey = entityEntry.Entity as IDataKeyFilterReadWrite;
if (hasDataKey != null && hasDataKey.DataKey == null)
// If the entity has a DataKey it will only update it if its null
// This allow for the code to define the DataKey on creation
hasDataKey.DataKey = accessKey;
}
}
This isn't a normal approach, but I added it as a possible approach in case its useful. However I don't think it would work with a tenant that had lots of entity classes that aren't linked to each other.
In the Example4 application there is a RetailOutlet
entity class, which is created (via the ITenantChangeService
service) when a new AuthP tenant is created. Then the other classes, ShopStock
and ShopSale
have links back to the RetailOutlet
entity class.
So, when some stock is sold the code looks like this
var stockToBuy = service.ReadSingle<ShopStock>(dto.ShopStockId);
var status = ShopSale.CreateSellAndUpdateStock(dto.NumBought, stockToBuy, null);
...context.Add(status.Result);
...context.SaveChanges();
And the ShopSale
constructor looks like this (notice the last line of code where the DataKey is set from the stock DataKey).
private ShopSale(int numSoldReturned, string returnReason, ShopStock foundStock)
{
if (numSoldReturned == 0) throw new ArgumentException("cannot be zero", nameof(numSoldReturned));
if (numSoldReturned < 0 && returnReason == null)
throw new ArgumentException("cannot be null if its a return", nameof(returnReason));
NumSoldReturned = numSoldReturned;
ReturnReason = returnReason;
StockItem = foundStock;
DataKey = foundStock.DataKey;
}
If you added the AddSharding
to the setting of the TenantType
, then you need to supply both the DataKey and a connection string. This done via the a service within the AuthP that matched the IGetShardingDataFromUser
. The connection string is applied the tenant DbContext constructor using EF Core's SetConnectionString
method - see ShardingSingleDbContext
for an example of this.
- Intro to multi-tenants (ASP.NET video)
- Articles in date order:
- 0. Improved Roles/Permissions
- 1. Setting up the database
- 2. Admin: adding users and tenants
- 3. Versioning your app
- 4. Hierarchical multi-tenant
- 5. Advanced technique with claims
- 6. Sharding multi-tenant setup
- 7. Three ways to add new users
- 8. The design of the sharding data
- 9. Down for maintenance article
- 10: Three ways to refresh claims
- 11. Features of Multilingual service
- 12. Custom databases - Part1
- Videos (old)
- Authentication explained
- Permissions explained
- Roles explained
- AuthUser explained
- Multi tenant explained
- Sharding explained
- How AuthP handles sharding
- How AuthP handles errors
- Languages & cultures explained
- JWT Token refresh explained
- Setup Permissions
- Setup Authentication
- Startup code
- Setup the custom database feature
- JWT Token configuration
- Multi tenant configuration
- Using Permissions
- Using JWT Tokens
- Creating a multi-tenant app
- Supporting multiple languages
- Unit Test your AuthP app