Edited based on comments
According to my understanding, you want to access current user (all information related to it), the role(s) you want to specify for a controller (or action) and parameters received by endpoint. Haven't tried for web api, but for asp.net core MVC, You can achieve this by using AuthorizationHandler in a policy-based authorization and combine with an injected service specifically created to determine the Roles-Resources access.
To do it, first setup the policy in Startup.ConfigureServices :
services.AddAuthorization(options =>
{
options.AddPolicy("UserResource", policy => policy.Requirements.Add( new UserResourceRequirement() ));
});
services.AddScoped<IAuthorizationHandler, UserResourceHandler>();
services.AddScoped<IRoleResourceService, RoleResourceService>();
next create the UserResourceHandler :
public class UserResourceHandler : AuthorizationHandler<UserResourceRequirement>
{
readonly IRoleResourceService _roleResourceService;
public UserResourceHandler (IRoleResourceService r)
{
_roleResourceService = r;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext authHandlerContext, UserResourceRequirement requirement)
{
if (context.Resource is AuthorizationFilterContext filterContext)
{
var area = (filterContext.RouteData.Values["area"] as string)?.ToLower();
var controller = (filterContext.RouteData.Values["controller"] as string)?.ToLower();
var action = (filterContext.RouteData.Values["action"] as string)?.ToLower();
var id = (filterContext.RouteData.Values["id"] as string)?.ToLower();
if (_roleResourceService.IsAuthorize(area, controller, action, id))
{
context.Succeed(requirement);
}
}
}
}
Accessing the parameters received by endpoint is achieved by casting context.Resource to AuthorizationFilterContext, so that we could access RouteData from it. As for UserResourceRequirement, we can leave it empty.
public class UserResourceRequirement : IAuthorizationRequirement { }
As for the IRoleResourceService, it's a plain service class so that we can inject anything to it. This service is the substitute of pairing a Role to an action in code so that we don't need to specify it in action's attribute. That way, we can have a freedom to choose the implementation, ex: from database, from config file, or hard-coded.
Accessing user in RoleResourceService is achieved by injecting IHttpContextAccessor. Please note that to make IHttpContextAccessor injectable, add services.AddHttpContextAccessor() in Startup.ConfigurationServices method body.
Here's an example getting the info from config file:
public class RoleResourceService : IRoleResourceService
{
readonly IConfiguration _config;
readonly IHttpContextAccessor _accessor;
readonly UserManager<AppUser> _userManager;
public class RoleResourceService(IConfiguration c, IHttpContextAccessor a, UserManager<AppUser> u)
{
_config = c;
_accessor = a;
_userManager = u;
}
public bool IsAuthorize(string area, string controller, string action, string id)
{
var roleConfig = _config.GetValue<string>($"RoleSetting:{area}:{controller}:{action}"); //assuming we have the setting in appsettings.json
var appUser = await _userManager.GetUserAsync(_accessor.HttpContext.User);
var userRoles = await _userManager.GetRolesAsync(appUser);
// all of needed data are available now, do the logic of authorization
return result;
}
}
Get the setting from database surely is a bit more complex, but it can be done since we can inject AppDbContext. For the hardcoded approach, exist plenty ways to do it.
After all is done, use the policy on an action:
[Authorize(Policy = "UserResource")] //dont need Role name because of the RoleResourceService
public ActionResult<IActionResult> GetSomething(int resourceId)
{
//existing code
}
In fact, we can use "UserResource" policy for any action that we want to apply.