Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can I authorize per multiple policies ? #6

Open
jalchr opened this issue May 25, 2018 · 16 comments
Open

Can I authorize per multiple policies ? #6

jalchr opened this issue May 25, 2018 · 16 comments
Labels
question Further information is requested

Comments

@jalchr
Copy link

jalchr commented May 25, 2018

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
Copy link
Member

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

@jalchr
Copy link
Author

jalchr commented Jun 1, 2018

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

@joemcbride
Copy link
Member

joemcbride commented Jun 6, 2018

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
Copy link
Author

jalchr commented Jun 11, 2018

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
Copy link
Member

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
Copy link

okarlsson commented Dec 12, 2018

@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
Copy link

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.

#49 (comment)

@Mousavi310
Copy link

Mousavi310 commented Dec 29, 2019

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 sungam3r added the question Further information is requested label May 21, 2020
@sungam3r
Copy link
Member

sungam3r commented Nov 1, 2020

@Mousavi310 Why do you use break in your example?

@OpenSpacesAndPlaces
Copy link

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
Copy link

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
Copy link
Member

sungam3r commented Nov 2, 2020

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

@OpenSpacesAndPlaces
Copy link

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

@sungam3r
Copy link
Member

sungam3r commented Nov 2, 2020

Exactly. This example is misleading.

@sungam3r
Copy link
Member

sungam3r commented May 1, 2022

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

@Shane32
Copy link
Member

Shane32 commented May 1, 2022

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:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

7 participants