authorization icon indicating copy to clipboard operation
authorization copied to clipboard

Can I authorize per multiple policies ?

Open jalchr opened this issue 6 years ago • 16 comments

Using the GraphQLAuthorize attribute, can I apply multiple policies at once ... like ["Admin", "Teacher"] . Then any user that has any of those claims get authorized.

jalchr avatar May 25 '18 06:05 jalchr

Not at present, no. You can have a policy like “TeacherOrAdmin”.

joemcbride avatar May 31 '18 14:05 joemcbride

I see that might be a good solution. Is this a limitation in "AuthorizeWith" ? Any future plans ?

jalchr avatar Jun 01 '18 04:06 jalchr

If I were to add multiple policy support, it would probably function like the .NET Core one does, in an & comparison. So it would end up as Admin AND Teacher, which is probably not what you want.

joemcbride avatar Jun 06 '18 21:06 joemcbride

mmm ... I believe it is more like Admin OR Teacher. The idea is to support multiple policies at same time ... and should not intersect with an AND

jalchr avatar Jun 11 '18 07:06 jalchr

I understand. If I were to implement it I would just prefer it to behave like ASP.NET Core does to avoid confusion.

https://stackoverflow.com/a/35610142/279764

joemcbride avatar Jun 12 '18 05:06 joemcbride

@jalchr I wrote my own extension and validation rule to make this happen. Based on the documentation here https://graphql-dotnet.github.io/docs/getting-started/authorization

The usage ends up like this:

graphQLQuery.Field<ResponseGraphType<AccountType>>(
	"me",
	resolve: context =>
	{
		// code to resolve here...
	}
).RequireRole("admin", "teacher");

The RequireRole can be written as an extension method like this. Which adds the roles comma separated as metadata on the field.

public static void RequireRole(this IProvideMetadata type, params string[] rolesToAdd)
{
	var roles = type.GetMetadata<List<string>>("Roles");

	if (roles == null)
	{
		roles = new List<string>();
		type.Metadata["Roles"] = roles;
	}

	roles.Add($"{string.Join(',', rolesToAdd)}");
}

Then we can add our own validation rule like this

public class FieldRoleValidationRule : IValidationRule
	{
		public INodeVisitor Validate(ValidationContext context)
		{
			var userContext = context.UserContext as GraphQLUserContext;
			var authenticated = userContext.User?.Identity.IsAuthenticated ?? false;

			return new EnterLeaveListener(_ =>
			{
				_.Match<Field>(fieldAst =>
				{
					var fieldDef = context.TypeInfo.GetFieldDef();
					if (fieldDef.RequiresRole() &&
						(!authenticated || !fieldDef.UserHasValidRole(userContext.User.Claims)))
					{
						context.ReportError(new ValidationError(
						  context.OriginalQuery,
						  "auth-required",
						  $"You are not authorized to run this query.",
						  fieldAst));
					}
				});
			});
		}
	}

