Skip to content
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

Same role name with diferent tenants #782

Closed
BogdanJak opened this issue Oct 14, 2024 · 2 comments · Fixed by #783 or #784
Closed

Same role name with diferent tenants #782

BogdanJak opened this issue Oct 14, 2024 · 2 comments · Fixed by #783 or #784

Comments

@BogdanJak
Copy link

Hi
What is the purpose of adding Tenant to the role?
I am trying to add the expensive Admin role but to the Master tenant and unfortunately I get an error that such a role already exists.

Bogdan

@falvarez1
Copy link

@neozhu FYI, even if you add another Role with the same name, you can't assign that role to a user. You also can't remove the role if you happen to manually set it in DB. I solved this by implementing a custom UserStore and UserManager.
Here's the code if it helps:

public class MultiTenantUserStore : UserStore<
    ApplicationUser,
    ApplicationRole,
    ApplicationDbContext,
    string,
    ApplicationUserClaim,
    ApplicationUserRole,
    ApplicationUserLogin,
    ApplicationUserToken,
    ApplicationRoleClaim>
{
    public MultiTenantUserStore(ApplicationDbContext context)
        : base(context)
    {
    }

    public override async Task AddToRoleAsync(ApplicationUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (user == null)
            throw new ArgumentNullException(nameof(user));
        if (string.IsNullOrWhiteSpace(normalizedRoleName))
            throw new ArgumentException("Value cannot be null or empty.", nameof(normalizedRoleName));

        var tenantId = user.TenantId;

        // Find the role within the user's tenant
        var roleEntity = await Context.Roles
            .FirstOrDefaultAsync(r => r.NormalizedName == normalizedRoleName && r.TenantId == tenantId, cancellationToken);

        if (roleEntity == null)
            throw new InvalidOperationException($"Role '{normalizedRoleName}' does not exist in the user's tenant.");

        // Check if the user is already in the role
        var userRole = await Context.UserRoles
            .FirstOrDefaultAsync(ur => ur.UserId == user.Id && ur.RoleId == roleEntity.Id && ur.TenantId == tenantId, cancellationToken);

        if (userRole != null)
            return; // User is already in the role

        // Create a new user-role link
        userRole = new ApplicationUserRole
        {
            UserId = user.Id,
            RoleId = roleEntity.Id,
            TenantId = tenantId
        };

        Context.UserRoles.Add(userRole);
    }

    public override async Task<bool> IsInRoleAsync(ApplicationUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (user == null)
            throw new ArgumentNullException(nameof(user));
        if (string.IsNullOrWhiteSpace(normalizedRoleName))
            throw new ArgumentException("Value cannot be null or empty.", nameof(normalizedRoleName));

        var tenantId = user.TenantId;

        // Find the role within the user's tenant
        var role = await Context.Roles
            .FirstOrDefaultAsync(r => r.NormalizedName == normalizedRoleName && r.TenantId == tenantId, cancellationToken);

        if (role == null)
            return false;

        // Check if the user has this role
        var userRole = await Context.UserRoles
            .FirstOrDefaultAsync(ur => ur.UserId == user.Id && ur.RoleId == role.Id && ur.TenantId == tenantId, cancellationToken);

        return userRole != null;
    }

    public override async Task RemoveFromRoleAsync(ApplicationUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (user == null)
            throw new ArgumentNullException(nameof(user));
        if (string.IsNullOrWhiteSpace(normalizedRoleName))
            throw new ArgumentException("Value cannot be null or empty.", nameof(normalizedRoleName));

        var tenantId = user.TenantId;

        // Find the role within the user's tenant
        var role = await Context.Roles
            .FirstOrDefaultAsync(r => r.NormalizedName == normalizedRoleName && r.TenantId == tenantId, cancellationToken);

        if (role != null)
        {
            // Find the user-role link within the tenant
            var userRole = await Context.UserRoles
                .FirstOrDefaultAsync(ur => ur.UserId == user.Id && ur.RoleId == role.Id && ur.TenantId == tenantId, cancellationToken);

            if (userRole != null)
            {
                Context.UserRoles.Remove(userRole);
            }
        }
    }    
}

public class MultiTenantUserManager : UserManager<ApplicationUser>
{
    private readonly RoleManager<ApplicationRole> _roleManager;

    public MultiTenantUserManager(
        IUserStore<ApplicationUser> store,
        IOptions<IdentityOptions> optionsAccessor,
        IPasswordHasher<ApplicationUser> passwordHasher,
        IEnumerable<IUserValidator<ApplicationUser>> userValidators,
        IEnumerable<IPasswordValidator<ApplicationUser>> passwordValidators,
        ILookupNormalizer keyNormalizer,
        IdentityErrorDescriber errors,
        IServiceProvider services,
        RoleManager<ApplicationRole> roleManager,
        ILogger<UserManager<ApplicationUser>> logger)
        : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
    {
        _roleManager = roleManager;
    }

