-
Notifications
You must be signed in to change notification settings - Fork 164
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
Replace authorization project with new rule #803
Conversation
@sungam3r This PR should be next to concentrate on. |
Bump @sungam3r - status? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found 38 potential problems in the proposed changes. Check the Files changed tab for more details.
else if (node is GraphQLArgument) | ||
{ | ||
// ignore arguments of directives | ||
if (context.TypeInfo.GetAncestor(2)?.Kind == ASTNodeKind.Field) | ||
{ | ||
// verify field argument | ||
var arg = context.TypeInfo.GetArgument(); | ||
if (arg != null) | ||
{ | ||
Validate(arg, node, context); | ||
} | ||
} | ||
} |
Check notice
Code scanning / CodeQL
Nested 'if' statements can be combined
if ((ti.WaitingOnFragments?.Count ?? 0) == 0) | ||
{ | ||
if (_todos != null) | ||
{ | ||
var count = _todos.Count; | ||
for (var i = 0; i < count; i++) | ||
{ | ||
var todo = _todos[i]; | ||
if (todo.WaitingOnFragments.Remove(fragmentName)) | ||
{ | ||
todo.AnyAuthenticated |= ti.AnyAuthenticated; | ||
todo.AnyAnonymous |= ti.AnyAnonymous; | ||
if (todo.WaitingOnFragments.Count == 0) | ||
{ | ||
_todos.RemoveAt(i); | ||
count--; | ||
if (todo.AnyAuthenticated || !todo.AnyAnonymous) | ||
{ | ||
Validate(todo.ValidationInfo); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
Check notice
Code scanning / CodeQL
Nested 'if' statements can be combined
else if (ifArg.Value is GraphQLVariable variable) | ||
{ | ||
if (context.Operation.Variables != null) | ||
{ | ||
var varDef = context.Operation.Variables.FirstOrDefault(x => x.Variable.Name == variable.Name); | ||
if (varDef != null && varDef.Type.Name() == "Boolean") | ||
{ | ||
if (context.Variables.TryGetValue(variable.Name.StringValue, out var value)) | ||
{ | ||
if (value is bool boolValue2) | ||
return boolValue2; | ||
} | ||
if (varDef.DefaultValue is GraphQLBooleanValue boolValue3) | ||
{ | ||
return boolValue3.BoolValue; | ||
} | ||
} | ||
} | ||
} |
Check notice
Code scanning / CodeQL
Nested 'if' statements can be combined
if (context.Variables.TryGetValue(variable.Name.StringValue, out var value)) | ||
{ | ||
if (value is bool boolValue2) | ||
return boolValue2; | ||
} |
Check notice
Code scanning / CodeQL
Nested 'if' statements can be combined
I remember this, I will look tomorrow. Is this the last PR of the large changes in the server? |
Yes. Just samples, tests and documentation after that, which while being a lot of code/text, is not substantive in terms of functionality. |
I doubt a bit about removed |
Below is a sample of a failing test that should pass if [Fact]
public void ignores_skipped_fields()
{
ConfigureAuthorizationOptions(options =>
{
options.AddPolicy("ClassPolicy", x => x.RequireClaim("admin"));
options.AddPolicy("SchemaPolicy", x => x.RequireClaim("some"));
});
ShouldPassRule(config =>
{
config.Query = @"query { post @skip(if: true) __typename }";
config.Schema = BasicSchema<BasicQueryWithAttributesAndClassPolicy>().AuthorizeWithPolicy("SchemaPolicy");
config.User = CreatePrincipal(claims: new Dictionary<string, string>
{
{ "some", "true" },
});
});
} |
@@ -92,7 +93,8 @@ private IValidationResult Validate(ValidationTestConfig config) | |||
Document = document, | |||
Operation = document.Definitions.OfType<GraphQLOperationDefinition>().First(), | |||
Rules = config.Rules, | |||
Variables = config.Variables | |||
Variables = config.Variables, | |||
RequestServices = ServiceProvider, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ServiceProvider
is root service provider. Maybe create and set service provider from scope ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing registered is scoped, so it makes little difference. I will have new tests anyway.
src/Transports.AspNetCore/AuthorizationVisitorBase.Validation.cs
Outdated
Show resolved
Hide resolved
src/Transports.AspNetCore/AuthorizationVisitorBase.Validation.cs
Outdated
Show resolved
Hide resolved
protected IAuthorizationService AuthorizationService { get; } | ||
|
||
/// <inheritdoc/> | ||
protected override bool Authorize() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Method looks like an action to perform (verb). The same for AuthorizeRole. Do they definitely have such names?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I named them so they all start with Authorize
, and so they are similar to the extension methods we have on defined within GraphQL.NET, which are Authorize
, AuthorizeWithRoles
and AuthorizeWithPolicy
. We could name them similar to the members they map to:
bool IsAuthenticated { get; }
,bool IsInRole(string role)
, andAuthorizationResult Authorize(string policy)
What do you think? I'm open to suggestions and would like to do whatever makes the most sense.
For reference here is the code:
protected override bool Authorize()
=> ClaimsPrincipal.Identity?.IsAuthenticated ?? false;
protected override bool AuthorizeRole(string role)
=> ClaimsPrincipal.IsInRole(role);
protected override AuthorizationResult AuthorizePolicy(string policy)
=> AuthorizationService.AuthorizeAsync(ClaimsPrincipal, policy).GetAwaiter().GetResult();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is bad that for all of them you can not use the name IsXXX
. The last one method is verb. There is no 100% symmetry. I don't know how to do better.
bool IsAuthenticated { get; },
bool IsInRole(string role), and
AuthorizationResult Authorize(string policy)
This variant at least looks reasonable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do. At least it matches the methods they call (by default).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked through all changes. What can I say? Well, the previous validation rule takes about 200 lines of code and is quite understandable. The current implementation is greatly complicated and overloaded. Honestly, I almost immediately lost my desire to understand the code. I watched it not in detail and I'm sure that this code will be more difficult to maintain. In addition, I still doubt the correctness of the selected approach with building error messages (removed message builder) though I'm fine with any way to assemble those messages. Howewer, in this situation, I am pleased that despite such complexity, it is limited by only one rule of validation and does not go beyond it. See my other comments about schema coordinates and other and feel free to merge since I can't help here more reviewing/tracking control flow.
Of course, as a user of this rule, I would need comprehensive documentation. |
Co-authored-by: Ivan Maximov <[email protected]>
Co-authored-by: Ivan Maximov <[email protected]>
I agree. The nature of properly applying the
I looked at the spec you referenced. I certainly see the value in using the spec here. I added an issue so we can come back to this. As this is a implementation detail (being an error message description) changing it after release is not a breaking change. I will review again when I have time; maybe soon.
I will await your comments on naming the |
Not spec itself, still RFC. Some RFCs are worked on for years without any real progress in spec :( |
With the vast number of changes made in this project as a whole (basically a 100% rewrite), maybe it would be better to retain |
Co-authored-by: Ivan Maximov <[email protected]>
Codecov Report
@@ Coverage Diff @@
## develop #803 +/- ##
===========================================
+ Coverage 28.78% 29.33% +0.54%
===========================================
Files 51 50 -1
Lines 1987 2127 +140
Branches 290 359 +69
===========================================
+ Hits 572 624 +52
- Misses 1330 1393 +63
- Partials 85 110 +25
Continue to review full report at Codecov.
|
Another option would be to update the old nuget package |
Do you mean something like |
Sure; something like that. |
I would not |
This is a complete rewrite of the authorization rule, with many new features as shown below.
New features:
[Authorize(Roles = "Admin")]
)[Authorize]
)AllowAnonymous
to allow querying an anonymous field of a protected type@skip
and@include
directives properlyIAuthorizationService
is pulled from the scoped DI, just in case it is a scoped service (although it probably isn't).Removed features:
ParseDictionary
within the input object, not to mention they may contain default values. As such, it is just as effective to protect the field or query argument that references the input type that would trigger the validation check. This could be done as described above, however, in another PR.IClaimsPrincipalAccessor
. This project is for ASP.NET Core and need not support custom authentication schemes. The GraphQL.Authorization project would be better suited for such a scenario. TheHttpContext.User
property may be set in application pipeline to change theClaimsPrincipal
that validation takes place against. If we reintroduce support forIClaimsPrincipalAccessor
, then it andIHttpContextAccessor
needs to be registered for the HTTP middleware, which it is not now. It would also complicate the authentication support for WebSocket connections. We may want to review this decision after the new authentication provisions for WebSocket connections are introduced, as WebSocket connections require special authentication code. Note that inheritance may be used to use any ClaimsPrincipal with the authorization rule.IAuthorizationErrorMessageBuilder
and does not include code to build error messages based on the exact reason the authentication check failed. This is by design, as this is improperly leaking security rules to the caller.AuthorizationVisitor
provides virtual methods if it is desired to override message generation. Further,ErrorInfoProvider
can override authentication messages just as it did before (as seen in the sample).Other changes/notes:
AddAuthorization
to be installed separately; installing the validation rule does not add or configure ASP.NET Core's authorization framework.AccessDeniedError
with the main project. It was one file, but I split it into 4 files to ease the review process.Known issues:
ValidationContext.GetRecursivelyReferencedFragments
does not consider@skip
and@include
directives, and so authorization checks may be performed on fragments that are not part of the operation due to@skip
or@include
. This should be fixed in GraphQL.NET.