Reputation: 17946
I have groups of pages that share common authorization requirements. Consider the following structure:
https://example.com/Company/{companyId}/Billing/Index
https://example.com/Company/{companyId}/Billing/Edit
https://example.com/Company/{companyId}/Billing/(other pages)
https://example.com/Company/{companyId}/Profile/Index
https://example.com/Company/{companyId}/Profile/Edit
https://example.com/Company/{companyId}/Profile/(other pages)
I need to ensure that users getting to any pages under /Company/{companyId}/Billing
have authorization to manage billing within the company with id = {companyId}
. Likewise, users requesting pages underneath /Company/{companyId}/Profile
must have authorization to manage profile information for the company included in the route. It is not a simple role ... it is a role as applied to a company, which configuration is in our database.
I have a solution that is working, but it just seems ... wrong. I'm having to sniff for the {companyId}
route id within the AuthorizationHandler
, and that just doesn't seem right. Surely this must be a common enough scenario (protecting resources not just based on role, but based on the details of the actual resource) that there's a smoother way.
I am including my current solution as an answer below, but welcome feedback and recommendations for a better implementation.
Upvotes: 2
Views: 2075
Reputation: 17946
Here's my current solution, but it just doesn't seem like this is "the right way". In particular, it seems to me like the company id should somehow be part of the CompanyAuthorizationRequirement
, and not something being sniffed in the authorization handler.
Create an IAuthorizationRequirement
with which to mark the resources that need to be protected:
public class CompanyAuthorizationRequirement : IAuthorizationRequirement
{
public CompanyAuthorizationRequirement(string permission)
{
this.Permission = permission;
}
public string Permission { get; private set; }
}
Create an AuthorizationHandler
to handle the requirement:
public class CompanyAuthorizationHandler : AuthorizationHandler<CompanyAuthorizationRequirement>
{
private readonly IActionContextAccessor actionContextAccessor;
private readonly IAuthorizationRepository authorizationRepository;
public CompanyAuthorizationHandler(IActionContextAccessor actionContextAccessor, IAuthorizationRepository authorizationRepository)
{
this.actionContextAccessor = actionContextAccessor;
this.authorizationRepository = authorizationRepository;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, CompanyAuthorizationRequirement requirement)
{
if (context.User.IsInRole(RoleNames.SystemAdministrator))
{
context.Succeed(requirement);
return;
}
// This is the part that smells wrong to me...
object companyIdRouteData = this.actionContextAccessor.ActionContext.RouteData.Values["companyId"];
if (companyIdRouteData == null)
{
companyIdRouteData = this.actionContextAccessor.ActionContext.HttpContext.Request.Query["companyId"];
}
if (companyIdRouteData == null || !int.TryParse(companyIdRouteData.ToString(), out int companyId))
{
context.Fail();
return;
}
string userId = context.User.Claims.First(c => c.Type == "sub").Value;
bool authorized = false;
switch (requirement.Permission)
{
case "billing":
authorized = await this.authorizationRepository.CanManageCompanyBilling(userId, companyId);
break;
case "profile":
authorized = await this.authorizationRepository.CanManageCompanyProfile(userId, companyId);
break;
}
if (authorized)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
}
Add the folder authorization conventions and policies in Startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeFolder("/");
options.Conventions.AuthorizeFolder("/Company/Billing", "CanManageCompanyBilling");
options.Conventions.AuthorizeFolder("/Company/Profile", "CanManageCompanyProfile");
// Change the routing on all pages underneath the Company folder to include the company id in the route.
options.Conventions.AddFolderRouteModelConvention("/Company", model =>
{
Regex templatePattern = new Regex("^Company(/|$)");
foreach (var selector in model.Selectors)
{
selector.AttributeRouteModel.Template = templatePattern.Replace(selector.AttributeRouteModel.Template, "Company/{company:int}$1");
}
});
});
services.AddAuthorization(options =>
{
options.AddPolicy("CanManageCompanyBilling", builder => builder.AddRequirements(new CompanyAuthorizationRequirement("billing")));
options.AddPolicy("CanManageCompanyProfile", builder => builder.AddRequirements(new CompanyAuthorizationRequirement("profile")));
});
}
Upvotes: 2