    public override async Task<IdentityResult> AddToRolesAsync(ApplicationUser user, IEnumerable<string> roles)
    {
        var tenantId = user.TenantId;

        var normalizedRoleNames = roles.Select(NormalizeName).ToList();

        var tenantRoles = await _roleManager.Roles
            .Where(r => normalizedRoleNames.Contains(r.NormalizedName) && r.TenantId == tenantId)
            .ToListAsync();

        if (tenantRoles.Count != roles.Count())
        {
            var missingRoles = roles.Except(tenantRoles.Select(r => r.Name), StringComparer.OrdinalIgnoreCase);
            return IdentityResult.Failed(new IdentityError
            {
                Code = "RoleNotFound",
                Description = $"Roles '{string.Join(", ", missingRoles)}' do not exist in the user's tenant."
            });
        }

        foreach (var role in tenantRoles)
        {
            var result = await AddToRoleAsync(user, role.Name);
            if (!result.Succeeded)
            {
                return result;
            }
        }

        return IdentityResult.Success;
    }

    public override async Task<IdentityResult> AddToRoleAsync(ApplicationUser user, string roleName)
    {
        var tenantId = user.TenantId;
        var normalizedRoleName = NormalizeName(roleName);

        var role = await _roleManager.Roles
            .FirstOrDefaultAsync(r => r.NormalizedName == normalizedRoleName && r.TenantId == tenantId);

        if (role == null)
        {
            return IdentityResult.Failed(new IdentityError
            {
                Code = "RoleNotFound",
                Description = $"Role '{roleName}' does not exist in the user's tenant."
            });
        }

        if (await IsInRoleAsync(user, role.Name, tenantId))
        {
            return IdentityResult.Failed(new IdentityError
            {
                Code = "UserAlreadyInRole",
                Description = $"User is already in role '{roleName}'."
            });
        }

        var userRoleStore = GetUserRoleStore();
        await userRoleStore.AddToRoleAsync(user, role.NormalizedName, CancellationToken.None);

        return await UpdateUserAsync(user);
    }

    // Override IsInRoleAsync to include TenantId
    public async Task<bool> IsInRoleAsync(ApplicationUser user, string roleName, string tenantId)
    {
        if (user == null)
            throw new ArgumentNullException(nameof(user));
        if (string.IsNullOrEmpty(roleName))
            throw new ArgumentException("Value cannot be null or empty.", nameof(roleName));

        var normalizedRoleName = NormalizeName(roleName);

        var userRoleStore = GetUserRoleStore();

        var roles = await userRoleStore.GetRolesAsync(user, CancellationToken.None);

        // Since roles can have the same name across different tenants, we need to ensure we only consider roles within the user's tenant
        var isInRole = await _roleManager.Roles.AnyAsync(r =>
            r.NormalizedName == normalizedRoleName &&
            r.TenantId == tenantId &&
            Context.UserRoles.Any(ur => ur.UserId == user.Id && ur.RoleId == r.Id && ur.TenantId == tenantId));

        return isInRole;
    }

    public override async Task<IdentityResult> RemoveFromRoleAsync(ApplicationUser user, string role)
    {
        var normalizedRoleName = NormalizeName(role);

        var userRoleStore = GetUserRoleStore();
        await userRoleStore.RemoveFromRoleAsync(user, normalizedRoleName, CancellationToken.None);

        return await UpdateUserAsync(user);
    }


    private IUserRoleStore<ApplicationUser> GetUserRoleStore()
    {
        if (Store is IUserRoleStore<ApplicationUser> userRoleStore)
        {
            return userRoleStore;
        }
        else
        {
            throw new NotSupportedException("The user store does not implement IUserRoleStore<ApplicationUser>.");
        }
    }

    private ApplicationDbContext Context
    {
        get
        {
            var store = Store as UserStore<ApplicationUser, ApplicationRole, ApplicationDbContext, string, ApplicationUserClaim, ApplicationUserRole, ApplicationUserLogin, ApplicationUserToken, ApplicationRoleClaim>;
            return store?.Context;
        }
    }
}

Then register it this way:

services.AddScoped<IUserStore<ApplicationUser>, MultiTenantUserStore>();
services.AddScoped<UserManager<ApplicationUser>, MultiTenantUserManager>();

@neozhu neozhu reopened this Oct 18, 2024
@neozhu
Copy link
Owner

neozhu commented Oct 18, 2024

Thank you for providing the code! I’ve fixed the issue, I appreciate your support.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants