graphql-dotnet / authorization

A toolset for authorizing access to graph types for GraphQL .NET.
MIT License
157 stars 38 forks source link

Can I authorize per multiple policies ? #6

Open jalchr opened 6 years ago

jalchr commented 6 years ago

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.

joemcbride commented 6 years ago

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

jalchr commented 6 years ago

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

joemcbride commented 6 years ago

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.

jalchr commented 6 years ago

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

joemcbride commented 6 years ago

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

okarlsson commented 5 years ago

@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
}
OpenSpacesAndPlaces commented 5 years ago

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

Mousavi310 commented 4 years ago

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>();
sungam3r commented 3 years ago

@Mousavi310 Why do you use break in your example?

OpenSpacesAndPlaces commented 3 years ago

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 commented 3 years ago

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

sungam3r commented 3 years ago

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

OpenSpacesAndPlaces commented 3 years ago

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

sungam3r commented 3 years ago

Exactly. This example is misleading.

sungam3r commented 2 years ago

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

Shane32 commented 2 years ago

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: