Make AuthorizeFilter work in endpoint routing (#9099)

* Make AuthorizeFilter work in endpoint routing

Fixes https://github.com/aspnet/AspNetCore/issues/8387
This commit is contained in:
Pranav K 2019-04-08 06:03:34 -07:00 committed by GitHub
parent 67e08728ca
commit b93bc433db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 1893 additions and 297 deletions

View File

@ -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();
});
}
}
}

View File

@ -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<StartupWithoutEndpointRouting, IdentityDbContext>
{
public IdentityUserAuthorizationWithoutEndpointRoutingTests(ServerFactory<StartupWithoutEndpointRouting, IdentityDbContext> serverFactory)
: base(serverFactory)
{
}
}
}

View File

@ -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<StartupWithoutEndpointRouting, IdentityDbContext>
{
public IdentityUserLoginWithoutEndpointRoutingTests(ServerFactory<StartupWithoutEndpointRouting, IdentityDbContext> serverFactory) : base(serverFactory)
{
}
}
}

View File

@ -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 =>
{

View File

@ -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<IdentityUser, IdentityDbContext>
{
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();
}
}
}

View File

@ -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<ICorsMetadata>();
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;
}

View File

@ -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<ICorsService>();
var policyProvider = Mock.Of<ICorsPolicyProvider>();
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<ICorsService>();
var policyProvider = Mock.Of<ICorsPolicyProvider>();
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<HttpContext>(), It.IsAny<string>()), Times.Never());
Mock.Get(corsService).Verify(v => v.EvaluatePolicy(It.IsAny<HttpContext>(), It.IsAny<CorsPolicy>()), Times.Never());
}
[Fact]
public async Task Invoke_HasEndpointWithEnableMetadata_MiddlewareHasPolicy_RunsCorsWithPolicyName()
{

View File

@ -127,13 +127,16 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
}
}
private static void AddEndpointMetadata(SelectorModel selector, IList<object> metadata)
private static void AddEndpointMetadata(SelectorModel selector, IList<object> 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]);
}
}
}

View File

@ -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> 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<IAuthorizeData>().ToArray();

View File

@ -35,6 +35,9 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
public IList<IActionConstraintMetadata> ActionConstraints { get; }
/// <summary>
/// Gets the <see cref="EndpointMetadata"/> associated with the <see cref="SelectorModel"/>.
/// </summary>
public IList<object> EndpointMetadata { get; }
}
}

View File

@ -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
/// </summary>
public class AuthorizeFilter : IAsyncAuthorizationFilter, IFilterFactory
{
// Property key set by authorization middleware when it is run
private const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareInvoked";
private AuthorizationPolicy _effectivePolicy;
/// <summary>
@ -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<AuthorizationPolicy> GetEffectivePolicyAsync(AuthorizationFilterContext context)
internal async Task<AuthorizationPolicy> 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<IAuthorizationPolicyProvider>();
var endpointAuthorizeData = endpoint.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();
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<IFilterMetadata> 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<IAllowAnonymous>() != null)
{
return true;
}
return false;
}
}

View File

@ -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.

View File

@ -17,11 +17,51 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
public class AuthorizationApplicationModelProviderTest
{
private readonly IOptions<MvcOptions> 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

View File

@ -266,16 +266,16 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
Assert.NotNull(anonymousAction.EndpointMetadata);
Assert.Collection(anonymousAction.EndpointMetadata,
metadata => Assert.IsType<AllowAnonymousAttribute>(metadata),
metadata => Assert.IsType<AuthorizeAttribute>(metadata));
metadata => Assert.IsType<AuthorizeAttribute>(metadata),
metadata => Assert.IsType<AllowAnonymousAttribute>(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<AuthorizeAttribute>(metadata).Policy),
metadata => Assert.Equal("ControllerPolicy", Assert.IsType<AuthorizeAttribute>(metadata).Policy));
metadata => Assert.Equal("ControllerPolicy", Assert.IsType<AuthorizeAttribute>(metadata).Policy),
metadata => Assert.Equal("ActionPolicy", Assert.IsType<AuthorizeAttribute>(metadata).Policy));
}
[Fact]
@ -291,6 +291,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
Assert.NotNull(action.EndpointMetadata);
Assert.Collection(action.EndpointMetadata,
metadata => Assert.IsType<RouteAttribute>(metadata),
metadata => Assert.IsType<HttpGetAttribute>(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<RouteAttribute>(metadata));
});
}
[Fact]

