Open Closed

Override the existing Users, Roles & Permissions Methodology #7882


User avatar
0
pvala created
  • ABP Framework version: v8.2.1
  • UI Type: Angular
  • Database System: EF Core (MySQL)
  • Tiered (for MVC) or Auth Server Separated (for Angular): yes, Angular with Microservice Architecture
  • Exception message and full stack trace:
  • Steps to reproduce the issue:

Hello Team,

We have a microservices based architecture solution for our project. We don't want to use the TenantId in the AbpUsers and AbpRoles tables as per our requirements. For that what we have done is, we have kept separate tables (UserTenantAssociation and RoleTenantAssociation), using these tables we will determine which user belongs to which Tenant. And for the Roles, we will have all the Roles in the AbpRoles table with all records having TenantId as NULL, which implies the Roles will be created only in the Host Tenant and not any other Tenant. The other Tenants will be using the same Roles as Host, and which Tenants have which specific Roles to use in their tenant, that will be determined using our custom RoleTenantAssociation table where RoleId (the Id of the role from the host tenant) and the TenantId of that Tenant will be stored).

Now, displaying the list of Roles and Users on the UI doesn't seem to be a problem as we have already done necessary changes in the Users and Roles repositories in the IdentityService to achiever this feat. But the problem arises when the User logs into the Tenants.

Let's say I have a User which belongs to a Tenant, and the User has a role assigned to it as "admin", now in the AbpUserRoles table, the UserId will be the Id of the User from AbpUsers table, TenantId will be TenantId of the Tenant in which the user is trying to log into and the RoleId will be the Id of the Role "admin" from AbpRoles table but it will have TenantId as NULL as the Role belongs to the Host and the same Role should be used by all the Tenants.

Now if we run the application and when the user logs into a Tenant, it doesn't have any Roles assigned to it in the CurrentUser class, and also the GrantedPolicies will also be empty since there are no roles assigned to the user in the currentUser section of application configuration api call.

I tried to check how the values are assigned to the CurrentUser, and I came to know that it gets the values from the Claims generated during the Authentication and are passed to JWT Token during the authentication.

https://github.com/abpframework/abp/blob/8e20aab617205936c299ed5c3c40e0c529a3f06b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpUserClaimsPrincipalFactory.cs#L14

this is the code I tried :

public class AbpUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<IdentityUser, IdentityRole>, ITransientDependency { public AbpUserClaimsPrincipalFactory( UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> options) : base( userManager, roleManager, options) { }

[UnitOfWork]
public override async Task&lt;ClaimsPrincipal&gt; CreateAsync(IdentityUser user)
{
    var principal = await base.CreateAsync(user).ConfigureAwait(false);

    if (user.TenantId.HasValue)
    {
        principal.Identities
            .First()
            .AddClaim(new Claim(AbpClaimTypes.TenantId, user.TenantId.ToString()));
    }

    return principal;
}

}

(I tried this code in Administration Service Domain project)

but when using it, the login page will just stay there even after clicking the login button with correct credentials, it doesn't redirect to the angular app.

I want to know how exactly the CurrentUser is assigned these values and I want to override it because we have different logic of fetching the roles (from our custom table). I specifically want to know how the roles are assigned to the current user.

Right now, what I have done is, I have manually updated the value of the RoleId in the AbpUserRoles table, I have updated the RoleId with the one which belongs to the host. And because of that when the user logs into the application, there in, the api/abp/application-configuration?includeLocalizationResources=false api is called and in response of that API call, the grantedPolicies in the "auth" section is an empty array and in the "currentUser" section the roles is an empty array.

example : { "auth": { "grantedPolicies": [] }, "currentUser": { "roles": [], }, } So, given the scenario, how exactly can I set these granted policies and the currentUser values in the application when the user logs in?


