Base solution for your next web application
Open Closed

Adding permissions to static roles #5856


User avatar
0
statuscast created

What is the best practice for adding permissions to static roles? It seems like the way AZ is structured we should be doing it in

MyProject.Authorization.Roles.AppConfig.Configure where we create all our static roles, however, that method doesn't seem to have access to the RoleManager.

All documentation only refers to setting permissions via your admin app, but we have static roles and permissions that we need to set programmatically.

Where is the best place during startup of the app to call RoleManager.SetGrantedPermissionsAsync() ?

thanks!


13 Answer(s)
  • User Avatar
    0
    aaron created
    Support Team

    Pass a list of GrantedPermissions to StaticRoleDefinition in MyProject.Authorization.Roles.AppConfig.Configure.

  • User Avatar
    0
    statuscast created

    That is certainly doable but probably not the best design as I would be forced to create a list for each role and send each list in as a parameter, or one big list with roles/permissions that i create on one side of the method that gets parsed on the other.

    Isn't there some easy way to get access to RoleManager instance from within AppConfig.Configure or is that too early in the start up process.?

  • User Avatar
    0
    aaron created
    Support Team

    It's the wrong place to do it.

    You can override CreateStaticRoles method in RoleManager.

  • User Avatar
    0
    statuscast created

    I'm not sure overriding CreateStaticRoles is correct either. It doesn't look like it is ever called during the startup process, and it also seems to be tenant specific.

    We are trying to assign a standard set of permissions to a standard set of roles that are given to every tenant.

    This kind of leads to a bigger question as well: If we ever add or remove roles (or permissions) via code, do we have iterate every existing tenant to remove/add them, or is there a way to remove a role/permission globally via code and have it effect all tenants?

    Example:
    we have a static role called "Manager" we give that role Permissions for X,Y, and Z. Tenant AAA is created, 1 user is given Manager role Tenant BBB is created, 3 users are given Manager role we change the code and take permissions Y away from the Manager role.

    Does removing the permission from the STATIC role remove it for each of the tenants automatically?

    AppConfig.Configure seems like a great place for setting up static roles and permissions (even though its going to be clunky because that method doesn't seem to have access to RoleManager. But it also leaves some open questions for me..

  • User Avatar
    0
    aaron created
    Support Team

    I'm not sure overriding CreateStaticRoles is correct either. It doesn't look like it is ever called during the startup process, and it also seems to be tenant specific.

    Static roles aren't used during the startup process.

    do we have iterate every existing tenant to remove/add them

    Yes.

    or is there a way to remove a role/permission globally via code and have it effect all tenants?

    You can write code to do that.

    Does removing the permission from the STATIC role remove it for each of the tenants automatically?

    No. It's not a valid assumption.

    • Feature specification: https://github.com/aspnetboilerplate/aspnetboilerplate/issues/3334
    • Similar question: https://github.com/aspnetzero/aspnet-zero-core/issues/1353
  • User Avatar
    0
    statuscast created

    "Static roles aren't used during the startup process."

    It's not about whether they are used, its about where they are created. I have been looking for a better place to set up static roles that have custom permissions. The default location in AZ is AppConfig.Configure which is called on startup but does not have access to our permission lists, so I am going to have to come up with some convoluted way of passing them in (big matrix of roles/permissions has to be assembled, passed into Configure, and then iterated). Just feel like there should be a better way, but no big deal..

    "or is there a way to remove a role/permission globally via code and have it effect all tenants? You can write code to do that."

    Yes, of course we can write code to do that, I was just asking if it was already taken care of. Thank you, again, no big deal.

    "Does removing the permission from the STATIC role remove it for each of the tenants automatically? No. It's not a valid assumption."

    There was no assumption to any of this, it was just a series of questions. Since literally the entire goal of Abp/Az is to reduce the amount of code that has to be scaffolded ourselves, whenever we run into an area that seems "generic" (like this), I start poking around to see if Abp/Az already handles it. In this case, I think it's perfectly reasonable to think perhaps you guys have written code to deal with newly inject or deleted roles/permissions on existing tenants. But it doesn't sound like you do. Again, not a big deal, we can write it all ourselves, but it certainly seems like a good candidate for Abp/Az code.

  • User Avatar
    0
    statuscast created

    Hey all --

    I apologize that I haven' been able to figure this out but there is no seemingly good place to do this. I have a ton of questions but the main one is simple:

    Where in the startup pipeline do I have access to, or the abilty to pass in via DI a RoleManager?

    I need somewhere during startup that I can run a check to make sure all Roles have the proper RolePermissions since there doesn't seem to be any out of the box code to do this.

  • User Avatar
    1
    BobIngham created

    My attempts to do exactly this are documented here: https://support.aspnetzero.com/QA/Questions/5873 My thoughts were to leverage the seed helper class to call each tenant:

    public static void SeedHostDb(NuagecareDbContext context)
    {
        context.SuppressAutoSetTenantId = true;
    
        //Host seed
        new InitialHostDbBuilder(context).Create();
    
        //Default tenant seed (in host database).
        new DefaultTenantBuilder(context).Create();
    
        //how do I get all tenants here?
        //foreach (var tenant in tenants)....
        new TenantRoleAndUserBuilder(context, tenant.id).Create();
        new DefaultTenantDataBuilder(context, tenant.id).Create();
    }
    

    and then, in my_ TenantRoleAndUserBuilder.Create()_ method:

    private void CreateRolesAndUsers()
    {
        //Carer role
        _context.Roles.Add(new Role(_tenantId, StaticRoleNames.Tenants.Carer, StaticRoleNames.Tenants.Carer) { IsStatic = true, IsDefault = true });
    
        var carerRole = _context.Roles.IgnoreQueryFilters().FirstOrDefault(r => r.TenantId == _tenantId && r.Name == StaticRoleNames.Tenants.Carer);
        if (carerRole == null)
        {
            //Grant permissions to carer role
            var permissions = PermissionFinder
                .GetAllPermissions(new AppAuthorizationProvider(false))
                .Where(p => p.MultiTenancySides.HasFlag(MultiTenancySides.Tenant))
                .ToList();
    
            foreach (var permission in permissions)
            {
                if (
                    permission.Name == "Pages.Tenant.Dashboard" ||
                    permission.Name == "Pages.NcEntities" ||
                    permission.Name == "Pages.NcEntity.Display" ||
                    permission.Name == "Pages.NcEntity.Display.Dashboard" ||
                    permission.Name == "Pages.NcEntity.Display.Profile" ||
                    permission.Name == "Pages.NcEntity.Display.Profile.Metrics" ||
                    permission.Name == "Pages.NcEntity.NcCarePlans" ||
                    permission.Name == "Pages.NcEntity.NcCarePlan.Read" ||
                    permission.Name == "Pages.NcEntity.NcWarnings"
                    )
                {
                    _context.Permissions.Add(
                        new RolePermissionSetting
                        {
                            TenantId = _tenantId,
                            Name = permission.Name,
                            IsGranted = true,
                            RoleId = carerRole.Id
                        });
                }
            }
            _context.SaveChanges();
        }
    }
    

    However, I can't work out how to get the list of tenants in the seed helper class. Is there a solution to the problem of creating roles for tenants with pre-defined permissions?

  • User Avatar
    0
    statuscast created

    In lieu of no answer, here's what I have come up with:

    The goal: Keeping all roles on all tenants in synch with permissions as an application evolves.

    The problem: This problem has not yet been tackled by the Abp/Az team and there is no "obvious" place to write our own code to do it. We would have preferred a way to iterate through all existing tenants on startup and write code to do all this, however,

    1) we couldn't find a good place in the startup pipeline where we had access to some needed modules like RoleManager and PermissionManager, etc.
    2) we were considering a few sql hacks but decided this felt a little too kludgy.
    

    Our solution: We ultimately decided to override LoginManager, and do a permissions check/grants on the tenant everytime a user logs in. It's not a perfect solution, and we may ultimately move this into some other startup code if we find a place, but for now, the small performance hit on login basically ensure that if we ever make a significant change or addition to permissions and roles, we just have to log our users out.

    So we overrode LoginManager with the following code:

    using System.Linq;
    using System.Threading.Tasks;
    using Abp.Authorization;
    using Abp.Authorization.Users;
    using Abp.Configuration;
    using Abp.Configuration.Startup;
    using Abp.Dependency;
    using Abp.Domain.Repositories;
    using Abp.Domain.Uow;
    using Abp.MultiTenancy;
    using Abp.Zero.Configuration;
    using Microsoft.AspNetCore.Identity;
    using StatusCast.Authorization.Roles;
    using StatusCast.Authorization.Users;
    using StatusCast.MultiTenancy;
    
    namespace StatusCast.Authorization
    {
        public class LogInManager : AbpLogInManager<Tenant, Role, User>
        {
            private readonly IPermissionManager _permissionManager;
    
            public LogInManager(
                UserManager userManager,
                IMultiTenancyConfig multiTenancyConfig,
                IRepository<Tenant> tenantRepository,
                IUnitOfWorkManager unitOfWorkManager,
                ISettingManager settingManager,
                IRepository<UserLoginAttempt, long> userLoginAttemptRepository,
                IUserManagementConfig userManagementConfig,
                IIocResolver iocResolver,
                RoleManager roleManager,
                IPasswordHasher<User> passwordHasher,
                IPermissionManager permissionManager,
                UserClaimsPrincipalFactory claimsPrincipalFactory)
                : base(
                      userManager, 
                      multiTenancyConfig, 
                      tenantRepository, 
                      unitOfWorkManager, 
                      settingManager, 
                      userLoginAttemptRepository, 
                      userManagementConfig, 
                      iocResolver, 
                      passwordHasher,
                      roleManager,
                      claimsPrincipalFactory)
            {
                _permissionManager = permissionManager;
            }
    
            public override async Task<AbpLoginResult<Tenant, User>> LoginAsync(string userNameOrEmailAddress, string plainPassword, string tenancyName = null, bool shouldLockout = true)
            {
                var result = await base.LoginAsync(userNameOrEmailAddress, plainPassword, tenancyName, shouldLockout);
                if (result.Result == AbpLoginResultType.Success)
                    await EnsurePermissionsForTenant();
                return result;
            }
    
            public override async Task<AbpLoginResult<Tenant, User>> LoginAsync(UserLoginInfo login, string tenancyName = null)
            {
                var result = await base.LoginAsync(login, tenancyName);
                if (result.Result == AbpLoginResultType.Success)
                    await EnsurePermissionsForTenant();
                return result;
            }
    
            private async Task EnsurePermissionsForTenant()
            {
                var allPermissions = _permissionManager.GetAllPermissions(MultiTenancySides.Tenant);
                
                //grant all permissions to admin role
                var adminRole = RoleManager.Roles.Single(r => r.Name == RoleTypes.Administrator);
                var grantedPermissions = allPermissions.Where(p => AppPermissions.StatusCastAdministratorRolePermissions.Contains(p.Name)).ToList();
                await RoleManager.SetGrantedPermissionsAsync(adminRole.Id, grantedPermissions);                       
    
                var userRole = RoleManager.Roles.Single(r => r.Name == RoleTypes.User);
                grantedPermissions = allPermissions.Where(p => AppPermissions.StatusCastUserRolePermissions.Contains(p.Name)).ToList();
                await RoleManager.SetGrantedPermissionsAsync(userRole.Id, grantedPermissions);
    
                var incidentManagerRole = RoleManager.Roles.Single(r => r.Name == RoleTypes.IncidentManager);
                grantedPermissions = allPermissions.Where(p => AppPermissions.StatusCastIncidentManagerRolePermissions.Contains(p.Name)).ToList();
                await RoleManager.SetGrantedPermissionsAsync(incidentManagerRole.Id, grantedPermissions);
    
                var everyoneRole = RoleManager.Roles.Single(r => r.Name == RoleTypes.Everyone);
                grantedPermissions = allPermissions.Where(p => AppPermissions.StatusCastEveryoneRolePermissions.Contains(p.Name)).ToList();
                await RoleManager.SetGrantedPermissionsAsync(everyoneRole.Id, grantedPermissions);
            }
        }
    }
    

    Obviously this is for the specific set of roles we use and can easily be modified if anyone is running into the same issue.

    I'd be interested to hear from the Abp/Az team if we missed anything or is there anything else we should be doing in this code block to ensure 100% accuracy between the permissions we have mapped in memory and what is managed/stored in the DB.

    take care, jasen

  • User Avatar
    0
    BobIngham created

    Hi Jasen, I took a different approach and successfully hooked into Nuagecare.Migrations.Seed.SeedHelper:

    public static void SeedHostDb(NuagecareDbContext context)
    {
        context.SuppressAutoSetTenantId = true;
    
        //Host seed
        new InitialHostDbBuilder(context).Create();
    
        //Default tenant seed (in host database).
        new DefaultTenantBuilder(context).Create();
         new DefaultTenantDataBuilder(context, 1).Create();
    
        var tenants = context.Tenants.ToList();
        foreach (var tenant in tenants)
        {
            new TenantRoleAndUserBuilder(context, tenant.Id).Create();
        }
    }
    

    And then, within TenantRoleAndUserBuilder(context, tenant.Id).Create():

    public void Create()
    {
        CreateRolesAndUsers();
    }
    

    And, in CreateRolesAndUsers() here is a sample role with base permissions allocated:

    var carerRole = _context.Roles.IgnoreQueryFilters().FirstOrDefault(r => r.TenantId == _tenantId && r.Name == StaticRoleNames.Tenants.Carer);
    if (carerRole == null)
    {
        carerRole = _context.Roles.Add(new Role(_tenantId, StaticRoleNames.Tenants.Carer, StaticRoleNames.Tenants.Carer) { IsStatic = true, IsDefault = true }).Entity;
        _context.SaveChanges();
    
        //Grant selected permissions to carer role
        var permissions = PermissionFinder
            .GetAllPermissions(new AppAuthorizationProvider(false))
            .Where(p => p.MultiTenancySides.HasFlag(MultiTenancySides.Tenant))
            .ToList();
    
        foreach (var permission in permissions)
        {
            if (
                permission.Name == "Pages.Tenant.Dashboard" ||
                permission.Name == "Pages.NcEntities" ||
                permission.Name == "Pages.NcEntity.Display" ||
                permission.Name == "Pages.NcEntity.Display.Dashboard" ||
                permission.Name == "Pages.NcEntity.Display.Profile" ||
                permission.Name == "Pages.NcEntity.Display.Profile.Metrics" ||
                permission.Name == "Pages.NcEntity.NcCarePlans" ||
                permission.Name == "Pages.NcEntity.NcCarePlan.Read" ||
                permission.Name == "Pages.NcEntity.NcWarnings"
                )
            {
                _context.Permissions.Add(
                    new RolePermissionSetting
                    {
                        TenantId = _tenantId,
                        Name = permission.Name,
                        IsGranted = true,
                        RoleId = carerRole.Id
                    });
            }
        }
        _context.SaveChanges();
    }
    

    That will create your roles with default permissions across all tenants.

  • User Avatar
    0
    statuscast created

    I'm not completely familiar with SeedHelper but is that called on Application startup every time? or only when the database is first created? I needed something to KEEP the database in sync with new roles and permissions that are created when we upgrade or create new features.

  • User Avatar
    0
    aaron created
    Support Team

    SeedHelper is called in PostInitialize method of your *EntityFrameworkCoreModule on application startup.

  • User Avatar
    0
    statuscast created

    awesome. That might be a better solution, however it's a pure DB one and requires me to make sure that I update this code whenever we also add permissions or roles elsewhere. I think I'm going to keep my solution for now as it doesn't require the person adding new permissions to know about that code, kind of self maintained.

    Anyway, thanks for the input!