View File

@ -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<ChallengeResult>(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<ClaimsAuthorizationRequirement>().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<IAuthorizationPolicyProvider>(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<ClaimsAuthorizationRequirement>().Select(c => c.ClaimType));
}
private AuthorizationFilterContext GetAuthorizationContext(
bool anonymous = false,
Action<IServiceCollection> registerServices = null)
@ -580,6 +647,7 @@ namespace Microsoft.AspNetCore.Mvc.Authorization
httpContext.SetupGet(c => c.RequestServices).Returns(serviceProvider);
var contextItems = new Dictionary<object, object>();
httpContext.SetupGet(c => c.Items).Returns(contextItems);
httpContext.SetupGet(c => c.Features).Returns(Mock.Of<IFeatureCollection>());
// AuthorizationFilterContext
var actionContext = new ActionContext(

View File

@ -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 = 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<ICorsAuthorizationFilter>().Any() ||
context.Result.Filters.OfType<CorsAuthorizationFilterFactory>().Any();
context.Result.Filters.OfType<CorsAuthorizationFilterFactory>().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<IDisableCorsAttribute>().Any() ||
controller.Attributes.OfType<IEnableCorsAttribute>().Any();
foreach (var action in controller.Actions)
{
if (selectorModel.EndpointMetadata[j] is HttpMethodMetadata httpMethodMetadata)
var corsOnAction = action.Attributes.OfType<IDisableCorsAttribute>().Any() ||
action.Attributes.OfType<IEnableCorsAttribute>().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);
}
}
}
}
}

View File

@ -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.

View File

@ -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";

View File

@ -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.

View File

@ -21,11 +21,59 @@ namespace Microsoft.AspNetCore.Mvc.Cors
{
public class CorsApplicationModelProviderTest
{
private readonly IOptions<MvcOptions> 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<CorsHttpMethodActionConstraint>(constraint);
var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType<HttpMethodMetadata>());
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<CorsHttpMethodActionConstraint>(constraint);
var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType<HttpMethodMetadata>());
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<CorsHttpMethodActionConstraint>(constraint);
var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType<HttpMethodMetadata>());
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<CorsHttpMethodActionConstraint>(constraint);
var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType<HttpMethodMetadata>());
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<CorsHttpMethodActionConstraint>(constraint);
var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType<HttpMethodMetadata>());
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<CorsHttpMethodActionConstraint>(constraint);
var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType<HttpMethodMetadata>());
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<CorsHttpMethodActionConstraint>(constraint);
var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType<HttpMethodMetadata>());
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<CorsHttpMethodActionConstraint>(constraint);
var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType<HttpMethodMetadata>());
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<CorsHttpMethodActionConstraint>(constraint);
var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType<HttpMethodMetadata>());
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<CorsHttpMethodActionConstraint>(constraint);
var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType<HttpMethodMetadata>());
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<CorsHttpMethodActionConstraint>(constraint);
var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType<HttpMethodMetadata>());
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<CorsHttpMethodActionConstraint>(constraint);
var httpMethodMetadata = Assert.Single(selector.EndpointMetadata.OfType<HttpMethodMetadata>());
Assert.False(httpMethodMetadata.AcceptCorsPreflight);
}
private static ApplicationModelProviderContext GetProviderContext(Type controllerType)

View File

@ -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]);
}

View File

@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Mvc.Cors
// Assert
var statusCodeResult = Assert.IsType<StatusCodeResult>(authorizationFilterContext.Result);
Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode);
Assert.Equal(StatusCodes.Status204NoContent, statusCodeResult.StatusCode);
}
}
}

