Duplicate Role Names on Asp.Net Identity and Multi-tenancy
Asked Answered
G

3

8

I'm developing a Multi-tenancy web application with ASP.Net MVC and Identity 2.0. I have extended the IdentityRole like this:

public class ApplicationRole : IdentityRole
{
    public ApplicationRole() : base() { }
    public ApplicationRole(string name) : base(name) { }

    public string TenantId { get; set; }
}

This because each Tenant will have individual sets of roles, like "Admin", "Staff", etc.

But the problem is, when I add a new Role, if the "Tenant A" has "Admin" role, when I add to "Tenant B" the "Admin" role, I get an IdentityResult error because "Admin" name is taken... Its is kinda obvious because the "Name" field on the AspNetRoles table is Unique...

IdentityResult roleResult = await RoleManager.CreateAsync(
  new ApplicationRole
  {
    Name = "Admin",
    TenantId = GetTenantId()
  });

But then how I can customize ASP.Net Identity so the "Name" field in the "AspNetRoles" can be unique with "TenantId", and not alone? I found info about extend the IdentityRole (like I did adding a field), but not about change it or replace it...

Guilder answered 23/1, 2017 at 6:16 Comment(0)
F
8

Simply alter database's schema on ApplicationDbContext's OnModelCreating method like this:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    var role = modelBuilder.Entity<IdentityRole>()
        .ToTable("AspNetRoles");
    role.Property(r => r.Name)
        .IsRequired()
        .HasMaxLength(256)
        .HasColumnAnnotation("Index", new IndexAnnotation(
            new IndexAttribute("RoleNameIndex") 
            { IsUnique = false }));
}

But you must customize RoleValidator class also. Because default role validator invalidates duplicated role names.

public class MyRoleValidator:RoleValidator<ApplicationRole>
{
     public override async Task<IdentityResult> ValidateAsync(ApplicationRole item)
     {
         // implement your validation logic here

         return IdentityResult.Success;
     }
}

Now every time you create the role manager you must set the role validator.

roleManager.RoleValidator=new MyRoleValidator();
Flatways answered 23/1, 2017 at 18:57 Comment(4)
Sam, when I try your solution, the add-migration command raises this error: Conflicting configuration settings were specified for column 'Name' on table 'IdentityRole': Index attribute property 'IsUnique' = 'False' conflicts with index attribute property 'IsUnique' = 'True'Guilder
But after that its throwing exception: Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.Eurhythmic
@ClickOk how did you solve this? You say that it throws error but you marked this solution as accepted, did it really work?Maricruzmaridel
@ibubi, sorry, I cant remember, this answer was ~3 years ago for an older project. But this message was edited after my comment, so probably this edit fixed the problem.Guilder
H
2

You can add a composite unique constraint on both RoleName and TenantId, but first, the unique constraint of the role name must be removed here is the code:

 public class ApplicationRoleConfiguration : IEntityTypeConfiguration<ApplicationRole>
{
    public void Configure(EntityTypeBuilder<ApplicationRole> builder)
    {
        //remove the current idenx
        builder.HasIndex(x => x.NormalizedName).IsUnique(false);
        // add composite constraint 
        builder.HasIndex(x => new { x.NormalizedName, x.TenantId }).IsUnique();
    }
}

then the role validator must be overridden to check the uniqueness of both role name and TenantId:

public class TenantRoleValidator : RoleValidator<ApplicationRole>
{
    private IdentityErrorDescriber Describer { get; set; }

    public TenantRoleValidator() : base()
    {

    }
    public override async Task<IdentityResult> ValidateAsync(RoleManager<ApplicationRole> manager, ApplicationRole role)
    {
        if (manager == null)
        {
            throw new ArgumentNullException(nameof(manager));
        }
        if (role == null)
        {
            throw new ArgumentNullException(nameof(role));
        }
        var errors = new List<IdentityError>();
        await ValidateRoleName(manager, role, errors);
        if (errors.Count > 0)
        {
            return IdentityResult.Failed(errors.ToArray());
        }
        return IdentityResult.Success;
    }
    private async Task ValidateRoleName(RoleManager<ApplicationRole> manager, ApplicationRole role,
    ICollection<IdentityError> errors)
    {
        var roleName = await manager.GetRoleNameAsync(role);
        if (string.IsNullOrWhiteSpace(roleName))
        {
            errors.Add(Describer.InvalidRoleName(roleName));
        }
        else
        {
            var owner = await manager.FindByNameAsync(roleName);
            if (owner != null
                && owner.TenantId == role.TenantId
                && !string.Equals(await manager.GetRoleIdAsync(owner), await manager.GetRoleIdAsync(role)))
            {
                errors.Add(Describer.DuplicateRoleName(roleName));
            }
        }
    }
}

Finally, register the new Role validator:

        services
          .AddIdentityCore<ApplicationUser>()
          .AddRoles<ApplicationRole>()
          .AddRoleValidator<TenantRoleValidator>()

Don't forget to migrate the changes to the database before running the code

Holophytic answered 17/12, 2021 at 22:13 Comment(7)
This was a complete answer with all parts clearly explained. Thanks.Airhead
thank you but,builder.HasIndex(x => x.NormalizedName).IsUnique(false); did not change any thing in migration do you now why?Pfosi
could you explain me why this did not work with me here is my question #75791240Pfosi
When adding your validator, don't forget to remove the default RoleValidator because validators are accumulated (not replaced) by default (github.com/dotnet/aspnetcore/issues/46066). After registering your validator, add this: var defaultRoleValidator = services.FirstOrDefault(descriptor => descriptor.ImplementationType == typeof(RoleValidator<ApplicationRole>)); if (defaultRoleValidator != null) { services.Remove(defaultRoleValidator); }Setose
The ApplicationRoleConfiguration class could be avoided if you don't have the NormalizedName index (it's my case).Setose
The TenantRoleValidator class contructor must includes IdentityErrorDescriber: public TenantRoleValidator(IdentityErrorDescriber? errors = null) : base(errors) { Describer = errors ?? new IdentityErrorDescriber(); }Setose
It's also important to know that the default implementation for the UserClaimsPrincipalFactory<TUser, TRole> class (github.com/dotnet/aspnetcore/blob/…) when using AddRoles (github.com/dotnet/aspnetcore/issues/…), add claims to the identity based on role name, not role id. This causes the claims to be loaded from the first role with the name of the user's role, which can belong to another tenant (depending on the order of the roles table).Setose
P
0

There's another check for uniqueness of role name in IdentityDbContext.ValidateEntity() which must be overriden too.

Polloch answered 23/6, 2022 at 8:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.