diff --git a/src/Mvc/Mvc.Core/test/Authorization/AuthorizeFilterTest.cs b/src/Mvc/Mvc.Core/test/Authorization/AuthorizeFilterTest.cs index 1d95168d4037..e5c390395b8e 100644 --- a/src/Mvc/Mvc.Core/test/Authorization/AuthorizeFilterTest.cs +++ b/src/Mvc/Mvc.Core/test/Authorization/AuthorizeFilterTest.cs @@ -251,7 +251,7 @@ public Task GetPolicyAsync(string policyName) return Task.FromResult(policyName == "true" ? _true : _false); } - public Task GetRequiredPolicyAsync() + public Task GetFallbackPolicyAsync() => Task.FromResult(null); } diff --git a/src/Mvc/test/Mvc.IntegrationTests/AuthorizeFilterIntegrationTest.cs b/src/Mvc/test/Mvc.IntegrationTests/AuthorizeFilterIntegrationTest.cs index 0729fbcd75ff..0a31d403dfbd 100644 --- a/src/Mvc/test/Mvc.IntegrationTests/AuthorizeFilterIntegrationTest.cs +++ b/src/Mvc/test/Mvc.IntegrationTests/AuthorizeFilterIntegrationTest.cs @@ -209,7 +209,7 @@ public Task GetPolicyAsync(string policyName) return Task.FromResult(new AuthorizationPolicy(requirements, new string[] { })); } - public Task GetRequiredPolicyAsync() + public Task GetFallbackPolicyAsync() { return Task.FromResult(null); } diff --git a/src/Security/Authorization/Core/ref/Microsoft.AspNetCore.Authorization.netcoreapp3.0.cs b/src/Security/Authorization/Core/ref/Microsoft.AspNetCore.Authorization.netcoreapp3.0.cs index 30c87d87b89d..ef38eddb47c9 100644 --- a/src/Security/Authorization/Core/ref/Microsoft.AspNetCore.Authorization.netcoreapp3.0.cs +++ b/src/Security/Authorization/Core/ref/Microsoft.AspNetCore.Authorization.netcoreapp3.0.cs @@ -45,12 +45,14 @@ protected AuthorizationHandler() { } public partial class AuthorizationMiddleware { public AuthorizationMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider policyProvider) { } + [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) { throw null; } } public partial class AuthorizationOptions { public AuthorizationOptions() { } public Microsoft.AspNetCore.Authorization.AuthorizationPolicy DefaultPolicy { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public Microsoft.AspNetCore.Authorization.AuthorizationPolicy FallbackPolicy { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public bool InvokeHandlersAfterFailure { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public void AddPolicy(string name, Microsoft.AspNetCore.Authorization.AuthorizationPolicy policy) { } public void AddPolicy(string name, System.Action configurePolicy) { } @@ -130,6 +132,7 @@ public partial class DefaultAuthorizationPolicyProvider : Microsoft.AspNetCore.A { public DefaultAuthorizationPolicyProvider(Microsoft.Extensions.Options.IOptions options) { } public System.Threading.Tasks.Task GetDefaultPolicyAsync() { throw null; } + public System.Threading.Tasks.Task GetFallbackPolicyAsync() { throw null; } public virtual System.Threading.Tasks.Task GetPolicyAsync(string policyName) { throw null; } } public partial class DefaultAuthorizationService : Microsoft.AspNetCore.Authorization.IAuthorizationService @@ -159,6 +162,7 @@ public partial interface IAuthorizationHandlerProvider public partial interface IAuthorizationPolicyProvider { System.Threading.Tasks.Task GetDefaultPolicyAsync(); + System.Threading.Tasks.Task GetFallbackPolicyAsync(); System.Threading.Tasks.Task GetPolicyAsync(string policyName); } public partial interface IAuthorizationRequirement diff --git a/src/Security/Authorization/Core/src/Policy/AuthorizationMiddleware.cs b/src/Security/Authorization/Core/src/AuthorizationMiddleware.cs similarity index 81% rename from src/Security/Authorization/Core/src/Policy/AuthorizationMiddleware.cs rename to src/Security/Authorization/Core/src/AuthorizationMiddleware.cs index e49ec7811835..184209557a53 100644 --- a/src/Security/Authorization/Core/src/Policy/AuthorizationMiddleware.cs +++ b/src/Security/Authorization/Core/src/AuthorizationMiddleware.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; @@ -23,21 +22,11 @@ public class AuthorizationMiddleware public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvider policyProvider) { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - - if (policyProvider == null) - { - throw new ArgumentNullException(nameof(policyProvider)); - } - - _next = next; - _policyProvider = policyProvider; + _next = next ?? throw new ArgumentNullException(nameof(next)); + _policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider)); } - public Task Invoke(HttpContext context) + public async Task Invoke(HttpContext context) { if (context == null) { @@ -49,18 +38,8 @@ public Task Invoke(HttpContext context) // Flag to indicate to other systems, e.g. MVC, that authorization middleware was run for this request context.Items[AuthorizationMiddlewareInvokedKey] = AuthorizationMiddlewareInvokedValue; - var authorizeData = endpoint?.Metadata.GetOrderedMetadata(); - if (authorizeData == null || authorizeData.Count() == 0) - { - return _next(context); - } - - return EvaluatePolicy(context, endpoint, authorizeData); - } - - private async Task EvaluatePolicy(HttpContext context, Endpoint endpoint, IEnumerable authorizeData) - { // IMPORTANT: Changes to authorization logic should be mirrored in MVC's AuthorizeFilter + var authorizeData = endpoint?.Metadata.GetOrderedMetadata() ?? Array.Empty(); var policy = await AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData); if (policy == null) { diff --git a/src/Security/Authorization/Core/src/AuthorizationOptions.cs b/src/Security/Authorization/Core/src/AuthorizationOptions.cs index 32df9d034a6c..d9121f60babf 100644 --- a/src/Security/Authorization/Core/src/AuthorizationOptions.cs +++ b/src/Security/Authorization/Core/src/AuthorizationOptions.cs @@ -27,6 +27,16 @@ public class AuthorizationOptions /// public AuthorizationPolicy DefaultPolicy { get; set; } = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + /// + /// Gets or sets the fallback authorization policy used by + /// when no IAuthorizeData have been provided. As a result, the uses the fallback policy + /// if there are no instances for a resource. If a resource has any + /// then they are evaluated instead of the fallback policy. By default the fallback policy is null, and usually will have no + /// effect unless you have the middleware in your pipeline. It is not used in any way by the + /// default . + /// + public AuthorizationPolicy FallbackPolicy { get; set; } + /// /// Add an authorization policy with the provided name. /// @@ -84,4 +94,4 @@ public AuthorizationPolicy GetPolicy(string name) return PolicyMap.ContainsKey(name) ? PolicyMap[name] : null; } } -} \ No newline at end of file +} diff --git a/src/Security/Authorization/Core/src/AuthorizationPolicy.cs b/src/Security/Authorization/Core/src/AuthorizationPolicy.cs index d68087791e59..f1833da95496 100644 --- a/src/Security/Authorization/Core/src/AuthorizationPolicy.cs +++ b/src/Security/Authorization/Core/src/AuthorizationPolicy.cs @@ -176,7 +176,17 @@ public static async Task CombineAsync(IAuthorizationPolicyP } } + // If we have no policy by now, use the fallback policy if we have one + if (policyBuilder == null) + { + var fallbackPolicy = await policyProvider.GetFallbackPolicyAsync(); + if (fallbackPolicy != null) + { + return fallbackPolicy; + } + } + return policyBuilder?.Build(); } } -} \ No newline at end of file +} diff --git a/src/Security/Authorization/Core/src/DefaultAuthorizationPolicyProvider.cs b/src/Security/Authorization/Core/src/DefaultAuthorizationPolicyProvider.cs index 03bd255cd6a6..9c9fddf9bb4c 100644 --- a/src/Security/Authorization/Core/src/DefaultAuthorizationPolicyProvider.cs +++ b/src/Security/Authorization/Core/src/DefaultAuthorizationPolicyProvider.cs @@ -15,6 +15,7 @@ public class DefaultAuthorizationPolicyProvider : IAuthorizationPolicyProvider { private readonly AuthorizationOptions _options; private Task _cachedDefaultPolicy; + private Task _cachedFallbackPolicy; /// /// Creates a new instance of . @@ -39,6 +40,15 @@ public Task GetDefaultPolicyAsync() return GetCachedPolicy(ref _cachedDefaultPolicy, _options.DefaultPolicy); } + /// + /// Gets the fallback authorization policy. + /// + /// The fallback authorization policy. + public Task GetFallbackPolicyAsync() + { + return GetCachedPolicy(ref _cachedFallbackPolicy, _options.FallbackPolicy); + } + private Task GetCachedPolicy(ref Task cachedPolicy, AuthorizationPolicy currentPolicy) { var local = cachedPolicy; diff --git a/src/Security/Authorization/Core/src/IAuthorizationPolicyProvider.cs b/src/Security/Authorization/Core/src/IAuthorizationPolicyProvider.cs index 9e9d0f468a5e..52e5b2314de2 100644 --- a/src/Security/Authorization/Core/src/IAuthorizationPolicyProvider.cs +++ b/src/Security/Authorization/Core/src/IAuthorizationPolicyProvider.cs @@ -22,5 +22,11 @@ public interface IAuthorizationPolicyProvider /// /// The default authorization policy. Task GetDefaultPolicyAsync(); + + /// + /// Gets the fallback authorization policy. + /// + /// The fallback authorization policy. + Task GetFallbackPolicyAsync(); } } diff --git a/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs b/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs index 7557f1b9ca81..289a9d0d07c8 100644 --- a/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs +++ b/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs @@ -40,6 +40,25 @@ public async Task NoEndpoint_AnonymousUser_Allows() Assert.True(next.Called); } + [Fact] + public async Task NoEndpointWithFallback_AnonymousUser_Challenges() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetFallbackPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(anonymous: true); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.False(next.Called); + } + [Fact] public async Task HasEndpointWithoutAuth_AnonymousUser_Allows() { @@ -59,6 +78,47 @@ public async Task HasEndpointWithoutAuth_AnonymousUser_Allows() Assert.True(next.Called); } + [Fact] + public async Task HasEndpointWithFallbackWithoutAuth_AnonymousUser_Challenges() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + policyProvider.Setup(p => p.GetFallbackPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(anonymous: true, endpoint: CreateEndpoint()); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.False(next.Called); + } + + [Fact] + public async Task HasEndpointWithOnlyFallbackAuth_AnonymousUser_Allows() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build()); + policyProvider.Setup(p => p.GetFallbackPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + var authenticationService = new TestAuthenticationService(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(anonymous: true, endpoint: CreateEndpoint(new AuthorizeAttribute()), authenticationService: authenticationService); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.True(next.Called); + } + [Fact] public async Task HasEndpointWithAuth_AnonymousUser_Challenges() { @@ -108,8 +168,11 @@ public async Task OnAuthorizationAsync_WillCallPolicyProvider() var policy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build(); var policyProvider = new Mock(); var getPolicyCount = 0; + var getFallbackPolicyCount = 0; policyProvider.Setup(p => p.GetPolicyAsync(It.IsAny())).ReturnsAsync(policy) .Callback(() => getPolicyCount++); + policyProvider.Setup(p => p.GetFallbackPolicyAsync()).ReturnsAsync(policy) + .Callback(() => getFallbackPolicyCount++); var next = new TestRequestDelegate(); var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); var context = GetHttpContext(anonymous: true, endpoint: CreateEndpoint(new AuthorizeAttribute("whatever"))); @@ -117,14 +180,17 @@ public async Task OnAuthorizationAsync_WillCallPolicyProvider() // Act & Assert await middleware.Invoke(context); Assert.Equal(1, getPolicyCount); + Assert.Equal(0, getFallbackPolicyCount); Assert.Equal(1, next.CalledCount); await middleware.Invoke(context); Assert.Equal(2, getPolicyCount); + Assert.Equal(0, getFallbackPolicyCount); Assert.Equal(2, next.CalledCount); await middleware.Invoke(context); Assert.Equal(3, getPolicyCount); + Assert.Equal(0, getFallbackPolicyCount); Assert.Equal(3, next.CalledCount); } diff --git a/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs b/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs index 230e95c4c4c6..1d048dc16491 100644 --- a/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs +++ b/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs @@ -1025,7 +1025,7 @@ public Task GetDefaultPolicyAsync() return Task.FromResult(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); } - public Task GetRequiredPolicyAsync() + public Task GetFallbackPolicyAsync() { return Task.FromResult(null); } @@ -1064,7 +1064,7 @@ public Task GetDefaultPolicyAsync() return Task.FromResult(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); } - public Task GetRequiredPolicyAsync() + public Task GetFallbackPolicyAsync() { return Task.FromResult(null); } diff --git a/src/Security/samples/CustomPolicyProvider/Authorization/MinimumAgePolicyProvider.cs b/src/Security/samples/CustomPolicyProvider/Authorization/MinimumAgePolicyProvider.cs index b9012603244b..1bdeb84c1a97 100644 --- a/src/Security/samples/CustomPolicyProvider/Authorization/MinimumAgePolicyProvider.cs +++ b/src/Security/samples/CustomPolicyProvider/Authorization/MinimumAgePolicyProvider.cs @@ -26,6 +26,8 @@ public MinimumAgePolicyProvider(IOptions options) } public Task GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync(); + + public Task GetFallbackPolicyAsync() => FallbackPolicyProvider.GetFallbackPolicyAsync(); // Policies are looked up by string name, so expect 'parameters' (like age) // to be embedded in the policy names. This is abstracted away from developers