View File

@ -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<object> EndpointMetadata { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public System.Collections.Generic.IList<Microsoft.AspNetCore.Mvc.Filters.IFilterMetadata> Filters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public System.Collections.Generic.IList<Microsoft.AspNetCore.Mvc.ApplicationModels.PageHandlerModel> HandlerMethods { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public System.Collections.Generic.IList<Microsoft.AspNetCore.Mvc.ApplicationModels.PagePropertyModel> HandlerProperties { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }

View File

@ -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> 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<IAuthorizeData>().ToArray();
if (authorizeData.Length > 0)

View File

@ -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<object> 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)
{

View File

@ -47,6 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
HandlerMethods = new List<PageHandlerModel>();
HandlerProperties = new List<PagePropertyModel>();
HandlerTypeAttributes = handlerAttributes;
EndpointMetadata = new List<object>(ActionDescriptor.EndpointMetadata ?? Array.Empty<object>());
}
/// <summary>
@ -71,6 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
HandlerMethods = new List<PageHandlerModel>(other.HandlerMethods.Select(m => new PageHandlerModel(m)));
HandlerProperties = new List<PagePropertyModel>(other.HandlerProperties.Select(p => new PagePropertyModel(p)));
HandlerTypeAttributes = other.HandlerTypeAttributes;
EndpointMetadata = new List<object>(other.EndpointMetadata);
}
/// <summary>
@ -154,5 +156,10 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
/// Gets the sequence of <see cref="PagePropertyModel"/> instances on <see cref="PageHandlerModel"/>.
/// </summary>
public IList<PagePropertyModel> HandlerProperties { get; }
/// <summary>
/// Gets the endpoint metadata for this action.
/// </summary>
public IList<object> EndpointMetadata { get; }
}
}

View File

@ -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<IPageConvention>
{
private readonly IServiceProvider _serviceProvider;
private MvcOptions _mvcOptions;
/// <summary>
/// Initializes a new instance of the <see cref="PageConventionCollection"/> class that is empty.
/// </summary>
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<IOptions<MvcOptions>>().Value;
return _mvcOptions;
}
}
/// <summary>
/// Creates and adds an <see cref="IPageApplicationModelConvention"/> that invokes an action on the
/// <see cref="PageApplicationModel"/> for the page with the specified name.

View File

@ -83,6 +83,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<RazorViewEngineOptions>, RazorPagesRazorViewEngineOptionsSetup>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<RazorPagesOptions>, RazorPagesOptionsSetup>());
// Routing
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, PageLoaderMatcherPolicy>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicPageEndpointMatcherPolicy>());

View File

