Browse Source

Match opt-in URLs the same way as IgnoredUrls

pull/26017/head
maliming 14 hours ago
parent
commit
ab1daeeacd
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 11
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AbpAspNetCoreUnitOfWorkOptions.cs
  2. 32
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AbpUnitOfWorkMiddleware.cs
  3. 3
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs
  4. 30
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkMiddleware_Tests.cs
  5. 43
      modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs
  6. 2
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenEndpoint_Integration_Tests.cs

11
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AbpAspNetCoreUnitOfWorkOptions.cs

@ -29,13 +29,10 @@ public class AbpAspNetCoreUnitOfWorkOptions
public bool CompleteUnitOfWorkOnResponseStarting { get; set; } = false;
/// <summary>
/// Absolute request path prefixes (matched by segment) that opt-in to
/// <see cref="CompleteUnitOfWorkOnResponseStarting"/> even when it is globally disabled (for example
/// "/connect" matches "/connect/token" but not "/connections"). Each prefix is matched against both
/// the request path and the path base + request path, so an endpoint configured with the path base
/// still matches. A trailing slash is normalized; blank, non-absolute, and root ("/") entries are
/// ignored - use <see cref="CompleteUnitOfWorkOnResponseStarting"/> to enable it for every request
/// handled by the middleware.
/// Request path prefixes that opt-in to <see cref="CompleteUnitOfWorkOnResponseStarting"/> even when
/// it is globally disabled. A request whose path starts with one of these values (for example
/// "/connect") is included, matched like <see cref="IgnoredUrls"/>. Use
/// <see cref="CompleteUnitOfWorkOnResponseStarting"/> to enable it for every request handled by the middleware.
/// </summary>
public List<string> CompleteUnitOfWorkOnResponseStartingUrls { get; } = new List<string>();
}

32
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AbpUnitOfWorkMiddleware.cs

