The following code authorize filter passes if the user is BOTH a Role
of "admin" AND
a Policy
of "New York". How do i change it to "admin" OR
"New York"
[Authorize(Policy = "New York", Roles = "admin")]
I want an OR
statement in this specific case, not an AND
.
There's no built-in functions to to do that.
But you could easily implement a custom LogicalOrPolicyProvider
(& also a handler) to achieve the same goal. The LogicalOrPolicyProvider
will construct policy dynamically according to the policy name, for example:
[Authorize(Policy="Choice: policy='New York'| role= ADMIN")]
The above attribute will generate a new policy that should fulfill a policy of 'New York' or requires a role of ADMIN
Further more, we could define some rules to handle more generic cases. Let's say want to compose the following requirements :
Choice: policy='New York'| role= ADMIN
Choice: policy='New York'| role= 'ADMIN'
Choice: policy='New York'| policy = 'WC' | role= root | role = 'GVN'
You could define you own rules as you like, I personally prefer to :
Choice
followed by separator :
(May has several optional space chars ' '
)policy=policyName
, if the policyName contains space, you should surround it with ''
. You could define many policy
as you likerole = roleName
. You could also define as many roles as you like.policy
& role
definitions are separated by |
.Let's define a LogicalOrRequirement
to hold all possible policies:
public class LogicalOrRequirement : IAuthorizationRequirement
{
public IList<AuthorizationPolicy> Policies { get; }
public LogicalOrRequirement(IList<AuthorizationPolicy> policies)
{
this.Policies = policies;
}
}
If any of these polices succeeds, just pass by:
public class LogicalOrAuthorizationHandler : AuthorizationHandler<LogicalOrRequirement>
{
public LogicalOrAuthorizationHandler(IHttpContextAccessor httpContextAccessor)
{
this._httpContextAccessor = httpContextAccessor;
}
private readonly IHttpContextAccessor _httpContextAccessor;
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, LogicalOrRequirement requirement)
{
var httpContext = this._httpContextAccessor.HttpContext;
var policyEvaluator = httpContext.RequestServices.GetRequiredService<IPolicyEvaluator>();
foreach (var policy in requirement.Policies)
{
var authenticateResult = await policyEvaluator.AuthenticateAsync(policy, httpContext);
if (authenticateResult.Succeeded)
{
context.Succeed(requirement);
}
}
}
}
Now let's build the policy dynamically by PolicyProvider
:
public class LogicalOrPolicyProvider : IAuthorizationPolicyProvider
{
const string POLICY_PREFIX = "Choice";
const string TOKEN_POLICY="policy";
const string TOKEN_ROLE="role";
public const string Format = "Choice: policy='p3' | policy='p2' | role='role1' | ...";
private AuthorizationOptions _authZOpts { get; }
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
public LogicalOrPolicyProvider(IOptions<AuthorizationOptions> options )
{
_authZOpts = options.Value;
FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
}
// Choice: policy= | policy= | role= | role = ...
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase))
{
var policyNames = policyName.Substring(POLICY_PREFIX.Length);
var startIndex = policyNames.IndexOf(":");
if(startIndex == -1 || startIndex == policyNames.Length)
{
throw new ArgumentException($"invalid syntax, must contains a ':' before tokens. The correct format is {Format}");
}
// skip the ":" , and turn it into the following list
// [[policy,policyName],[policy,policName],...[role,roleName],...,]
var list= policyNames.Substring(startIndex+1)
.Split("|")
.Select(p => p.Split("=").Select(e => e.Trim().Trim('\'')).ToArray() )
;
// build policy for roleNames
var rolesPolicyBuilder = new AuthorizationPolicyBuilder();
var roleNames =list.Where(arr => arr[0].ToLower() == TOKEN_ROLE)
.Select(arr => arr[1])
.ToArray();
var rolePolicy = rolesPolicyBuilder.RequireRole(roleNames).Build();
// get policies with all related names
var polices1= list.Where(arr => arr[0].ToLower() == TOKEN_POLICY);
var polices=polices1
.Select(arr => arr[1])
.Select(name => this._authZOpts.GetPolicy(name)) // if the policy with the name doesn exit => null
.Where(p => p != null) // filter null policy
.Append(rolePolicy)
.ToList();
var pb= new AuthorizationPolicyBuilder();
pb.AddRequirements(new LogicalOrRequirement(polices));
return Task.FromResult(pb.Build());
}
return FallbackPolicyProvider.GetPolicyAsync(policyName);
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
{
return FallbackPolicyProvider.GetDefaultPolicyAsync();
}
}
Lastly, don't forget to register the two services in your Startup.cs
:
services.AddSingleton<IAuthorizationPolicyProvider, LogicalOrPolicyProvider>();
services.AddSingleton<IAuthorizationHandler, LogicalOrAuthorizationHandler>();
Now, whenever you want to logical or
composition, just add a [Authorize(Policy="Choice: ...")]
:
[Authorize(Policy="Choice: policy='New York'| role= ADMIN")]
public IActionResult Privacy()
{
return View();
}