ASP.NET Core Authorize & UserRoles in Multy Tenancy

159 Views Asked by At

We have am ASP.NET Core 6.0 multi tenant application with a single database. The tenant tables have a TenantId column to show only tenant data. We don't need to discuss whether is better or not to have multiple databases, for our purpose we need it this way.

All tenants share the users table and there is a UserTenant table to filter which users a tenant can see. But it is not relevant for this question.

We need to Authorize some controllers and for that we need to do this using roles:

[Authorize(Roles = "RoleA,RoleB,RoleC")]
public class SomeController : Controller

This way, a user can get into the controller only if belongs to one of those roles.

The problem is we need to assign roles to users in a certain tenant. So a "UserA" can have the "RoleA" in "TenantA", but not in "TenantB".

The Roles would be the same for all the tenants, so no tenantId in the table "aspnetroles", but we do need another property for table "aspnetuserroles" since the key sould be "UserId, RoleId,TenantId".

Our Application user is configured this way:

public class ApplicationUser : IdentityUser<int>
{
    public string Name { get; set; } = String.Empty;
    public string Surname { get; set; } = String.Empty;
    public string? Alias { get; set; }
    ....
}

public class ApplicationRole : IdentityRole<int> { }

public class ApplicationUserLogin : IdentityRole<int> { }

How can we change the Identity UserRoles in order to, not only store the UserId, RoleId and TenantId but also to work with the Authorize in the controller?

Thanks in advance.

UPDATE 1

I have been able to customize the aspnetuserroles table, the Client is our Tenant. This way we have already the table modified:

public class ApplicationUserRole : IdentityUserRole<int>
{
    public int ClientId { get; set; }
    public Client Client { get; set; }

    public static void OnModelCreating(ModelBuilder modelBuilder)
    {
        var e = modelBuilder.Entity<ApplicationUserRole>();

        e.HasKey(p => new { p.UserId, p.RoleId, p.ClientId });

        e.HasOne(p => p.Client).WithMany().HasForeignKey(tr => tr.ClientId).HasConstraintName("FK_UserRole_Client").IsRequired().OnDelete(DeleteBehavior.Cascade);
    }
}

UPDATE 2

We have been able to modify the UserManager so we can add roles using the TenantId, in our case the ClientId.

public class ApplicationUserManager : UserManager<ApplicationUser>
{
    private readonly UserStore<ApplicationUser, ApplicationRole, DataContext, int, IdentityUserClaim<int>, ApplicationUserRole, IdentityUserLogin<int>, IdentityUserToken<int>, IdentityRoleClaim<int>> _store;

    public ApplicationUserManager(
        IUserStore<ApplicationUser> store,
        IOptions<IdentityOptions> optionsAccessor,
        IPasswordHasher<ApplicationUser> passwordHasher,
        IEnumerable<IUserValidator<ApplicationUser>> userValidators,
        IEnumerable<IPasswordValidator<ApplicationUser>> passwordValidators,
        ILookupNormalizer keyNormalizer,
        IdentityErrorDescriber errors,
        IServiceProvider services,
        ILogger<UserManager<ApplicationUser>> logger)
        : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
    {
        _store = (UserStore<ApplicationUser, ApplicationRole, DataContext, int, IdentityUserClaim<int>, ApplicationUserRole, IdentityUserLogin<int>, IdentityUserToken<int>, IdentityRoleClaim<int>>)store;
    }


    public virtual async Task<IdentityResult> AddToRoleByClientAsync(ApplicationUser user, string role, int clientId)
    {
        ThrowIfDisposed();

        if (user == null)
            throw new ArgumentNullException(nameof(user));

        if (string.IsNullOrWhiteSpace(role))
            throw new ArgumentNullException(nameof(role));

        if (clientId == 0)
            throw new ArgumentNullException(nameof(clientId));

        var normalizedRole = NormalizeName(role);

        var applicationRole = _store.Context.Roles.FirstOrDefault(p => p.NormalizedName == normalizedRole);
        if (applicationRole == null)
            return IdentityResult.Failed(ErrorDescriber.InvalidRoleName(normalizedRole));

        if (await IsInRoleByRoleIdClientAsync(user, applicationRole.Id, clientId))
            return IdentityResult.Failed(ErrorDescriber.UserAlreadyInRole(normalizedRole));

        _store.Context.Set<ApplicationUserRole>().Add(new ApplicationUserRole { RoleId = applicationRole.Id, UserId = user.Id, ClientId = clientId });

        return await UpdateUserAsync(user);
    }

    private async Task<bool> IsInRoleByRoleIdClientAsync(ApplicationUser user, int roleId, int clientId, CancellationToken cancellationToken = default(CancellationToken))
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (user == null)
            throw new ArgumentNullException(nameof(user));

        if (roleId == 0)
            throw new ArgumentNullException(nameof(roleId));

        if (clientId == 0)
            throw new ArgumentNullException(nameof(clientId));

        var userRole = await _store.Context.Set<ApplicationUserRole>().FindAsync(new object[] { user.Id, roleId, clientId }, cancellationToken);
        return userRole != null;
    }

    public async Task<bool> IsInRoleByClientAsync(ApplicationUser user, string role, int clientId)
    {
        ThrowIfDisposed();

        if (user == null)
            throw new ArgumentNullException(nameof(user));

        if (string.IsNullOrWhiteSpace(role))
            throw new ArgumentNullException(nameof(role));

        if (clientId == 0)
            throw new ArgumentNullException(nameof(clientId));

        var normalizedRole = NormalizeName(role);

        var applicationRole = _store.Context.Roles.FirstOrDefault(p => p.NormalizedName == normalizedRole);
        if (applicationRole == null)
            return false;

        var userRole = await _store.Context.Set<ApplicationUserRole>().FindAsync(new object[] { user.Id, applicationRole.Id, clientId });
        return userRole != null;
    }
}

We still need to modify the [Authorize(Roles = "admin")] in order to check the roles of the current Tenant and not all of them. We are investigating this.

0

There are 0 best solutions below