-
-
Notifications
You must be signed in to change notification settings - Fork 83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Multitenancy with TheIdServer #1084
Comments
Hi, thanks to contribute.
If you need a single DB for all tenants that also means rewriting all stores to get the tenant id and choose the good configuration. It's not a trivial operation. good luck :) |
There also the OIDC discovery document generator to rewrite per tenant if you override the request path. At least provide a new implementation for IServerUrls to replace the DefaultServerUrls |
@aguacongas I'm working on this multi tenency idea i've had with TheIdServer and need a bit of help. I'm trying to create a factory for the AspnetCore User Manager and Role Manager that TheIdServer implements but I can't seem to find the code in your Repo. Could you please point me in the right direction? Here is an example of what i'm trying to do with the base UserManager and RoleManager. The ultimate goal of this is to pass a specific instance of the ApplicationDbContext to this factory so the managers interact with the appropriate database. public static class IdentityManagerFactory
{
public static UserManager<User> CreateUserManager(IdentityContext context, IServiceProvider serviceProvider)
{
var userStore = new UserStore<User, Role, IdentityContext, string>(context);
var optionsAccessor = new OptionsWrapper<IdentityOptions>(new IdentityOptions());
var passwordHasher = new PasswordHasher<User>();
var userValidators = new List<IUserValidator<User>>();
var passwordValidators = new List<IPasswordValidator<User>>();
var lookupNormalizer = new UpperInvariantLookupNormalizer();
var identityErrorDescriber = new IdentityErrorDescriber();
var logger = new Logger<UserManager<User>>(new LoggerFactory());
return new UserManager<User>(
userStore, optionsAccessor, passwordHasher, userValidators, passwordValidators,
lookupNormalizer, identityErrorDescriber, serviceProvider, logger);
}
public static RoleManager<Role> CreateRoleManager(IdentityContext context)
{
var roleStore = new RoleStore<Role, IdentityContext, string>(context);
var roleValidators = new List<IRoleValidator<Role>>();
var lookupNormalizer = new UpperInvariantLookupNormalizer();
var identityErrorDescriber = new IdentityErrorDescriber();
var logger = new Logger<RoleManager<Role>>(new LoggerFactory());
return new RoleManager<Role>(
roleStore, roleValidators, lookupNormalizer, identityErrorDescriber, logger);
}
} |
You cannot create a static factory. Instances must be created by DI. I guess you need a way to get the good connection string per tenant and don't forget TheIdServer also support MongoDB and RavenDB. I'm not sure it's the good way. May be the best choice is to add the tenant id on each entities and provides a service getting the tenantId to each stores so they can filter entities by the current tenant id. This way there no needs to create a new db per tenant. What you tink ? |
That said, you can override the default DI to provide the DB context, RavenDB for exemple: public static class ServiceCollectionExtensions
{
public static ServiceCollection AddTenants(this ServiceCollection services)
{
services.AddScoped(p =>
{
// return your DB context factory implementation by tenant
var tenantService = p.GetRequiredService<IGetContextCurrentTenant>();
return tenant.GetDbContext<ApplicationDbContext>();
});
// same for each DB context
...
services.AddTransient(p =>
{
// returns your RavenDB IAsyncDocumentSession factory implementation by tenant
var tenantService = p.GetRequiredService<IGetRavenDbAsynSessionForCurrentTenant>();
return tenant.GetRavenDbAsyncSession();
});
services.AddTransient(p =>
{
// return your MongoDB IMongoDatabase factory implementation by tenant
var tenantService = p.GetRequiredService<IGetMongoDbDatabaseForCurrentTenant>();
return tenant.GetMongoDbDatabase();
});
return services
}
} |
Hey @aguacongas, Wanted to provide some context. After looking over your code I agree that TheIdServer would not benefit from built in multi-tenancy. I'm still proceeding to fit to to my needs though. Having said that I think I just have one more question for you for clarification. Is your OperationalDbContext the same as the PersistedGrantDbContext defined by duende? |
More or less except there's one table per grant kind but the goal is to store temporary tokens. |
By the way can you open a PR when your job is done ? I'm interested by your work even if that's not cover all DB kinds supported by TheIdServer nor all functionnalities. |
I'll share a Repo link with you to look at. I ended up starting from the Dotnet template you have as overall it was less confusing. I did run into an issue though. I'm attempting to create my own migrations and i'm getting an error when trying to create a migration for the PersistedGrantDbContext/OperationalDbContext:
I tested it without my context override and it looks like it might exist in the unmodified code also, but I haven't determined if its something I've done. I'm also not sure what Claim model it is talking about, since i started with the Dotnet Template I don't have all the models easily assessable to me. Here is my override context incase its relavant. public class PersistedGrantContext : OperationalDbContext, IMultiTenantDbContext
{
private const string DefaultSchema = "persisted_grant";
private readonly IConfiguration _configuration;
private readonly IWebHostEnvironment _environment;
public PersistedGrantContext(DbContextOptions<OperationalDbContext> options, ExtendedTenantInfo tenantInfo, IConfiguration configuration, IWebHostEnvironment environment)
: base(options)
{
TenantInfo = tenantInfo;
_configuration = configuration;
_environment = environment;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var connectionString = TenantInfo.ConnectionString!;
if (!_environment.IsProduction())
{
connectionString += "_Dev";
}
optionsBuilder.UseNpgsql(DatabaseExtensions.BuildPostgresConnectionString(_configuration, _environment, connectionString) ?? throw new InvalidOperationException());
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema(DefaultSchema);
modelBuilder.ConfigureMultiTenant();
base.OnModelCreating(modelBuilder);
//foreach (var entity in modelBuilder.Model.GetEntityTypes())
//{
// var tableName = entity.GetTableName().ToSnakeCase();
// modelBuilder.Entity(entity.Name).ToTable(tableName);
// foreach (var property in entity.GetProperties())
// {
// var columnName = property.Name.ToSnakeCase();
// modelBuilder.Entity(entity.Name).Property(property.Name).HasColumnName(columnName);
// }
//}
modelBuilder.Entity<AuthorizationCode>().IsMultiTenant();
modelBuilder.Entity<ReferenceToken>().IsMultiTenant();
modelBuilder.Entity<RefreshToken>().IsMultiTenant();
modelBuilder.Entity<UserConsent>().IsMultiTenant();
modelBuilder.Entity<DeviceCode>().IsMultiTenant();
modelBuilder.Entity<OneTimeToken>().IsMultiTenant();
modelBuilder.Entity<DataProtectionKey>().IsMultiTenant();
modelBuilder.Entity<KeyRotationKey>().IsMultiTenant();
modelBuilder.Entity<UserSession>().IsMultiTenant();
modelBuilder.Entity<Saml2PArtifact>().IsMultiTenant();
modelBuilder.Entity<BackChannelAuthenticationRequest>().IsMultiTenant();
}
public ITenantInfo TenantInfo { get; }
public TenantMismatchMode TenantMismatchMode => TenantMismatchMode.Throw;
public TenantNotSetMode TenantNotSetMode => TenantNotSetMode.Throw;
} and the factory incase your curious public class PersistedGrantContextFactory
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IConfiguration _configuration;
private readonly IWebHostEnvironment _environment;
public PersistedGrantContextFactory(IHttpContextAccessor httpContextAccessor, IConfiguration configuration, IWebHostEnvironment environment)
{
_httpContextAccessor = httpContextAccessor;
_configuration = configuration;
_environment = environment;
}
public PersistedGrantContext CreateDbContext(DbContextOptions<OperationalDbContext> options, ExtendedTenantInfo providedTenantInfo = null)
{
ExtendedTenantInfo tenantInfo = providedTenantInfo;
if (tenantInfo == null && _httpContextAccessor.HttpContext != null)
{
tenantInfo = _httpContextAccessor.HttpContext.GetMultiTenantContext<ExtendedTenantInfo>()?.TenantInfo;
}
tenantInfo ??= new ExtendedTenantInfo();
return new PersistedGrantContext(options, tenantInfo, _configuration, _environment);
}
} I also looked in your OperationalDbContext and I don't see any "Claim" type. Have you seen this before? |
No, not yet |
So i figured out the above problem. Some of your type names exactly match Duende Type names which caused imports to get screwed up. Claim was a type on a duende type name instead of string. Next thing i'm having trouble understand is your ApplicationDbContext has the type of User and Role. But your UserManager code and Role Manager code accept IdentityRole and ApplicationUser. Where is the code that intercepts thie request of the UserManager/RoleManager to change this model to a User or a Role to match the type of the DbContext? When i'm trying to see roles i get the following error: which is because ApplicationDbContext has a DbSet for Role which doesn't inherit from IdentityRole. I'm not finding any custom implementations of UserManager or RoleManager so i'm quite confused. |
There are custom |
Looking at these stores, is the type in the Database a Role and User or is it still IdentityRole and IdentityUser. I'm seeing conversions happening in places so i'm not sure what the end result is that is being committed. assuming IdentityRole and IdentityUser, but i might be missing something. |
Entities in DB are defined in Aguacongas.IdentityServer.Store/Entity.If you need to add/update some columns, you should update |
I am incredibly interested in what you have built with the Id Server.
Today I'm working on standing up and creating an Identity Server with there QuickStart UI that I'm slowly adding onto. I looked into your solution but there is no good way today to quickly add multitenancy support to TheIdServer that I can see, at least not by using your templates. I attempted to pull your entire code base down to see if I could get it to run but I failed for many hours.
Today I'm using Finbuckle for multitenancy on Identity servers which is very easy to setup. I have it configured for each tenant to have its own database and I have a seed script for each of those databases. By AspNetCore Entity framework context along with the persistentgrant context and Configuration context are all configured for multitenant support with IdentityServer.
My default tenant falls back to a certain Database if one is not entered but my pathing assumes that https://myUrl.com/TenantId/whateverExistingPathHere. and then i use a simple middleware to get my tenant for that request and then process to the page
I'd like to do the same with TheIdServer and see how this works for me. The idea I had is that eventually I'd have a tenant drop down on the blazer app to select my tenant and work from that tenant but it would always default to my primary like i have it now.
Would you be able to guide me on the different things I might have to override in TheIdServer to potentially accomplish this?
If successful I'd be willing to open a PR back into your repo with it setup to provide optional multiTenant configurations for other users of TheIdServer.
The text was updated successfully, but these errors were encountered: