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.
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
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.
"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.
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..
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.?
That seems to have done the trick. Couldn't find that anywhere in any docs, so thank you.
IMHO, Abp.GeneralTree should be. It's another great little utiility component that almost every SaaS vendor needs. Since you have it built already, why not just throw it in? ;-)
This does not work either. Did you try this or just ask me to try it?
AspNetZero is doing something somewhere that prevents a the basic override. If this didn't work in AspNetBoilerPlate I would think this was architecturally buried somehwere that AZ couldn't control. But this is definitely an AZ issue. Can we get it moved in to the Git issues if no one has a way to resolve?
It's severly limiting -- the only work around is to build our own separate repository for 1 configuration value.
This does not work either. Did you try this or just ask me to try it?
AspNetZero is doing something somewhere that prevents a the basic override. If this didn't work in AspNetBoilerPlate I would think this was architecturally buried somehwere that AZ couldn't control. But this is definitely an AZ issue. Can we get it moved in to the Git issues if no one has a way to resolve?
It's severly limiting -- the only work around is to build our own separate repository for 1 configuration value.
No, it doesn't.
Guys -- all you have to do is add this one line of code to EntityFrameworkCore\StatusCastDbContext.cs in OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//Increase your Setting Value
modelBuilder.Entity<Abp.Configuration.Setting>().Property(u => u.Value).HasColumnType("nvarchar(max)");
modelBuilder.Entity<Abp.Application.Editions.Edition>().Property(u => u.DisplayName).HasColumnType(("nvarchar(max)"));
Can you please just give it a try and see why it isn't working rather than randomly closing this issue on me, or telling me to fix it in ways that aren't even possible. I really need some resolution on this as your column sizes for the Settings table is artificially way too small. As I've pointed out, this works perfectly in AspNetZero.
Here's a video of what I'm doing from downloading the template from my account to making the change: https://drive.google.com/file/d/1S4yNKa3oLAxUiEzX_4S9ln5mBeQ7z2DB/view?usp=sharing
thank you.