From b93bc433db66175d2b07b128ec9990f7a4dd7e1b Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 8 Apr 2019 06:03:34 -0700 Subject: [PATCH] Make AuthorizeFilter work in endpoint routing (#9099) * Make AuthorizeFilter work in endpoint routing Fixes https://github.com/aspnet/AspNetCore/issues/8387 --- .../test/testassets/TestServer/Startup.cs | 37 +- ...uthorizationWithoutEndpointRoutingTests.cs | 16 + ...ityUserLoginWithoutEndpointRoutingTests.cs | 16 + .../Identity.DefaultUI.WebSite/StartupBase.cs | 11 +- .../StartupWithoutEndpointRouting.cs | 53 ++ .../CORS/src/Infrastructure/CorsMiddleware.cs | 15 + .../test/UnitTests/CorsMiddlewareTests.cs | 62 ++- .../ActionAttributeRouteModel.cs | 11 +- .../AuthorizationApplicationModelProvider.cs | 14 +- .../src/ApplicationModels/SelectorModel.cs | 3 + .../src/Authorization/AuthorizeFilter.cs | 48 +- .../Routing/ActionConstraintMatcherPolicy.cs | 4 - ...thorizationApplicationModelProviderTest.cs | 59 ++- ...ControllerActionDescriptorProviderTests.cs | 12 +- .../test/Authorization/AuthorizeFilterTest.cs | 108 +++- .../src/CorsApplicationModelProvider.cs | 58 +- .../Mvc.Cors/src/CorsAuthorizationFilter.cs | 3 +- .../src/CorsHttpMethodActionConstraint.cs | 1 - .../src/DisableCorsAuthorizationFilter.cs | 2 +- .../test/CorsApplicationModelProviderTest.cs | 106 ++-- .../test/CorsAuthorizationFilterTest.cs | 8 +- .../DisableCorsAuthorizationFilterTest.cs | 2 +- ...AspNetCore.Mvc.RazorPages.netcoreapp3.0.cs | 1 + ...thorizationPageApplicationModelProvider.cs | 14 +- .../CompiledPageActionDescriptorBuilder.cs | 14 +- .../ApplicationModels/PageApplicationModel.cs | 7 + .../PageConventionCollection.cs | 21 + .../MvcRazorPagesMvcCoreBuilderExtensions.cs | 3 + .../PageConventionCollectionExtensions.cs | 177 +++++-- .../RazorPagesOptionsSetup.cs | 30 ++ .../src/Infrastructure/DefaultPageLoader.cs | 2 +- .../PageActionInvokerProvider.cs | 1 - .../Mvc.RazorPages/src/RazorPagesOptions.cs | 2 +- ...izationPageApplicationModelProviderTest.cs | 29 +- ...CompiledPageActionDescriptorBuilderTest.cs | 35 +- .../PageConventionCollectionTest.cs | 6 +- .../MvcRazorPagesMvcBuilderExtensionsTest.cs | 3 +- .../PageConventionCollectionExtensionsTest.cs | 494 ++++++++++++++++-- .../Infrastructure/DefaultPageLoaderTest.cs | 38 +- .../PageActionDescriptorProviderTest.cs | 2 +- .../ActionConstraintMatcherPolicyTest.cs | 7 - .../AuthMiddlewareAndFilterTest.cs | 13 + .../AuthMiddlewareAndFilterTestBase.cs | 277 ++++++++++ ...wareAndFilterWithoutEndpointRoutingTest.cs | 13 + .../test/Mvc.FunctionalTests/CorsTestsBase.cs | 20 +- ...lAuthorizationFilterEndpointRoutingTest.cs | 7 - .../Infrastructure/HttpClientExtensions.cs | 2 +- .../RazorPagesWithBasePathTest.cs | 6 +- .../RazorPagesWithEndpointRoutingTest.cs | 4 +- src/Mvc/test/WebSites/CorsWebSite/Startup.cs | 1 + .../WebSites/SecurityWebSite/BearerAuth.cs | 42 ++ .../AuthorizedActionsController.cs | 19 + .../Controllers/AuthorizedController.cs | 20 + .../Controllers/LoginController.cs | 46 ++ .../AllowAnonymousPageViaConvention.cshtml | 2 + .../AllowAnonymousPageViaConvention.cshtml.cs | 14 + .../Pages/AllowAnonymousPageViaModel.cshtml | 2 + .../AllowAnonymousPageViaModel.cshtml.cs | 15 + .../Pages/AuthorizePageViaConvention.cshtml | 2 + .../AuthorizePageViaConvention.cshtml.cs | 13 + .../Pages/AuthorizePageViaModel.cshtml | 2 + .../Pages/AuthorizePageViaModel.cshtml.cs | 15 + .../SecurityWebSite/Pages/PagesHome.cshtml | 2 + .../SecurityWebSite/Pages/_ViewImports.cshtml | 1 + .../SecurityWebSite/SecurityWebSite.csproj | 3 +- .../test/WebSites/SecurityWebSite/Startup.cs | 3 +- .../StartupWithGlobalAuthFilter.cs | 56 ++ ...hGlobalAuthFilterWithoutEndpointRouting.cs | 49 ++ ...WithGlobalDenyAnonymousFilterWithUseMvc.cs | 6 + 69 files changed, 1893 insertions(+), 297 deletions(-) create mode 100644 src/Identity/test/Identity.FunctionalTests/IdentityUserTests/IdentityUserAuthorizationWithoutEndpointRoutingTests.cs create mode 100644 src/Identity/test/Identity.FunctionalTests/IdentityUserTests/IdentityUserLoginWithoutEndpointRoutingTests.cs create mode 100644 src/Identity/testassets/Identity.DefaultUI.WebSite/StartupWithoutEndpointRouting.cs create mode 100644 src/Mvc/Mvc.RazorPages/src/DependencyInjection/RazorPagesOptionsSetup.cs create mode 100644 src/Mvc/test/Mvc.FunctionalTests/AuthMiddlewareAndFilterTest.cs create mode 100644 src/Mvc/test/Mvc.FunctionalTests/AuthMiddlewareAndFilterTestBase.cs create mode 100644 src/Mvc/test/Mvc.FunctionalTests/AuthMiddlewareAndFilterWithoutEndpointRoutingTest.cs create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/BearerAuth.cs create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/Controllers/AuthorizedActionsController.cs create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/Controllers/AuthorizedController.cs create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/Controllers/LoginController.cs create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaConvention.cshtml create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaConvention.cshtml.cs create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaModel.cshtml create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaModel.cshtml.cs create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaConvention.cshtml create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaConvention.cshtml.cs create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaModel.cshtml create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaModel.cshtml.cs create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/Pages/PagesHome.cshtml create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/Pages/_ViewImports.cshtml create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalAuthFilter.cs create mode 100644 src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalAuthFilterWithoutEndpointRouting.cs diff --git a/src/Components/test/testassets/TestServer/Startup.cs b/src/Components/test/testassets/TestServer/Startup.cs index 6e95cb8f22..7d6d635d69 100644 --- a/src/Components/test/testassets/TestServer/Startup.cs +++ b/src/Components/test/testassets/TestServer/Startup.cs @@ -1,5 +1,4 @@ using BasicTestApp; -using BasicTestApp.RouterTest; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Hosting; @@ -38,7 +37,17 @@ namespace TestServer app.UseDeveloperExceptionPage(); } - AllowCorsForAnyLocalhostPort(app); + // It's not enough just to return "Access-Control-Allow-Origin: *", because + // browsers don't allow wildcards in conjunction with credentials. So we must + // specify explicitly which origin we want to allow. + app.UseCors(policy => + { + policy.SetIsOriginAllowed(host => host.StartsWith("http://localhost:") || host.StartsWith("http://127.0.0.1:")) + .AllowAnyHeader() + .WithExposedHeaders("MyCustomHeader") + .AllowAnyMethod() + .AllowCredentials(); + }); app.UseRouting(); @@ -82,29 +91,5 @@ namespace TestServer }); }); } - - private static void AllowCorsForAnyLocalhostPort(IApplicationBuilder app) - { - // It's not enough just to return "Access-Control-Allow-Origin: *", because - // browsers don't allow wildcards in conjunction with credentials. So we must - // specify explicitly which origin we want to allow. - app.Use((context, next) => - { - if (context.Request.Headers.TryGetValue("origin", out var incomingOriginValue)) - { - var origin = incomingOriginValue.ToArray()[0]; - if (origin.StartsWith("http://localhost:") || origin.StartsWith("http://127.0.0.1:")) - { - context.Response.Headers.Add("Access-Control-Allow-Origin", origin); - context.Response.Headers.Add("Access-Control-Allow-Credentials", "true"); - context.Response.Headers.Add("Access-Control-Allow-Methods", "HEAD,GET,PUT,POST,DELETE,OPTIONS"); - context.Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type,TestHeader,another-header"); - context.Response.Headers.Add("Access-Control-Expose-Headers", "MyCustomHeader,TestHeader,another-header"); - } - } - - return next(); - }); - } } } diff --git a/src/Identity/test/Identity.FunctionalTests/IdentityUserTests/IdentityUserAuthorizationWithoutEndpointRoutingTests.cs b/src/Identity/test/Identity.FunctionalTests/IdentityUserTests/IdentityUserAuthorizationWithoutEndpointRoutingTests.cs new file mode 100644 index 0000000000..ea8334e8da --- /dev/null +++ b/src/Identity/test/Identity.FunctionalTests/IdentityUserTests/IdentityUserAuthorizationWithoutEndpointRoutingTests.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Identity.DefaultUI.WebSite; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; + +namespace Microsoft.AspNetCore.Identity.FunctionalTests.IdentityUserTests +{ + public class IdentityUserAuthorizationWithoutEndpointRoutingTests : AuthorizationTests + { + public IdentityUserAuthorizationWithoutEndpointRoutingTests(ServerFactory serverFactory) + : base(serverFactory) + { + } + } +} diff --git a/src/Identity/test/Identity.FunctionalTests/IdentityUserTests/IdentityUserLoginWithoutEndpointRoutingTests.cs b/src/Identity/test/Identity.FunctionalTests/IdentityUserTests/IdentityUserLoginWithoutEndpointRoutingTests.cs new file mode 100644 index 0000000000..8d77e16be5 --- /dev/null +++ b/src/Identity/test/Identity.FunctionalTests/IdentityUserTests/IdentityUserLoginWithoutEndpointRoutingTests.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Identity.DefaultUI.WebSite; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Identity.FunctionalTests.IdentityUserTests +{ + public class IdentityUserLoginWithoutEndpointRoutingTests : LoginTests + { + public IdentityUserLoginWithoutEndpointRoutingTests(ServerFactory serverFactory) : base(serverFactory) + { + } + } +} diff --git a/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs b/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs index 69658e0a3c..14ae5b2db4 100644 --- a/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs +++ b/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs @@ -57,7 +57,7 @@ namespace Identity.DefaultUI.WebSite } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // This prevents running out of file watchers on some linux machines ((PhysicalFileProvider)env.WebRootFileProvider).UseActivePolling = false; @@ -80,14 +80,7 @@ namespace Identity.DefaultUI.WebSite app.UseRouting(); app.UseAuthentication(); - - // This has to be disabled due to https://github.com/aspnet/AspNetCore/issues/8387 - // - // UseAuthorization does not currently work with Razor pages, and it impacts - // many of the tests here. Uncomment when this is fixed so that we test what is recommended - // for users. - // - //app.UseAuthorization(); + app.UseAuthorization(); app.UseEndpoints(endpoints => { diff --git a/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupWithoutEndpointRouting.cs b/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupWithoutEndpointRouting.cs new file mode 100644 index 0000000000..d8916594d3 --- /dev/null +++ b/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupWithoutEndpointRouting.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; + +namespace Identity.DefaultUI.WebSite +{ + public class StartupWithoutEndpointRouting : StartupBase + { + public StartupWithoutEndpointRouting(IConfiguration configuration) : base(configuration) + { + } + + public override void ConfigureServices(IServiceCollection services) + { + base.ConfigureServices(services); + services.AddMvc(options => options.EnableEndpointRouting = false); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public override void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // This prevents running out of file watchers on some linux machines + ((PhysicalFileProvider)env.WebRootFileProvider).UseActivePolling = false; + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseDatabaseErrorPage(); + } + else + { + app.UseExceptionHandler("/Error"); + app.UseHsts(); + } + + app.UseAuthentication(); + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseCookiePolicy(); + + app.UseMvc(); + } + } +} diff --git a/src/Middleware/CORS/src/Infrastructure/CorsMiddleware.cs b/src/Middleware/CORS/src/Infrastructure/CorsMiddleware.cs index 4e44a09fb4..91c5be9054 100644 --- a/src/Middleware/CORS/src/Infrastructure/CorsMiddleware.cs +++ b/src/Middleware/CORS/src/Infrastructure/CorsMiddleware.cs @@ -145,8 +145,23 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure // Get the most significant CORS metadata for the endpoint // For backwards compatibility reasons this is then downcast to Enable/Disable metadata var corsMetadata = endpoint?.Metadata.GetMetadata(); + if (corsMetadata is IDisableCorsAttribute) { + var isOptionsRequest = string.Equals( + context.Request.Method, + CorsConstants.PreflightHttpMethod, + StringComparison.OrdinalIgnoreCase); + + var isCorsPreflightRequest = isOptionsRequest && context.Request.Headers.ContainsKey(CorsConstants.AccessControlRequestMethod); + + if (isCorsPreflightRequest) + { + // If this is a preflight request, and we disallow CORS, complete the request + context.Response.StatusCode = StatusCodes.Status204NoContent; + return; + } + await _next(context); return; } diff --git a/src/Middleware/CORS/test/UnitTests/CorsMiddlewareTests.cs b/src/Middleware/CORS/test/UnitTests/CorsMiddlewareTests.cs index f1e82104a5..08edb4d58b 100644 --- a/src/Middleware/CORS/test/UnitTests/CorsMiddlewareTests.cs +++ b/src/Middleware/CORS/test/UnitTests/CorsMiddlewareTests.cs @@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Endpoints; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -626,6 +625,67 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure Times.Once); } + [Fact] + public async Task Invoke_HasEndpointWithEnableMetadata_HasSignificantDisableCors_ReturnsNoContentForPreflightRequest() + { + // Arrange + var corsService = Mock.Of(); + var policyProvider = Mock.Of(); + var loggerFactory = NullLoggerFactory.Instance; + + var middleware = new CorsMiddleware( + c => { throw new Exception("Should not be called."); }, + corsService, + loggerFactory, + "DefaultPolicyName"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute(), new DisableCorsAttribute()), "Test endpoint")); + httpContext.Request.Method = "OPTIONS"; + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + httpContext.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { "GET" }); + + // Act + await middleware.Invoke(httpContext, policyProvider); + + // Assert + Assert.Equal(StatusCodes.Status204NoContent, httpContext.Response.StatusCode); + } + + [Fact] + public async Task Invoke_HasEndpointWithEnableMetadata_HasSignificantDisableCors_ExecutesNextMiddleware() + { + // Arrange + var executed = false; + var corsService = Mock.Of(); + var policyProvider = Mock.Of(); + var loggerFactory = NullLoggerFactory.Instance; + + var middleware = new CorsMiddleware( + c => + { + executed = true; + return Task.CompletedTask; + }, + corsService, + loggerFactory, + "DefaultPolicyName"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute(), new DisableCorsAttribute()), "Test endpoint")); + httpContext.Request.Method = "GET"; + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + httpContext.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { "GET" }); + + // Act + await middleware.Invoke(httpContext, policyProvider); + + // Assert + Assert.True(executed); + Mock.Get(policyProvider).Verify(v => v.GetPolicyAsync(It.IsAny(), It.IsAny()), Times.Never()); + Mock.Get(corsService).Verify(v => v.EvaluatePolicy(It.IsAny(), It.IsAny()), Times.Never()); + } + [Fact] public async Task Invoke_HasEndpointWithEnableMetadata_MiddlewareHasPolicy_RunsCorsWithPolicyName() { diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/ActionAttributeRouteModel.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/ActionAttributeRouteModel.cs index b7a60f3362..6d7a678093 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/ActionAttributeRouteModel.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationModels/ActionAttributeRouteModel.cs @@ -127,13 +127,16 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels } } - private static void AddEndpointMetadata(SelectorModel selector, IList metadata) + private static void AddEndpointMetadata(SelectorModel selector, IList controllerMetadata) { - if (metadata != null) + if (controllerMetadata != null) { - for (var i = 0; i < metadata.Count; i++) + // It is criticial to get the order in which metadata appears in endpoint metadata correct. More significant metadata + // must appear later in the sequence. In this case, the values in `controllerMetadata` should have their order + // preserved, but appear earlier than the entries in `selector.EndpointMetadata`. + for (var i = 0; i < controllerMetadata.Count; i++) { - selector.EndpointMetadata.Add(metadata[i]); + selector.EndpointMetadata.Insert(i, controllerMetadata[i]); } } } diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/AuthorizationApplicationModelProvider.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/AuthorizationApplicationModelProvider.cs index 34c7a31dd4..8cf079d0a7 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/AuthorizationApplicationModelProvider.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationModels/AuthorizationApplicationModelProvider.cs @@ -6,16 +6,21 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.ApplicationModels { internal class AuthorizationApplicationModelProvider : IApplicationModelProvider { + private readonly MvcOptions _mvcOptions; private readonly IAuthorizationPolicyProvider _policyProvider; - public AuthorizationApplicationModelProvider(IAuthorizationPolicyProvider policyProvider) + public AuthorizationApplicationModelProvider( + IAuthorizationPolicyProvider policyProvider, + IOptions mvcOptions) { _policyProvider = policyProvider; + _mvcOptions = mvcOptions.Value; } public int Order => -1000 + 10; @@ -32,6 +37,13 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels throw new ArgumentNullException(nameof(context)); } + if (_mvcOptions.EnableEndpointRouting) + { + // When using endpoint routing, the AuthorizationMiddleware does the work that Auth filters would otherwise perform. + // Consequently we do not need to convert authorization attributes to filters. + return; + } + foreach (var controllerModel in context.Result.Controllers) { var controllerModelAuthData = controllerModel.Attributes.OfType().ToArray(); diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/SelectorModel.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/SelectorModel.cs index d376f6c0d2..5ff6d1072a 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/SelectorModel.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationModels/SelectorModel.cs @@ -35,6 +35,9 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public IList ActionConstraints { get; } + /// + /// Gets the associated with the . + /// public IList EndpointMetadata { get; } } } diff --git a/src/Mvc/Mvc.Core/src/Authorization/AuthorizeFilter.cs b/src/Mvc/Mvc.Core/src/Authorization/AuthorizeFilter.cs index 63cfb5b503..0d2059b166 100644 --- a/src/Mvc/Mvc.Core/src/Authorization/AuthorizeFilter.cs +++ b/src/Mvc/Mvc.Core/src/Authorization/AuthorizeFilter.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Policy; +using Microsoft.AspNetCore.Http.Endpoints; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Filters; @@ -22,9 +23,6 @@ namespace Microsoft.AspNetCore.Mvc.Authorization /// public class AuthorizeFilter : IAsyncAuthorizationFilter, IFilterFactory { - // Property key set by authorization middleware when it is run - private const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareInvoked"; - private AuthorizationPolicy _effectivePolicy; /// @@ -116,6 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Authorization { return Task.FromResult(Policy); } + if (PolicyProvider == null) { throw new InvalidOperationException( @@ -127,7 +126,7 @@ namespace Microsoft.AspNetCore.Mvc.Authorization return AuthorizationPolicy.CombineAsync(PolicyProvider, AuthorizeData); } - private async Task GetEffectivePolicyAsync(AuthorizationFilterContext context) + internal async Task GetEffectivePolicyAsync(AuthorizationFilterContext context) { if (_effectivePolicy != null) { @@ -154,6 +153,27 @@ namespace Microsoft.AspNetCore.Mvc.Authorization } } + var endpoint = context.HttpContext.GetEndpoint(); + if (endpoint != null) + { + // When doing endpoint routing, MVC does not create filters for any authorization specific metadata i.e [Authorize] does not + // get translated into AuthorizeFilter. Consequently, there are some rough edges when an application uses a mix of AuthorizeFilter + // explicilty configured by the user (e.g. global auth filter), and uses endpoint metadata. + // To keep the behavior of AuthFilter identical to pre-endpoint routing, we will gather auth data from endpoint metadata + // and produce a policy using this. This would mean we would have effectively run some auth twice, but it maintains compat. + var policyProvider = PolicyProvider ?? context.HttpContext.RequestServices.GetRequiredService(); + var endpointAuthorizeData = endpoint.Metadata.GetOrderedMetadata() ?? Array.Empty(); + + var endpointPolicy = await AuthorizationPolicy.CombineAsync(policyProvider, endpointAuthorizeData); + if (endpointPolicy != null) + { + builder.Combine(endpointPolicy); + } + + // We cannot cache the policy since it varies by endpoint metadata. + canCache = false; + } + effectivePolicy = builder?.Build() ?? effectivePolicy; // We can cache the effective policy when there is no custom policy provider @@ -173,12 +193,6 @@ namespace Microsoft.AspNetCore.Mvc.Authorization throw new ArgumentNullException(nameof(context)); } - if (context.HttpContext.Items.ContainsKey(AuthorizationMiddlewareInvokedKey)) - { - // Authorization has already run in middleware. Don't re-run for performance - return; - } - if (!context.IsEffectivePolicy(this)) { return; @@ -196,7 +210,7 @@ namespace Microsoft.AspNetCore.Mvc.Authorization var authenticateResult = await policyEvaluator.AuthenticateAsync(effectivePolicy, context.HttpContext); // Allow Anonymous skips all authorization - if (HasAllowAnonymous(context.Filters)) + if (HasAllowAnonymous(context)) { return; } @@ -226,8 +240,9 @@ namespace Microsoft.AspNetCore.Mvc.Authorization return AuthorizationApplicationModelProvider.GetFilter(policyProvider, AuthorizeData); } - private static bool HasAllowAnonymous(IList filters) + private static bool HasAllowAnonymous(AuthorizationFilterContext context) { + var filters = context.Filters; for (var i = 0; i < filters.Count; i++) { if (filters[i] is IAllowAnonymousFilter) @@ -236,6 +251,15 @@ namespace Microsoft.AspNetCore.Mvc.Authorization } } + // When doing endpoint routing, MVC does not add AllowAnonymousFilters for AllowAnonymousAttributes that + // were discovered on controllers and actions. To maintain compat with 2.x, + // we'll check for the presence of IAllowAnonymous in endpoint metadata. + var endpoint = context.HttpContext.GetEndpoint(); + if (endpoint?.Metadata?.GetMetadata() != null) + { + return true; + } + return false; } } diff --git a/src/Mvc/Mvc.Core/src/Routing/ActionConstraintMatcherPolicy.cs b/src/Mvc/Mvc.Core/src/Routing/ActionConstraintMatcherPolicy.cs index f71c3294e3..a653898749 100644 --- a/src/Mvc/Mvc.Core/src/Routing/ActionConstraintMatcherPolicy.cs +++ b/src/Mvc/Mvc.Core/src/Routing/ActionConstraintMatcherPolicy.cs @@ -67,10 +67,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing { // This one is OK, we implement this in endpoint routing. } - else if (actionConstraint.GetType().FullName == "Microsoft.AspNetCore.Mvc.Cors.CorsHttpMethodActionConstraint") - { - // This one is OK, we implement this in endpoint routing. - } else if (actionConstraint.GetType() == typeof(ConsumesAttribute)) { // This one is OK, we implement this in endpoint routing. diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/AuthorizationApplicationModelProviderTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/AuthorizationApplicationModelProviderTest.cs index 0e1f46a760..99ca2e0d18 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/AuthorizationApplicationModelProviderTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/AuthorizationApplicationModelProviderTest.cs @@ -17,11 +17,51 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels { public class AuthorizationApplicationModelProviderTest { + private readonly IOptions OptionsWithoutEndpointRouting = Options.Create(new MvcOptions { EnableEndpointRouting = false }); + + [Fact] + public void OnProvidersExecuting_AuthorizeAttribute_DoesNothing_WhenEnableRoutingIsEnabled() + { + // Arrange + var provider = new AuthorizationApplicationModelProvider( + new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions())), + Options.Create(new MvcOptions())); + var controllerType = typeof(AccountController); + var context = CreateProviderContext(controllerType); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var controller = Assert.Single(context.Result.Controllers); + Assert.Empty(controller.Filters); + } + + [Fact] + public void OnProvidersExecuting_AllowAnonymousAttribute_DoesNothing_WhenEnableRoutingIsEnabled() + { + // Arrange + var provider = new AuthorizationApplicationModelProvider( + new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions())), + Options.Create(new MvcOptions())); + var controllerType = typeof(AnonymousController); + var context = CreateProviderContext(controllerType); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var controller = Assert.Single(context.Result.Controllers); + Assert.Empty(controller.Filters); + } + [Fact] public void CreateControllerModel_AuthorizeAttributeAddsAuthorizeFilter() { // Arrange - var provider = new AuthorizationApplicationModelProvider(new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions()))); + var provider = new AuthorizationApplicationModelProvider( + new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions())), + OptionsWithoutEndpointRouting); var controllerType = typeof(AccountController); var context = CreateProviderContext(controllerType); @@ -41,7 +81,9 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels options.Value.AddPolicy("Base", policy => policy.RequireClaim("Basic").RequireClaim("Basic2")); options.Value.AddPolicy("Derived", policy => policy.RequireClaim("Derived")); - var provider = new AuthorizationApplicationModelProvider(new DefaultAuthorizationPolicyProvider(options)); + var provider = new AuthorizationApplicationModelProvider( + new DefaultAuthorizationPolicyProvider(options), + OptionsWithoutEndpointRouting); var context = CreateProviderContext(typeof(DerivedController)); // Act @@ -65,7 +107,9 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public void CreateControllerModelAndActionModel_AllowAnonymousAttributeAddsAllowAnonymousFilter() { // Arrange - var provider = new AuthorizationApplicationModelProvider(new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions()))); + var provider = new AuthorizationApplicationModelProvider( + new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions())), + OptionsWithoutEndpointRouting); var context = CreateProviderContext(typeof(AnonymousController)); // Act @@ -91,7 +135,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels authOptions.Value.AddPolicy("Base", authorizationPolicy); var policyProvider = new DefaultAuthorizationPolicyProvider(authOptions); - var provider = new AuthorizationApplicationModelProvider(policyProvider); + var provider = new AuthorizationApplicationModelProvider(policyProvider, OptionsWithoutEndpointRouting); var context = CreateProviderContext(typeof(BaseController)); // Act @@ -119,7 +163,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels .Returns(Task.FromResult(authorizationPolicy)) .Verifiable(); - var provider = new AuthorizationApplicationModelProvider(authorizationPolicyProviderMock.Object); + var provider = new AuthorizationApplicationModelProvider(authorizationPolicyProviderMock.Object, OptionsWithoutEndpointRouting); // Act var action = GetBaseControllerActionModel(provider); @@ -136,9 +180,8 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels { // Arrange var provider = new AuthorizationApplicationModelProvider( - new DefaultAuthorizationPolicyProvider( - Options.Create(new AuthorizationOptions()) - )); + new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions())), + OptionsWithoutEndpointRouting); var context = CreateProviderContext(typeof(NoAuthController)); // Act diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/ControllerActionDescriptorProviderTests.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/ControllerActionDescriptorProviderTests.cs index 7540ed8a8b..c52f7a322a 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/ControllerActionDescriptorProviderTests.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/ControllerActionDescriptorProviderTests.cs @@ -266,16 +266,16 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels Assert.NotNull(anonymousAction.EndpointMetadata); Assert.Collection(anonymousAction.EndpointMetadata, - metadata => Assert.IsType(metadata), - metadata => Assert.IsType(metadata)); + metadata => Assert.IsType(metadata), + metadata => Assert.IsType(metadata)); var authorizeAction = Assert.Single(descriptors, a => a.RouteValues["action"] == "AuthorizeAction"); Assert.NotNull(authorizeAction.EndpointMetadata); Assert.Collection(authorizeAction.EndpointMetadata, - metadata => Assert.Equal("ActionPolicy", Assert.IsType(metadata).Policy), - metadata => Assert.Equal("ControllerPolicy", Assert.IsType(metadata).Policy)); + metadata => Assert.Equal("ControllerPolicy", Assert.IsType(metadata).Policy), + metadata => Assert.Equal("ActionPolicy", Assert.IsType(metadata).Policy)); } [Fact] @@ -291,6 +291,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels Assert.NotNull(action.EndpointMetadata); Assert.Collection(action.EndpointMetadata, + metadata => Assert.IsType(metadata), metadata => Assert.IsType(metadata), metadata => { @@ -298,8 +299,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels Assert.False(httpMethodMetadata.AcceptCorsPreflight); Assert.Equal("GET", Assert.Single(httpMethodMetadata.HttpMethods)); - }, - metadata => Assert.IsType(metadata)); + }); } [Fact] diff --git a/src/Mvc/Mvc.Core/test/Authorization/AuthorizeFilterTest.cs b/src/Mvc/Mvc.Core/test/Authorization/AuthorizeFilterTest.cs index 88e25a37a4..5ec73c90d6 100644 --- a/src/Mvc/Mvc.Core/test/Authorization/AuthorizeFilterTest.cs +++ b/src/Mvc/Mvc.Core/test/Authorization/AuthorizeFilterTest.cs @@ -8,11 +8,15 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Infrastructure; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Moq; using Xunit; @@ -20,6 +24,8 @@ namespace Microsoft.AspNetCore.Mvc.Authorization { public class AuthorizeFilterTest { + private readonly ActionContext ActionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + [Fact] public void InvalidUser() { @@ -49,26 +55,6 @@ namespace Microsoft.AspNetCore.Mvc.Authorization Assert.IsType(authorizationContext.Result); } - [Fact] - public async Task OnAuthorizationAsync_AuthorizationMiddlewareHasRun_NoOp() - { - // Arrange - var authorizationContext = GetAuthorizationContext(anonymous: true); - authorizationContext.HttpContext.Items["__AuthorizationMiddlewareInvoked"] = new object(); - - var authorizeFilterFactory = new AuthorizeFilter(); - var filterFactory = authorizeFilterFactory as IFilterFactory; - var authorizeFilter = (AuthorizeFilter)filterFactory.CreateInstance( - authorizationContext.HttpContext.RequestServices); - authorizationContext.Filters.Add(authorizeFilter); - - // Act - await authorizeFilter.OnAuthorizationAsync(authorizationContext); - - // Assert - Assert.Null(authorizationContext.Result); - } - [Fact] public async Task AuthorizeFilter_CreatedWithAuthorizeData_ThrowsWhenOnAuthorizationAsyncIsCalled() { @@ -527,6 +513,87 @@ namespace Microsoft.AspNetCore.Mvc.Authorization Assert.Same(policyProvider, actual.PolicyProvider); } + [Fact] + public async Task GetEffectivePolicyAsync_ReturnsCurrentPolicy_WhenNoEndpointMetadataIsAvailable() + { + // Arrange + var policy = new AuthorizationPolicyBuilder() + .RequireAssertion(_ => true) + .Build(); + var filter = new AuthorizeFilter(policy); + + var context = new AuthorizationFilterContext(ActionContext, new[] { filter }); + + // Act + var effectivePolicy = await filter.GetEffectivePolicyAsync(context); + + // Assert + // + // Verify the policy is cached + Assert.Same(effectivePolicy, await filter.GetEffectivePolicyAsync(context)); + } + + [Fact] + public async Task GetEffectivePolicyAsync_CombinesPoliciesFromAuthFilters() + { + // Arrange + var policy1 = new AuthorizationPolicyBuilder() + .RequireClaim("Claim1") + .Build(); + + var policy2 = new AuthorizationPolicyBuilder() + .RequireClaim("Claim2") + .Build(); + var filter1 = new AuthorizeFilter(policy1); + var filter2 = new AuthorizeFilter(policy2); + + var context = new AuthorizationFilterContext(ActionContext, new[] { filter1, filter2 }); + + // Act + var effectivePolicy = await filter1.GetEffectivePolicyAsync(context); + + // Assert + Assert.NotSame(policy1, effectivePolicy); + Assert.NotSame(policy2, effectivePolicy); + Assert.Equal(new[] { "Claim1", "Claim2" }, effectivePolicy.Requirements.Cast().Select(c => c.ClaimType)); + } + + [Fact] + public async Task GetEffectivePolicyAsync_CombinesPoliciesFromEndpoint() + { + // Arrange + var policy1 = new AuthorizationPolicyBuilder() + .RequireClaim("Claim1") + .Build(); + + var policy2 = new AuthorizationPolicyBuilder() + .RequireClaim("Claim2") + .Build(); + + var filter = new AuthorizeFilter(policy1); + var options = new AuthorizationOptions(); + options.AddPolicy("policy2", policy2); + var policyProvider = new DefaultAuthorizationPolicyProvider(Options.Create(options)); + + ActionContext.HttpContext.RequestServices = new ServiceCollection() + .AddSingleton(policyProvider) + .BuildServiceProvider(); + + ActionContext.HttpContext.SetEndpoint(new Endpoint( + _ => null, + new EndpointMetadataCollection(new AuthorizeAttribute("policy2")), + "test")); + var context = new AuthorizationFilterContext(ActionContext, new[] { filter, }); + + // Act + var effectivePolicy = await filter.GetEffectivePolicyAsync(context); + + // Assert + Assert.NotSame(policy1, effectivePolicy); + Assert.NotSame(policy2, effectivePolicy); + Assert.Equal(new[] { "Claim1", "Claim2" }, effectivePolicy.Requirements.Cast().Select(c => c.ClaimType)); + } + private AuthorizationFilterContext GetAuthorizationContext( bool anonymous = false, Action registerServices = null) @@ -580,6 +647,7 @@ namespace Microsoft.AspNetCore.Mvc.Authorization httpContext.SetupGet(c => c.RequestServices).Returns(serviceProvider); var contextItems = new Dictionary(); httpContext.SetupGet(c => c.Items).Returns(contextItems); + httpContext.SetupGet(c => c.Features).Returns(Mock.Of()); // AuthorizationFilterContext var actionContext = new ActionContext( diff --git a/src/Mvc/Mvc.Cors/src/CorsApplicationModelProvider.cs b/src/Mvc/Mvc.Cors/src/CorsApplicationModelProvider.cs index 5afd4e6385..415e358ea0 100644 --- a/src/Mvc/Mvc.Cors/src/CorsApplicationModelProvider.cs +++ b/src/Mvc/Mvc.Cors/src/CorsApplicationModelProvider.cs @@ -7,11 +7,19 @@ using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.Cors { internal class CorsApplicationModelProvider : IApplicationModelProvider { + private readonly MvcOptions _mvcOptions; + + public CorsApplicationModelProvider(IOptions mvcOptions) + { + _mvcOptions = mvcOptions.Value; + } + public int Order => -1000 + 10; public void OnProvidersExecuted(ApplicationModelProviderContext context) @@ -20,6 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Cors { throw new ArgumentNullException(nameof(context)); } + // Intentionally empty. } @@ -30,8 +39,21 @@ namespace Microsoft.AspNetCore.Mvc.Cors throw new ArgumentNullException(nameof(context)); } + if (_mvcOptions.EnableEndpointRouting) + { + // When doing endpoint routing, translate IEnableCorsAttribute to an HttpMethodMetadata with CORS enabled. + ConfigureCorsEndpointMetadata(context.Result); + } + else + { + ConfigureCorsFilters(context); + } + } + + private static void ConfigureCorsFilters(ApplicationModelProviderContext context) + { var isCorsEnabledGlobally = context.Result.Filters.OfType().Any() || - context.Result.Filters.OfType().Any(); + context.Result.Filters.OfType().Any(); foreach (var controllerModel in context.Result.Controllers) { @@ -67,13 +89,13 @@ namespace Microsoft.AspNetCore.Mvc.Cors if (isCorsEnabledGlobally || corsOnController || corsOnAction) { - UpdateActionToAcceptCorsPreflight(actionModel); + ConfigureCorsActionConstraint(actionModel); } } } } - private static void UpdateActionToAcceptCorsPreflight(ActionModel actionModel) + private static void ConfigureCorsActionConstraint(ActionModel actionModel) { for (var i = 0; i < actionModel.Selectors.Count; i++) { @@ -86,12 +108,36 @@ namespace Microsoft.AspNetCore.Mvc.Cors selectorModel.ActionConstraints[j] = new CorsHttpMethodActionConstraint(httpConstraint); } } + } + } - for (int j = 0; j < selectorModel.EndpointMetadata.Count; j++) + private static void ConfigureCorsEndpointMetadata(ApplicationModel applicationModel) + { + foreach (var controller in applicationModel.Controllers) + { + var corsOnController = controller.Attributes.OfType().Any() || + controller.Attributes.OfType().Any(); + + foreach (var action in controller.Actions) { - if (selectorModel.EndpointMetadata[j] is HttpMethodMetadata httpMethodMetadata) + var corsOnAction = action.Attributes.OfType().Any() || + action.Attributes.OfType().Any(); + + if (!corsOnController && !corsOnAction) { - selectorModel.EndpointMetadata[j] = new HttpMethodMetadata(httpMethodMetadata.HttpMethods, true); + // No CORS here. + continue; + } + + foreach (var selector in action.Selectors) + { + for (var i = 0; i < selector.EndpointMetadata.Count; i++) + { + if (selector.EndpointMetadata[i] is HttpMethodMetadata httpMethodMetadata) + { + selector.EndpointMetadata[i] = new HttpMethodMetadata(httpMethodMetadata.HttpMethods, acceptCorsPreflight: true); + } + } } } } diff --git a/src/Mvc/Mvc.Cors/src/CorsAuthorizationFilter.cs b/src/Mvc/Mvc.Cors/src/CorsAuthorizationFilter.cs index d4b374849d..f40003abd6 100644 --- a/src/Mvc/Mvc.Cors/src/CorsAuthorizationFilter.cs +++ b/src/Mvc/Mvc.Cors/src/CorsAuthorizationFilter.cs @@ -110,8 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.Cors !StringValues.IsNullOrEmpty(accessControlRequestMethod)) { // If this was a preflight, there is no need to run anything else. - // Also the response is always 200 so that anyone after mvc can handle the pre flight request. - context.Result = new StatusCodeResult(StatusCodes.Status200OK); + context.Result = new StatusCodeResult(StatusCodes.Status204NoContent); } // Continue with other filters and action. diff --git a/src/Mvc/Mvc.Cors/src/CorsHttpMethodActionConstraint.cs b/src/Mvc/Mvc.Cors/src/CorsHttpMethodActionConstraint.cs index d909180ac2..2128b2479c 100644 --- a/src/Mvc/Mvc.Cors/src/CorsHttpMethodActionConstraint.cs +++ b/src/Mvc/Mvc.Cors/src/CorsHttpMethodActionConstraint.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.Cors { - // Don't casually change the name of this. We reference the full type name in ActionConstraintCache. internal class CorsHttpMethodActionConstraint : HttpMethodActionConstraint { private readonly string OriginHeader = "Origin"; diff --git a/src/Mvc/Mvc.Cors/src/DisableCorsAuthorizationFilter.cs b/src/Mvc/Mvc.Cors/src/DisableCorsAuthorizationFilter.cs index 183add11a9..ed5a6ea2e3 100644 --- a/src/Mvc/Mvc.Cors/src/DisableCorsAuthorizationFilter.cs +++ b/src/Mvc/Mvc.Cors/src/DisableCorsAuthorizationFilter.cs @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.Cors !StringValues.IsNullOrEmpty(accessControlRequestMethod)) { // Short circuit if the request is preflight as that should not result in action execution. - context.Result = new StatusCodeResult(StatusCodes.Status200OK); + context.Result = new StatusCodeResult(StatusCodes.Status204NoContent); } // Let the action be executed. diff --git a/src/Mvc/Mvc.Cors/test/CorsApplicationModelProviderTest.cs b/src/Mvc/Mvc.Cors/test/CorsApplicationModelProviderTest.cs index 6018faa7da..e2153b007d 100644 --- a/src/Mvc/Mvc.Cors/test/CorsApplicationModelProviderTest.cs +++ b/src/Mvc/Mvc.Cors/test/CorsApplicationModelProviderTest.cs @@ -21,11 +21,59 @@ namespace Microsoft.AspNetCore.Mvc.Cors { public class CorsApplicationModelProviderTest { + private readonly IOptions OptionsWithoutEndpointRouting = Options.Create(new MvcOptions { EnableEndpointRouting = false }); + [Fact] - public void CreateControllerModel_EnableCorsAttributeAddsCorsAuthorizationFilterFactory() + public void OnProvidersExecuting_SetsEndpointMetadata_IfCorsAttributeIsPresentOnController() { // Arrange - var corsProvider = new CorsApplicationModelProvider(); + var corsProvider = new CorsApplicationModelProvider(Options.Create(new MvcOptions())); + var context = GetProviderContext(typeof(CorsController)); + + // Act + corsProvider.OnProvidersExecuting(context); + + // Assert + var model = Assert.Single(context.Result.Controllers); + Assert.Empty(model.Filters); + + var action = Assert.Single(model.Actions); + var selector = Assert.Single(action.Selectors); + var constraint = Assert.Single(selector.ActionConstraints, c => c is HttpMethodActionConstraint); + Assert.IsNotType(constraint); + + var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType()); + Assert.True(httpMethodMetadata.AcceptCorsPreflight); + } + + [Fact] + public void OnProvidersExecuting_SetsEndpointMetadata_IfCorsAttributeIsPresentOnAction() + { + // Arrange + var corsProvider = new CorsApplicationModelProvider(Options.Create(new MvcOptions())); + var context = GetProviderContext(typeof(DisableCorsActionController)); + + // Act + corsProvider.OnProvidersExecuting(context); + + // Assert + var model = Assert.Single(context.Result.Controllers); + Assert.Empty(model.Filters); + + var action = Assert.Single(model.Actions); + var selector = Assert.Single(action.Selectors); + var constraint = Assert.Single(selector.ActionConstraints, c => c is HttpMethodActionConstraint); + Assert.IsNotType(constraint); + + var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType()); + Assert.True(httpMethodMetadata.AcceptCorsPreflight); + } + + [Fact] + public void OnProvidersExecuting_WithoutGlobalAuthorizationFilter_EnableCorsAttributeAddsCorsAuthorizationFilterFactory() + { + // Arrange + var corsProvider = new CorsApplicationModelProvider(OptionsWithoutEndpointRouting); var context = GetProviderContext(typeof(CorsController)); // Act @@ -38,15 +86,13 @@ namespace Microsoft.AspNetCore.Mvc.Cors var selector = Assert.Single(action.Selectors); var constraint = Assert.Single(selector.ActionConstraints, c => c is HttpMethodActionConstraint); Assert.IsType(constraint); - var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType()); - Assert.True(httpMethodMetadata.AcceptCorsPreflight); } [Fact] - public void CreateControllerModel_DisableCorsAttributeAddsDisableCorsAuthorizationFilter() + public void OnProvidersExecuting_WithoutGlobalAuthorizationFilter_DisableCorsAttributeAddsDisableCorsAuthorizationFilter() { // Arrange - var corsProvider = new CorsApplicationModelProvider(); + var corsProvider = new CorsApplicationModelProvider(OptionsWithoutEndpointRouting); var context = GetProviderContext(typeof(DisableCorsController)); // Act @@ -59,15 +105,13 @@ namespace Microsoft.AspNetCore.Mvc.Cors var selector = Assert.Single(action.Selectors); var constraint = Assert.Single(selector.ActionConstraints, c => c is HttpMethodActionConstraint); Assert.IsType(constraint); - var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType()); - Assert.True(httpMethodMetadata.AcceptCorsPreflight); } [Fact] - public void CreateControllerModel_CustomCorsFilter_EnablesCorsPreflight() + public void OnProvidersExecuting_WithoutGlobalAuthorizationFilter_CustomCorsFilter_EnablesCorsPreflight() { // Arrange - var corsProvider = new CorsApplicationModelProvider(); + var corsProvider = new CorsApplicationModelProvider(OptionsWithoutEndpointRouting); var context = GetProviderContext(typeof(CustomCorsFilterController)); // Act @@ -79,15 +123,13 @@ namespace Microsoft.AspNetCore.Mvc.Cors var selector = Assert.Single(action.Selectors); var constraint = Assert.Single(selector.ActionConstraints, c => c is HttpMethodActionConstraint); Assert.IsType(constraint); - var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType()); - Assert.True(httpMethodMetadata.AcceptCorsPreflight); } [Fact] public void BuildActionModel_EnableCorsAttributeAddsCorsAuthorizationFilterFactory() { // Arrange - var corsProvider = new CorsApplicationModelProvider(); + var corsProvider = new CorsApplicationModelProvider(OptionsWithoutEndpointRouting); var context = GetProviderContext(typeof(EnableCorsController)); // Act @@ -100,15 +142,13 @@ namespace Microsoft.AspNetCore.Mvc.Cors var selector = Assert.Single(action.Selectors); var constraint = Assert.Single(selector.ActionConstraints, c => c is HttpMethodActionConstraint); Assert.IsType(constraint); - var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType()); - Assert.True(httpMethodMetadata.AcceptCorsPreflight); } [Fact] - public void BuildActionModel_DisableCorsAttributeAddsDisableCorsAuthorizationFilter() + public void BuildActionModel_WithoutGlobalAuthorizationFilter_DisableCorsAttributeAddsDisableCorsAuthorizationFilter() { // Arrange - var corsProvider = new CorsApplicationModelProvider(); + var corsProvider = new CorsApplicationModelProvider(OptionsWithoutEndpointRouting); var context = GetProviderContext(typeof(DisableCorsActionController)); // Act @@ -121,15 +161,13 @@ namespace Microsoft.AspNetCore.Mvc.Cors var selector = Assert.Single(action.Selectors); var constraint = Assert.Single(selector.ActionConstraints, c => c is HttpMethodActionConstraint); Assert.IsType(constraint); - var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType()); - Assert.True(httpMethodMetadata.AcceptCorsPreflight); } [Fact] - public void BuildActionModel_CustomCorsAuthorizationFilterOnAction_EnablesCorsPreflight() + public void BuildActionModel_WithoutGlobalAuthorizationFilter_CustomCorsAuthorizationFilterOnAction_EnablesCorsPreflight() { // Arrange - var corsProvider = new CorsApplicationModelProvider(); + var corsProvider = new CorsApplicationModelProvider(OptionsWithoutEndpointRouting); var context = GetProviderContext(typeof(CustomCorsFilterOnActionController)); // Act @@ -141,15 +179,13 @@ namespace Microsoft.AspNetCore.Mvc.Cors var selector = Assert.Single(action.Selectors); var constraint = Assert.Single(selector.ActionConstraints, c => c is HttpMethodActionConstraint); Assert.IsType(constraint); - var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType()); - Assert.True(httpMethodMetadata.AcceptCorsPreflight); } [Fact] - public void CreateControllerModel_EnableCorsGloballyEnablesCorsPreflight() + public void OnProvidersExecuting_WithoutGlobalAuthorizationFilter_EnableCorsGloballyEnablesCorsPreflight() { // Arrange - var corsProvider = new CorsApplicationModelProvider(); + var corsProvider = new CorsApplicationModelProvider(OptionsWithoutEndpointRouting); var context = GetProviderContext(typeof(RegularController)); context.Result.Filters.Add( @@ -164,15 +200,13 @@ namespace Microsoft.AspNetCore.Mvc.Cors var selector = Assert.Single(action.Selectors); var constraint = Assert.Single(selector.ActionConstraints, c => c is HttpMethodActionConstraint); Assert.IsType(constraint); - var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType()); - Assert.True(httpMethodMetadata.AcceptCorsPreflight); } [Fact] - public void CreateControllerModel_DisableCorsGloballyEnablesCorsPreflight() + public void OnProvidersExecuting_WithoutGlobalAuthorizationFilter_DisableCorsGloballyEnablesCorsPreflight() { // Arrange - var corsProvider = new CorsApplicationModelProvider(); + var corsProvider = new CorsApplicationModelProvider(OptionsWithoutEndpointRouting); var context = GetProviderContext(typeof(RegularController)); context.Result.Filters.Add(new DisableCorsAuthorizationFilter()); @@ -185,15 +219,13 @@ namespace Microsoft.AspNetCore.Mvc.Cors var selector = Assert.Single(action.Selectors); var constraint = Assert.Single(selector.ActionConstraints, c => c is HttpMethodActionConstraint); Assert.IsType(constraint); - var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType()); - Assert.True(httpMethodMetadata.AcceptCorsPreflight); } [Fact] - public void CreateControllerModel_CustomCorsFilterGloballyEnablesCorsPreflight() + public void OnProvidersExecuting_WithoutGlobalAuthorizationFilter_CustomCorsFilterGloballyEnablesCorsPreflight() { // Arrange - var corsProvider = new CorsApplicationModelProvider(); + var corsProvider = new CorsApplicationModelProvider(OptionsWithoutEndpointRouting); var context = GetProviderContext(typeof(RegularController)); context.Result.Filters.Add(new CustomCorsFilterAttribute()); @@ -206,15 +238,13 @@ namespace Microsoft.AspNetCore.Mvc.Cors var selector = Assert.Single(action.Selectors); var constraint = Assert.Single(selector.ActionConstraints, c => c is HttpMethodActionConstraint); Assert.IsType(constraint); - var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType()); - Assert.True(httpMethodMetadata.AcceptCorsPreflight); } [Fact] - public void CreateControllerModel_CorsNotInUseDoesNotOverrideHttpConstraints() + public void OnProvidersExecuting_WithoutGlobalAuthorizationFilter_CorsNotInUseDoesNotOverrideHttpConstraints() { // Arrange - var corsProvider = new CorsApplicationModelProvider(); + var corsProvider = new CorsApplicationModelProvider(OptionsWithoutEndpointRouting); var context = GetProviderContext(typeof(RegularController)); // Act @@ -226,8 +256,6 @@ namespace Microsoft.AspNetCore.Mvc.Cors var selector = Assert.Single(action.Selectors); var constraint = Assert.Single(selector.ActionConstraints, c => c is HttpMethodActionConstraint); Assert.IsNotType(constraint); - var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType()); - Assert.False(httpMethodMetadata.AcceptCorsPreflight); } private static ApplicationModelProviderContext GetProviderContext(Type controllerType) diff --git a/src/Mvc/Mvc.Cors/test/CorsAuthorizationFilterTest.cs b/src/Mvc/Mvc.Cors/test/CorsAuthorizationFilterTest.cs index 9bcefed3af..0a3f91ea00 100644 --- a/src/Mvc/Mvc.Cors/test/CorsAuthorizationFilterTest.cs +++ b/src/Mvc/Mvc.Cors/test/CorsAuthorizationFilterTest.cs @@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.Cors // Assert var response = authorizationContext.HttpContext.Response; - Assert.Equal(200, response.StatusCode); + Assert.Equal(204, response.StatusCode); Assert.Equal("http://example.com", response.Headers[CorsConstants.AccessControlAllowOrigin]); Assert.Equal("header1,header2", response.Headers[CorsConstants.AccessControlAllowHeaders]); @@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.Cors } [Fact] - public async Task PreFlight_FailedMatch_Writes200() + public async Task PreFlight_FailedMatch_RespondsWith204NoContent() { // Arrange var mockEngine = GetFailingEngine(); @@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.Mvc.Cors await authorizationContext.Result.ExecuteResultAsync(authorizationContext); // Assert - Assert.Equal(200, authorizationContext.HttpContext.Response.StatusCode); + Assert.Equal(204, authorizationContext.HttpContext.Response.StatusCode); Assert.Empty(authorizationContext.HttpContext.Response.Headers); } @@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.Mvc.Cors // Assert var response = authorizationContext.HttpContext.Response; - Assert.Equal(200, response.StatusCode); + Assert.Equal(204, response.StatusCode); Assert.Equal("http://example.com", response.Headers[CorsConstants.AccessControlAllowOrigin]); Assert.Equal("exposed1,exposed2", response.Headers[CorsConstants.AccessControlExposeHeaders]); } diff --git a/src/Mvc/Mvc.Cors/test/DisableCorsAuthorizationFilterTest.cs b/src/Mvc/Mvc.Cors/test/DisableCorsAuthorizationFilterTest.cs index 3e4045a8df..aa1fd124a5 100644 --- a/src/Mvc/Mvc.Cors/test/DisableCorsAuthorizationFilterTest.cs +++ b/src/Mvc/Mvc.Cors/test/DisableCorsAuthorizationFilterTest.cs @@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Mvc.Cors // Assert var statusCodeResult = Assert.IsType(authorizationFilterContext.Result); - Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode); + Assert.Equal(StatusCodes.Status204NoContent, statusCodeResult.StatusCode); } } } diff --git a/src/Mvc/Mvc.RazorPages/ref/Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp3.0.cs b/src/Mvc/Mvc.RazorPages/ref/Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp3.0.cs index 37ae8ad5c6..6ced9ae64c 100644 --- a/src/Mvc/Mvc.RazorPages/ref/Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp3.0.cs +++ b/src/Mvc/Mvc.RazorPages/ref/Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp3.0.cs @@ -49,6 +49,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public Microsoft.AspNetCore.Mvc.RazorPages.PageActionDescriptor ActionDescriptor { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } public string AreaName { get { throw null; } } public System.Reflection.TypeInfo DeclaredModelType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.Collections.Generic.IList EndpointMetadata { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } public System.Collections.Generic.IList Filters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } public System.Collections.Generic.IList HandlerMethods { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } public System.Collections.Generic.IList HandlerProperties { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } diff --git a/src/Mvc/Mvc.RazorPages/src/ApplicationModels/AuthorizationPageApplicationModelProvider.cs b/src/Mvc/Mvc.RazorPages/src/ApplicationModels/AuthorizationPageApplicationModelProvider.cs index 78fc43500e..2c38d8935e 100644 --- a/src/Mvc/Mvc.RazorPages/src/ApplicationModels/AuthorizationPageApplicationModelProvider.cs +++ b/src/Mvc/Mvc.RazorPages/src/ApplicationModels/AuthorizationPageApplicationModelProvider.cs @@ -5,16 +5,21 @@ using System; using System.Linq; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.ApplicationModels { internal class AuthorizationPageApplicationModelProvider : IPageApplicationModelProvider { private readonly IAuthorizationPolicyProvider _policyProvider; + private readonly MvcOptions _mvcOptions; - public AuthorizationPageApplicationModelProvider(IAuthorizationPolicyProvider policyProvider) + public AuthorizationPageApplicationModelProvider( + IAuthorizationPolicyProvider policyProvider, + IOptions mvcOptions) { _policyProvider = policyProvider; + _mvcOptions = mvcOptions.Value; } // The order is set to execute after the DefaultPageApplicationModelProvider. @@ -27,6 +32,13 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels throw new ArgumentNullException(nameof(context)); } + if (_mvcOptions.EnableEndpointRouting) + { + // When using endpoint routing, the AuthorizationMiddleware does the work that Auth filters would otherwise perform. + // Consequently we do not need to convert authorization attributes to filters. + return; + } + var pageModel = context.PageApplicationModel; var authorizeData = pageModel.HandlerTypeAttributes.OfType().ToArray(); if (authorizeData.Length > 0) diff --git a/src/Mvc/Mvc.RazorPages/src/ApplicationModels/CompiledPageActionDescriptorBuilder.cs b/src/Mvc/Mvc.RazorPages/src/ApplicationModels/CompiledPageActionDescriptorBuilder.cs index 96f7b895af..79968c8c39 100644 --- a/src/Mvc/Mvc.RazorPages/src/ApplicationModels/CompiledPageActionDescriptorBuilder.cs +++ b/src/Mvc/Mvc.RazorPages/src/ApplicationModels/CompiledPageActionDescriptorBuilder.cs @@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels ActionConstraints = actionDescriptor.ActionConstraints, AttributeRouteInfo = actionDescriptor.AttributeRouteInfo, BoundProperties = boundProperties, - EndpointMetadata = actionDescriptor.EndpointMetadata, + EndpointMetadata = CreateEndPointMetadata(applicationModel), FilterDescriptors = filters, HandlerMethods = handlerMethods, HandlerTypeInfo = applicationModel.HandlerType, @@ -61,6 +61,18 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels }; } + private static IList CreateEndPointMetadata(PageApplicationModel applicationModel) + { + var handlerMetatdata = applicationModel.HandlerTypeAttributes; + var endpointMetadata = applicationModel.EndpointMetadata; + + // It is criticial to get the order in which metadata appears in endpoint metadata correct. More significant metadata + // must appear later in the sequence. + // In this case, handlerMetadata is attributes on the Page \ PageModel, and endPointMetadata is configured via conventions. and + // We consider the latter to be more significant. + return Enumerable.Concat(handlerMetatdata, endpointMetadata).ToList(); + } + // Internal for unit testing internal static HandlerMethodDescriptor[] CreateHandlerMethods(PageApplicationModel applicationModel) { diff --git a/src/Mvc/Mvc.RazorPages/src/ApplicationModels/PageApplicationModel.cs b/src/Mvc/Mvc.RazorPages/src/ApplicationModels/PageApplicationModel.cs index 5077e26a27..4ec7f6cbf7 100644 --- a/src/Mvc/Mvc.RazorPages/src/ApplicationModels/PageApplicationModel.cs +++ b/src/Mvc/Mvc.RazorPages/src/ApplicationModels/PageApplicationModel.cs @@ -47,6 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels HandlerMethods = new List(); HandlerProperties = new List(); HandlerTypeAttributes = handlerAttributes; + EndpointMetadata = new List(ActionDescriptor.EndpointMetadata ?? Array.Empty()); } /// @@ -71,6 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels HandlerMethods = new List(other.HandlerMethods.Select(m => new PageHandlerModel(m))); HandlerProperties = new List(other.HandlerProperties.Select(p => new PagePropertyModel(p))); HandlerTypeAttributes = other.HandlerTypeAttributes; + EndpointMetadata = new List(other.EndpointMetadata); } /// @@ -154,5 +156,10 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels /// Gets the sequence of instances on . /// public IList HandlerProperties { get; } + + /// + /// Gets the endpoint metadata for this action. + /// + public IList EndpointMetadata { get; } } } \ No newline at end of file diff --git a/src/Mvc/Mvc.RazorPages/src/ApplicationModels/PageConventionCollection.cs b/src/Mvc/Mvc.RazorPages/src/ApplicationModels/PageConventionCollection.cs index 0a73a0da39..5f738762d9 100644 --- a/src/Mvc/Mvc.RazorPages/src/ApplicationModels/PageConventionCollection.cs +++ b/src/Mvc/Mvc.RazorPages/src/ApplicationModels/PageConventionCollection.cs @@ -5,15 +5,21 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.ApplicationModels { public class PageConventionCollection : Collection { + private readonly IServiceProvider _serviceProvider; + private MvcOptions _mvcOptions; + /// /// Initializes a new instance of the class that is empty. /// public PageConventionCollection() + : this((IServiceProvider)null) { } @@ -27,6 +33,21 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels { } + internal PageConventionCollection(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + internal MvcOptions MvcOptions + { + get + { + // Avoid eagerly getting to the MvcOptions from the options setup for RazorPagesOptions. + _mvcOptions ??= _serviceProvider.GetRequiredService>().Value; + return _mvcOptions; + } + } + /// /// Creates and adds an that invokes an action on the /// for the page with the specified name. diff --git a/src/Mvc/Mvc.RazorPages/src/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs b/src/Mvc/Mvc.RazorPages/src/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs index 6a4f62dff3..c152a9a54d 100644 --- a/src/Mvc/Mvc.RazorPages/src/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs +++ b/src/Mvc/Mvc.RazorPages/src/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs @@ -83,6 +83,9 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddEnumerable( ServiceDescriptor.Transient, RazorPagesRazorViewEngineOptionsSetup>()); + services.TryAddEnumerable( + ServiceDescriptor.Transient, RazorPagesOptionsSetup>()); + // Routing services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); diff --git a/src/Mvc/Mvc.RazorPages/src/DependencyInjection/PageConventionCollectionExtensions.cs b/src/Mvc/Mvc.RazorPages/src/DependencyInjection/PageConventionCollectionExtensions.cs index 577c8b5ed3..e2a4d40843 100644 --- a/src/Mvc/Mvc.RazorPages/src/DependencyInjection/PageConventionCollectionExtensions.cs +++ b/src/Mvc/Mvc.RazorPages/src/DependencyInjection/PageConventionCollectionExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.Mvc.Filters; @@ -60,7 +61,31 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Adds a to the page with the specified name. + /// Adds the specified to . + /// The added convention will apply to all handler properties and parameters on handler methods. + /// + /// The to configure. + /// The to apply. + /// The . + public static PageConventionCollection Add(this PageConventionCollection conventions, IParameterModelBaseConvention convention) + { + if (conventions == null) + { + throw new ArgumentNullException(nameof(conventions)); + } + + if (convention == null) + { + throw new ArgumentNullException(nameof(convention)); + } + + var adapter = new ParameterModelBaseConventionAdapter(convention); + conventions.Add(adapter); + return conventions; + } + + /// + /// Allows anonymous access to the page with the specified name. /// /// The to configure. /// The page name. @@ -77,13 +102,22 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName)); } - var anonymousFilter = new AllowAnonymousFilter(); - conventions.AddPageApplicationModelConvention(pageName, model => model.Filters.Add(anonymousFilter)); + conventions.AddPageApplicationModelConvention(pageName, model => + { + if (conventions.MvcOptions.EnableEndpointRouting) + { + model.EndpointMetadata.Add(new AllowAnonymousAttribute()); + } + else + { + model.Filters.Add(new AllowAnonymousFilter()); + } + }); return conventions; } /// - /// Adds a to the page with the specified name located in the specified area. + /// Allows anonymous access to the page with the specified name located in the specified area. /// /// The to configure. /// The area name. @@ -115,13 +149,22 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName)); } - var anonymousFilter = new AllowAnonymousFilter(); - conventions.AddAreaPageApplicationModelConvention(areaName, pageName, model => model.Filters.Add(anonymousFilter)); + conventions.AddAreaPageApplicationModelConvention(areaName, pageName, model => + { + if (conventions.MvcOptions.EnableEndpointRouting) + { + model.EndpointMetadata.Add(new AllowAnonymousAttribute()); + } + else + { + model.Filters.Add(new AllowAnonymousFilter()); + } + }); return conventions; } /// - /// Adds a to all pages under the specified folder. + /// Allows anonymous access to all pages under the specified folder. /// /// The to configure. /// The folder path. @@ -138,37 +181,22 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(folderPath)); } - var anonymousFilter = new AllowAnonymousFilter(); - conventions.AddFolderApplicationModelConvention(folderPath, model => model.Filters.Add(anonymousFilter)); + conventions.AddFolderApplicationModelConvention(folderPath, model => + { + if (conventions.MvcOptions.EnableEndpointRouting) + { + model.EndpointMetadata.Add(new AllowAnonymousAttribute()); + } + else + { + model.Filters.Add(new AllowAnonymousFilter()); + } + }); return conventions; } /// - /// Adds the specified to . - /// The added convention will apply to all handler properties and parameters on handler methods. - /// - /// The to configure. - /// The to apply. - /// The . - public static PageConventionCollection Add(this PageConventionCollection conventions, IParameterModelBaseConvention convention) - { - if (conventions == null) - { - throw new ArgumentNullException(nameof(conventions)); - } - - if (convention == null) - { - throw new ArgumentNullException(nameof(convention)); - } - - var adapter = new ParameterModelBaseConventionAdapter(convention); - conventions.Add(adapter); - return conventions; - } - - /// - /// Adds a to all pages under the specified area folder. + /// Allows anonymous access to all pages under the specified area folder. /// /// The to configure. /// The area name. @@ -200,13 +228,22 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(folderPath)); } - var anonymousFilter = new AllowAnonymousFilter(); - conventions.AddAreaFolderApplicationModelConvention(areaName, folderPath, model => model.Filters.Add(anonymousFilter)); + conventions.AddAreaFolderApplicationModelConvention(areaName, folderPath, model => + { + if (conventions.MvcOptions.EnableEndpointRouting) + { + model.EndpointMetadata.Add(new AllowAnonymousAttribute()); + } + else + { + model.Filters.Add(new AllowAnonymousFilter()); + } + }); return conventions; } /// - /// Adds a with the specified policy to the page with the specified name. + /// Requires authorization with the specified policy for the page with the specified name. /// /// The to configure. /// The page name. @@ -224,13 +261,22 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName)); } - var authorizeFilter = new AuthorizeFilter(policy); - conventions.AddPageApplicationModelConvention(pageName, model => model.Filters.Add(authorizeFilter)); + conventions.AddPageApplicationModelConvention(pageName, model => + { + if (conventions.MvcOptions.EnableEndpointRouting) + { + model.EndpointMetadata.Add(new AuthorizeAttribute(policy)); + } + else + { + model.Filters.Add(new AuthorizeFilter(policy)); + } + }); return conventions; } /// - /// Adds a to the page with the specified name. + /// Requires authorization for the specified page. /// /// The to configure. /// The page name. @@ -239,7 +285,7 @@ namespace Microsoft.Extensions.DependencyInjection AuthorizePage(conventions, pageName, policy: string.Empty); /// - /// Adds a with default policy to the page with the specified name. + /// Requires authorization for the specified area page. /// /// The to configure. /// The area name. @@ -255,7 +301,7 @@ namespace Microsoft.Extensions.DependencyInjection => AuthorizeAreaPage(conventions, areaName, pageName, policy: string.Empty); /// - /// Adds a with the specified policy to the page with the specified name. + /// Requires authorization for the specified area page with the specified policy. /// /// The to configure. /// The area name. @@ -289,13 +335,22 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName)); } - var authorizeFilter = new AuthorizeFilter(policy); - conventions.AddAreaPageApplicationModelConvention(areaName, pageName, model => model.Filters.Add(authorizeFilter)); + conventions.AddAreaPageApplicationModelConvention(areaName, pageName, model => + { + if (conventions.MvcOptions.EnableEndpointRouting) + { + model.EndpointMetadata.Add(new AuthorizeAttribute(policy)); + } + else + { + model.Filters.Add(new AuthorizeFilter(policy)); + } + }); return conventions; } /// - /// Adds a with the specified policy to all pages under the specified folder. + /// Requires authorization for all pages under the specified folder. /// /// The to configure. /// The folder path. @@ -313,13 +368,22 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(folderPath)); } - var authorizeFilter = new AuthorizeFilter(policy); - conventions.AddFolderApplicationModelConvention(folderPath, model => model.Filters.Add(authorizeFilter)); + conventions.AddFolderApplicationModelConvention(folderPath, model => + { + if (conventions.MvcOptions.EnableEndpointRouting) + { + model.EndpointMetadata.Add(new AuthorizeAttribute(policy)); + } + else + { + model.Filters.Add(new AuthorizeFilter(policy)); + } + }); return conventions; } /// - /// Adds a to all pages under the specified folder. + /// Requires authorization for all pages under the specified folder. /// /// The to configure. /// The folder path. @@ -328,7 +392,7 @@ namespace Microsoft.Extensions.DependencyInjection AuthorizeFolder(conventions, folderPath, policy: string.Empty); /// - /// Adds a with the default policy to all pages under the specified folder. + /// Requires authorization with the default policy for all pages under the specified folder. /// /// The to configure. /// The area name. @@ -344,7 +408,7 @@ namespace Microsoft.Extensions.DependencyInjection => AuthorizeAreaFolder(conventions, areaName, folderPath, policy: string.Empty); /// - /// Adds a with the specified policy to all pages under the specified folder. + /// Requires authorization with the specified policy for all pages under the specified folder. /// /// The to configure. /// The area name. @@ -378,8 +442,17 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(folderPath)); } - var authorizeFilter = new AuthorizeFilter(policy); - conventions.AddAreaFolderApplicationModelConvention(areaName, folderPath, model => model.Filters.Add(authorizeFilter)); + conventions.AddAreaFolderApplicationModelConvention(areaName, folderPath, model => + { + if (conventions.MvcOptions.EnableEndpointRouting) + { + model.EndpointMetadata.Add(new AuthorizeAttribute(policy)); + } + else + { + model.Filters.Add(new AuthorizeFilter(policy)); + } + }); return conventions; } diff --git a/src/Mvc/Mvc.RazorPages/src/DependencyInjection/RazorPagesOptionsSetup.cs b/src/Mvc/Mvc.RazorPages/src/DependencyInjection/RazorPagesOptionsSetup.cs new file mode 100644 index 0000000000..d3f90f8391 --- /dev/null +++ b/src/Mvc/Mvc.RazorPages/src/DependencyInjection/RazorPagesOptionsSetup.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + internal class RazorPagesOptionsSetup : IConfigureOptions + { + private readonly IServiceProvider _serviceProvider; + + public RazorPagesOptionsSetup(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + } + + public void Configure(RazorPagesOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.Conventions = new PageConventionCollection(_serviceProvider); + } + } +} diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DefaultPageLoader.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DefaultPageLoader.cs index dbf25a406d..6942a49531 100644 --- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DefaultPageLoader.cs +++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DefaultPageLoader.cs @@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure _viewCompilerProvider = viewCompilerProvider; _endpointFactory = endpointFactory; - _conventions = pageOptions.Value.Conventions; + _conventions = pageOptions.Value.Conventions ?? throw new ArgumentNullException(nameof(RazorPagesOptions.Conventions)); _globalFilters = mvcOptions.Value.Filters; } diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionInvokerProvider.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionInvokerProvider.cs index d163b5564b..7e8cd9be60 100644 --- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionInvokerProvider.cs +++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionInvokerProvider.cs @@ -6,7 +6,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; diff --git a/src/Mvc/Mvc.RazorPages/src/RazorPagesOptions.cs b/src/Mvc/Mvc.RazorPages/src/RazorPagesOptions.cs index 03b394839f..358990b203 100644 --- a/src/Mvc/Mvc.RazorPages/src/RazorPagesOptions.cs +++ b/src/Mvc/Mvc.RazorPages/src/RazorPagesOptions.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages /// Gets a collection of instances that are applied during /// route and page model construction. /// - public PageConventionCollection Conventions { get; } = new PageConventionCollection(); + public PageConventionCollection Conventions { get; internal set; } /// /// Application relative path used as the root of discovery for Razor Page files. diff --git a/src/Mvc/Mvc.RazorPages/test/ApplicationModels/AuthorizationPageApplicationModelProviderTest.cs b/src/Mvc/Mvc.RazorPages/test/ApplicationModels/AuthorizationPageApplicationModelProviderTest.cs index 8da0c88579..297c751bab 100644 --- a/src/Mvc/Mvc.RazorPages/test/ApplicationModels/AuthorizationPageApplicationModelProviderTest.cs +++ b/src/Mvc/Mvc.RazorPages/test/ApplicationModels/AuthorizationPageApplicationModelProviderTest.cs @@ -17,12 +17,14 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels { public class AuthorizationPageApplicationModelProviderTest { + private readonly IOptions OptionsWithoutEndpointRouting = Options.Create(new MvcOptions { EnableEndpointRouting = false }); + [Fact] public void OnProvidersExecuting_IgnoresAttributesOnHandlerMethods() { // Arrange var policyProvider = new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions())); - var authorizationProvider = new AuthorizationPageApplicationModelProvider(policyProvider); + var authorizationProvider = new AuthorizationPageApplicationModelProvider(policyProvider, OptionsWithoutEndpointRouting); var typeInfo = typeof(PageWithAuthorizeHandlers).GetTypeInfo(); var context = GetApplicationProviderContext(typeInfo); @@ -51,12 +53,31 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels } } + [Fact] + public void OnProvidersExecuting_DoesNothingWithEndpointRouting() + { + // Arrange + var policyProvider = new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions())); + var authorizationProvider = new AuthorizationPageApplicationModelProvider(policyProvider, Options.Create(new MvcOptions())); + var typeInfo = typeof(TestPage).GetTypeInfo(); + var context = GetApplicationProviderContext(typeInfo); + + // Act + authorizationProvider.OnProvidersExecuting(context); + + // Assert + Assert.Collection( + context.PageApplicationModel.Filters, + f => Assert.IsType(f), + f => Assert.IsType(f)); + } + [Fact] public void OnProvidersExecuting_AddsAuthorizeFilter_IfModelHasAuthorizationAttributes() { // Arrange var policyProvider = new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions())); - var authorizationProvider = new AuthorizationPageApplicationModelProvider(policyProvider); + var authorizationProvider = new AuthorizationPageApplicationModelProvider(policyProvider, OptionsWithoutEndpointRouting); var context = GetApplicationProviderContext(typeof(TestPage).GetTypeInfo()); // Act @@ -94,7 +115,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels options.Value.AddPolicy("Derived", policy => policy.RequireClaim("Derived")); var policyProvider = new DefaultAuthorizationPolicyProvider(options); - var authorizationProvider = new AuthorizationPageApplicationModelProvider(policyProvider); + var authorizationProvider = new AuthorizationPageApplicationModelProvider(policyProvider, OptionsWithoutEndpointRouting); var context = GetApplicationProviderContext(typeof(TestPageWithDerivedModel).GetTypeInfo()); @@ -138,7 +159,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels { // Arrange var policyProvider = new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions())); - var authorizationProvider = new AuthorizationPageApplicationModelProvider(policyProvider); + var authorizationProvider = new AuthorizationPageApplicationModelProvider(policyProvider, OptionsWithoutEndpointRouting); var context = GetApplicationProviderContext(typeof(PageWithAnonymousModel).GetTypeInfo()); // Act diff --git a/src/Mvc/Mvc.RazorPages/test/ApplicationModels/CompiledPageActionDescriptorBuilderTest.cs b/src/Mvc/Mvc.RazorPages/test/ApplicationModels/CompiledPageActionDescriptorBuilderTest.cs index a67ea21a9b..b97c35ab6c 100644 --- a/src/Mvc/Mvc.RazorPages/test/ApplicationModels/CompiledPageActionDescriptorBuilderTest.cs +++ b/src/Mvc/Mvc.RazorPages/test/ApplicationModels/CompiledPageActionDescriptorBuilderTest.cs @@ -41,7 +41,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels // Assert Assert.Same(actionDescriptor.ActionConstraints, actual.ActionConstraints); Assert.Same(actionDescriptor.AttributeRouteInfo, actual.AttributeRouteInfo); - Assert.Same(actionDescriptor.EndpointMetadata, actual.EndpointMetadata); Assert.Same(actionDescriptor.RelativePath, actual.RelativePath); Assert.Same(actionDescriptor.RouteValues, actual.RouteValues); Assert.Same(actionDescriptor.ViewEnginePath, actual.ViewEnginePath); @@ -394,6 +393,40 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels }); } + [Fact] + public void CreateDescriptor_CombinesEndpointMetadataFromHandlerTypeAttributesAndAttributesOnModel() + { + // Arrange + var metadata1 = "metadata1"; + var metadata2 = "metadata2"; + var metadata3 = "metadata3"; + var metadata4 = "metadata4"; + var metadata5 = "metadata5"; + var metadata6 = "metadata6"; + + var actionDescriptor = new PageActionDescriptor + { + ActionConstraints = new List(), + AttributeRouteInfo = new AttributeRouteInfo(), + EndpointMetadata = new List { metadata3, metadata4, }, + FilterDescriptors = new List(), + RelativePath = "/Foo", + RouteValues = new Dictionary(), + ViewEnginePath = "/Pages/Foo", + }; + var handlerTypeInfo = typeof(object).GetTypeInfo(); + var pageApplicationModel = new PageApplicationModel(actionDescriptor, handlerTypeInfo, new[] { metadata1, metadata2, }); + pageApplicationModel.EndpointMetadata.Add(metadata5); + pageApplicationModel.EndpointMetadata.Add(metadata6); + var globalFilters = new FilterCollection(); + + // Act + var actual = CompiledPageActionDescriptorBuilder.Build(pageApplicationModel, globalFilters); + + // Assert + Assert.Equal(new[] { metadata1, metadata2, metadata3, metadata4, metadata5, metadata6 }, actual.EndpointMetadata); + } + private class HandlerWithIgnoredProperties { [BindProperty] diff --git a/src/Mvc/Mvc.RazorPages/test/ApplicationModels/PageConventionCollectionTest.cs b/src/Mvc/Mvc.RazorPages/test/ApplicationModels/PageConventionCollectionTest.cs index c1bc7e1367..ddcb1be5e6 100644 --- a/src/Mvc/Mvc.RazorPages/test/ApplicationModels/PageConventionCollectionTest.cs +++ b/src/Mvc/Mvc.RazorPages/test/ApplicationModels/PageConventionCollectionTest.cs @@ -1,7 +1,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNetCore.Testing; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.ApplicationModels @@ -77,7 +79,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public void RemoveType_RemovesAllOfType() { // Arrange - var collection = new PageConventionCollection + var collection = new PageConventionCollection(Mock.Of()) { new FooPageConvention(), new BarPageConvention(), @@ -97,7 +99,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public void GenericRemoveType_RemovesAllOfType() { // Arrange - var collection = new PageConventionCollection + var collection = new PageConventionCollection(Mock.Of()) { new FooPageConvention(), new BarPageConvention(), diff --git a/src/Mvc/Mvc.RazorPages/test/DependencyInjection/MvcRazorPagesMvcBuilderExtensionsTest.cs b/src/Mvc/Mvc.RazorPages/test/DependencyInjection/MvcRazorPagesMvcBuilderExtensionsTest.cs index 2979301efe..854ca7ece4 100644 --- a/src/Mvc/Mvc.RazorPages/test/DependencyInjection/MvcRazorPagesMvcBuilderExtensionsTest.cs +++ b/src/Mvc/Mvc.RazorPages/test/DependencyInjection/MvcRazorPagesMvcBuilderExtensionsTest.cs @@ -16,7 +16,8 @@ namespace Microsoft.Extensions.DependencyInjection public void AddRazorPagesOptions_AddsConventions() { // Arrange - var services = new ServiceCollection().AddOptions(); + var services = new ServiceCollection().AddOptions() + .AddSingleton, RazorPagesOptionsSetup>(); var applicationModelConvention = Mock.Of(); var routeModelConvention = Mock.Of(); var builder = new MvcBuilder(services, new ApplicationPartManager()); diff --git a/src/Mvc/Mvc.RazorPages/test/DependencyInjection/PageConventionCollectionExtensionsTest.cs b/src/Mvc/Mvc.RazorPages/test/DependencyInjection/PageConventionCollectionExtensionsTest.cs index 063fad5022..356bce0b8f 100644 --- a/src/Mvc/Mvc.RazorPages/test/DependencyInjection/PageConventionCollectionExtensionsTest.cs +++ b/src/Mvc/Mvc.RazorPages/test/DependencyInjection/PageConventionCollectionExtensionsTest.cs @@ -4,10 +4,12 @@ using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Options; using Moq; using Xunit; @@ -20,7 +22,7 @@ namespace Microsoft.Extensions.DependencyInjection { // Arrange var filter = Mock.Of(); - var conventions = new PageConventionCollection(); + var conventions = GetConventions(); var models = new[] { CreateApplicationModel("/Pages/Index.cshtml", "/Index"), @@ -40,10 +42,45 @@ namespace Microsoft.Extensions.DependencyInjection } [Fact] - public void AuthorizePage_AddsAllowAnonymousFilterToSpecificPage() + public void AuthorizePage_AddsAllowAnonymousAttributeToSpecificPage() { // Arrange - var conventions = new PageConventionCollection(); + var conventions = GetConventions(); + var models = new[] + { + CreateApplicationModel("/Pages/Index.cshtml", "/Index"), + CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account"), + CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact"), + }; + + // Act + conventions.AuthorizeFolder("/Users"); + conventions.AllowAnonymousToPage("/Users/Contact"); + ApplyConventions(conventions, models); + + // Assert + Assert.Collection(models, + model => Assert.Empty(model.EndpointMetadata), + model => + { + Assert.Equal("/Users/Account", model.ViewEnginePath); + Assert.Empty(model.Filters); + Assert.IsType(Assert.Single(model.EndpointMetadata)); + }, + model => + { + Assert.Equal("/Users/Contact", model.ViewEnginePath); + Assert.Empty(model.Filters); + Assert.IsType(model.EndpointMetadata[0]); + Assert.IsType(model.EndpointMetadata[1]); + }); + } + + [Fact] + public void AuthorizePage_WithoutEndpointRouting_AddsAllowAnonymousFilterToSpecificPage() + { + // Arrange + var conventions = GetConventions(enableEndpointRouting: false); var models = new[] { CreateApplicationModel("/Pages/Index.cshtml", "/Index"), @@ -73,10 +110,36 @@ namespace Microsoft.Extensions.DependencyInjection } [Fact] - public void AllowAnonymousToAreaPage_AddsAllowAnonymousFilterToSpecificPage() + public void AllowAnonymousToAreaPage_AddsAllowAnonymousAttributeToSpecificPage() { // Arrange - var conventions = new PageConventionCollection(); + var conventions = GetConventions(); + var models = new[] + { + CreateApplicationModel("/Profile.cshtml", "/Profile"), + CreateApplicationModel("/Areas/Accounts/Pages/Profile.cshtml", "/Profile", "Accounts"), + }; + + // Act + conventions.AllowAnonymousToAreaPage("Accounts", "/Profile"); + ApplyConventions(conventions, models); + + // Assert + Assert.Collection(models, + model => Assert.Empty(model.Filters), + model => + { + Assert.Equal("/Areas/Accounts/Pages/Profile.cshtml", model.RelativePath); + Assert.Empty(model.Filters); + Assert.IsType(Assert.Single(model.EndpointMetadata)); + }); + } + + [Fact] + public void AllowAnonymousToAreaPage_WithoutEndpointRouting_AddsAllowAnonymousFilterToSpecificPage() + { + // Arrange + var conventions = GetConventions(enableEndpointRouting: false); var models = new[] { CreateApplicationModel("/Profile.cshtml", "/Profile"), @@ -100,10 +163,141 @@ namespace Microsoft.Extensions.DependencyInjection [Theory] [InlineData("/Users")] [InlineData("/Users/")] - public void AuthorizePage_AddsAllowAnonymousFilterToPagesUnderFolder(string folderName) + public void AuthorizePage_AddsAllowAnonymousAttributeToPageUnderFolder(string folderName) { // Arrange - var conventions = new PageConventionCollection(); + var conventions = GetConventions(); + var models = new[] + { + CreateApplicationModel("/Pages/Index.cshtml", "/Index"), + CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account"), + CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact"), + }; + + // Act + conventions.AuthorizeFolder("/"); + conventions.AllowAnonymousToFolder(folderName); + ApplyConventions(conventions, models); + + // Assert + Assert.Collection(models, + model => + { + Assert.Equal("/Index", model.ViewEnginePath); + Assert.Empty(model.Filters); + Assert.Collection(model.EndpointMetadata, + metadata => Assert.IsType(metadata)); + }, + model => + { + Assert.Equal("/Users/Account", model.ViewEnginePath); + Assert.Empty(model.Filters); + Assert.Collection(model.EndpointMetadata, + metadata => Assert.IsType(metadata), + metadata => Assert.IsType(metadata)); + }, + model => + { + Assert.Equal("/Users/Contact", model.ViewEnginePath); + Assert.Collection(model.EndpointMetadata, + metadata => Assert.IsType(metadata), + metadata => Assert.IsType(metadata)); + }); + } + + [Theory] + [InlineData("/Users")] + [InlineData("/Users/")] + public void AuthorizePage_WithoutEndpointRouting_AddsAllowAnonymousFilterToPageUnderFolder(string folderName) + { + // Arrange + var conventions = GetConventions(enableEndpointRouting: false); + var models = new[] + { + CreateApplicationModel("/Pages/Index.cshtml", "/Index"), + CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account"), + CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact"), + }; + + // Act + conventions.AuthorizeFolder("/"); + conventions.AllowAnonymousToFolder(folderName); + ApplyConventions(conventions, models); + + // Assert + Assert.Collection(models, + model => + { + Assert.Equal("/Index", model.ViewEnginePath); + Assert.IsType(Assert.Single(model.Filters)); + }, + model => + { + Assert.Equal("/Users/Account", model.ViewEnginePath); + Assert.IsType(model.Filters[0]); + Assert.IsType(model.Filters[1]); + }, + model => + { + Assert.Equal("/Users/Contact", model.ViewEnginePath); + Assert.IsType(model.Filters[0]); + Assert.IsType(model.Filters[1]); + }); + } + + [Theory] + [InlineData("/Users")] + [InlineData("/Users/")] + public void AuthorizePage_AddsAllowAnonymousAttributeToPagesUnderFolder(string folderName) + { + // Arrange + var conventions = GetConventions(); + var models = new[] + { + CreateApplicationModel("/Pages/Index.cshtml", "/Index"), + CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account"), + CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact"), + }; + + // Act + conventions.AuthorizeFolder("/"); + conventions.AllowAnonymousToFolder("/Users"); + ApplyConventions(conventions, models); + + // Assert + Assert.Collection(models, + model => + { + Assert.Equal("/Index", model.ViewEnginePath); + Assert.Empty(model.Filters); + Assert.Collection(model.EndpointMetadata, + metadata => Assert.IsType(metadata)); + }, + model => + { + Assert.Equal("/Users/Account", model.ViewEnginePath); + Assert.Empty(model.Filters); + Assert.Collection(model.EndpointMetadata, + metadata => Assert.IsType(metadata), + metadata => Assert.IsType(metadata)); + }, + model => + { + Assert.Equal("/Users/Contact", model.ViewEnginePath); + Assert.Empty(model.Filters); + Assert.Collection(model.EndpointMetadata, + metadata => Assert.IsType(metadata), + metadata => Assert.IsType(metadata)); + }); + } + + [Theory] + [InlineData("/Users")] + [InlineData("/Users/")] + public void AuthorizePage_WithoutEndpointRouting_AddsAllowAnonymousFilterToPagesUnderFolder(string folderName) + { + // Arrange + var conventions = GetConventions(enableEndpointRouting: false); var models = new[] { CreateApplicationModel("/Pages/Index.cshtml", "/Index"), @@ -138,10 +332,47 @@ namespace Microsoft.Extensions.DependencyInjection } [Fact] - public void AllowAnonymousToAreaFolder_AddsAllowAnonymousFilterToFolderInArea() + public void AllowAnonymousToAreaFolder_AddsEndpointMetadata() { // Arrange - var conventions = new PageConventionCollection(); + var conventions = GetConventions(); + var models = new[] + { + CreateApplicationModel("/Profile.cshtml", "/Profile"), + CreateApplicationModel("/Mange/Profile.cshtml", "/Manage/Profile"), + CreateApplicationModel("/Areas/Accounts/Pages/Manage/Profile.cshtml", "/Manage/Profile", "Accounts"), + CreateApplicationModel("/Areas/Accounts/Pages/Manage/2FA.cshtml", "/Manage/2FA", "Accounts"), + CreateApplicationModel("/Areas/Accounts/Pages/View/OrderHistory.cshtml", "/View/OrderHistory", "Accounts"), + }; + + // Act + conventions.AllowAnonymousToAreaFolder("Accounts", "/Manage"); + ApplyConventions(conventions, models); + + // Assert + Assert.Collection(models, + model => Assert.Empty(model.EndpointMetadata), + model => Assert.Empty(model.EndpointMetadata), + model => + { + Assert.Equal("/Areas/Accounts/Pages/Manage/Profile.cshtml", model.RelativePath); + Assert.Empty(model.Filters); + Assert.IsType(Assert.Single(model.EndpointMetadata)); + }, + model => + { + Assert.Equal("/Areas/Accounts/Pages/Manage/2FA.cshtml", model.RelativePath); + Assert.Empty(model.Filters); + Assert.IsType(Assert.Single(model.EndpointMetadata)); + }, + model => Assert.Empty(model.EndpointMetadata)); + } + + [Fact] + public void AllowAnonymousToAreaFolder_WithoutEndpointRouting_AddsAllowAnonymousFilterToFolderInArea() + { + // Arrange + var conventions = GetConventions(enableEndpointRouting: false); var models = new[] { CreateApplicationModel("/Profile.cshtml", "/Profile"), @@ -173,10 +404,39 @@ namespace Microsoft.Extensions.DependencyInjection } [Fact] - public void AuthorizePage_AddsAuthorizeFilterWithPolicyToSpecificPage() + public void AuthorizePage_AddsAuthorizeAttributeWithPolicyToSpecificPage() { // Arrange - var conventions = new PageConventionCollection(); + var conventions = GetConventions(); + var models = new[] + { + CreateApplicationModel("/Pages/Index.cshtml", "/Index"), + CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account"), + CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact"), + }; + + // Act + conventions.AuthorizePage("/Users/Account", "Manage-Accounts"); + ApplyConventions(conventions, models); + + // Assert + Assert.Collection(models, + model => Assert.Empty(model.Filters), + model => + { + Assert.Equal("/Users/Account", model.ViewEnginePath); + Assert.Empty(model.Filters); + var authorizeData = Assert.IsType(Assert.Single(model.EndpointMetadata)); + Assert.Equal("Manage-Accounts", authorizeData.Policy); + }, + model => Assert.Empty(model.Filters)); + } + + [Fact] + public void AuthorizePage_WithoutEndpointRouting_AddsAuthorizeFilterWithPolicyToSpecificPage() + { + // Arrange + var conventions = GetConventions(enableEndpointRouting: false); var models = new[] { CreateApplicationModel("/Pages/Index.cshtml", "/Index"), @@ -202,10 +462,37 @@ namespace Microsoft.Extensions.DependencyInjection } [Fact] - public void AuthorizeAreaPage_AddsAuthorizeFilterWithDefaultPolicyToAreaPage() + public void AuthorizeAreaPage_AddsAuthorizeAttributeWithDefaultPolicyToAreaPage() { // Arrange - var conventions = new PageConventionCollection(); + var conventions = GetConventions(); + var models = new[] + { + CreateApplicationModel("/Profile.cshtml", "/Profile"), + CreateApplicationModel("/Areas/Accounts/Pages/Profile.cshtml", "/Profile", "Accounts"), + }; + + // Act + conventions.AuthorizeAreaPage("Accounts", "/Profile"); + ApplyConventions(conventions, models); + + // Assert + Assert.Collection(models, + model => Assert.Empty(model.Filters), + model => + { + Assert.Equal("/Areas/Accounts/Pages/Profile.cshtml", model.RelativePath); + Assert.Empty(model.Filters); + var authorizeAttribute = Assert.IsType(Assert.Single(model.EndpointMetadata)); + Assert.Empty(authorizeAttribute.Policy); + }); + } + + [Fact] + public void AuthorizeAreaPage_WithoutEndpointRouting_AddsAuthorizeFilterWithDefaultPolicyToAreaPage() + { + // Arrange + var conventions = GetConventions(enableEndpointRouting: false); var models = new[] { CreateApplicationModel("/Profile.cshtml", "/Profile"), @@ -229,10 +516,37 @@ namespace Microsoft.Extensions.DependencyInjection } [Fact] - public void AuthorizeAreaPage_AddsAuthorizeFilterWithCustomPolicyToAreaPage() + public void AuthorizeAreaPage_AddsAuthorizeAttributeWithCustomPolicyToAreaPage() { // Arrange - var conventions = new PageConventionCollection(); + var conventions = GetConventions(); + var models = new[] + { + CreateApplicationModel("/Profile.cshtml", "/Profile"), + CreateApplicationModel("/Areas/Accounts/Pages/Profile.cshtml", "/Profile", "Accounts"), + }; + + // Act + conventions.AuthorizeAreaPage("Accounts", "/Profile", "custom"); + ApplyConventions(conventions, models); + + // Assert + Assert.Collection(models, + model => Assert.Empty(model.Filters), + model => + { + Assert.Equal("/Areas/Accounts/Pages/Profile.cshtml", model.RelativePath); + Assert.Empty(model.Filters); + var authorizeAttribute = Assert.IsType(Assert.Single(model.EndpointMetadata)); + Assert.Equal("custom", authorizeAttribute.Policy); + }); + } + + [Fact] + public void AuthorizeAreaPage_WithoutEndpointRouting_AddsAuthorizeFilterWithCustomPolicyToAreaPage() + { + // Arrange + var conventions = GetConventions(enableEndpointRouting: false); var models = new[] { CreateApplicationModel("/Profile.cshtml", "/Profile"), @@ -256,10 +570,39 @@ namespace Microsoft.Extensions.DependencyInjection } [Fact] - public void AuthorizePage_AddsAuthorizeFilterWithoutPolicyToSpecificPage() + public void AuthorizePage_AddsAuthorizeAttributeWithoutPolicyToSpecificPage() { // Arrange - var conventions = new PageConventionCollection(); + var conventions = GetConventions(); + var models = new[] + { + CreateApplicationModel("/Pages/Index.cshtml", "/Index"), + CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account"), + CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact"), + }; + + // Act + conventions.AuthorizePage("/Users/Account"); + ApplyConventions(conventions, models); + + // Assert + Assert.Collection(models, + model => Assert.Empty(model.Filters), + model => + { + Assert.Equal("/Users/Account", model.ViewEnginePath); + Assert.Empty(model.Filters); + var authorizeData = Assert.IsType(Assert.Single(model.EndpointMetadata)); + Assert.Equal(string.Empty, authorizeData.Policy); + }, + model => Assert.Empty(model.Filters)); + } + + [Fact] + public void AuthorizePage_WithoutEndpointRouting_AddsAuthorizeFilterWithoutPolicyToSpecificPage() + { + // Arrange + var conventions = GetConventions(enableEndpointRouting: false); var models = new[] { CreateApplicationModel("/Pages/Index.cshtml", "/Index"), @@ -287,10 +630,10 @@ namespace Microsoft.Extensions.DependencyInjection [Theory] [InlineData("/Users")] [InlineData("/Users/")] - public void AuthorizePage_AddsAuthorizeFilterWithPolicyToPagesUnderFolder(string folderName) + public void AuthorizePage_WithoutEndpointRouting_AddsAuthorizeFilterWithPolicyToPagesUnderFolder(string folderName) { // Arrange - var conventions = new PageConventionCollection(); + var conventions = GetConventions(); var models = new[] { CreateApplicationModel("/Pages/Index.cshtml", "/Index"), @@ -308,15 +651,15 @@ namespace Microsoft.Extensions.DependencyInjection model => { Assert.Equal("/Users/Account", model.ViewEnginePath); - var authorizeFilter = Assert.IsType(Assert.Single(model.Filters)); - var authorizeData = Assert.IsType(Assert.Single(authorizeFilter.AuthorizeData)); + Assert.Empty(model.Filters); + var authorizeData = Assert.IsType(Assert.Single(model.EndpointMetadata)); Assert.Equal("Manage-Accounts", authorizeData.Policy); }, model => { Assert.Equal("/Users/Contact", model.ViewEnginePath); - var authorizeFilter = Assert.IsType(Assert.Single(model.Filters)); - var authorizeData = Assert.IsType(Assert.Single(authorizeFilter.AuthorizeData)); + Assert.Empty(model.Filters); + var authorizeData = Assert.IsType(Assert.Single(model.EndpointMetadata)); Assert.Equal("Manage-Accounts", authorizeData.Policy); }); } @@ -324,10 +667,10 @@ namespace Microsoft.Extensions.DependencyInjection [Theory] [InlineData("/Users")] [InlineData("/Users/")] - public void AuthorizePage_AddsAuthorizeFilterWithoutPolicyToPagesUnderFolder(string folderName) + public void AuthorizePage_WithoutEndpointRouting_AddsAuthorizeFilterWithoutPolicyToPagesUnderFolder(string folderName) { // Arrange - var conventions = new PageConventionCollection(); + var conventions = GetConventions(enableEndpointRouting: false); var models = new[] { CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), @@ -359,10 +702,49 @@ namespace Microsoft.Extensions.DependencyInjection } [Fact] - public void AuthorizeAreaFolder_AddsAuthorizeFilterWithDefaultPolicyToAreaPagesInFolder() + public void AuthorizeAreaFolder_AddsAuthorizeAttributeWithDefaultPolicyToAreaPagesInFolder() { // Arrange - var conventions = new PageConventionCollection(); + var conventions = GetConventions(); + var models = new[] + { + CreateApplicationModel("/Profile.cshtml", "/Profile"), + CreateApplicationModel("/Mange/Profile.cshtml", "/Manage/Profile"), + CreateApplicationModel("/Areas/Accounts/Pages/Manage/Profile.cshtml", "/Manage/Profile", "Accounts"), + CreateApplicationModel("/Areas/Accounts/Pages/Manage/2FA.cshtml", "/Manage/2FA", "Accounts"), + CreateApplicationModel("/Areas/Accounts/Pages/View/OrderHistory.cshtml", "/View/OrderHistory", "Accounts"), + }; + + // Act + conventions.AuthorizeAreaFolder("Accounts", "/Manage"); + ApplyConventions(conventions, models); + + // Assert + Assert.Collection(models, + model => Assert.Empty(model.Filters), + model => Assert.Empty(model.Filters), + model => + { + Assert.Equal("/Areas/Accounts/Pages/Manage/Profile.cshtml", model.RelativePath); + Assert.Empty(model.Filters); + var authorizeData = Assert.IsType(Assert.Single(model.EndpointMetadata)); + Assert.Empty(authorizeData.Policy); + }, + model => + { + Assert.Equal("/Areas/Accounts/Pages/Manage/2FA.cshtml", model.RelativePath); + Assert.Empty(model.Filters); + var authorizeData = Assert.IsType(Assert.Single(model.EndpointMetadata)); + Assert.Empty(authorizeData.Policy); + }, + model => Assert.Empty(model.Filters)); + } + + [Fact] + public void AuthorizeAreaFolder_WithoutEndpointRouting_AddsAuthorizeFilterWithDefaultPolicyToAreaPagesInFolder() + { + // Arrange + var conventions = GetConventions(enableEndpointRouting: false); var models = new[] { CreateApplicationModel("/Profile.cshtml", "/Profile"), @@ -398,10 +780,49 @@ namespace Microsoft.Extensions.DependencyInjection } [Fact] - public void AuthorizeAreaFolder_AddsAuthorizeFilterWithCustomPolicyToAreaPagesInFolder() + public void AuthorizeAreaFolder_AddsAuthorizeAttributeWithCustomPolicyToAreaPagesInFolder() { // Arrange - var conventions = new PageConventionCollection(); + var conventions = GetConventions(); + var models = new[] + { + CreateApplicationModel("/Profile.cshtml", "/Profile"), + CreateApplicationModel("/Mange/Profile.cshtml", "/Manage/Profile"), + CreateApplicationModel("/Areas/Accounts/Pages/Manage/Profile.cshtml", "/Manage/Profile", "Accounts"), + CreateApplicationModel("/Areas/Accounts/Pages/Manage/2FA.cshtml", "/Manage/2FA", "Accounts"), + CreateApplicationModel("/Areas/Accounts/Pages/View/OrderHistory.cshtml", "/View/OrderHistory", "Accounts"), + }; + + // Act + conventions.AuthorizeAreaFolder("Accounts", "/Manage", "custom"); + ApplyConventions(conventions, models); + + // Assert + Assert.Collection(models, + model => Assert.Empty(model.EndpointMetadata), + model => Assert.Empty(model.EndpointMetadata), + model => + { + Assert.Equal("/Areas/Accounts/Pages/Manage/Profile.cshtml", model.RelativePath); + Assert.Empty(model.Filters); + var authorizeData = Assert.IsType(Assert.Single(model.EndpointMetadata)); + Assert.Equal("custom", authorizeData.Policy); + }, + model => + { + Assert.Equal("/Areas/Accounts/Pages/Manage/2FA.cshtml", model.RelativePath); + Assert.Empty(model.Filters); + var authorizeData = Assert.IsType(Assert.Single(model.EndpointMetadata)); + Assert.Equal("custom", authorizeData.Policy); + }, + model => Assert.Empty(model.Filters)); + } + + [Fact] + public void AuthorizeAreaFolder_WithoutEndpointRouting_AddsAuthorizeFilterWithCustomPolicyToAreaPagesInFolder() + { + // Arrange + var conventions = GetConventions(enableEndpointRouting: false); var models = new[] { CreateApplicationModel("/Profile.cshtml", "/Profile"), @@ -440,7 +861,7 @@ namespace Microsoft.Extensions.DependencyInjection public void AddPageRoute_AddsRouteToSelector() { // Arrange - var conventions = new PageConventionCollection(); + var conventions = GetConventions(); var models = new[] { new PageRouteModel("/Pages/Index.cshtml", "/Index") @@ -502,7 +923,7 @@ namespace Microsoft.Extensions.DependencyInjection public void AddAreaPageRoute_AddsRouteToSelector() { // Arrange - var conventions = new PageConventionCollection(); + var conventions = GetConventions(); var models = new[] { new PageRouteModel("/Pages/Profile.cshtml", "/Profile") @@ -544,7 +965,7 @@ namespace Microsoft.Extensions.DependencyInjection selector => { Assert.Equal("Accounts/Profile", selector.AttributeRouteModel.Template); - Assert.True (selector.AttributeRouteModel.SuppressLinkGeneration); + Assert.True(selector.AttributeRouteModel.SuppressLinkGeneration); }, selector => { @@ -554,6 +975,15 @@ namespace Microsoft.Extensions.DependencyInjection }); } + private PageConventionCollection GetConventions(bool enableEndpointRouting = true) + { + var options = new MvcOptions { EnableEndpointRouting = enableEndpointRouting }; + var serviceProvider = new ServiceCollection() + .AddSingleton>(Options.Options.Create(options)) + .BuildServiceProvider(); + return new PageConventionCollection(serviceProvider); + } + private static SelectorModel CreateSelectorModel(string template, bool suppressLinkGeneration = false) { return new SelectorModel diff --git a/src/Mvc/Mvc.RazorPages/test/Infrastructure/DefaultPageLoaderTest.cs b/src/Mvc/Mvc.RazorPages/test/Infrastructure/DefaultPageLoaderTest.cs index 2177cf64d7..8537534501 100644 --- a/src/Mvc/Mvc.RazorPages/test/Infrastructure/DefaultPageLoaderTest.cs +++ b/src/Mvc/Mvc.RazorPages/test/Infrastructure/DefaultPageLoaderTest.cs @@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { public class DefaultPageLoaderTest { + private readonly IOptions RazorPagesOptions = Options.Create(new RazorPagesOptions { Conventions = new PageConventionCollection(Mock.Of()) }); private readonly IActionDescriptorCollectionProvider ActionDescriptorCollectionProvider; public DefaultPageLoaderTest() @@ -36,7 +37,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure var compilerProvider = GetCompilerProvider(); - var razorPagesOptions = Options.Create(new RazorPagesOptions()); var mvcOptions = Options.Create(new MvcOptions()); var endpointFactory = new ActionEndpointFactory(Mock.Of()); @@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure providers, compilerProvider, endpointFactory, - razorPagesOptions, + RazorPagesOptions, mvcOptions); // Act @@ -121,7 +121,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure var compilerProvider = GetCompilerProvider(); - var razorPagesOptions = Options.Create(new RazorPagesOptions()); var mvcOptions = Options.Create(new MvcOptions()); var endpointFactory = new ActionEndpointFactory(transformer.Object); @@ -147,7 +146,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure providers, compilerProvider, endpointFactory, - razorPagesOptions, + RazorPagesOptions, mvcOptions); // Act @@ -163,7 +162,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure // Arrange var descriptor = new PageActionDescriptor(); var compilerProvider = GetCompilerProvider(); - var razorPagesOptions = Options.Create(new RazorPagesOptions()); var mvcOptions = Options.Create(new MvcOptions()); var endpointFactory = new ActionEndpointFactory(Mock.Of()); @@ -212,7 +210,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure providers, compilerProvider, endpointFactory, - razorPagesOptions, + RazorPagesOptions, mvcOptions); // Act @@ -242,7 +240,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure var compilerProvider = GetCompilerProvider(); - var razorPagesOptions = Options.Create(new RazorPagesOptions()); var mvcOptions = Options.Create(new MvcOptions()); var endpointFactory = new ActionEndpointFactory(transformer.Object); @@ -268,7 +265,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure providers, compilerProvider, endpointFactory, - razorPagesOptions, + RazorPagesOptions, mvcOptions); // Act @@ -298,7 +295,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure var compilerProvider = GetCompilerProvider(); - var razorPagesOptions = Options.Create(new RazorPagesOptions()); var mvcOptions = Options.Create(new MvcOptions()); var endpointFactory = new ActionEndpointFactory(transformer.Object); @@ -333,7 +329,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure providers, compilerProvider, endpointFactory, - razorPagesOptions, + RazorPagesOptions, mvcOptions); // Act @@ -358,7 +354,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure Assert.Same(model, m); }) .Verifiable(); - var conventionCollection = new PageConventionCollection + var conventionCollection = new PageConventionCollection(Mock.Of()) { convention.Object, }; @@ -392,7 +388,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure Assert.Same(model, m); }) .Verifiable(); - var conventionCollection = new PageConventionCollection + var conventionCollection = new PageConventionCollection(Mock.Of()) { globalConvention.Object, }; @@ -424,7 +420,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure Assert.Same(handlerModel, m); }) .Verifiable(); - var conventionCollection = new PageConventionCollection(); + var conventionCollection = new PageConventionCollection(Mock.Of()); // Act DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); @@ -451,7 +447,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure Assert.Same(handlerModel, m); }) .Verifiable(); - var conventionCollection = new PageConventionCollection { handlerModelConvention.Object }; + var conventionCollection = new PageConventionCollection(Mock.Of()) { handlerModelConvention.Object }; // Act DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); @@ -482,7 +478,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure m.Page.HandlerMethods.Remove(m); }) .Verifiable(); - var conventionCollection = new PageConventionCollection(); + var conventionCollection = new PageConventionCollection(Mock.Of()); // Act DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); @@ -513,7 +509,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure Assert.Same(parameterModel, m); }) .Verifiable(); - var conventionCollection = new PageConventionCollection(); + var conventionCollection = new PageConventionCollection(Mock.Of()); // Act DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); @@ -544,7 +540,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure Assert.Same(parameterModel, m); }) .Verifiable(); - var conventionCollection = new PageConventionCollection { parameterModelConvention.Object }; + var conventionCollection = new PageConventionCollection(Mock.Of()) { parameterModelConvention.Object }; // Act DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); @@ -579,7 +575,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure model.Handler.Parameters.Remove(model); }) .Verifiable(); - var conventionCollection = new PageConventionCollection(); + var conventionCollection = new PageConventionCollection(Mock.Of()); // Act DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); @@ -613,7 +609,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure Assert.Same(propertyModel, m); }) .Verifiable(); - var conventionCollection = new PageConventionCollection(); + var conventionCollection = new PageConventionCollection(Mock.Of()); // Act DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); @@ -644,7 +640,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure Assert.Same(propertyModel, m); }) .Verifiable(); - var conventionCollection = new PageConventionCollection { propertyModelConvention.Object }; + var conventionCollection = new PageConventionCollection(Mock.Of()) { propertyModelConvention.Object }; // Act DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); @@ -676,7 +672,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure model.Page.HandlerProperties.Remove(model); }) .Verifiable(); - var conventionCollection = new PageConventionCollection(); + var conventionCollection = new PageConventionCollection(Mock.Of()); // Act DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); diff --git a/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionDescriptorProviderTest.cs b/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionDescriptorProviderTest.cs index a42d2ec032..10d968a991 100644 --- a/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionDescriptorProviderTest.cs +++ b/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionDescriptorProviderTest.cs @@ -371,7 +371,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure private static IOptions GetRazorPagesOptions() { - return Options.Create(new RazorPagesOptions()); + return Options.Create(new RazorPagesOptions { Conventions = new PageConventionCollection(Mock.Of()) }); } private class TestPageRouteModelProvider : IPageRouteModelProvider diff --git a/src/Mvc/Mvc/test/Routing/ActionConstraintMatcherPolicyTest.cs b/src/Mvc/Mvc/test/Routing/ActionConstraintMatcherPolicyTest.cs index df7a7a5ab9..1da67ef346 100644 --- a/src/Mvc/Mvc/test/Routing/ActionConstraintMatcherPolicyTest.cs +++ b/src/Mvc/Mvc/test/Routing/ActionConstraintMatcherPolicyTest.cs @@ -360,13 +360,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing new ConsumesAttribute("text/json"), }, }, - new ActionDescriptor() - { - ActionConstraints = new List() - { - new CorsHttpMethodActionConstraint(new HttpMethodActionConstraint(new[]{ "GET", })), - }, - }, }; var endpoints = actions.Select(CreateEndpoint).ToArray(); diff --git a/src/Mvc/test/Mvc.FunctionalTests/AuthMiddlewareAndFilterTest.cs b/src/Mvc/test/Mvc.FunctionalTests/AuthMiddlewareAndFilterTest.cs new file mode 100644 index 0000000000..15b82680a3 --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/AuthMiddlewareAndFilterTest.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class AuthMiddlewareAndFilterTest : AuthMiddlewareAndFilterTestBase + { + public AuthMiddlewareAndFilterTest(MvcTestFixture fixture) + : base(fixture) + { + } + } +} diff --git a/src/Mvc/test/Mvc.FunctionalTests/AuthMiddlewareAndFilterTestBase.cs b/src/Mvc/test/Mvc.FunctionalTests/AuthMiddlewareAndFilterTestBase.cs new file mode 100644 index 0000000000..7f4c205109 --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/AuthMiddlewareAndFilterTestBase.cs @@ -0,0 +1,277 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public abstract class AuthMiddlewareAndFilterTestBase : IClassFixture> where TStartup : class + { + protected AuthMiddlewareAndFilterTestBase(MvcTestFixture fixture) + { + var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); + Client = factory.CreateDefaultClient(); + } + + private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => builder.UseStartup(); + + public HttpClient Client { get; } + + [Fact] + public async Task AllowAnonymousOnActionsWork() + { + // Arrange & Act + var response = await Client.GetAsync("AuthorizedActions/ActionWithoutAllowAnonymous"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + } + + [Fact] + public async Task GlobalAuthFilter_AppliesToActionsWithoutAnyAuthAttributes() + { + var action = "AuthorizedActions/ActionWithoutAuthAttribute"; + var response = await Client.GetAsync(action); + + await AssertAuthorizeResponse(response); + + // We should be able to login with ClaimA alone + var authCookie = await GetAuthCookieAsync("LoginClaimA"); + + var request = new HttpRequestMessage(HttpMethod.Get, action); + request.Headers.Add("Cookie", authCookie); + + response = await Client.SendAsync(request); + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + } + + [Fact] + public async Task GlobalAuthFilter_CombinesWithAuthAttributeSpecifiedOnAction() + { + var action = "AuthorizedActions/ActionWithAuthAttribute"; + var response = await Client.GetAsync(action); + + await AssertAuthorizeResponse(response); + + // LoginClaimA should be enough for the global auth filter, but not for the auth attribute on the action. + var authCookie = await GetAuthCookieAsync("LoginClaimA"); + + var request = new HttpRequestMessage(HttpMethod.Get, action); + request.Headers.Add("Cookie", authCookie); + + response = await Client.SendAsync(request); + await AssertAuthorizeResponse(response); + + authCookie = await GetAuthCookieAsync("LoginClaimAB"); + request = new HttpRequestMessage(HttpMethod.Get, action); + request.Headers.Add("Cookie", authCookie); + + response = await Client.SendAsync(request); + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + } + + [Fact] + public async Task AllowAnonymousOnPageConfiguredViaConventionWorks() + { + // Arrange & Act + var response = await Client.GetAsync("AllowAnonymousPageViaConvention"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + } + + [Fact] + public async Task AllowAnonymousOnPageConfiguredViaModelWorks() + { + // Arrange & Act + var response = await Client.GetAsync("AllowAnonymousPageViaModel"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + } + + [Fact] + public async Task GlobalAuthFilterAppliedToPageWorks() + { + // Arrange & Act + var response = await Client.GetAsync("PagesHome"); + + // Assert + await AssertAuthorizeResponse(response); + + // We should be able to login with ClaimA alone + var authCookie = await GetAuthCookieAsync("LoginClaimA"); + + var request = new HttpRequestMessage(HttpMethod.Get, "PagesHome"); + request.Headers.Add("Cookie", authCookie); + + response = await Client.SendAsync(request); + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + } + + [Fact] + public async Task CanLoginWithBearer() + { + var request = new HttpRequestMessage(HttpMethod.Get, "/Authorized/Api"); + var response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + + var token = await GetBearerTokenAsync(); + + request = new HttpRequestMessage(HttpMethod.Get, "/Authorized/Api"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task CanLoginWithBearerAfterAnonymous() + { + var request = new HttpRequestMessage(HttpMethod.Get, "/Authorized/AllowAnonymous"); + var response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + request = new HttpRequestMessage(HttpMethod.Get, "/Authorized/Api"); + response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + + var token = await GetBearerTokenAsync(); + + request = new HttpRequestMessage(HttpMethod.Get, "/Authorized/Api"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task CanLoginWithCookie() + { + var request = new HttpRequestMessage(HttpMethod.Get, "/Authorized/Cookie"); + var response = await Client.SendAsync(request); + await AssertAuthorizeResponse(response); + + var cookie = await GetAuthCookieAsync("LoginDefaultScheme"); + + request = new HttpRequestMessage(HttpMethod.Get, "/Authorized/Cookie"); + request.Headers.Add("Cookie", cookie); + response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task CanLoginWithCookieAfterAnonymous() + { + var request = new HttpRequestMessage(HttpMethod.Get, "/Authorized/AllowAnonymous"); + var response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + request = new HttpRequestMessage(HttpMethod.Get, "/Authorized/Cookie"); + response = await Client.SendAsync(request); + await AssertAuthorizeResponse(response); + + var cookie = await GetAuthCookieAsync("LoginDefaultScheme"); + + request = new HttpRequestMessage(HttpMethod.Get, "/Authorized/Cookie"); + request.Headers.Add("Cookie", cookie); + response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task CanLoginWithBearerAfterCookie() + { + var request = new HttpRequestMessage(HttpMethod.Get, "/Authorized/Cookie"); + var response = await Client.SendAsync(request); + await AssertAuthorizeResponse(response); + + var cookie = await GetAuthCookieAsync("LoginDefaultScheme"); + + request = new HttpRequestMessage(HttpMethod.Get, "/Authorized/Cookie"); + request.Headers.Add("Cookie", cookie); + response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + request = new HttpRequestMessage(HttpMethod.Get, "/Authorized/Api"); + request.Headers.Add("Cookie", cookie); + response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + + var token = await GetBearerTokenAsync(); + + request = new HttpRequestMessage(HttpMethod.Get, "/Authorized/Api"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + request.Headers.Add("Cookie", cookie); + response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public Task GlobalAuthFilter_CombinesWithAuthAttributeOnPageModel() + { + // Arrange + var page = "AuthorizePageViaModel"; + + return LoginAB(page); + } + + [Fact] + public Task GlobalAuthFilter_CombinesWithAuthAttributeSpecifiedViaConvention() + { + // Arrange + var page = "AuthorizePageViaConvention"; + + return LoginAB(page); + } + + private async Task LoginAB(string url) + { + var response = await Client.GetAsync(url); + + // Assert + await AssertAuthorizeResponse(response); + + // ClaimA should be insufficient + var authCookie = await GetAuthCookieAsync("LoginClaimA"); + + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("Cookie", authCookie); + + response = await Client.SendAsync(request); + await AssertAuthorizeResponse(response); + + authCookie = await GetAuthCookieAsync("LoginClaimAB"); + request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("Cookie", authCookie); + + response = await Client.SendAsync(request); + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + } + + private async Task AssertAuthorizeResponse(HttpResponseMessage response) + { + await response.AssertStatusCodeAsync(HttpStatusCode.Redirect); + Assert.Equal("/Account/Login", response.Headers.Location.LocalPath); + } + + private async Task GetAuthCookieAsync(string action) + { + var response = await Client.PostAsync($"Login/{action}", null); + + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + Assert.True(response.Headers.Contains("Set-Cookie")); + return response.Headers.GetValues("Set-Cookie").FirstOrDefault(); + } + + private async Task GetBearerTokenAsync() + { + var response = await Client.GetAsync("/Login/LoginBearerClaimA"); + return await response.Content.ReadAsStringAsync(); + } + } +} diff --git a/src/Mvc/test/Mvc.FunctionalTests/AuthMiddlewareAndFilterWithoutEndpointRoutingTest.cs b/src/Mvc/test/Mvc.FunctionalTests/AuthMiddlewareAndFilterWithoutEndpointRoutingTest.cs new file mode 100644 index 0000000000..f378e328eb --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/AuthMiddlewareAndFilterWithoutEndpointRoutingTest.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class AuthMiddlewareAndFilterWithoutEndpointRoutingTest : AuthMiddlewareAndFilterTestBase + { + public AuthMiddlewareAndFilterWithoutEndpointRoutingTest(MvcTestFixture fixture) + : base(fixture) + { + } + } +} diff --git a/src/Mvc/test/Mvc.FunctionalTests/CorsTestsBase.cs b/src/Mvc/test/Mvc.FunctionalTests/CorsTestsBase.cs index 91ccaf578d..637a427b11 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/CorsTestsBase.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/CorsTestsBase.cs @@ -119,7 +119,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests // Assert // MVC applied the policy and since that did not pass, there were no access control headers. - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); Assert.Collection( response.Headers.OrderBy(h => h.Key), h => @@ -187,7 +187,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests var response = await Client.SendAsync(request); // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); var responseHeaders = response.Headers; Assert.Equal( new[] { "http://example.com" }, @@ -273,7 +273,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests // Assert // Since there are no response headers, the client should step in to block the content. - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); Assert.Empty(response.Headers); // Nothing gets executed for a pre-flight request. @@ -297,7 +297,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests var response = await Client.SendAsync(request); // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); var responseHeaders = response.Headers; Assert.Equal( new[] { "*" }, @@ -329,7 +329,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests var response = await Client.SendAsync(request); // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); var responseHeaders = response.Headers; Assert.Equal( new[] { "http://example.com" }, @@ -351,10 +351,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests [Fact] public async Task DisableCorsFilter_RunsBeforeOtherAuthorizationFilters() { - // Controller has an authorization filter and Cors filter and the action has a DisableCors filter - // In this scenario, the CorsFilter should be executed before any other authorization filters - // i.e irrespective of where the Cors filter is applied(controller or action), Cors filters must - // always be executed before any other type of authorization filters. + // Controller enables authorization and Cors, the action has a DisableCorsAttribute. + // We expect the CorsMiddleware to execute and no-op // Arrange var request = new HttpRequestMessage( @@ -370,7 +368,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests var response = await Client.SendAsync(request); // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); Assert.Empty(response.Headers); // Nothing gets executed for a pre-flight request. @@ -393,7 +391,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests var response = await Client.SendAsync(request); // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); Assert.Empty(response.Headers); // Nothing gets executed for a pre-flight request. diff --git a/src/Mvc/test/Mvc.FunctionalTests/GlobalAuthorizationFilterEndpointRoutingTest.cs b/src/Mvc/test/Mvc.FunctionalTests/GlobalAuthorizationFilterEndpointRoutingTest.cs index 49d8818478..fbc6b7a398 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/GlobalAuthorizationFilterEndpointRoutingTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/GlobalAuthorizationFilterEndpointRoutingTest.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.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Xunit; @@ -21,11 +20,5 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests builder.UseStartup(); public WebApplicationFactory Factory { get; } - - [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8387")] - public override Task DeniesAnonymousUsers_ByDefault() - { - return Task.CompletedTask; - } } } diff --git a/src/Mvc/test/Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs b/src/Mvc/test/Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs index 184e83cd31..39b2149cf6 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs @@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests return response; } - string responseContent = null; + string responseContent = string.Join(Environment.NewLine, response.Headers); try { responseContent = await response.Content.ReadAsStringAsync(); diff --git a/src/Mvc/test/Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs b/src/Mvc/test/Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs index 08b735034f..4a11189cb2 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs @@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("Hello from /Admin/RouteTemplate my-user-id 4", content.Trim()); } - [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8387")] + [Fact] public async Task AuthConvention_IsAppliedOnBasePathRelativePaths_ForFiles() { // Act @@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("/Login?ReturnUrl=%2FConventions%2FAuth", response.Headers.Location.PathAndQuery); } - [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8387")] + [Fact] public async Task AuthConvention_IsAppliedOnBasePathRelativePaths_For_Folders() { // Act @@ -372,7 +372,7 @@ Hello from /Pages/Shared/"; Assert.Equal(expected, response.Trim(), ignoreLineEndingDifferences: true); } - [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8387")] + [Fact] public async Task AuthorizeFolderConvention_CanBeAppliedToAreaPages() { // Act diff --git a/src/Mvc/test/Mvc.FunctionalTests/RazorPagesWithEndpointRoutingTest.cs b/src/Mvc/test/Mvc.FunctionalTests/RazorPagesWithEndpointRoutingTest.cs index da5d688cf7..183372722d 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/RazorPagesWithEndpointRoutingTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/RazorPagesWithEndpointRoutingTest.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public HttpClient Client { get; } - [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8387")] + [Fact] public async Task Authorize_AppliedUsingConvention_Works() { // Act @@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("Hello from Anonymous", content.Trim()); } - [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8387")] + [Fact] public async Task Authorize_AppliedUsingAttributeOnModel_Works() { // Act diff --git a/src/Mvc/test/WebSites/CorsWebSite/Startup.cs b/src/Mvc/test/WebSites/CorsWebSite/Startup.cs index dbfc16d1cd..5aa8fc7f63 100644 --- a/src/Mvc/test/WebSites/CorsWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/CorsWebSite/Startup.cs @@ -74,6 +74,7 @@ namespace CorsWebSite public virtual void Configure(IApplicationBuilder app) { app.UseRouting(); + app.UseCors(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); diff --git a/src/Mvc/test/WebSites/SecurityWebSite/BearerAuth.cs b/src/Mvc/test/WebSites/SecurityWebSite/BearerAuth.cs new file mode 100644 index 0000000000..b0fdc2cb8a --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/BearerAuth.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// 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.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace SecurityWebSite +{ + public static class BearerAuth + { + static BearerAuth() + { + Key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128))); + Credentials = new SigningCredentials(Key, SecurityAlgorithms.HmacSha256); + } + + public static readonly SymmetricSecurityKey Key; + public static readonly SigningCredentials Credentials; + public static readonly string Issuer = "issuer.contoso.com"; + public static readonly string Audience = "audience.contoso.com"; + + public static TokenValidationParameters CreateTokenValidationParameters() + { + return new TokenValidationParameters() + { + ValidIssuer = Issuer, + ValidAudience = Audience, + IssuerSigningKey = Key, + }; + } + + public static string GetTokenText(IEnumerable claims) + { + var token = new JwtSecurityToken(Issuer, Audience, claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: Credentials); + return new JwtSecurityTokenHandler().WriteToken(token); + } + } +} diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Controllers/AuthorizedActionsController.cs b/src/Mvc/test/WebSites/SecurityWebSite/Controllers/AuthorizedActionsController.cs new file mode 100644 index 0000000000..960c6153d2 --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/Controllers/AuthorizedActionsController.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace SecurityWebSite.Controllers +{ + public class AuthorizedActionsController : ControllerBase + { + [AllowAnonymous] + public IActionResult ActionWithoutAllowAnonymous() => Ok(); + + public IActionResult ActionWithoutAuthAttribute() => Ok(); + + [Authorize("RequireClaimB")] + public IActionResult ActionWithAuthAttribute() => Ok(); + } +} diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Controllers/AuthorizedController.cs b/src/Mvc/test/WebSites/SecurityWebSite/Controllers/AuthorizedController.cs new file mode 100644 index 0000000000..3d8829ddf6 --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/Controllers/AuthorizedController.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace SecurityWebSite.Controllers +{ + [Authorize] // requires any authenticated user (aka the application cookie typically) + public class AuthorizedController : ControllerBase + { + [Authorize(AuthenticationSchemes = "Bearer")] + public IActionResult Api() => Ok(); + + public IActionResult Cookie() => Ok(); + + [AllowAnonymous] + public IActionResult AllowAnonymous() => Ok(); + } +} diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Controllers/LoginController.cs b/src/Mvc/test/WebSites/SecurityWebSite/Controllers/LoginController.cs new file mode 100644 index 0000000000..e5b80fc48d --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/Controllers/LoginController.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace SecurityWebSite.Controllers +{ + [AllowAnonymous] + public class LoginController : ControllerBase + { + [HttpPost] + public async Task LoginDefaultScheme() + { + var identity = new ClaimsIdentity(new[] { new Claim("ClaimA", "Value") }, CookieAuthenticationDefaults.AuthenticationScheme); + await HttpContext.SignInAsync(scheme: null, new ClaimsPrincipal(identity)); + return Ok(); + } + + [HttpPost] + public async Task LoginClaimA() + { + var identity = new ClaimsIdentity(new[] { new Claim("ClaimA", "Value") }); + await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity)); + return Ok(); + } + + [HttpPost] + public async Task LoginClaimAB() + { + var identity = new ClaimsIdentity(new[] { new Claim("ClaimA", "Value"), new Claim("ClaimB", "Value") }); + await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity)); + return Ok(); + } + + public IActionResult LoginBearerClaimA() + { + var identity = new ClaimsIdentity(new[] { new Claim("ClaimA", "Value") }); + return Content(BearerAuth.GetTokenText(identity.Claims)); + } + } +} diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaConvention.cshtml b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaConvention.cshtml new file mode 100644 index 0000000000..8f8b6cb30a --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaConvention.cshtml @@ -0,0 +1,2 @@ +@page +@model AllowAnonymousPageViaConvention \ No newline at end of file diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaConvention.cshtml.cs b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaConvention.cshtml.cs new file mode 100644 index 0000000000..3c479caafb --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaConvention.cshtml.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace SecurityWebSite +{ + public class AllowAnonymousPageViaConvention : PageModel + { + public IActionResult OnGet() => Page(); + } +} diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaModel.cshtml b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaModel.cshtml new file mode 100644 index 0000000000..b09c8131d9 --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaModel.cshtml @@ -0,0 +1,2 @@ +@page +@model AllowAnonymousPageViaModel \ No newline at end of file diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaModel.cshtml.cs b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaModel.cshtml.cs new file mode 100644 index 0000000000..ebbe824296 --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AllowAnonymousPageViaModel.cshtml.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace SecurityWebSite +{ + [AllowAnonymous] + public class AllowAnonymousPageViaModel : PageModel + { + public IActionResult OnGet() => Page(); + } +} diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaConvention.cshtml b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaConvention.cshtml new file mode 100644 index 0000000000..4081393374 --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaConvention.cshtml @@ -0,0 +1,2 @@ +@page +@model AuthorizePageViaConvention \ No newline at end of file diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaConvention.cshtml.cs b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaConvention.cshtml.cs new file mode 100644 index 0000000000..8ce1c26ee2 --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaConvention.cshtml.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace SecurityWebSite +{ + public class AuthorizePageViaConvention : PageModel + { + public IActionResult OnGet() => Page(); + } +} diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaModel.cshtml b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaModel.cshtml new file mode 100644 index 0000000000..8da44d60d8 --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaModel.cshtml @@ -0,0 +1,2 @@ +@page +@model AuthorizePageViaModel \ No newline at end of file diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaModel.cshtml.cs b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaModel.cshtml.cs new file mode 100644 index 0000000000..bd4e75ade0 --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/Pages/AuthorizePageViaModel.cshtml.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace SecurityWebSite +{ + [Authorize("RequireClaimB")] + public class AuthorizePageViaModel : PageModel + { + public IActionResult OnGet() => Page(); + } +} diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Pages/PagesHome.cshtml b/src/Mvc/test/WebSites/SecurityWebSite/Pages/PagesHome.cshtml new file mode 100644 index 0000000000..e8a33ba88f --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/Pages/PagesHome.cshtml @@ -0,0 +1,2 @@ +@page +Hello from PagesHome \ No newline at end of file diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Pages/_ViewImports.cshtml b/src/Mvc/test/WebSites/SecurityWebSite/Pages/_ViewImports.cshtml new file mode 100644 index 0000000000..22e61baa9f --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/Pages/_ViewImports.cshtml @@ -0,0 +1 @@ +@namespace SecurityWebSite \ No newline at end of file diff --git a/src/Mvc/test/WebSites/SecurityWebSite/SecurityWebSite.csproj b/src/Mvc/test/WebSites/SecurityWebSite/SecurityWebSite.csproj index 15b5e378a4..cad7056165 100644 --- a/src/Mvc/test/WebSites/SecurityWebSite/SecurityWebSite.csproj +++ b/src/Mvc/test/WebSites/SecurityWebSite/SecurityWebSite.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -11,6 +11,7 @@ + diff --git a/src/Mvc/test/WebSites/SecurityWebSite/Startup.cs b/src/Mvc/test/WebSites/SecurityWebSite/Startup.cs index 93bae9b71f..627292d01a 100644 --- a/src/Mvc/test/WebSites/SecurityWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/SecurityWebSite/Startup.cs @@ -21,7 +21,8 @@ namespace SecurityWebSite { options.LoginPath = "/Home/Login"; options.LogoutPath = "/Home/Logout"; - }).AddCookie("Cookie2"); + }) + .AddCookie("Cookie2"); services.AddScoped(); } diff --git a/src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalAuthFilter.cs b/src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalAuthFilter.cs new file mode 100644 index 0000000000..f94c2712dd --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalAuthFilter.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.Extensions.DependencyInjection; + +namespace SecurityWebSite +{ + public class StartupWithGlobalAuthFilter + { + public void ConfigureServices(IServiceCollection services) + { + services + .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie() + .AddJwtBearer(options => + { + options.TokenValidationParameters = BearerAuth.CreateTokenValidationParameters(); + }); + + services.AddAuthorization(options => + { + options.AddPolicy("RequireClaimA", policy => policy.RequireClaim("ClaimA")); + options.AddPolicy("RequireClaimB", policy => policy.RequireClaim("ClaimB")); + }); + + services.AddMvc(o => + { + o.Filters.Add(new AuthorizeFilter("RequireClaimA")); + }) + .AddRazorPagesOptions(options => + { + options.Conventions.AllowAnonymousToPage("/AllowAnonymousPageViaConvention"); + options.Conventions.AuthorizePage("/AuthorizePageViaConvention", "RequireClaimB"); + }) + .SetCompatibilityVersion(CompatibilityVersion.Latest); + } + + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + endpoints.MapRazorPages(); + }); + } + } +} diff --git a/src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalAuthFilterWithoutEndpointRouting.cs b/src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalAuthFilterWithoutEndpointRouting.cs new file mode 100644 index 0000000000..41c88d81ce --- /dev/null +++ b/src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalAuthFilterWithoutEndpointRouting.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.Extensions.DependencyInjection; + +namespace SecurityWebSite +{ + public class StartupWithGlobalAuthFilterWithoutEndpointRouting + { + public void ConfigureServices(IServiceCollection services) + { + services + .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie() + .AddJwtBearer(options => + { + options.TokenValidationParameters = BearerAuth.CreateTokenValidationParameters(); + }); + + services.AddAuthorization(options => + { + options.AddPolicy("RequireClaimA", policy => policy.RequireClaim("ClaimA")); + options.AddPolicy("RequireClaimB", policy => policy.RequireClaim("ClaimB")); + }); + + services.AddMvc(o => + { + o.EnableEndpointRouting = false; + o.Filters.Add(new AuthorizeFilter("RequireClaimA")); + }) + .AddRazorPagesOptions(options => + { + options.Conventions.AllowAnonymousToPage("/AllowAnonymousPageViaConvention"); + options.Conventions.AuthorizePage("/AuthorizePageViaConvention", "RequireClaimB"); + }) + .SetCompatibilityVersion(CompatibilityVersion.Latest); + } + + public void Configure(IApplicationBuilder app) + { + app.UseAuthentication(); + app.UseMvcWithDefaultRoute(); + } + } +} diff --git a/src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalDenyAnonymousFilterWithUseMvc.cs b/src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalDenyAnonymousFilterWithUseMvc.cs index 9977b1cf73..3bed81bf47 100644 --- a/src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalDenyAnonymousFilterWithUseMvc.cs +++ b/src/Mvc/test/WebSites/SecurityWebSite/StartupWithGlobalDenyAnonymousFilterWithUseMvc.cs @@ -22,6 +22,12 @@ namespace SecurityWebSite options.LogoutPath = "/Home/Logout"; }).AddCookie("Cookie2"); + services.AddAuthorization(options => + { + options.AddPolicy("RequireClaimA", policy => policy.RequireClaim("ClaimA")); + options.AddPolicy("RequireClaimB", policy => policy.RequireClaim("ClaimB")); + }); + services.AddMvc(o => { o.EnableEndpointRouting = false;