authorization
authorization copied to clipboard
Can I authorize per multiple policies ?
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.
Not at present, no. You can have a policy like “TeacherOrAdmin”.
I see that might be a good solution. Is this a limitation in "AuthorizeWith" ? Any future plans ?
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.
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
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
@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
}
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
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 Why do you use break
in your example?
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;
.
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:
- Policy Passes (isValid = true)
- Policy Fails (returns isValid set to true)
@Mousavi310
Then I don't understand the meaning of the method at all - SatisfiesAtLeastOnePolicyAsync. Why does it break on the first false result?
You read more closely than I did :) - I failed to read the method name "SatisfiesAtLeastOnePolicyAsync".
Exactly. This example is misleading.
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
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