And use dependency injection to add it as an IValidationRule (I'm, using Autofac here).

builder.RegisterType<FieldRoleValidationRule>().As<IValidationRule>().InstancePerDependency();

Now we can create another extension method that validates the roles against the users claims

public static bool UserHasValidRole(this IProvideMetadata type, IEnumerable<Claim> claims)
{
	var roles = type.GetMetadata<IEnumerable<string>>("Roles", new List<string>());
       // Code to check roles agains claims here
}

okarlsson avatar Dec 12 '18 11:12 okarlsson

If you need differing policies based on role - then MetaData as above is the way to go.

If you just need to give multiple users with different roles access to the same stuff, then IAuthorizationRequirement or Evaluator works.

https://github.com/graphql-dotnet/authorization/issues/49#issuecomment-512007347

OpenSpacesAndPlaces avatar Jul 16 '19 22:07 OpenSpacesAndPlaces

Another workaround is to implement custom IAuthorizationEvaluator:

public class MyAuthorizationEvaluator : IAuthorizationEvaluator
{
    private readonly AuthorizationSettings _settings;

    public MyAuthorizationEvaluator(AuthorizationSettings settings)
    {
        _settings = settings;
    }

    public async Task<AuthorizationResult> Evaluate(
        ClaimsPrincipal principal,
        object userContext,
        Dictionary<string, object> inputVariables,
        IEnumerable<string> policies)
    {
        if (policies == null || !policies.Any())
        {
            return AuthorizationResult.Success();
        }

        var context = new AuthorizationContext
        {
            User = principal ?? new ClaimsPrincipal(new ClaimsIdentity()), 
            UserContext = userContext
        };

        return await SatisfiesAtLeastOnePolicyAsync(policies, context) ? 
            AuthorizationResult.Success() : AuthorizationResult.Fail(context.Errors);
    }

    private async Task<bool> SatisfiesAtLeastOnePolicyAsync(IEnumerable<string> policies, AuthorizationContext context )
    {
        var isValid = false;
        foreach (var policy in policies)
        {
            var authorizationPolicy = _settings.GetPolicy(policy);
            if (authorizationPolicy == null)
            {
                context.ReportError($"Required policy '{policy}' is not present.");
                break;
            }

            foreach (var r in authorizationPolicy.Requirements)
            {
                if (await r.AuthorizeAndVerify(context))
                {
                    isValid = true;
                }
            }
        }

        return isValid;
    }
}

And an extension for finding errors:

public static class AuthorizationRequirementExtensions
{
    public static async Task<bool> AuthorizeAndVerify(this IAuthorizationRequirement requirement, AuthorizationContext context)
    {
        int originalErrorsCount = context.Errors.Count();
        await requirement.Authorize(context);
        if (context.Errors.Count() > originalErrorsCount)
        {
            return false;
        }

        return true;
    }
}

Then register it in the IOC:

services.AddSingleton<IAuthorizationEvaluator, MyAuthorizationEvaluator>();

Mousavi310 avatar Dec 29 '19 08:12 Mousavi310

@Mousavi310 Why do you use break in your example?

sungam3r avatar Nov 01 '20 23:11 sungam3r

Why do you use break in your example?

Just looks like a stylistic choice - break is going to end the loop and return the initialized value of isValid = false;.

OpenSpacesAndPlaces avatar Nov 02 '20 13:11 OpenSpacesAndPlaces

Taking a second look - in that sample it should really be:

context.ReportError($"Required policy '{policy}' is not present.");
isValid = false;
break;

The way that's written a case like this would not be correct:

  1. Policy Passes (isValid = true)
  2. Policy Fails (returns isValid set to true)

@Mousavi310

OpenSpacesAndPlaces avatar Nov 02 '20 13:11 OpenSpacesAndPlaces

Then I don't understand the meaning of the method at all - SatisfiesAtLeastOnePolicyAsync. Why does it break on the first false result?

sungam3r avatar Nov 02 '20 15:11 sungam3r

You read more closely than I did :) - I failed to read the method name "SatisfiesAtLeastOnePolicyAsync".

OpenSpacesAndPlaces avatar Nov 02 '20 17:11 OpenSpacesAndPlaces

Exactly. This example is misleading.

sungam3r avatar Nov 02 '20 19:11 sungam3r

Initial problem can be solved by role-based auth - https://github.com/graphql-dotnet/graphql-dotnet/pull/3067 . "Admin" and "Teacher" from initial post look more like roles, not policies. ping @Shane32

sungam3r avatar May 01 '22 20:05 sungam3r

Agree @sungam3r . As of GraphQL v5, roles can be applied to the GraphQL schema rather than only policies, which would work in the method requested. (Requires implementation by the authorization rule within this repository, which has not been done here yet.)

I can also explain how authorization works in ASP.Net Core, but I am not sure how it applies to this repository.

Typical ASP.Net Core authorization rules would either apply a single policy ** or ** one or more roles directly. A policy typically contains one or more requirements, one of which could be "is a member of at least one role in the supplied list". However, custom authorization requirements can be written for any desired behavior.

Links:

  • https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-6.0
  • https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder?view=aspnetcore-6.0
  • https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authorization.infrastructure.assertionrequirement?view=aspnetcore-6.0

Shane32 avatar May 01 '22 20:05 Shane32