I would like to require one policy for all actions on a controller, and I would like to also require a second policy for all calls to HTTP "edit methods" (POST, PUT, PATCH, and DELETE). That is, the edit methods should require both policies. Due to implementation requirements, and also a desire to keep the code DRY, I need the latter policy to be applied at the controller level, not duplicated on all the action methods.
As a simple example, I have a PeopleController
, and I also have two permissions, implemented as Policies, ViewPeople
and EditPeople
. Right now I have:
[Authorize("ViewPeople")]
public class PeopleController : Controller { }
How do I go about adding the EditPeople
policy/permission such that it "stacks" and only applies to the edit verbs?
I've run into two problems which both seem to be a real pain:
I tried working around the former with a custom Requirement and AuthorizationHandler, like so:
public class ViewEditRolesRequirement : IAuthorizationRequirement
{
public ViewEditRolesRequirement(Roles[] editRoles, Roles[] viewRoles)
=> (EditRoles, ViewRoles) = (editRoles, viewRoles);
public Roles[] EditRoles { get; }
public Roles[] ViewRoles { get; }
}
public class ViewEditRolesHandler : AuthorizationHandler<ViewEditRolesRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ViewEditRolesRequirement requirement)
{
if (context.User != null)
{
var canView = requirement.ViewRoles.Any(r => context.User.IsInRole(r.ToString()));
var canEdit = requirement.EditRoles.Any(r => context.User.IsInRole(r.ToString()));
if (context. // Wait, why can't I get to the bloody HttpRequest??
}
return Task.CompletedTask;
}
}
... but I got as far as if (context.
before I realized that I didn't have access to the request object.
Is my only choice to override the OnActionExecuting
method in the controller and do my authorization there? I assume that's frowned upon, at the very least?
You can't access the Request in a custom AuthorizationHandler, so I can't check the HttpMethod...
Actually, we can access the Request in an AuthorizationHandler. We do that by casting the context.Resource
with the as
keyword. Here is an example:
services.AddAuthorization(config =>
{
config.AddPolicy("View", p => p.RequireAssertion(context =>
{
var filterContext = context.Resource as AuthorizationFilterContext;
var httpMethod = filterContext.HttpContext.Request.Method;
// add conditional authorization here
return true;
}));
config.AddPolicy("Edit", p => p.RequireAssertion(context =>
{
var filterContext = context.Resource as AuthorizationFilterContext;
var httpMethod = filterContext.HttpContext.Request.Method;
// add conditional authorization here
return true;
}));
});
You can't have more than one AuthorizeAttribute....
Actually, we can have more than one AuthorizeAttribute. Note from the docs that the attribute has AllowMultiple=true
. That allows us to "stack" them. Here is an example:
[Authorize(Policy="View")]
[Authorize(Policy="Edit")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
...
}