diff --git a/src/Security/Authorization/Core/src/AuthorizationBuilder.cs b/src/Security/Authorization/Core/src/AuthorizationBuilder.cs new file mode 100644 index 000000000000..793d2ab2709e --- /dev/null +++ b/src/Security/Authorization/Core/src/AuthorizationBuilder.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Authorization; + +/// +/// Used to configure authorization +/// +public class AuthorizationBuilder +{ + /// + /// Initializes a new instance of . + /// + /// The services being configured. + public AuthorizationBuilder(IServiceCollection services) + => Services = services; + + /// + /// The services being configured. + /// + public virtual IServiceCollection Services { get; } + + /// + /// Determines whether authorization handlers should be invoked after . + /// Defaults to true. + /// + /// The builder. + public virtual AuthorizationBuilder SetInvokeHandlersAfterFailure(bool invoke) + { + Services.Configure(o => o.InvokeHandlersAfterFailure = invoke); + return this; + } + + /// + /// Sets the default authorization policy. Defaults to require authenticated users. + /// + /// + /// The default policy used when evaluating with no policy name specified. + /// + /// The builder. + public virtual AuthorizationBuilder SetDefaultPolicy(AuthorizationPolicy policy) + { + Services.Configure(o => o.DefaultPolicy = policy); + return this; + } + + /// + /// Sets the fallback authorization policy used by + /// when no IAuthorizeData have been provided. As a result, the AuthorizationMiddleware 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 AuthorizationMiddleware in your pipeline. It is not used in any way by the + /// default . + /// + /// The builder. + public virtual AuthorizationBuilder SetFallbackPolicy(AuthorizationPolicy? policy) + { + Services.Configure(o => o.FallbackPolicy = policy); + return this; + } + + /// + /// Adds a which can be used by . + /// + /// The name of this policy. + /// The .> + /// The builder. + public virtual AuthorizationBuilder AddPolicy(string name, AuthorizationPolicy policy) + { + Services.Configure(o => o.AddPolicy(name, policy)); + return this; + } + + /// + /// Add a policy that is built from a delegate with the provided name. + /// + /// The name of the policy. + /// The delegate that will be used to build the policy. + /// The builder. + public virtual AuthorizationBuilder AddPolicy(string name, Action configurePolicy) + { + Services.Configure(o => o.AddPolicy(name, configurePolicy)); + return this; + } + + /// + /// Add a policy that is built from a delegate with the provided name and used as the default policy. + /// + /// The name of the default policy. + /// The default .> + /// The builder. + public virtual AuthorizationBuilder AddDefaultPolicy(string name, AuthorizationPolicy policy) + { + SetDefaultPolicy(policy); + return AddPolicy(name, policy); + } + + /// + /// Add a policy that is built from a delegate with the provided name and used as the DefaultPolicy. + /// + /// The name of the DefaultPolicy. + /// The delegate that will be used to build the DefaultPolicy. + /// The builder. + public virtual AuthorizationBuilder AddDefaultPolicy(string name, Action configurePolicy) + { + if (configurePolicy == null) + { + throw new ArgumentNullException(nameof(configurePolicy)); + } + + var policyBuilder = new AuthorizationPolicyBuilder(); + configurePolicy(policyBuilder); + return AddDefaultPolicy(name, policyBuilder.Build()); + } + + /// + /// Add a policy that is built from a delegate with the provided name and used as the FallbackPolicy. + /// + /// The name of the FallbackPolicy. + /// The Fallback .> + /// The builder. + public virtual AuthorizationBuilder AddFallbackPolicy(string name, AuthorizationPolicy policy) + { + SetFallbackPolicy(policy); + return AddPolicy(name, policy); + } + + /// + /// Add a policy that is built from a delegate with the provided name and used as the FallbackPolicy. + /// + /// The name of the Fallback policy. + /// The delegate that will be used to build the Fallback policy. + /// The builder. + public virtual AuthorizationBuilder AddFallbackPolicy(string name, Action configurePolicy) + { + if (configurePolicy == null) + { + throw new ArgumentNullException(nameof(configurePolicy)); + } + + var policyBuilder = new AuthorizationPolicyBuilder(); + configurePolicy(policyBuilder); + return AddFallbackPolicy(name, policyBuilder.Build()); + } +} diff --git a/src/Security/Authorization/Core/src/AuthorizationOptions.cs b/src/Security/Authorization/Core/src/AuthorizationOptions.cs index 1b53575a6127..041553819e57 100644 --- a/src/Security/Authorization/Core/src/AuthorizationOptions.cs +++ b/src/Security/Authorization/Core/src/AuthorizationOptions.cs @@ -14,7 +14,7 @@ public class AuthorizationOptions private Dictionary PolicyMap { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); /// - /// Determines whether authentication handlers should be invoked after . + /// Determines whether authorization handlers should be invoked after . /// Defaults to true. /// public bool InvokeHandlersAfterFailure { get; set; } = true; diff --git a/src/Security/Authorization/Core/src/PublicAPI.Unshipped.txt b/src/Security/Authorization/Core/src/PublicAPI.Unshipped.txt index 30396d6df216..38f8cdf2c053 100644 --- a/src/Security/Authorization/Core/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authorization/Core/src/PublicAPI.Unshipped.txt @@ -1,3 +1,15 @@ #nullable enable +Microsoft.AspNetCore.Authorization.AuthorizationBuilder +Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AuthorizationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> void Microsoft.AspNetCore.Authorization.Infrastructure.PassThroughAuthorizationHandler.PassThroughAuthorizationHandler(Microsoft.Extensions.Options.IOptions! options) -> void static Microsoft.AspNetCore.Authorization.AuthorizationPolicy.CombineAsync(Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider! policyProvider, System.Collections.Generic.IEnumerable! authorizeData, System.Collections.Generic.IEnumerable! policies) -> System.Threading.Tasks.Task! +virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddDefaultPolicy(string! name, Microsoft.AspNetCore.Authorization.AuthorizationPolicy! policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder! +virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddDefaultPolicy(string! name, System.Action! configurePolicy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder! +virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddFallbackPolicy(string! name, Microsoft.AspNetCore.Authorization.AuthorizationPolicy! policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder! +virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddFallbackPolicy(string! name, System.Action! configurePolicy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder! +virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddPolicy(string! name, Microsoft.AspNetCore.Authorization.AuthorizationPolicy! policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder! +virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddPolicy(string! name, System.Action! configurePolicy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder! +virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.SetDefaultPolicy(Microsoft.AspNetCore.Authorization.AuthorizationPolicy! policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder! +virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.SetFallbackPolicy(Microsoft.AspNetCore.Authorization.AuthorizationPolicy? policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder! +virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.SetInvokeHandlersAfterFailure(bool invoke) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder! diff --git a/src/Security/Authorization/Policy/src/PolicyServiceCollectionExtensions.cs b/src/Security/Authorization/Policy/src/PolicyServiceCollectionExtensions.cs index 4c4a958a098e..dd7cfd7ccc39 100644 --- a/src/Security/Authorization/Policy/src/PolicyServiceCollectionExtensions.cs +++ b/src/Security/Authorization/Policy/src/PolicyServiceCollectionExtensions.cs @@ -12,6 +12,14 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class PolicyServiceCollectionExtensions { + /// + /// Adds authorization services to the specified . + /// + /// The to add services to. + /// The so that additional calls can be chained. + public static AuthorizationBuilder AddAuthorizationBuilder(this IServiceCollection services) + => new AuthorizationBuilder(services.AddAuthorization()); + /// /// Adds the authorization policy evaluator service to the specified . /// diff --git a/src/Security/Authorization/Policy/src/PublicAPI.Unshipped.txt b/src/Security/Authorization/Policy/src/PublicAPI.Unshipped.txt index 9c11d1e7b2d6..4150e1ff1c75 100644 --- a/src/Security/Authorization/Policy/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authorization/Policy/src/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ #nullable enable static Microsoft.AspNetCore.Builder.AuthorizationEndpointConventionBuilderExtensions.RequireAuthorization(this TBuilder builder, Microsoft.AspNetCore.Authorization.AuthorizationPolicy! policy) -> TBuilder static Microsoft.AspNetCore.Builder.AuthorizationEndpointConventionBuilderExtensions.RequireAuthorization(this TBuilder builder, System.Action! configurePolicy) -> TBuilder +static Microsoft.Extensions.DependencyInjection.PolicyServiceCollectionExtensions.AddAuthorizationBuilder(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder! diff --git a/src/Security/Authorization/test/AuthorizationBuilderTests.cs b/src/Security/Authorization/test/AuthorizationBuilderTests.cs new file mode 100644 index 000000000000..46c2b6606682 --- /dev/null +++ b/src/Security/Authorization/test/AuthorizationBuilderTests.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Authorization.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authorization.Test; + +public class AuthorizationBuilderTests +{ + [Fact] + public void CanSetFallbackPolicy() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var builder = TestHelpers.CreateAuthorizationBuilder() + // Act + .SetFallbackPolicy(policy); + + var options = builder.Services.BuildServiceProvider().GetRequiredService>().Value; + + // Assert + Assert.Equal(policy, options.FallbackPolicy); + } + + [Fact] + public void CanUnSetFallbackPolicy() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var builder = TestHelpers.CreateAuthorizationBuilder() + .SetFallbackPolicy(policy) + // Act + .SetFallbackPolicy(null); + + var options = builder.Services.BuildServiceProvider().GetRequiredService>().Value; + + // Assert + Assert.Null(options.FallbackPolicy); + } + + [Fact] + public void CanSetDefaultPolicy() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var builder = TestHelpers.CreateAuthorizationBuilder() + // Act + .SetDefaultPolicy(policy); + + var options = builder.Services.BuildServiceProvider().GetRequiredService>().Value; + + // Assert + Assert.Equal(policy, options.DefaultPolicy); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CanSetInvokeHandlersAfterFailure(bool invoke) + { + // Arrange + var builder = TestHelpers.CreateAuthorizationBuilder() + // Act + .SetInvokeHandlersAfterFailure(invoke); + + var options = builder.Services.BuildServiceProvider().GetRequiredService>().Value; + + // Assert + Assert.Equal(invoke, options.InvokeHandlersAfterFailure); + } + + [Fact] + public void CanAddPolicyInstance() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var builder = TestHelpers.CreateAuthorizationBuilder() + // Act + .AddPolicy("name", policy); + + var options = builder.Services.BuildServiceProvider().GetRequiredService>().Value; + + // Assert + Assert.Equal(policy, options.GetPolicy("name")); + } + + [Fact] + public void CanAddPolicyDelegate() + { + // Arrange + var builder = TestHelpers.CreateAuthorizationBuilder() + // Act + .AddPolicy("name", p => p.RequireAssertion(_ => true)); + + var options = builder.Services.BuildServiceProvider().GetRequiredService>().Value; + + // Assert + var policy = options.GetPolicy("name"); + Assert.NotNull(policy); + Assert.Equal(1, policy.Requirements.Count); + Assert.IsType(policy.Requirements.First()); + } +} + +internal class TestHelpers +{ + public static AuthorizationBuilder CreateAuthorizationBuilder() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOptions(); + return services.AddAuthorizationBuilder(); + } +} diff --git a/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs b/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs index 937adbc55eaa..fcab4e0388b1 100644 --- a/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs +++ b/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs @@ -37,12 +37,7 @@ public async Task Authorize_ShouldAllowIfClaimIsPresent() { // Arrange var authorizationService = BuildAuthorizationService(services => - { - services.AddAuthorization(options => - { - options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); - }); - }); + services.AddAuthorizationBuilder().AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage"))); var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") })); // Act @@ -58,13 +53,10 @@ public async Task Authorize_ShouldAllowIfClaimIsPresentWithSpecifiedAuthType() // Arrange var authorizationService = BuildAuthorizationService(services => { - services.AddAuthorization(options => + services.AddAuthorizationBuilder().AddPolicy("Basic", policy => { - options.AddPolicy("Basic", policy => - { - policy.AddAuthenticationSchemes("Basic"); - policy.RequireClaim("Permission", "CanViewPage"); - }); + policy.AddAuthenticationSchemes("Basic"); + policy.RequireClaim("Permission", "CanViewPage"); }); }); var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic")); @@ -81,12 +73,7 @@ public async Task Authorize_ShouldAllowIfClaimIsAmongValues() { // Arrange var authorizationService = BuildAuthorizationService(services => - { - services.AddAuthorization(options => - { - options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")); - }); - }); + services.AddAuthorizationBuilder().AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything"))); var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { @@ -113,10 +100,7 @@ public async Task Authorize_ShouldInvokeAllHandlersByDefault() { services.AddSingleton(handler1); services.AddSingleton(handler2); - services.AddAuthorization(options => - { - options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); - }); + services.AddAuthorizationBuilder().AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); }); // Act @@ -141,11 +125,9 @@ public async Task Authorize_ShouldInvokeAllHandlersDependingOnSetting(bool invok { services.AddSingleton(handler1); services.AddSingleton(handler2); - services.AddAuthorization(options => - { - options.InvokeHandlersAfterFailure = invokeAllHandlers; - options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); - }); + services.AddAuthorizationBuilder() + .AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())) + .SetInvokeHandlersAfterFailure(invokeAllHandlers); }); // Act @@ -196,10 +178,7 @@ public async Task CanFailWithReasons() services.AddSingleton(handler1); services.AddSingleton(handler2); services.AddSingleton(handler3); - services.AddAuthorization(options => - { - options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); - }); + services.AddAuthorizationBuilder().AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); }); // Act @@ -222,12 +201,7 @@ public async Task Authorize_ShouldFailWhenAllRequirementsNotHandled() { // Arrange var authorizationService = BuildAuthorizationService(services => - { - services.AddAuthorization(options => - { - options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")); - }); - }); + services.AddAuthorizationBuilder().AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything"))); var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { @@ -249,12 +223,7 @@ public async Task Authorize_ShouldNotAllowIfClaimTypeIsNotPresent() { // Arrange var authorizationService = BuildAuthorizationService(services => - { - services.AddAuthorization(options => - { - options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")); - }); - }); + services.AddAuthorizationBuilder().AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything"))); var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { @@ -275,12 +244,7 @@ public async Task Authorize_ShouldNotAllowIfClaimValueIsNotPresent() { // Arrange var authorizationService = BuildAuthorizationService(services => - { - services.AddAuthorization(options => - { - options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); - }); - }); + services.AddAuthorizationBuilder().AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage"))); var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { @@ -301,12 +265,7 @@ public async Task Authorize_ShouldNotAllowIfNoClaims() { // Arrange var authorizationService = BuildAuthorizationService(services => - { - services.AddAuthorization(options => - { - options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); - }); - }); + services.AddAuthorizationBuilder().AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage"))); var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[0], @@ -325,12 +284,7 @@ public async Task Authorize_ShouldNotAllowIfUserIsNull() { // Arrange var authorizationService = BuildAuthorizationService(services => - { - services.AddAuthorization(options => - { - options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); - }); - }); + services.AddAuthorizationBuilder().AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage"))); // Act var allowed = await authorizationService.AuthorizeAsync(null, null, "Basic"); @@ -344,12 +298,7 @@ public async Task Authorize_ShouldNotAllowIfNotCorrectAuthType() { // Arrange var authorizationService = BuildAuthorizationService(services => - { - services.AddAuthorization(options => - { - options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); - }); - }); + services.AddAuthorizationBuilder().AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage"))); var user = new ClaimsPrincipal(new ClaimsIdentity()); // Act @@ -364,12 +313,7 @@ public async Task Authorize_ShouldAllowWithNoAuthType() { // Arrange var authorizationService = BuildAuthorizationService(services => - { - services.AddAuthorization(options => - { - options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); - }); - }); + services.AddAuthorizationBuilder().AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage"))); var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { @@ -517,12 +461,7 @@ public async Task RolePolicyCanBlockNoRole() { // Arrange var authorizationService = BuildAuthorizationService(services => - { - services.AddAuthorization(options => - { - options.AddPolicy("Basic", policy => policy.RequireRole("Admin", "Users")); - }); - }); + services.AddAuthorizationBuilder().AddPolicy("Basic", policy => policy.RequireRole("Admin", "Users"))); var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { @@ -541,12 +480,7 @@ public async Task RolePolicyCanBlockNoRole() public void PolicyThrowsWithNoRequirements() { Assert.Throws(() => BuildAuthorizationService(services => - { - services.AddAuthorization(options => - { - options.AddPolicy("Basic", policy => { }); - }); - })); + services.AddAuthorizationBuilder().AddPolicy("Basic", policy => { }))); } [Fact] @@ -554,12 +488,7 @@ public async Task RequireUserNameFailsForWrongUserName() { // Arrange var authorizationService = BuildAuthorizationService(services => - { - services.AddAuthorization(options => - { - options.AddPolicy("Hao", policy => policy.RequireUserName("Hao")); - }); - }); + services.AddAuthorizationBuilder().AddPolicy("Hao", policy => policy.RequireUserName("Hao"))); var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] {