@ -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
}
/// <summary>
/// Adds a <see cref="AllowAnonymousFilter"/> to the page with the specified name.
/// Adds the specified <paramref name="convention"/> to <paramref name="conventions"/>.
/// The added convention will apply to all handler properties and parameters on handler methods.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="convention">The <see cref="IParameterModelBaseConvention"/> to apply.</param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
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;
}
/// <summary>
/// Allows anonymous access to the page with the specified name.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="pageName">The page name.</param>
@ -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;
}
/// <summary>
/// Adds a <see cref="AllowAnonymousFilter"/> 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.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="areaName">The area name.</param>
@ -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;
}
/// <summary>
/// Adds a <see cref="AllowAnonymousFilter"/> to all pages under the specified folder.
/// Allows anonymous access to all pages under the specified folder.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="folderPath">The folder path.</param>
@ -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;
}
/// <summary>
/// Adds the specified <paramref name="convention"/> to <paramref name="conventions"/>.
/// The added convention will apply to all handler properties and parameters on handler methods.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="convention">The <see cref="IParameterModelBaseConvention"/> to apply.</param>
/// <returns>The <see cref="PageConventionCollection"/>.</returns>
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;
}
/// <summary>
/// Adds a <see cref="AllowAnonymousFilter"/> to all pages under the specified area folder.
/// Allows anonymous access to all pages under the specified area folder.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="areaName">The area name.</param>
@ -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;
}
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with the specified policy to the page with the specified name.
/// Requires authorization with the specified policy for the page with the specified name.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="pageName">The page name.</param>
@ -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;
}
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> to the page with the specified name.
/// Requires authorization for the specified page.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="pageName">The page name.</param>
@ -239,7 +285,7 @@ namespace Microsoft.Extensions.DependencyInjection
AuthorizePage(conventions, pageName, policy: string.Empty);
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with default policy to the page with the specified name.
/// Requires authorization for the specified area page.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="areaName">The area name.</param>
@ -255,7 +301,7 @@ namespace Microsoft.Extensions.DependencyInjection
=> AuthorizeAreaPage(conventions, areaName, pageName, policy: string.Empty);
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with the specified policy to the page with the specified name.
/// Requires authorization for the specified area page with the specified policy.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="areaName">The area name.</param>
@ -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;
}
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with the specified policy to all pages under the specified folder.
/// Requires authorization for all pages under the specified folder.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="folderPath">The folder path.</param>
@ -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;
}
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> to all pages under the specified folder.
/// Requires authorization for all pages under the specified folder.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="folderPath">The folder path.</param>
@ -328,7 +392,7 @@ namespace Microsoft.Extensions.DependencyInjection
AuthorizeFolder(conventions, folderPath, policy: string.Empty);
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with the default policy to all pages under the specified folder.
/// Requires authorization with the default policy for all pages under the specified folder.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="areaName">The area name.</param>
@ -344,7 +408,7 @@ namespace Microsoft.Extensions.DependencyInjection
=> AuthorizeAreaFolder(conventions, areaName, folderPath, policy: string.Empty);
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with the specified policy to all pages under the specified folder.
/// Requires authorization with the specified policy for all pages under the specified folder.
/// </summary>
/// <param name="conventions">The <see cref="PageConventionCollection"/> to configure.</param>
/// <param name="areaName">The area name.</param>
@ -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;
}

View File

@ -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<RazorPagesOptions>
{
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);
}
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
/// Gets a collection of <see cref="IPageConvention"/> instances that are applied during
/// route and page model construction.
/// </summary>
public PageConventionCollection Conventions { get; } = new PageConventionCollection();
public PageConventionCollection Conventions { get; internal set; }
/// <summary>
/// Application relative path used as the root of discovery for Razor Page files.

View File