@ -70,35 +70,9 @@ public class AbpUnitOfWorkMiddleware : AbpMiddlewareBase, ITransientDependency
private bool ShouldCompleteOnResponseStarting(HttpContext context)
{
if (_options.CompleteUnitOfWorkOnResponseStarting)
{
return true;
}
foreach (var url in _options.CompleteUnitOfWorkOnResponseStartingUrls)
{
if (string.IsNullOrWhiteSpace(url))
{
continue;
}
// Normalize a trailing slash ("/connect/" behaves like "/connect") and ignore non-absolute entries.
var prefix = url.TrimEnd('/');
if (!prefix.StartsWith("/", StringComparison.Ordinal))
{
continue;
}
// Match both the request path and the path base + path, so an absolute endpoint that includes
// the path base still matches when the path base is stripped from Request.Path.
if (context.Request.Path.StartsWithSegments(prefix, StringComparison.OrdinalIgnoreCase) ||
context.Request.PathBase.Add(context.Request.Path).StartsWithSegments(prefix, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
return _options.CompleteUnitOfWorkOnResponseStarting ||
(context.Request.Path.Value != null &&
_options.CompleteUnitOfWorkOnResponseStartingUrls.Any(x => context.Request.Path.Value.StartsWith(x, StringComparison.OrdinalIgnoreCase)));
}
protected async override Task<bool> ShouldSkipAsync(HttpContext context, RequestDelegate next)

3
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs

@ -164,9 +164,6 @@ public class AbpAspNetCoreMvcTestModule : AbpModule
app.UseStaticFiles();
app.UseAbpRequestLocalization();
app.UseAbpSecurityHeaders();
// Moves the "/pathbase-test" prefix into Request.PathBase so a unit of work opt-in test can
// exercise path base matching; a no-op for every other request.
app.UsePathBase("/pathbase-test");
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

30
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkMiddleware_Tests.cs

@ -119,34 +119,4 @@ public class UnitOfWorkMiddleware_Tests : AspNetCoreMvcTestBase
var result = await GetResponseAsStringAsync("/api/unitofwork-test/CommitBeforeResponseFlush");
result.ShouldBe("first:completed");
}
[Fact]
public async Task Opt_In_Url_With_A_Non_Segment_Prefix_Should_Not_Match()
{
Options.CompleteUnitOfWorkOnResponseStartingUrls.Add("/api/unitofwork-test/Commit");
var result = await GetResponseAsStringAsync("/api/unitofwork-test/CommitBeforeResponseFlush");
result.ShouldBe("first:not-completed");
}
[Fact]
public async Task Blank_Or_Malformed_Opt_In_Urls_Are_Ignored()
{
Options.CompleteUnitOfWorkOnResponseStartingUrls.Add("");
Options.CompleteUnitOfWorkOnResponseStartingUrls.Add(" ");
Options.CompleteUnitOfWorkOnResponseStartingUrls.Add("api/no-leading-slash");
var result = await GetResponseAsStringAsync("/api/unitofwork-test/CommitBeforeResponseFlush");
result.ShouldBe("first:not-completed");
}
[Fact]
public async Task Opt_In_Url_Including_The_Path_Base_Matches()
{
// The request path is "/api/..." (the path base is stripped), so this only matches via path base + path.
Options.CompleteUnitOfWorkOnResponseStartingUrls.Add("/pathbase-test/api/unitofwork-test/CommitBeforeResponseFlush");
var result = await GetResponseAsStringAsync("/pathbase-test/api/unitofwork-test/CommitBeforeResponseFlush");
result.ShouldBe("first:completed");
}
}

43
modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs

@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Server;
using Volo.Abp.AspNetCore.MultiTenancy;
@ -51,40 +48,12 @@ public class AbpOpenIddictAspNetCoreModule : AbpModule
options.RemoveClientIdClaim();
});
// Complete data written while processing OpenIddict requests before the response starts.
// Derived from the configured OpenIddict server endpoint paths (including the device endpoint).
context.Services.AddOptions<AbpAspNetCoreUnitOfWorkOptions>()
.Configure<IOptions<OpenIddictServerOptions>>((uowOptions, serverOptions) =>
{
foreach (var path in GetServerEndpointPaths(serverOptions.Value))
{
if (!uowOptions.CompleteUnitOfWorkOnResponseStartingUrls.Contains(path))
{
uowOptions.CompleteUnitOfWorkOnResponseStartingUrls.Add(path);
}
}
});
}
private static IEnumerable<string> GetServerEndpointPaths(OpenIddictServerOptions serverOptions)
{
var endpoints = serverOptions.TokenEndpointUris
.Concat(serverOptions.AuthorizationEndpointUris)
.Concat(serverOptions.DeviceAuthorizationEndpointUris)
.Concat(serverOptions.PushedAuthorizationEndpointUris)
.Concat(serverOptions.EndSessionEndpointUris)
.Concat(serverOptions.RevocationEndpointUris)
.Concat(serverOptions.EndUserVerificationEndpointUris);
foreach (var uri in endpoints)
// Complete tokens/authorizations/sessions written while processing OpenIddict requests before the response starts.
Configure<AbpAspNetCoreUnitOfWorkOptions>(options =>
{
// Normalize the (usually relative) endpoint URI to an absolute path, e.g. "connect/token" -> "/connect/token".
var path = uri.IsAbsoluteUri ? uri.AbsolutePath : "/" + uri.OriginalString.RemovePreFix("./").TrimStart('/');
if (path.Length > 1)
{
yield return path;
}
}
options.CompleteUnitOfWorkOnResponseStartingUrls.AddIfNotContains("/connect");
options.CompleteUnitOfWorkOnResponseStartingUrls.AddIfNotContains("/device");
});
}
private void AddOpenIddictServer(IServiceCollection services)

2
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenEndpoint_Integration_Tests.cs

@ -34,7 +34,7 @@ public class OpenIddictTokenEndpoint_Integration_Tests : AbpWebApplicationFactor
[Fact]
public async Task Token_Row_Is_Committed_Before_The_Connect_Token_Response_Is_Sent()
{
// The OpenIddict module opts its endpoint paths (including "/connect/token") in by default.
// The OpenIddict module opts "/connect" in by default.
var response = await RequestTokenAsync();
response.StatusCode.ShouldBe(HttpStatusCode.OK);

Loading…
Cancel
Save