46 Answer(s)
  • User Avatar
    0
    pvala created

    Yes, the user id is correct

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    But the UserManager.FindByIdAsync returns null.

    Can you try to call this method manually?

  • User Avatar
    0
    pvala created

    I got why we are getting this. It's because in the IdentityDynamicClaimsPrincipalContributor, it's still using framework's UserManager, I need to override it along with the IdentityDynamicClaimsPrincipalContributorCache class in order to use my UserManager.

    In which project should I put these files in Identity Service and also, do I need to write any configuration in the Module.cs file of that project?

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    In your Domain module.

    services.TryAddScoped<YourIdentityUserManager>();
    services.TryAddScoped(typeof(UserManager<IdentityUser>), provider => provider.GetService(typeof(YourIdentityUserManager)));
    
  • User Avatar
    0
    pvala created

    Hi, I tried to put IdentityDynamicClaimsPrincipalContributor and IdentityDynamicClaimsPrincipalContributorCache files in my IdentityService Domain project and added the code you provided in the Domain module.cs file but it didn't work, it's not hitting the breakpoint in the files that I added (IdentityDynamicClaimsPrincipalContributor and IdentityDynamicClaimsPrincipalContributorCache).

    I also tried with adding these two files and the required code change in the AuthServer project, but that didn't work either.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    I tried to put IdentityDynamicClaimsPrincipalContributor and IdentityDynamicClaimsPrincipalContributorCache files in my IdentityService Domain project and added the code you provided in the Domain module.cs file

    Can you try to add all these files to the Domain?

    Also, share the code you applied.

  • User Avatar
    0
    pvala created

    These are my files :

    IdentityDynamicClaimsPrincipalContributorCache.cs

    using Microsoft.AspNetCore.Identity;
    using Microsoft.Extensions.Caching.Distributed;
    using Microsoft.Extensions.Logging.Abstractions;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Volo.Abp.Caching;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Identity;
    using Volo.Abp.MultiTenancy;
    using Volo.Abp.Security.Claims;
    
    namespace G1.health.IdentityService;
    
    public class IdentityDynamicClaimsPrincipalContributorCache : ITransientDependency
    {
        public ILogger<IdentityDynamicClaimsPrincipalContributorCache> Logger { get; set; }
    
        protected IDistributedCache<AbpDynamicClaimCacheItem> DynamicClaimCache { get; }
        protected ICurrentTenant CurrentTenant { get; }
        protected Users.IdentityUserManager UserManager { get; }
        protected IUserClaimsPrincipalFactory<IdentityUser> UserClaimsPrincipalFactory { get; }
        protected IOptions<AbpClaimsPrincipalFactoryOptions> AbpClaimsPrincipalFactoryOptions { get; }
        protected IOptions<IdentityDynamicClaimsPrincipalContributorCacheOptions> CacheOptions { get; }
    
        public IdentityDynamicClaimsPrincipalContributorCache(
            IDistributedCache<AbpDynamicClaimCacheItem> dynamicClaimCache,
            ICurrentTenant currentTenant,
            Users.IdentityUserManager userManager,
            IUserClaimsPrincipalFactory<IdentityUser> userClaimsPrincipalFactory,
            IOptions<AbpClaimsPrincipalFactoryOptions> abpClaimsPrincipalFactoryOptions,
            IOptions<IdentityDynamicClaimsPrincipalContributorCacheOptions> cacheOptions)
        {
            DynamicClaimCache = dynamicClaimCache;
            CurrentTenant = currentTenant;
            UserManager = userManager;
            UserClaimsPrincipalFactory = userClaimsPrincipalFactory;
            AbpClaimsPrincipalFactoryOptions = abpClaimsPrincipalFactoryOptions;
            CacheOptions = cacheOptions;
    
            Logger = NullLogger<IdentityDynamicClaimsPrincipalContributorCache>.Instance;
        }
    
        public virtual async Task<AbpDynamicClaimCacheItem> GetAsync(Guid userId, Guid? tenantId = null)
        {
            Logger.LogDebug($"Get dynamic claims cache for user: {userId}");
    
            if (AbpClaimsPrincipalFactoryOptions.Value.DynamicClaims.IsNullOrEmpty())
            {
                var emptyCacheItem = new AbpDynamicClaimCacheItem();
                await DynamicClaimCache.SetAsync(AbpDynamicClaimCacheItem.CalculateCacheKey(userId, tenantId), emptyCacheItem, new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = CacheOptions.Value.CacheAbsoluteExpiration
                });
    
                return emptyCacheItem;
            }
    
            return await DynamicClaimCache.GetOrAddAsync(AbpDynamicClaimCacheItem.CalculateCacheKey(userId, tenantId), async () =>
            {
                using (CurrentTenant.Change(tenantId))
                {
                    Logger.LogDebug($"Filling dynamic claims cache for user 1: {userId}");
    
                    var user = await UserManager.GetByIdAsync(userId);
                    var principal = await UserClaimsPrincipalFactory.CreateAsync(user);
    
                    var dynamicClaims = new AbpDynamicClaimCacheItem();
                    foreach (var claimType in AbpClaimsPrincipalFactoryOptions.Value.DynamicClaims)
                    {
                        var claims = principal.Claims.Where(x => x.Type == claimType).ToList();
                        if (claims.Any())
                        {
                            dynamicClaims.Claims.AddRange(claims.Select(claim => new AbpDynamicClaim(claimType, claim.Value)));
                        }
                        else
                        {
                            dynamicClaims.Claims.Add(new AbpDynamicClaim(claimType, null));
                        }
                    }
    
                    return dynamicClaims;
                }
            }, () => new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = CacheOptions.Value.CacheAbsoluteExpiration
            });
        }
    
        public virtual async Task ClearAsync(Guid userId, Guid? tenantId = null)
        {
            Logger.LogDebug($"Remove dynamic claims cache for user: {userId}");
            await DynamicClaimCache.RemoveAsync(AbpDynamicClaimCacheItem.CalculateCacheKey(userId, tenantId));
        }
    }
    

    IdentityDynamicClaimsPrincipalContributor.cs

    using Microsoft.Extensions.Logging;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Security.Principal;
    using System.Threading.Tasks;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Domain.Entities;
    using Volo.Abp.Security.Claims;
    
    namespace G1.health.IdentityService;
    
    [Dependency(ReplaceServices = true)]
    public class IdentityDynamicClaimsPrincipalContributor : AbpDynamicClaimsPrincipalContributorBase
    {
        public async override Task ContributeAsync(AbpClaimsPrincipalContributorContext context)
        {
            var identity = context.ClaimsPrincipal.Identities.FirstOrDefault();
            var userId = identity?.FindUserId();
            if (userId == null)
            {
                return;
            }
    
            var dynamicClaimsCache = context.GetRequiredService<IdentityDynamicClaimsPrincipalContributorCache>();
            AbpDynamicClaimCacheItem dynamicClaims;
            try
            {
                dynamicClaims = await dynamicClaimsCache.GetAsync(userId.Value, identity.FindTenantId());
            }
            catch (EntityNotFoundException e)
            {
                // In case if user not found, We force to clear the claims principal.
                context.ClaimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
                var logger = context.GetRequiredService<ILogger<IdentityDynamicClaimsPrincipalContributor>>();
                logger.LogWarning(e, $"User not found: {userId.Value}");
                return;
            }
    
            if (dynamicClaims.Claims.IsNullOrEmpty())
            {
                return;
            }
    
            await AddDynamicClaimsAsync(context, identity, dynamicClaims.Claims);
        }
    }
    

    IdentityServiceDomainModule.cs

    using Microsoft.AspNetCore.Identity;
    using Microsoft.Extensions.DependencyInjection.Extensions;
    using Volo.Abp.Identity;
    using Volo.Abp.Modularity;
    using Volo.Abp.OpenIddict;
    //using Volo.Chat;
    
    namespace G1.health.IdentityService;
    
    [DependsOn(
        typeof(AbpIdentityProDomainModule),
        typeof(AbpOpenIddictProDomainModule),
        typeof(IdentityServiceDomainSharedModule)
    )]
    //[DependsOn(typeof(ChatDomainModule))]
        public class IdentityServiceDomainModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            context.Services.TryAddScoped<Users.IdentityUserManager>();
            context.Services.TryAddScoped(typeof(UserManager<IdentityUser>), provider => provider.GetService(typeof(Users.IdentityUserManager)));
        }
    }
    
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Please set a breakpoint to check the DynamicContributors.

    services.PostConfigure<AbpClaimsPrincipalFactoryOptions>(options =>
    {
        options.DynamicContributors.AddIfNotContains(xxx);
    });
    
  • User Avatar
    0
    pvala created

    I am getting this error while adding my contributor

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    typeof(YourContributor)

  • User Avatar
    0
    pvala created

    Yes, after doing that, this is what I got. In the result, I am not getting anything, 0 items.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    We will add our IdentitySessionDynamicClaimsPrincipalContributor if your module depends the AbpIdentityProDomainModule

  • User Avatar
    0
    pvala created

    Yes, my module does depend on the AbpIdentityProDomainModule

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Can you try to inject the IOptions<AbpClaimsPrincipalFactoryOptions> to check the values?

    Thanks.

  • User Avatar
    0
    pvala created

    Where should I inject this IOptions<AbpClaimsPrincipalFactoryOptions>?

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var app = context.GetApplicationBuilder();
        var env = context.GetEnvironment();
    
        var options = context.ServiceProvider.GetRequiredService<IOptions<AbpClaimsPrincipalFactoryOptions>>();
    
    
  • User Avatar
    0
    pvala created

    Yes, I am getting my contributor here.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    ok, You can remove the built-in contributors.

    And replace the IdentityUserManager as well.

    services.TryAddScoped<YourIdentityUserManager>();
    services.TryAddScoped(typeof(UserManager<IdentityUser>), provider => provider.GetService(typeof(YourIdentityUserManager)))
    services.TryAddScoped(typeof(IdentityUserManager), provider => provider.GetService(typeof(YourIdentityUserManager)))
    
  • User Avatar
    0
    pvala created

    Hi, I tried this changes,

    1. services.TryAddScoped(); is giving syntax error, it says I must pass an argument, that's why I had to comment that line.
    2. After that, I tried to put those changes in both IdentityService and AuthServer, one by one, but it doesn't work in either of them.
    3. I commented the 2nd line of the code you have given, because don't have any UserManager<IdentityUser> file of our own to override it anywhere, we only have IdentityUserManager file to override, but even in that scenario, after commenting that specific line, it doesn't work.

    Is it possible to schedule a remote call in order to look into this issue and resolve this?

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Can you share a simple project, I can download and check the code.

    https://wetransfer.com/ liming.ma@volosoft.com

  • User Avatar
    0
    pvala created

    This is a big issue, sending a sample project would not be possible, as there are database change as well, and because of that, integration will take time for you to replicate the exact same scenario. If we can connect remotely, that would be helpful.

Made with ❤️ on ABP v9.0.0-preview Updated on September 19, 2024, 10:13