@ -17,12 +17,14 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
public class AuthorizationPageApplicationModelProviderTest
{
private readonly IOptions<MvcOptions> 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<PageHandlerPageFilter>(f),
f => Assert.IsType<HandleOptionsRequestsPageFilter>(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

View File

@ -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<IActionConstraintMetadata>(),
AttributeRouteInfo = new AttributeRouteInfo(),
EndpointMetadata = new List<object> { metadata3, metadata4, },
FilterDescriptors = new List<FilterDescriptor>(),
RelativePath = "/Foo",
RouteValues = new Dictionary<string, string>(),
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]

View File

@ -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<IServiceProvider>())
{
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<IServiceProvider>())
{
new FooPageConvention(),
new BarPageConvention(),

View File

@ -16,7 +16,8 @@ namespace Microsoft.Extensions.DependencyInjection
public void AddRazorPagesOptions_AddsConventions()
{
// Arrange
var services = new ServiceCollection().AddOptions();
var services = new ServiceCollection().AddOptions()
.AddSingleton<IConfigureOptions<RazorPagesOptions>, RazorPagesOptionsSetup>();
var applicationModelConvention = Mock.Of<IPageApplicationModelConvention>();
var routeModelConvention = Mock.Of<IPageRouteModelConvention>();
var builder = new MvcBuilder(services, new ApplicationPartManager());

View File

@ -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<IFilterMetadata>();
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<AuthorizeAttribute>(Assert.Single(model.EndpointMetadata));
},
model =>
{
Assert.Equal("/Users/Contact", model.ViewEnginePath);
Assert.Empty(model.Filters);
Assert.IsType<AuthorizeAttribute>(model.EndpointMetadata[0]);
Assert.IsType<AllowAnonymousAttribute>(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<AllowAnonymousAttribute>(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<AuthorizeAttribute>(metadata));
},
model =>
{
Assert.Equal("/Users/Account", model.ViewEnginePath);
Assert.Empty(model.Filters);
Assert.Collection(model.EndpointMetadata,
metadata => Assert.IsType<AuthorizeAttribute>(metadata),
metadata => Assert.IsType<AllowAnonymousAttribute>(metadata));
},
model =>
{
Assert.Equal("/Users/Contact", model.ViewEnginePath);
Assert.Collection(model.EndpointMetadata,
metadata => Assert.IsType<AuthorizeAttribute>(metadata),
metadata => Assert.IsType<AllowAnonymousAttribute>(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<AuthorizeFilter>(Assert.Single(model.Filters));
},
model =>
{
Assert.Equal("/Users/Account", model.ViewEnginePath);
Assert.IsType<AuthorizeFilter>(model.Filters[0]);
Assert.IsType<AllowAnonymousFilter>(model.Filters[1]);
},
model =>
{
Assert.Equal("/Users/Contact", model.ViewEnginePath);
Assert.IsType<AuthorizeFilter>(model.Filters[0]);
Assert.IsType<AllowAnonymousFilter>(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<AuthorizeAttribute>(metadata));
},
model =>
{
Assert.Equal("/Users/Account", model.ViewEnginePath);
Assert.Empty(model.Filters);
Assert.Collection(model.EndpointMetadata,
metadata => Assert.IsType<AuthorizeAttribute>(metadata),
metadata => Assert.IsType<AllowAnonymousAttribute>(metadata));
},
model =>
{
Assert.Equal("/Users/Contact", model.ViewEnginePath);
Assert.Empty(model.Filters);
Assert.Collection(model.EndpointMetadata,
metadata => Assert.IsType<AuthorizeAttribute>(metadata),
metadata => Assert.IsType<AllowAnonymousAttribute>(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<AllowAnonymousAttribute>(Assert.Single(model.EndpointMetadata));
},
model =>
{
Assert.Equal("/Areas/Accounts/Pages/Manage/2FA.cshtml", model.RelativePath);
Assert.Empty(model.Filters);
Assert.IsType<AllowAnonymousAttribute>(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<AuthorizeAttribute>(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<AuthorizeAttribute>(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<AuthorizeAttribute>(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<AuthorizeAttribute>(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<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Empty(model.Filters);
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(model.EndpointMetadata));
Assert.Equal("Manage-Accounts", authorizeData.Policy);
},
model =>
{
Assert.Equal("/Users/Contact", model.ViewEnginePath);
var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Empty(model.Filters);
var authorizeData = Assert.IsType<AuthorizeAttribute>(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<AuthorizeAttribute>(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<AuthorizeAttribute>(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<AuthorizeAttribute>(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<AuthorizeAttribute>(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<IOptions<MvcOptions>>(Options.Options.Create(options))
.BuildServiceProvider();
return new PageConventionCollection(serviceProvider);
}
private static SelectorModel CreateSelectorModel(string template, bool suppressLinkGeneration = false)
{
return new SelectorModel

View File

@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
public class DefaultPageLoaderTest
{
private readonly IOptions<RazorPagesOptions> RazorPagesOptions = Options.Create(new RazorPagesOptions { Conventions = new PageConventionCollection(Mock.Of<IServiceProvider>()) });
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<RoutePatternTransformer>());
@ -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<RoutePatternTransformer>());
@ -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<IServiceProvider>())
{
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<IServiceProvider>())
{
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<IServiceProvider>());
// 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<IServiceProvider>()) { 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<IServiceProvider>());
// 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<IServiceProvider>());
// 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<IServiceProvider>()) { 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<IServiceProvider>());
// 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<IServiceProvider>());
// 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<IServiceProvider>()) { 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<IServiceProvider>());
// Act
DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel);

View File

@ -371,7 +371,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
private static IOptions<RazorPagesOptions> GetRazorPagesOptions()
{
return Options.Create(new RazorPagesOptions());
return Options.Create(new RazorPagesOptions { Conventions = new PageConventionCollection(Mock.Of<IServiceProvider>()) });
}
private class TestPageRouteModelProvider : IPageRouteModelProvider

View File

@ -360,13 +360,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
new ConsumesAttribute("text/json"),
},
},
new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>()
{
new CorsHttpMethodActionConstraint(new HttpMethodActionConstraint(new[]{ "GET", })),
},
},
};
var endpoints = actions.Select(CreateEndpoint).ToArray();

View File

@ -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<SecurityWebSite.StartupWithGlobalAuthFilter>
{
public AuthMiddlewareAndFilterTest(MvcTestFixture<SecurityWebSite.StartupWithGlobalAuthFilter> fixture)
: base(fixture)
{
}
}
}

View File

@ -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<TStartup> : IClassFixture<MvcTestFixture<TStartup>> where TStartup : class
{
protected AuthMiddlewareAndFilterTestBase(MvcTestFixture<TStartup> fixture)
{
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
Client = factory.CreateDefaultClient();
}
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => builder.UseStartup<TStartup>();
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<string> 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<string> GetBearerTokenAsync()
{
var response = await Client.GetAsync("/Login/LoginBearerClaimA");
return await response.Content.ReadAsStringAsync();
}
}
}

View File

@ -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<SecurityWebSite.StartupWithGlobalAuthFilterWithoutEndpointRouting>
{
public AuthMiddlewareAndFilterWithoutEndpointRoutingTest(MvcTestFixture<SecurityWebSite.StartupWithGlobalAuthFilterWithoutEndpointRouting> fixture)
: base(fixture)
{
}
}
}

View File

@ -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.

View File

@ -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<SecurityWebSite.StartupWithGlobalDenyAnonymousFilter>();
public WebApplicationFactory<SecurityWebSite.StartupWithGlobalDenyAnonymousFilter> Factory { get; }
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8387")]
public override Task DeniesAnonymousUsers_ByDefault()
{
return Task.CompletedTask;
}
}
}

View File

@ -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();

View File

@ -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

View File

@ -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

View File

@ -74,6 +74,7 @@ namespace CorsWebSite
public virtual void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseCors();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();

View File

@ -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<Claim> claims)
{
var token = new JwtSecurityToken(Issuer, Audience, claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: Credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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<IActionResult> 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<IActionResult> LoginClaimA()
{
var identity = new ClaimsIdentity(new[] { new Claim("ClaimA", "Value") });
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));
return Ok();
}
[HttpPost]
public async Task<IActionResult> 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));
}
}
}

