Search code examples
c#.netasp.net-coreasp.net-identityclaims-based-identity

.Net Core [Authorize] - Or instead of And for permission test


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.


Solution

  • 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 :

    1. Start with the token Choice followed by separator :(May has several optional space chars ' ')
    2. A policy is defined by policy=policyName, if the policyName contains space, you should surround it with ''. You could define many policy as you like
    3. The role is defined by role = roleName. You could also define as many roles as you like.
    4. All policy & role definitions are separated by |.

    An Implementation for The above Design:

    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();
    }