Skip to content

Commit e29c495

Browse files
authored
Transfer endpoint metadata from PageActionDescriptor to CompiledPageActionDescriptor (#19061)
Fixes #17300
1 parent a085554 commit e29c495

File tree

6 files changed

+165
-5
lines changed

6 files changed

+165
-5
lines changed

src/Mvc/Mvc.Abstractions/src/Abstractions/ActionDescriptor.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public ActionDescriptor()
4747

4848
/// <summary>
4949
/// Gets or sets the endpoint metadata for this action.
50+
/// This API is meant for infrastructure and should not be used by application code.
5051
/// </summary>
5152
public IList<object> EndpointMetadata { get; set; }
5253

src/Mvc/Mvc.RazorPages/src/Infrastructure/DefaultPageLoader.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ private ConcurrentDictionary<PageActionDescriptor, Task<CompiledPageActionDescri
6767
}
6868

6969
public override Task<CompiledPageActionDescriptor> LoadAsync(PageActionDescriptor actionDescriptor)
70+
=> LoadAsync(actionDescriptor, EndpointMetadataCollection.Empty);
71+
72+
internal Task<CompiledPageActionDescriptor> LoadAsync(PageActionDescriptor actionDescriptor, EndpointMetadataCollection endpointMetadata)
7073
{
7174
if (actionDescriptor == null)
7275
{
@@ -79,10 +82,10 @@ public override Task<CompiledPageActionDescriptor> LoadAsync(PageActionDescripto
7982
return compiledDescriptorTask;
8083
}
8184

82-
return cache.GetOrAdd(actionDescriptor, LoadAsyncCore(actionDescriptor));
85+
return cache.GetOrAdd(actionDescriptor, LoadAsyncCore(actionDescriptor, endpointMetadata));
8386
}
8487

85-
private async Task<CompiledPageActionDescriptor> LoadAsyncCore(PageActionDescriptor actionDescriptor)
88+
private async Task<CompiledPageActionDescriptor> LoadAsyncCore(PageActionDescriptor actionDescriptor, EndpointMetadataCollection endpointMetadata)
8689
{
8790
var viewDescriptor = await Compiler.CompileAsync(actionDescriptor.RelativePath);
8891
var context = new PageApplicationModelProviderContext(actionDescriptor, viewDescriptor.Type.GetTypeInfo());
@@ -110,7 +113,18 @@ private async Task<CompiledPageActionDescriptor> LoadAsyncCore(PageActionDescrip
110113
routeNames: new HashSet<string>(StringComparer.OrdinalIgnoreCase),
111114
action: compiled,
112115
routes: Array.Empty<ConventionalRouteEntry>(),
113-
conventions: Array.Empty<Action<EndpointBuilder>>(),
116+
conventions: new Action<EndpointBuilder>[]
117+
{
118+
b =>
119+
{
120+
// Metadata from PageActionDescriptor is less significant than the one discovered from the compiled type.
121+
// Consequently, we'll insert it at the beginning.
122+
for (var i = endpointMetadata.Count - 1; i >=0; i--)
123+
{
124+
b.Metadata.Insert(0, endpointMetadata[i]);
125+
}
126+
},
127+
},
114128
createInertEndpoints: false);
115129

116130
// In some test scenarios there's no route so the endpoint isn't created. This is fine because

src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointMatcherPolicy.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,19 @@ public async Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
160160
var loadedEndpoints = new List<Endpoint>(endpoints);
161161
for (var j = 0; j < loadedEndpoints.Count; j++)
162162
{
163-
var compiled = await _loader.LoadAsync(loadedEndpoints[j].Metadata.GetMetadata<PageActionDescriptor>());
163+
var metadata = loadedEndpoints[j].Metadata;
164+
var pageActionDescriptor = metadata.GetMetadata<PageActionDescriptor>();
165+
166+
CompiledPageActionDescriptor compiled;
167+
if (_loader is DefaultPageLoader defaultPageLoader)
168+
{
169+
compiled = await defaultPageLoader.LoadAsync(pageActionDescriptor, endpoint.Metadata);
170+
}
171+
else
172+
{
173+
compiled = await _loader.LoadAsync(pageActionDescriptor);
174+
}
175+
164176
loadedEndpoints[j] = compiled.Endpoint;
165177
}
166178

src/Mvc/Mvc.RazorPages/src/Infrastructure/PageLoaderMatcherPolicy.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,16 @@ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
7878
{
7979
// We found an endpoint instance that has a PageActionDescriptor, but not a
8080
// CompiledPageActionDescriptor. Update the CandidateSet.
81-
var compiled = _loader.LoadAsync(page);
81+
Task<CompiledPageActionDescriptor> compiled;
82+
if (_loader is DefaultPageLoader defaultPageLoader)
83+
{
84+
compiled = defaultPageLoader.LoadAsync(page, endpoint.Metadata);
85+
}
86+
else
87+
{
88+
compiled = _loader.LoadAsync(page);
89+
}
90+
8291
if (compiled.IsCompletedSuccessfully)
8392
{
8493
candidates.ReplaceEndpoint(i, compiled.Result.Endpoint, candidate.Values);
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Linq;
5+
using System.Net;
6+
using System.Net.Http;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Hosting;
9+
using Xunit;
10+
11+
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
12+
{
13+
public class AuthMiddlewareUsingRequireAuthTest : IClassFixture<MvcTestFixture<SecurityWebSite.StartupWithRequireAuth>>
14+
{
15+
public AuthMiddlewareUsingRequireAuthTest(MvcTestFixture<SecurityWebSite.StartupWithRequireAuth> fixture)
16+
{
17+
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
18+
Client = factory.CreateDefaultClient();
19+
}
20+
21+
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) =>
22+
builder.UseStartup<SecurityWebSite.StartupWithRequireAuth>();
23+
24+
public HttpClient Client { get; }
25+
26+
[Fact]
27+
public async Task RequireAuthConfiguredGlobally_AppliesToControllers()
28+
{
29+
// Arrange
30+
var action = "Home/Index";
31+
var response = await Client.GetAsync(action);
32+
33+
await AssertAuthorizeResponse(response);
34+
35+
// We should be able to login with ClaimA alone
36+
var authCookie = await GetAuthCookieAsync("LoginClaimA");
37+
38+
var request = new HttpRequestMessage(HttpMethod.Get, action);
39+
request.Headers.Add("Cookie", authCookie);
40+
41+
response = await Client.SendAsync(request);
42+
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
43+
}
44+
45+
[Fact]
46+
public async Task RequireAuthConfiguredGlobally_AppliesToRazorPages()
47+
{
48+
// Arrange
49+
var action = "PagesHome";
50+
var response = await Client.GetAsync(action);
51+
52+
await AssertAuthorizeResponse(response);
53+
54+
// We should be able to login with ClaimA alone
55+
var authCookie = await GetAuthCookieAsync("LoginClaimA");
56+
57+
var request = new HttpRequestMessage(HttpMethod.Get, action);
58+
request.Headers.Add("Cookie", authCookie);
59+
60+
response = await Client.SendAsync(request);
61+
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
62+
}
63+
64+
private async Task AssertAuthorizeResponse(HttpResponseMessage response)
65+
{
66+
await response.AssertStatusCodeAsync(HttpStatusCode.Redirect);
67+
Assert.Equal("/Home/Login", response.Headers.Location.LocalPath);
68+
}
69+
70+
private async Task<string> GetAuthCookieAsync(string action)
71+
{
72+
var response = await Client.PostAsync($"Login/{action}", null);
73+
74+
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
75+
Assert.True(response.Headers.Contains("Set-Cookie"));
76+
return response.Headers.GetValues("Set-Cookie").FirstOrDefault();
77+
}
78+
}
79+
}
80+
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Authentication.Cookies;
5+
using Microsoft.AspNetCore.Authorization.Policy;
6+
using Microsoft.AspNetCore.Builder;
7+
using Microsoft.AspNetCore.Mvc;
8+
using Microsoft.Extensions.DependencyInjection;
9+
10+
namespace SecurityWebSite
11+
{
12+
public class StartupWithRequireAuth
13+
{
14+
// This method gets called by the runtime. Use this method to add services to the container.
15+
public void ConfigureServices(IServiceCollection services)
16+
{
17+
// Add framework services.
18+
services.AddControllersWithViews().SetCompatibilityVersion(CompatibilityVersion.Latest);
19+
services.AddRazorPages();
20+
services.AddAntiforgery();
21+
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
22+
{
23+
options.LoginPath = "/Home/Login";
24+
options.LogoutPath = "/Home/Logout";
25+
})
26+
.AddCookie("Cookie2");
27+
}
28+
29+
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
30+
public void Configure(IApplicationBuilder app)
31+
{
32+
app.UseRouting();
33+
34+
app.UseAuthentication();
35+
app.UseAuthorization();
36+
37+
app.UseEndpoints(endpoints =>
38+
{
39+
endpoints.MapRazorPages().RequireAuthorization();
40+
endpoints.MapDefaultControllerRoute().RequireAuthorization();
41+
});
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)