View File

@ -0,0 +1,2 @@
@page
@model AllowAnonymousPageViaConvention

View File

@ -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();
}
}

View File

@ -0,0 +1,2 @@
@page
@model AllowAnonymousPageViaModel

View File

@ -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();
}
}

View File

@ -0,0 +1,2 @@
@page
@model AuthorizePageViaConvention

View File

@ -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();
}
}

View File

@ -0,0 +1,2 @@
@page
@model AuthorizePageViaModel

View File

@ -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();
}
}

View File

@ -0,0 +1,2 @@
@page
Hello from PagesHome

View File

@ -0,0 +1 @@
@namespace SecurityWebSite

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@ -11,6 +11,7 @@
<Reference Include="Microsoft.AspNetCore.Authorization.Policy" />
<Reference Include="Microsoft.AspNetCore.Authentication.Cookies" />
<Reference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
<Reference Include="Microsoft.AspNetCore.Hosting" />
</ItemGroup>

View File

@ -21,7 +21,8 @@ namespace SecurityWebSite
{
options.LoginPath = "/Home/Login";
options.LogoutPath = "/Home/Logout";
}).AddCookie("Cookie2");
})
.AddCookie("Cookie2");
services.AddScoped<IPolicyEvaluator, CountingPolicyEvaluator>();
}

View File

@ -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();
});
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;