diff --git a/build/dependencies.props b/build/dependencies.props index e24177e98d..f20cec2237 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -16,87 +16,87 @@ 0.42.1 2.1.0 2.1.0-rc1-final - 2.2.0-preview1-34784 + 2.2.0-preview1-34816 2.2.0-preview1-17099 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 5.2.6 2.8.0 2.8.0 - 2.2.0-preview1-34784 + 2.2.0-preview1-34816 1.7.0 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 2.1.0 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 2.0.9 2.1.2 2.2.0-preview1-26618-02 - 2.2.0-preview1-34784 - 2.2.0-preview1-34784 + 2.2.0-preview1-34816 + 2.2.0-preview1-34816 15.6.1 4.7.49 2.0.3 diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/Abstractions/ActionDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/Abstractions/ActionDescriptor.cs index a1ced7244b..20ab53e773 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/Abstractions/ActionDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/Abstractions/ActionDescriptor.cs @@ -36,6 +36,8 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions /// public IList ActionConstraints { get; set; } + public IList EndpointMetadata { get; set; } + /// /// The set of parameters associated with this action. /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/SelectorModel.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/SelectorModel.cs index e838e17c04..d376f6c0d2 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/SelectorModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/SelectorModel.cs @@ -12,6 +12,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public SelectorModel() { ActionConstraints = new List(); + EndpointMetadata = new List(); } public SelectorModel(SelectorModel other) @@ -22,6 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels } ActionConstraints = new List(other.ActionConstraints); + EndpointMetadata = new List(other.EndpointMetadata); if (other.AttributeRouteModel != null) { @@ -32,5 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public AttributeRouteModel AttributeRouteModel { get; set; } public IList ActionConstraints { get; } + + public IList EndpointMetadata { get; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs index f343701440..f24859eb20 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing.Metadata; using Resources = Microsoft.AspNetCore.Mvc.Core.Resources; namespace Microsoft.AspNetCore.Mvc.Internal @@ -163,6 +164,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal } AddActionConstraints(actionDescriptor, actionSelector, controllerConstraints); + + // REVIEW: Need to get metadata from controller + actionDescriptor.EndpointMetadata = actionSelector.EndpointMetadata.ToList(); } return actionDescriptors; diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultApplicationModelProvider.cs index fc380933c2..00ad6bf594 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultApplicationModelProvider.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing.Metadata; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Options; @@ -641,6 +642,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal } AddRange(selectorModel.ActionConstraints, attributes.OfType()); + AddRange(selectorModel.EndpointMetadata, attributes); // Simple case, all HTTP method attributes apply var httpMethods = attributes @@ -652,6 +654,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal if (httpMethods.Length > 0) { selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(httpMethods)); + selectorModel.EndpointMetadata.Add(new HttpMethodMetadata(httpMethods)); } return selectorModel; diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs index dda3d9f47f..649f5febfa 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs @@ -3,15 +3,18 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.EndpointConstraints; using Microsoft.AspNetCore.Routing.Matchers; +using Microsoft.AspNetCore.Routing.Metadata; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Primitives; @@ -316,6 +319,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal metadata.Add(source); metadata.Add(action); + if (action.EndpointMetadata != null) + { + metadata.AddRange(action.EndpointMetadata); + } + if (!string.IsNullOrEmpty(routeName)) { metadata.Add(new RouteNameMetadata(routeName)); @@ -334,11 +342,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Currently they need to implement IActionConstraintMetadata foreach (var actionConstraint in action.ActionConstraints) { - if (actionConstraint is HttpMethodActionConstraint httpMethodActionConstraint) - { - metadata.Add(new HttpMethodEndpointConstraint(httpMethodActionConstraint.HttpMethods)); - } - else if (actionConstraint is IEndpointConstraintMetadata) + if (actionConstraint is IEndpointConstraintMetadata) { // The constraint might have been added earlier, e.g. it is also a filter descriptor if (!metadata.Contains(actionConstraint)) diff --git a/src/Microsoft.AspNetCore.Mvc.Cors/Internal/CorsApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.Cors/Internal/CorsApplicationModelProvider.cs index 4410412e28..3c5d23010d 100644 --- a/src/Microsoft.AspNetCore.Mvc.Cors/Internal/CorsApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Cors/Internal/CorsApplicationModelProvider.cs @@ -6,7 +6,7 @@ using System.Linq; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Internal; -using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.Routing.Metadata; namespace Microsoft.AspNetCore.Mvc.Cors.Internal { @@ -67,17 +67,18 @@ namespace Microsoft.AspNetCore.Mvc.Cors.Internal if (isCorsEnabledGlobally || corsOnController || corsOnAction) { - UpdateHttpMethodActionConstraint(actionModel); + UpdateActionToAcceptCorsPreflight(actionModel); } } } } - private static void UpdateHttpMethodActionConstraint(ActionModel actionModel) + private static void UpdateActionToAcceptCorsPreflight(ActionModel actionModel) { for (var i = 0; i < actionModel.Selectors.Count; i++) { var selectorModel = actionModel.Selectors[i]; + for (var j = 0; j < selectorModel.ActionConstraints.Count; j++) { if (selectorModel.ActionConstraints[j] is HttpMethodActionConstraint httpConstraint) @@ -85,6 +86,14 @@ namespace Microsoft.AspNetCore.Mvc.Cors.Internal selectorModel.ActionConstraints[j] = new CorsHttpMethodActionConstraint(httpConstraint); } } + + for (int j = 0; j < selectorModel.EndpointMetadata.Count; j++) + { + if (selectorModel.EndpointMetadata[j] is HttpMethodMetadata httpMethodMetadata) + { + selectorModel.EndpointMetadata[j] = new HttpMethodMetadata(httpMethodMetadata.HttpMethods, true); + } + } } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorProviderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorProviderTests.cs index 56b44dc4fa..db7a1e3ce1 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorProviderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorProviderTests.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing.Metadata; using Microsoft.Extensions.Options; using Moq; using Xunit; @@ -251,6 +252,62 @@ namespace Microsoft.AspNetCore.Mvc.Internal Assert.Equal(nameof(ConventionallyRoutedController.ConventionalAction), actionConstraint.Value); } + [Fact] + public void GetDescriptors_ActionWithHttpMethods_AddedToEndpointMetadata() + { + // Arrange & Act + var descriptors = GetDescriptors( + typeof(AttributeRoutedController).GetTypeInfo()); + + // Assert + var action = Assert.Single(descriptors); + + Assert.NotNull(action.EndpointMetadata); + + Assert.Collection(action.EndpointMetadata, + metadata => Assert.IsType(metadata), + metadata => + { + var httpMethodMetadata = Assert.IsType(metadata); + + Assert.False(httpMethodMetadata.AcceptCorsPreflight); + Assert.Equal("GET", Assert.Single(httpMethodMetadata.HttpMethods)); + }); + } + + [Fact] + public void GetDescriptors_ActionWithMultipleHttpMethods_SingleHttpMethodMetadata() + { + // Arrange & Act + var descriptors = GetDescriptors( + typeof(NonDuplicatedAttributeRouteController).GetTypeInfo()); + + // Assert + var actions = descriptors + .OfType() + .Where(d => d.ActionName == nameof(NonDuplicatedAttributeRouteController.DifferentHttpMethods)); + + Assert.Collection(actions, + InspectElement("GET"), + InspectElement("POST"), + InspectElement("PUT"), + InspectElement("PATCH"), + InspectElement("DELETE")); + + Action InspectElement(string httpMethod) + { + return (descriptor) => + { + var httpMethodAttribute = Assert.Single(descriptor.EndpointMetadata.OfType()); + Assert.Equal(httpMethod, httpMethodAttribute.HttpMethods.Single(), ignoreCase: true); + + var httpMethodMetadata = Assert.Single(descriptor.EndpointMetadata.OfType()); + Assert.Equal(httpMethod, httpMethodMetadata.HttpMethods.Single(), ignoreCase: true); + Assert.False(httpMethodMetadata.AcceptCorsPreflight); + }; + } + } + [Fact] public void GetDescriptors_AddsControllerAndActionDefaults_ToAttributeRoutedActions() { diff --git a/test/Microsoft.AspNetCore.Mvc.Cors.Test/Internal/CorsApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Cors.Test/Internal/CorsApplicationModelProviderTest.cs index 3b4c88964f..a9538a9b24 100644 --- a/test/Microsoft.AspNetCore.Mvc.Cors.Test/Internal/CorsApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Cors.Test/Internal/CorsApplicationModelProviderTest.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 System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Cors; @@ -10,6 +11,7 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Routing.Metadata; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; @@ -36,6 +38,8 @@ namespace Microsoft.AspNetCore.Mvc.Cors.Internal 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] @@ -55,10 +59,12 @@ namespace Microsoft.AspNetCore.Mvc.Cors.Internal 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_ReplacesHttpConstraints() + public void CreateControllerModel_CustomCorsFilter_EnablesCorsPreflight() { // Arrange var corsProvider = new CorsApplicationModelProvider(); @@ -73,6 +79,8 @@ namespace Microsoft.AspNetCore.Mvc.Cors.Internal 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] @@ -92,6 +100,8 @@ namespace Microsoft.AspNetCore.Mvc.Cors.Internal 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] @@ -111,10 +121,12 @@ namespace Microsoft.AspNetCore.Mvc.Cors.Internal 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_ReplacesHttpConstraints() + public void BuildActionModel_CustomCorsAuthorizationFilterOnAction_EnablesCorsPreflight() { // Arrange var corsProvider = new CorsApplicationModelProvider(); @@ -129,10 +141,12 @@ namespace Microsoft.AspNetCore.Mvc.Cors.Internal 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_EnableCorsGloballyReplacesHttpMethodConstraints() + public void CreateControllerModel_EnableCorsGloballyEnablesCorsPreflight() { // Arrange var corsProvider = new CorsApplicationModelProvider(); @@ -150,10 +164,12 @@ namespace Microsoft.AspNetCore.Mvc.Cors.Internal 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_DisableCorsGloballyReplacesHttpMethodConstraints() + public void CreateControllerModel_DisableCorsGloballyEnablesCorsPreflight() { // Arrange var corsProvider = new CorsApplicationModelProvider(); @@ -169,10 +185,12 @@ namespace Microsoft.AspNetCore.Mvc.Cors.Internal 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_CustomCorsFilterGloballyReplacesHttpMethodConstraints() + public void CreateControllerModel_CustomCorsFilterGloballyEnablesCorsPreflight() { // Arrange var corsProvider = new CorsApplicationModelProvider(); @@ -188,6 +206,8 @@ namespace Microsoft.AspNetCore.Mvc.Cors.Internal 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] @@ -206,6 +226,8 @@ namespace Microsoft.AspNetCore.Mvc.Cors.Internal 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/test/Microsoft.AspNetCore.Mvc.FunctionalTests/CorsDispatchingTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/CorsDispatchingTests.cs new file mode 100644 index 0000000000..f5281eee9e --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/CorsDispatchingTests.cs @@ -0,0 +1,34 @@ +// 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class CorsGlobalRoutingTests : CorsTestsBase + { + public CorsGlobalRoutingTests(MvcTestFixture fixture) + : base(fixture) + { + } + + [Fact] // This intentionally returns a 405 with global routing + public override async Task PreflightRequestOnNonCorsEnabledController_DoesNotMatchTheAction() + { + // Arrange + var request = new HttpRequestMessage(new HttpMethod("OPTIONS"), "http://localhost/NonCors/Post"); + request.Headers.Add(CorsConstants.Origin, "http://example.com"); + request.Headers.Add(CorsConstants.AccessControlRequestMethod, "POST"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/CorsTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/CorsTests.cs index 5d92c7e6eb..4d241ddeb0 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/CorsTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/CorsTests.cs @@ -1,354 +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 System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Cors.Infrastructure; -using Xunit; - namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class CorsTests : IClassFixture> + public class CorsTests : CorsTestsBase { public CorsTests(MvcTestFixture fixture) + : base(fixture) { - Client = fixture.CreateDefaultClient(); - } - - public HttpClient Client { get; } - - [Theory] - [InlineData("GET")] - [InlineData("HEAD")] - [InlineData("POST")] - public async Task ResourceWithSimpleRequestPolicy_Allows_SimpleRequests(string method) - { - // Arrange - var origin = "http://example.com"; - var request = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Cors/GetBlogComments"); - request.Headers.Add(CorsConstants.Origin, origin); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var content = await response.Content.ReadAsStringAsync(); - Assert.Equal("[\"comment1\",\"comment2\",\"comment3\"]", content); - var responseHeaders = response.Headers; - var header = Assert.Single(response.Headers); - Assert.Equal(CorsConstants.AccessControlAllowOrigin, header.Key); - Assert.Equal(new[] { "*" }, header.Value.ToArray()); - } - - [Fact] - public async Task OptionsRequest_NonPreflight_ExecutesOptionsAction() - { - // Arrange - var request = new HttpRequestMessage(new HttpMethod("OPTIONS"), "http://localhost/NonCors/GetOptions"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var content = await response.Content.ReadAsStringAsync(); - Assert.Equal("[\"Create\",\"Update\",\"Delete\"]", content); - Assert.Empty(response.Headers); - } - - [Fact] - public async Task PreflightRequestOnNonCorsEnabledController_ExecutesOptionsAction() - { - // Arrange - var request = new HttpRequestMessage(new HttpMethod("OPTIONS"), "http://localhost/NonCors/GetOptions"); - request.Headers.Add(CorsConstants.Origin, "http://example.com"); - request.Headers.Add(CorsConstants.AccessControlRequestMethod, "POST"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var content = await response.Content.ReadAsStringAsync(); - Assert.Equal("[\"Create\",\"Update\",\"Delete\"]", content); - Assert.Empty(response.Headers); - } - - [Fact] - public async Task PreflightRequestOnNonCorsEnabledController_DoesNotMatchTheAction() - { - // Arrange - var request = new HttpRequestMessage(new HttpMethod("OPTIONS"), "http://localhost/NonCors/Post"); - request.Headers.Add(CorsConstants.Origin, "http://example.com"); - request.Headers.Add(CorsConstants.AccessControlRequestMethod, "POST"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Theory] - [InlineData("GET")] - [InlineData("HEAD")] - [InlineData("POST")] - [InlineData("PUT")] - public async Task PolicyFailed_Disallows_PreFlightRequest(string method) - { - // Arrange - var request = new HttpRequestMessage( - new HttpMethod(CorsConstants.PreflightHttpMethod), - "http://localhost/Cors/GetBlogComments"); - - // Adding a custom header makes it a non-simple request. - request.Headers.Add(CorsConstants.Origin, "http://example.com"); - request.Headers.Add(CorsConstants.AccessControlRequestMethod, method); - request.Headers.Add(CorsConstants.AccessControlRequestHeaders, "Custom"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - // MVC applied the policy and since that did not pass, there were no access control headers. - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Empty(response.Headers); - - // It should short circuit and hence no result. - var content = await response.Content.ReadAsStringAsync(); - Assert.Equal(string.Empty, content); - } - - [Fact] - public async Task SuccessfulCorsRequest_AllowsCredentials_IfThePolicyAllowsCredentials() - { - // Arrange - var request = new HttpRequestMessage( - HttpMethod.Put, - "http://localhost/Cors/EditUserComment?userComment=abcd"); - - // Adding a custom header makes it a non-simple request. - request.Headers.Add(CorsConstants.Origin, "http://example.com"); - request.Headers.Add(CorsConstants.AccessControlExposeHeaders, "exposed1,exposed2"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var responseHeaders = response.Headers; - Assert.Equal( - new[] { "http://example.com" }, - responseHeaders.GetValues(CorsConstants.AccessControlAllowOrigin).ToArray()); - Assert.Equal( - new[] { "true" }, - responseHeaders.GetValues(CorsConstants.AccessControlAllowCredentials).ToArray()); - Assert.Equal( - new[] { "exposed1,exposed2" }, - responseHeaders.GetValues(CorsConstants.AccessControlExposeHeaders).ToArray()); - - var content = await response.Content.ReadAsStringAsync(); - Assert.Equal("abcd", content); - } - - [Fact] - public async Task SuccessfulPreflightRequest_AllowsCredentials_IfThePolicyAllowsCredentials() - { - // Arrange - var request = new HttpRequestMessage( - new HttpMethod(CorsConstants.PreflightHttpMethod), - "http://localhost/Cors/EditUserComment?userComment=abcd"); - - // Adding a custom header makes it a non-simple request. - request.Headers.Add(CorsConstants.Origin, "http://example.com"); - request.Headers.Add(CorsConstants.AccessControlRequestMethod, "PUT"); - request.Headers.Add(CorsConstants.AccessControlRequestHeaders, "header1,header2"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var responseHeaders = response.Headers; - Assert.Equal( - new[] { "http://example.com" }, - responseHeaders.GetValues(CorsConstants.AccessControlAllowOrigin).ToArray()); - Assert.Equal( - new[] { "true" }, - responseHeaders.GetValues(CorsConstants.AccessControlAllowCredentials).ToArray()); - Assert.Equal( - new[] { "header1,header2" }, - responseHeaders.GetValues(CorsConstants.AccessControlAllowHeaders).ToArray()); - Assert.Equal( - new[] { "PUT" }, - responseHeaders.GetValues(CorsConstants.AccessControlAllowMethods).ToArray()); - - var content = await response.Content.ReadAsStringAsync(); - Assert.Empty(content); - } - - [Fact] - public async Task PolicyFailed_Allows_ActualRequest_WithMissingResponseHeaders() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Put, "http://localhost/Cors/GetUserComments"); - - // Adding a custom header makes it a non simple request. - request.Headers.Add(CorsConstants.Origin, "http://example2.com"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - // MVC applied the policy and since that did not pass, there were no access control headers. - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Empty(response.Headers); - - // It still have executed the action. - var content = await response.Content.ReadAsStringAsync(); - Assert.Equal("[\"usercomment1\",\"usercomment2\",\"usercomment3\"]", content); - } - - [Theory] - [InlineData("GET")] - [InlineData("HEAD")] - [InlineData("POST")] - public async Task DisableCors_ActionsCanOverride_ControllerLevel(string method) - { - // Arrange - var request = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Cors/GetExclusiveContent"); - - // Exclusive content is not available on other sites. - request.Headers.Add(CorsConstants.Origin, "http://example.com"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - // Since there are no response headers, the client should step in to block the content. - Assert.Empty(response.Headers); - var content = await response.Content.ReadAsStringAsync(); - Assert.Equal("exclusive", content); - } - - [Theory] - [InlineData("GET")] - [InlineData("HEAD")] - [InlineData("POST")] - public async Task DisableCors_PreFlight_ActionsCanOverride_ControllerLevel(string method) - { - // Arrange - var request = new HttpRequestMessage( - new HttpMethod(CorsConstants.PreflightHttpMethod), - "http://localhost/Cors/GetExclusiveContent"); - - // Exclusive content is not available on other sites. - request.Headers.Add(CorsConstants.Origin, "http://example.com"); - request.Headers.Add(CorsConstants.AccessControlRequestMethod, method); - request.Headers.Add(CorsConstants.AccessControlRequestHeaders, "Custom"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - // Since there are no response headers, the client should step in to block the content. - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Empty(response.Headers); - - // Nothing gets executed for a pre-flight request. - var content = await response.Content.ReadAsStringAsync(); - Assert.Empty(content); - } - - [Theory] - [InlineData("http://localhost/api/store/actionusingcontrollercorssettings")] - [InlineData("http://localhost/api/store/actionwithcorssettings")] - public async Task CorsFilter_RunsBeforeOtherAuthorizationFilters(string url) - { - // Arrange - var request = new HttpRequestMessage(new HttpMethod(CorsConstants.PreflightHttpMethod), url); - - // Adding a custom header makes it a non-simple request. - request.Headers.Add(CorsConstants.Origin, "http://example.com"); - request.Headers.Add(CorsConstants.AccessControlRequestMethod, "GET"); - request.Headers.Add(CorsConstants.AccessControlRequestHeaders, "Custom"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var responseHeaders = response.Headers; - Assert.Equal( - new[] { "http://example.com" }, - responseHeaders.GetValues(CorsConstants.AccessControlAllowOrigin).ToArray()); - Assert.Equal( - new[] { "true" }, - responseHeaders.GetValues(CorsConstants.AccessControlAllowCredentials).ToArray()); - Assert.Equal( - new[] { "Custom" }, - responseHeaders.GetValues(CorsConstants.AccessControlAllowHeaders).ToArray()); - - var content = await response.Content.ReadAsStringAsync(); - Assert.Empty(content); - } - - [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. - - // Arrange - var request = new HttpRequestMessage( - new HttpMethod(CorsConstants.PreflightHttpMethod), - "http://localhost/api/store/actionwithcorsdisabled"); - - // Adding a custom header makes it a non-simple request. - request.Headers.Add(CorsConstants.Origin, "http://example.com"); - request.Headers.Add(CorsConstants.AccessControlRequestMethod, "GET"); - request.Headers.Add(CorsConstants.AccessControlRequestHeaders, "Custom"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Empty(response.Headers); - - // Nothing gets executed for a pre-flight request. - var content = await response.Content.ReadAsStringAsync(); - Assert.Empty(content); - } - - [Fact] - public async Task CorsFilter_OnAction_PreferredOverController_AndAuthorizationFiltersRunAfterCors() - { - // Arrange - var request = new HttpRequestMessage( - new HttpMethod(CorsConstants.PreflightHttpMethod), - "http://localhost/api/store/actionwithdifferentcorspolicy"); - request.Headers.Add(CorsConstants.Origin, "http://notexpecteddomain.com"); - request.Headers.Add(CorsConstants.AccessControlRequestMethod, "GET"); - request.Headers.Add(CorsConstants.AccessControlRequestHeaders, "Custom"); - - // Act - var response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Empty(response.Headers); - - // Nothing gets executed for a pre-flight request. - var content = await response.Content.ReadAsStringAsync(); - Assert.Empty(content); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/CorsTestsBase.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/CorsTestsBase.cs new file mode 100644 index 0000000000..899ce4e792 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/CorsTestsBase.cs @@ -0,0 +1,359 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.AspNetCore.Hosting; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public abstract class CorsTestsBase : IClassFixture> where TStartup : class + { + protected CorsTestsBase(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; } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + [InlineData("POST")] + public async Task ResourceWithSimpleRequestPolicy_Allows_SimpleRequests(string method) + { + // Arrange + var origin = "http://example.com"; + var request = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Cors/GetBlogComments"); + request.Headers.Add(CorsConstants.Origin, origin); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("[\"comment1\",\"comment2\",\"comment3\"]", content); + var responseHeaders = response.Headers; + var header = Assert.Single(response.Headers); + Assert.Equal(CorsConstants.AccessControlAllowOrigin, header.Key); + Assert.Equal(new[] { "*" }, header.Value.ToArray()); + } + + [Fact] + public async Task OptionsRequest_NonPreflight_ExecutesOptionsAction() + { + // Arrange + var request = new HttpRequestMessage(new HttpMethod("OPTIONS"), "http://localhost/NonCors/GetOptions"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("[\"Create\",\"Update\",\"Delete\"]", content); + Assert.Empty(response.Headers); + } + + [Fact] + public async Task PreflightRequestOnNonCorsEnabledController_ExecutesOptionsAction() + { + // Arrange + var request = new HttpRequestMessage(new HttpMethod("OPTIONS"), "http://localhost/NonCors/GetOptions"); + request.Headers.Add(CorsConstants.Origin, "http://example.com"); + request.Headers.Add(CorsConstants.AccessControlRequestMethod, "POST"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("[\"Create\",\"Update\",\"Delete\"]", content); + Assert.Empty(response.Headers); + } + + [Fact] + public virtual async Task PreflightRequestOnNonCorsEnabledController_DoesNotMatchTheAction() + { + // Arrange + var request = new HttpRequestMessage(new HttpMethod("OPTIONS"), "http://localhost/NonCors/Post"); + request.Headers.Add(CorsConstants.Origin, "http://example.com"); + request.Headers.Add(CorsConstants.AccessControlRequestMethod, "POST"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + [InlineData("POST")] + [InlineData("PUT")] + public async Task PolicyFailed_Disallows_PreFlightRequest(string method) + { + // Arrange + var request = new HttpRequestMessage( + new HttpMethod(CorsConstants.PreflightHttpMethod), + "http://localhost/Cors/GetBlogComments"); + + // Adding a custom header makes it a non-simple request. + request.Headers.Add(CorsConstants.Origin, "http://example.com"); + request.Headers.Add(CorsConstants.AccessControlRequestMethod, method); + request.Headers.Add(CorsConstants.AccessControlRequestHeaders, "Custom"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + // MVC applied the policy and since that did not pass, there were no access control headers. + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers); + + // It should short circuit and hence no result. + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal(string.Empty, content); + } + + [Fact] + public async Task SuccessfulCorsRequest_AllowsCredentials_IfThePolicyAllowsCredentials() + { + // Arrange + var request = new HttpRequestMessage( + HttpMethod.Put, + "http://localhost/Cors/EditUserComment?userComment=abcd"); + + // Adding a custom header makes it a non-simple request. + request.Headers.Add(CorsConstants.Origin, "http://example.com"); + request.Headers.Add(CorsConstants.AccessControlExposeHeaders, "exposed1,exposed2"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseHeaders = response.Headers; + Assert.Equal( + new[] { "http://example.com" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowOrigin).ToArray()); + Assert.Equal( + new[] { "true" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowCredentials).ToArray()); + Assert.Equal( + new[] { "exposed1,exposed2" }, + responseHeaders.GetValues(CorsConstants.AccessControlExposeHeaders).ToArray()); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("abcd", content); + } + + [Fact] + public async Task SuccessfulPreflightRequest_AllowsCredentials_IfThePolicyAllowsCredentials() + { + // Arrange + var request = new HttpRequestMessage( + new HttpMethod(CorsConstants.PreflightHttpMethod), + "http://localhost/Cors/EditUserComment?userComment=abcd"); + + // Adding a custom header makes it a non-simple request. + request.Headers.Add(CorsConstants.Origin, "http://example.com"); + request.Headers.Add(CorsConstants.AccessControlRequestMethod, "PUT"); + request.Headers.Add(CorsConstants.AccessControlRequestHeaders, "header1,header2"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseHeaders = response.Headers; + Assert.Equal( + new[] { "http://example.com" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowOrigin).ToArray()); + Assert.Equal( + new[] { "true" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowCredentials).ToArray()); + Assert.Equal( + new[] { "header1,header2" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowHeaders).ToArray()); + Assert.Equal( + new[] { "PUT" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowMethods).ToArray()); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Empty(content); + } + + [Fact] + public async Task PolicyFailed_Allows_ActualRequest_WithMissingResponseHeaders() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Put, "http://localhost/Cors/GetUserComments"); + + // Adding a custom header makes it a non simple request. + request.Headers.Add(CorsConstants.Origin, "http://example2.com"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + // MVC applied the policy and since that did not pass, there were no access control headers. + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers); + + // It still have executed the action. + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("[\"usercomment1\",\"usercomment2\",\"usercomment3\"]", content); + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + [InlineData("POST")] + public async Task DisableCors_ActionsCanOverride_ControllerLevel(string method) + { + // Arrange + var request = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Cors/GetExclusiveContent"); + + // Exclusive content is not available on other sites. + request.Headers.Add(CorsConstants.Origin, "http://example.com"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Since there are no response headers, the client should step in to block the content. + Assert.Empty(response.Headers); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("exclusive", content); + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + [InlineData("POST")] + public async Task DisableCors_PreFlight_ActionsCanOverride_ControllerLevel(string method) + { + // Arrange + var request = new HttpRequestMessage( + new HttpMethod(CorsConstants.PreflightHttpMethod), + "http://localhost/Cors/GetExclusiveContent"); + + // Exclusive content is not available on other sites. + request.Headers.Add(CorsConstants.Origin, "http://example.com"); + request.Headers.Add(CorsConstants.AccessControlRequestMethod, method); + request.Headers.Add(CorsConstants.AccessControlRequestHeaders, "Custom"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + // Since there are no response headers, the client should step in to block the content. + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers); + + // Nothing gets executed for a pre-flight request. + var content = await response.Content.ReadAsStringAsync(); + Assert.Empty(content); + } + + [Theory] + [InlineData("http://localhost/api/store/actionusingcontrollercorssettings")] + [InlineData("http://localhost/api/store/actionwithcorssettings")] + public async Task CorsFilter_RunsBeforeOtherAuthorizationFilters(string url) + { + // Arrange + var request = new HttpRequestMessage(new HttpMethod(CorsConstants.PreflightHttpMethod), url); + + // Adding a custom header makes it a non-simple request. + request.Headers.Add(CorsConstants.Origin, "http://example.com"); + request.Headers.Add(CorsConstants.AccessControlRequestMethod, "GET"); + request.Headers.Add(CorsConstants.AccessControlRequestHeaders, "Custom"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseHeaders = response.Headers; + Assert.Equal( + new[] { "http://example.com" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowOrigin).ToArray()); + Assert.Equal( + new[] { "true" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowCredentials).ToArray()); + Assert.Equal( + new[] { "Custom" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowHeaders).ToArray()); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Empty(content); + } + + [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. + + // Arrange + var request = new HttpRequestMessage( + new HttpMethod(CorsConstants.PreflightHttpMethod), + "http://localhost/api/store/actionwithcorsdisabled"); + + // Adding a custom header makes it a non-simple request. + request.Headers.Add(CorsConstants.Origin, "http://example.com"); + request.Headers.Add(CorsConstants.AccessControlRequestMethod, "GET"); + request.Headers.Add(CorsConstants.AccessControlRequestHeaders, "Custom"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers); + + // Nothing gets executed for a pre-flight request. + var content = await response.Content.ReadAsStringAsync(); + Assert.Empty(content); + } + + [Fact] + public async Task CorsFilter_OnAction_PreferredOverController_AndAuthorizationFiltersRunAfterCors() + { + // Arrange + var request = new HttpRequestMessage( + new HttpMethod(CorsConstants.PreflightHttpMethod), + "http://localhost/api/store/actionwithdifferentcorspolicy"); + request.Headers.Add(CorsConstants.Origin, "http://notexpecteddomain.com"); + request.Headers.Add(CorsConstants.AccessControlRequestMethod, "GET"); + request.Headers.Add(CorsConstants.AccessControlRequestHeaders, "Custom"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers); + + // Nothing gets executed for a pre-flight request. + var content = await response.Content.ReadAsStringAsync(); + Assert.Empty(content); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningGlobalRoutingTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningGlobalRoutingTests.cs index af2147949f..a79ebc9516 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningGlobalRoutingTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningGlobalRoutingTests.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.Net; +using System.Net.Http; using System.Threading.Tasks; using Newtonsoft.Json; using Xunit; @@ -29,5 +30,27 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.True(result); } + + // This behaves differently right now because the action/endpoint constraints are always + // executed after the DFA nodes like (HttpMethodMatcherPolicy). You don't have the flexibility + // to do what this test is doing in old-style routing. + [Fact] + public override async Task VersionedApi_CanUseConstraintOrder_ToChangeSelectedAction() + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Delete, "http://localhost/" + "Customers/5?version=2"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Customers", result.Controller); + Assert.Equal("AnyV2OrHigherWithId", result.Action); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTestsBase.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTestsBase.cs index 152c08a799..01e6ef154b 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTestsBase.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTestsBase.cs @@ -508,7 +508,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests } [Fact] - public async Task VersionedApi_CanUseConstraintOrder_ToChangeSelectedAction() + public virtual async Task VersionedApi_CanUseConstraintOrder_ToChangeSelectedAction() { // Arrange var message = new HttpRequestMessage(HttpMethod.Delete, "http://localhost/" + "Customers/5?version=2"); @@ -551,7 +551,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal(path, actualUrl); } - private class RoutingResult + protected class RoutingResult { public string[] ExpectedUrls { get; set; } diff --git a/test/WebSites/CorsWebSite/Program.cs b/test/WebSites/CorsWebSite/Program.cs new file mode 100644 index 0000000000..b58c5ff2f4 --- /dev/null +++ b/test/WebSites/CorsWebSite/Program.cs @@ -0,0 +1,26 @@ +// 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.IO; +using Microsoft.AspNetCore.Hosting; + +namespace CorsWebSite +{ + public class Program + { + public static void Main(string[] args) + { + var host = CreateWebHostBuilder(args) + .Build(); + + host.Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + new WebHostBuilder() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup() + .UseKestrel() + .UseIISIntegration(); + } +} diff --git a/test/WebSites/CorsWebSite/Startup.cs b/test/WebSites/CorsWebSite/Startup.cs index 6ee44e7f39..5c8e115755 100644 --- a/test/WebSites/CorsWebSite/Startup.cs +++ b/test/WebSites/CorsWebSite/Startup.cs @@ -76,20 +76,5 @@ namespace CorsWebSite { app.UseMvc(); } - - public static void Main(string[] args) - { - var host = CreateWebHostBuilder(args) - .Build(); - - host.Run(); - } - - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - new WebHostBuilder() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseStartup() - .UseKestrel() - .UseIISIntegration(); } } diff --git a/test/WebSites/CorsWebSite/StartupWithGlobalRouting.cs b/test/WebSites/CorsWebSite/StartupWithGlobalRouting.cs new file mode 100644 index 0000000000..f9e2a744c7 --- /dev/null +++ b/test/WebSites/CorsWebSite/StartupWithGlobalRouting.cs @@ -0,0 +1,80 @@ +// 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.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace CorsWebSite +{ + public class StartupWithGlobalRouting + { + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc(options => options.EnableGlobalRouting = true); + services.Configure(options => + { + options.AddPolicy( + "AllowAnySimpleRequest", + builder => + { + builder.AllowAnyOrigin() + .WithMethods("GET", "POST", "HEAD"); + }); + + options.AddPolicy( + "AllowSpecificOrigin", + builder => + { + builder.WithOrigins("http://example.com"); + }); + + options.AddPolicy( + "WithCredentials", + builder => + { + builder.AllowCredentials() + .WithOrigins("http://example.com"); + }); + + options.AddPolicy( + "WithCredentialsAnyOrigin", + builder => + { + builder.AllowCredentials() + .AllowAnyOrigin() + .AllowAnyHeader() + .WithMethods("PUT", "POST") + .WithExposedHeaders("exposed1", "exposed2"); + }); + + options.AddPolicy( + "AllowAll", + builder => + { + builder.AllowCredentials() + .AllowAnyMethod() + .AllowAnyHeader() + .AllowAnyOrigin(); + }); + + options.AddPolicy( + "Allow example.com", + builder => + { + builder.AllowCredentials() + .AllowAnyMethod() + .AllowAnyHeader() + .WithOrigins("http://example.com"); + }); + }); + } + + public void Configure(IApplicationBuilder app) + { + app.UseMvc(); + } + } +}