diff --git a/README.md b/README.md index 1d88207c..60506fb8 100644 --- a/README.md +++ b/README.md @@ -404,8 +404,8 @@ Both roles and policies are supported for output graph types, fields on output g and query arguments. If multiple policies are specified, all must match; if multiple roles are specified, any one role must match. You may also use `.Authorize()` and/or the `[Authorize]` attribute to validate that the user has authenticated. You may also use -`.AllowAnonymous()` and/or `[AllowAnonymous]` to allow fields to be returned to -unauthenticated users within an graph that has an authorization requirement defined. +`.AllowAnonymous()` and/or `[AllowAnonymous]` to allow fields to bypass authorization +requirements defined on the type that contains the field. Please note that authorization rules do not apply to values returned within introspection requests, potentially leaking information about protected areas of the schema to unauthenticated users. diff --git a/src/Transports.AspNetCore/AuthorizationVisitorBase.cs b/src/Transports.AspNetCore/AuthorizationVisitorBase.cs index 12415f3f..570b0061 100644 --- a/src/Transports.AspNetCore/AuthorizationVisitorBase.cs +++ b/src/Transports.AspNetCore/AuthorizationVisitorBase.cs @@ -65,10 +65,7 @@ public virtual async ValueTask EnterAsync(ASTNode node, ValidationContext contex _onlyAnonymousSelected.Push(ti); // Fields, unlike types, are validated immediately. - if (!fieldAnonymousAllowed) - { - await ValidateAsync(field, node, context); - } + await ValidateAsync(field, node, context); } // prep for descendants, if any diff --git a/tests/Transports.AspNetCore.Tests/AuthorizationTests.cs b/tests/Transports.AspNetCore.Tests/AuthorizationTests.cs index 4b394e9a..37c4866e 100644 --- a/tests/Transports.AspNetCore.Tests/AuthorizationTests.cs +++ b/tests/Transports.AspNetCore.Tests/AuthorizationTests.cs @@ -775,6 +775,37 @@ public async Task EndToEnd(bool authenticated) actual.ShouldBe("""{"errors":[{"message":"Access denied for field \u0027parent\u0027 on type \u0027QueryType\u0027.","locations":[{"line":1,"column":3}],"extensions":{"code":"ACCESS_DENIED","codes":["ACCESS_DENIED"]}}]}"""); } + [Theory] + [InlineData("Role1", false, false)] // User with Role1, child requires Role2 - should fail at child level + [InlineData("Role2", false, false)] // User with Role2, query requires Role1 - should fail at query level + [InlineData("Role1,Role2", false, true)] // User with both roles - should pass + [InlineData(null, false, false)] // Unauthenticated user - should fail at query level + [InlineData("Role1", true, false)] // User with Role1, child requires Role2 and is anonymous - should fail + [InlineData("Role2", true, true)] // User with Role2, child requires Role2 and is anonymous - should pass + [InlineData("Role1,Role2", true, true)] // User with both roles, child is anonymous - should pass + [InlineData(null, true, false)] // Unauthenticated user, child is anonymous - should fail as Role2 is missing + public void BothAnonymousAndRequirements(string? userRoles, bool childIsAnonymous, bool expectedIsValid) + { + // Set up query to require Role1 + _query.AuthorizeWithRoles("Role1"); + + // Set up child field to require Role2 and optionally be anonymous + _field.AuthorizeWithRoles("Role2"); + if (childIsAnonymous) + _field.AllowAnonymous(); + + // Set up user principal based on test parameters + if (userRoles != null) + { + var roles = userRoles.Split(','); + var claims = roles.Select(role => new Claim(ClaimTypes.Role, role)).ToArray(); + _principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookie")); + } + + var ret = Validate(@"{ parent { child } }"); + ret.IsValid.ShouldBe(expectedIsValid); + } + public enum Mode { None,