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!
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.?
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!
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? ;-)
Hi,
Trying to build an entity that uses Abp.GeneralTree.
Step 1: Define entity
namespace StatusCast.Components
{
public interface IComponent : IEntity
{
}
public partial class Component : Entity<long>, IComponent, IGeneralTree<Component, long>,
IExtendableObject, ICreationAudited, IMustHaveTenant, ISoftDelete
{
// Abp built in
public string ExtensionData { get; set; }
public virtual DateTime CreationTime { get; set; }
public int TenantId { get; set; }
public bool IsDeleted { get; set; }
public long? CreatorUserId { get; set; }
// more...
Step 2: Add to EF
public class MyDbContext : AbpZeroDbContext<Tenant, Role, User, StatusCastDbContext>, IAbpPersistedGrantDbContext
{
// StatusCast Entities
public virtual DbSet<Component> Components { get; set; }
}
Step 3: Create basic AppService:
public class ComponentAppService : MyAppServiceBase, IComponentAppService
{
private readonly IGeneralTreeManager<Component, long> _componentTreeManager;
public ComponentAppService(IGeneralTreeManager<Component, long> componentTreeManager)
{
_componentTreeManager = componentTreeManager;
}
public async Task<long> CreateComponentAsync(ComponentCreateDto input)
{
var component = ObjectMapper.Map<Component>(input);
await _componentTreeManager.CreateAsync(component);
return component.Id;
}
}
Step 4: Call it via a controller:
[Route("api/[controller]/[action]")]
public class ComponentsController : StatusCastControllerBase
{
private readonly IComponentAppService _componentsService;
public ComponentsController(
IComponentAppService componentsService
)
{
_componentsService = componentsService;
}
[ApiExplorerSettings(GroupName = "public")]
[HttpPost]
[Route("/api/Component")]
public async Task<long> Create(ComponentCreateDto input)
{
return await _componentsService.CreateComponentAsync(input);
}
}
=============================================== When calling the controller, I am getting the following error:
Exception thrown: 'Castle.MicroKernel.Handlers.HandlerException' in Castle.Windsor.dll Exception thrown: 'Castle.MicroKernel.Handlers.HandlerException' in Castle.Windsor.dll Exception thrown: 'Castle.MicroKernel.Handlers.HandlerException' in System.Private.CoreLib.dll 'dotnet.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.1.5\System.Diagnostics.StackTrace.dll'. Cannot find or open the PDB file. 'dotnet.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.1.5\System.Reflection.Metadata.dll'. Cannot find or open the PDB file. Abp.AspNetCore.Mvc.ExceptionHandling.AbpExceptionFilter: 2018-10-28 10:30:37,119 [20] ERROR Abp.AspNetCore.Mvc.ExceptionHandling.AbpExceptionFilter Can't create component 'StatusCast.Components.ComponentAppService' as it has dependencies to be satisfied.
'StatusCast.Components.ComponentAppService' is waiting for the following dependencies:
Castle.MicroKernel.Handlers.HandlerException: Can't create component 'StatusCast.Components.ComponentAppService' as it has dependencies to be satisfied.
'StatusCast.Components.ComponentAppService' is waiting for the following dependencies:
============================================================
I have a feeling there is some additional startup code I need to do to get Abp.TreeManager properly available, but don't really know what that would be as there is very little documentation. Any help would be appreciated.
thanks! jasen