Skip to content

Commit a009e6c

Browse files
committed
Add support for UsePathBase in actuators
1 parent c853b47 commit a009e6c

File tree

9 files changed

+126
-15
lines changed

9 files changed

+126
-15
lines changed

src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryEndpointHandler.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information.
44

5+
using Microsoft.AspNetCore.Http;
56
using Microsoft.Extensions.Logging;
67
using Microsoft.Extensions.Options;
78
using Steeltoe.Common;
@@ -19,25 +20,28 @@ internal sealed class CloudFoundryEndpointHandler : ICloudFoundryEndpointHandler
1920
{
2021
private readonly IOptionsMonitor<ManagementOptions> _managementOptionsMonitor;
2122
private readonly IOptionsMonitor<CloudFoundryEndpointOptions> _endpointOptionsMonitor;
23+
private readonly IHttpContextAccessor _httpContextAccessor;
2224
private readonly IEndpointOptionsMonitorProvider[] _optionsMonitorProviderArray;
2325
private readonly ILogger<HypermediaService> _hypermediaServiceLogger;
2426

2527
public EndpointOptions Options => _endpointOptionsMonitor.CurrentValue;
2628

2729
public CloudFoundryEndpointHandler(IOptionsMonitor<ManagementOptions> managementOptionsMonitor,
2830
IOptionsMonitor<CloudFoundryEndpointOptions> endpointOptionsMonitor, IEnumerable<IEndpointOptionsMonitorProvider> endpointOptionsMonitorProviders,
29-
ILoggerFactory loggerFactory)
31+
IHttpContextAccessor httpContextAccessor, ILoggerFactory loggerFactory)
3032
{
3133
ArgumentNullException.ThrowIfNull(managementOptionsMonitor);
3234
ArgumentNullException.ThrowIfNull(endpointOptionsMonitor);
3335
ArgumentNullException.ThrowIfNull(endpointOptionsMonitorProviders);
36+
ArgumentNullException.ThrowIfNull(httpContextAccessor);
3437
ArgumentNullException.ThrowIfNull(loggerFactory);
3538

3639
IEndpointOptionsMonitorProvider[] optionsMonitorProviderArray = endpointOptionsMonitorProviders.ToArray();
3740
ArgumentGuard.ElementsNotNull(optionsMonitorProviderArray);
3841

3942
_managementOptionsMonitor = managementOptionsMonitor;
4043
_endpointOptionsMonitor = endpointOptionsMonitor;
44+
_httpContextAccessor = httpContextAccessor;
4145
_optionsMonitorProviderArray = optionsMonitorProviderArray;
4246
_hypermediaServiceLogger = loggerFactory.CreateLogger<HypermediaService>();
4347
}
@@ -46,8 +50,8 @@ public async Task<Links> InvokeAsync(string baseUrl, CancellationToken cancellat
4650
{
4751
ArgumentException.ThrowIfNullOrWhiteSpace(baseUrl);
4852

49-
var hypermediaService =
50-
new HypermediaService(_managementOptionsMonitor, _endpointOptionsMonitor, _optionsMonitorProviderArray, _hypermediaServiceLogger);
53+
var hypermediaService = new HypermediaService(_managementOptionsMonitor, _endpointOptionsMonitor, _optionsMonitorProviderArray, _httpContextAccessor,
54+
_hypermediaServiceLogger);
5155

5256
Links result = hypermediaService.Invoke(new Uri(baseUrl));
5357
return await Task.FromResult(result);

src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryEndpointMiddleware.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ internal sealed class CloudFoundryEndpointMiddleware(
2828
? headerScheme.ToString()
2929
: httpContext.Request.Scheme;
3030

31-
string uri = $"{scheme}://{httpContext.Request.Host}{httpContext.Request.PathBase}{httpContext.Request.Path}";
32-
return Task.FromResult<string?>(uri);
31+
string requestUri = $"{scheme}://{httpContext.Request.Host}{httpContext.Request.Path}";
32+
return Task.FromResult<string?>(requestUri);
3333
}
3434

3535
protected override async Task<Links> InvokeEndpointHandlerAsync(string? uri, CancellationToken cancellationToken)

src/Management/src/Endpoint/Actuators/CloudFoundry/EndpointServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public static IServiceCollection AddCloudFoundryActuator(this IServiceCollection
4343
{
4444
ArgumentNullException.ThrowIfNull(services);
4545

46+
services.AddHttpContextAccessor();
47+
4648
services.AddCoreActuatorServices<CloudFoundryEndpointOptions, ConfigureCloudFoundryEndpointOptions, CloudFoundryEndpointMiddleware,
4749
ICloudFoundryEndpointHandler, CloudFoundryEndpointHandler, string, Links>(configureMiddleware);
4850

src/Management/src/Endpoint/Actuators/Hypermedia/EndpointServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public static IServiceCollection AddHypermediaActuator(this IServiceCollection s
3939
{
4040
ArgumentNullException.ThrowIfNull(services);
4141

42+
services.AddHttpContextAccessor();
43+
4244
services.AddCoreActuatorServices<HypermediaEndpointOptions, ConfigureHypermediaEndpointOptions, HypermediaEndpointMiddleware,
4345
IHypermediaEndpointHandler, HypermediaEndpointHandler, string, Links>(configureMiddleware);
4446

src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaEndpointHandler.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information.
44

5+
using Microsoft.AspNetCore.Http;
56
using Microsoft.Extensions.Logging;
67
using Microsoft.Extensions.Options;
78
using Steeltoe.Common;
@@ -17,25 +18,28 @@ internal sealed class HypermediaEndpointHandler : IHypermediaEndpointHandler
1718
{
1819
private readonly IOptionsMonitor<ManagementOptions> _managementOptionsMonitor;
1920
private readonly IOptionsMonitor<HypermediaEndpointOptions> _endpointOptionsMonitor;
21+
private readonly IHttpContextAccessor _httpContextAccessor;
2022
private readonly IEndpointOptionsMonitorProvider[] _endpointOptionsMonitorProviderArray;
2123
private readonly ILogger<HypermediaService> _hypermediaServiceLogger;
2224

2325
public EndpointOptions Options => _endpointOptionsMonitor.CurrentValue;
2426

2527
public HypermediaEndpointHandler(IOptionsMonitor<ManagementOptions> managementOptionsMonitor,
2628
IOptionsMonitor<HypermediaEndpointOptions> endpointOptionsMonitor, IEnumerable<IEndpointOptionsMonitorProvider> endpointOptionsMonitorProviders,
27-
ILoggerFactory loggerFactory)
29+
IHttpContextAccessor httpContextAccessor, ILoggerFactory loggerFactory)
2830
{
2931
ArgumentNullException.ThrowIfNull(managementOptionsMonitor);
3032
ArgumentNullException.ThrowIfNull(endpointOptionsMonitor);
3133
ArgumentNullException.ThrowIfNull(endpointOptionsMonitorProviders);
34+
ArgumentNullException.ThrowIfNull(httpContextAccessor);
3235
ArgumentNullException.ThrowIfNull(loggerFactory);
3336

3437
IEndpointOptionsMonitorProvider[] endpointOptionsMonitorProviderArray = endpointOptionsMonitorProviders.ToArray();
3538
ArgumentGuard.ElementsNotNull(endpointOptionsMonitorProviderArray);
3639

3740
_managementOptionsMonitor = managementOptionsMonitor;
3841
_endpointOptionsMonitor = endpointOptionsMonitor;
42+
_httpContextAccessor = httpContextAccessor;
3943
_endpointOptionsMonitorProviderArray = endpointOptionsMonitorProviderArray;
4044
_hypermediaServiceLogger = loggerFactory.CreateLogger<HypermediaService>();
4145
}
@@ -44,7 +48,9 @@ public Task<Links> InvokeAsync(string baseUrl, CancellationToken cancellationTok
4448
{
4549
ArgumentException.ThrowIfNullOrWhiteSpace(baseUrl);
4650

47-
var service = new HypermediaService(_managementOptionsMonitor, _endpointOptionsMonitor, _endpointOptionsMonitorProviderArray, _hypermediaServiceLogger);
51+
var service = new HypermediaService(_managementOptionsMonitor, _endpointOptionsMonitor, _endpointOptionsMonitorProviderArray, _httpContextAccessor,
52+
_hypermediaServiceLogger);
53+
4854
Links result = service.Invoke(new Uri(baseUrl));
4955
return Task.FromResult(result);
5056
}

src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaEndpointMiddleware.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,7 @@ internal sealed class HypermediaEndpointMiddleware(
2323
? headerScheme.ToString()
2424
: httpContext.Request.Scheme;
2525

26-
// request.Host automatically includes or excludes the port based on whether it is standard for the scheme
27-
// ... except when we manually change the scheme to match the X-Forwarded-Proto
28-
string requestUri = scheme == "https" && httpContext.Request.Host.Port == 443
29-
? $"{scheme}://{httpContext.Request.Host.Host}{httpContext.Request.PathBase}{httpContext.Request.Path}"
30-
: $"{scheme}://{httpContext.Request.Host}{httpContext.Request.PathBase}{httpContext.Request.Path}";
31-
26+
string requestUri = $"{scheme}://{httpContext.Request.Host}{httpContext.Request.Path}";
3227
return Task.FromResult<string?>(requestUri);
3328
}
3429

src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaService.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information.
44

5+
using Microsoft.AspNetCore.Http;
56
using Microsoft.Extensions.Logging;
67
using Microsoft.Extensions.Options;
78
using Steeltoe.Common;
@@ -16,37 +17,44 @@ internal sealed class HypermediaService
1617
private readonly IOptionsMonitor<ManagementOptions> _managementOptionsMonitor;
1718
private readonly EndpointOptions _endpointOptions;
1819
private readonly ICollection<IEndpointOptionsMonitorProvider> _endpointOptionsMonitorProviders;
20+
private readonly IHttpContextAccessor _httpContextAccessor;
1921
private readonly ILogger<HypermediaService> _logger;
2022

2123
public HypermediaService(IOptionsMonitor<ManagementOptions> managementOptionsMonitor,
2224
IOptionsMonitor<HypermediaEndpointOptions> hypermediaEndpointOptionsMonitor,
23-
ICollection<IEndpointOptionsMonitorProvider> endpointOptionsMonitorProviders, ILogger<HypermediaService> logger)
25+
ICollection<IEndpointOptionsMonitorProvider> endpointOptionsMonitorProviders, IHttpContextAccessor httpContextAccessor,
26+
ILogger<HypermediaService> logger)
2427
{
2528
ArgumentNullException.ThrowIfNull(managementOptionsMonitor);
2629
ArgumentNullException.ThrowIfNull(hypermediaEndpointOptionsMonitor);
2730
ArgumentNullException.ThrowIfNull(endpointOptionsMonitorProviders);
31+
ArgumentNullException.ThrowIfNull(httpContextAccessor);
2832
ArgumentGuard.ElementsNotNull(endpointOptionsMonitorProviders);
2933
ArgumentNullException.ThrowIfNull(logger);
3034

3135
_managementOptionsMonitor = managementOptionsMonitor;
3236
_endpointOptions = hypermediaEndpointOptionsMonitor.CurrentValue;
3337
_endpointOptionsMonitorProviders = endpointOptionsMonitorProviders;
38+
_httpContextAccessor = httpContextAccessor;
3439
_logger = logger;
3540
}
3641

3742
public HypermediaService(IOptionsMonitor<ManagementOptions> managementOptionsMonitor,
3843
IOptionsMonitor<CloudFoundryEndpointOptions> cloudFoundryEndpointOptionsMonitor,
39-
ICollection<IEndpointOptionsMonitorProvider> endpointOptionsMonitorProviders, ILogger<HypermediaService> logger)
44+
ICollection<IEndpointOptionsMonitorProvider> endpointOptionsMonitorProviders, IHttpContextAccessor httpContextAccessor,
45+
ILogger<HypermediaService> logger)
4046
{
4147
ArgumentNullException.ThrowIfNull(managementOptionsMonitor);
4248
ArgumentNullException.ThrowIfNull(cloudFoundryEndpointOptionsMonitor);
4349
ArgumentNullException.ThrowIfNull(endpointOptionsMonitorProviders);
50+
ArgumentNullException.ThrowIfNull(httpContextAccessor);
4451
ArgumentGuard.ElementsNotNull(endpointOptionsMonitorProviders);
4552
ArgumentNullException.ThrowIfNull(logger);
4653

4754
_managementOptionsMonitor = managementOptionsMonitor;
4855
_endpointOptions = cloudFoundryEndpointOptionsMonitor.CurrentValue;
4956
_endpointOptionsMonitorProviders = endpointOptionsMonitorProviders;
57+
_httpContextAccessor = httpContextAccessor;
5058
_logger = logger;
5159
}
5260

@@ -68,6 +76,11 @@ public Links Invoke(Uri baseUrl)
6876
bool skipExposureCheck = PermissionsProvider.IsCloudFoundryRequest(baseUrl.PathAndQuery);
6977
string? basePath = managementOptions.GetBasePath(baseUrl.AbsolutePath);
7078

79+
if (_httpContextAccessor.HttpContext?.Request != null)
80+
{
81+
basePath = $"{_httpContextAccessor.HttpContext.Request.PathBase}{basePath}";
82+
}
83+
7184
foreach (EndpointOptions endpointOptions in _endpointOptionsMonitorProviders.Select(provider => provider.Get()).OrderBy(options => options.Id))
7285
{
7386
if (endpointOptions.Id == null || !endpointOptions.IsEnabled(managementOptions))

src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundryActuatorTest.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,57 @@ public async Task Hides_disabled_actuators_and_ignores_exposure()
421421
""");
422422
}
423423

424+
[Fact]
425+
public async Task Ignores_exposure_with_UsePathBase()
426+
{
427+
using var scope = new EnvironmentVariableScope("VCAP_APPLICATION", VcapApplicationForMock);
428+
429+
var appSettings = new Dictionary<string, string?>
430+
{
431+
["Management:Endpoints:Actuator:Exposure:Include:0"] = "*",
432+
["Management:Endpoints:Actuator:Exposure:Exclude:1"] = "loggers"
433+
};
434+
435+
WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create();
436+
builder.Configuration.AddCloudFoundry();
437+
builder.Configuration.AddInMemoryCollection(appSettings);
438+
builder.Services.AddCloudFoundryActuator(false);
439+
builder.Services.AddLoggersActuator(false);
440+
441+
await using WebApplication host = builder.Build();
442+
443+
host.UsePathBase("/some/prefix");
444+
host.UseRouting();
445+
host.UseCloudFoundrySecurity();
446+
host.UseActuatorEndpoints();
447+
448+
host.Services.GetRequiredService<HttpClientHandlerFactory>().Using(CloudControllerPermissionsMock.GetHttpMessageHandler());
449+
await host.StartAsync(TestContext.Current.CancellationToken);
450+
using HttpClient httpClient = host.GetTestClient();
451+
452+
HttpResponseMessage response = await AuthenticatedGetAsync(httpClient, new Uri("http://localhost/some/prefix/cloudfoundryapplication"));
453+
454+
response.StatusCode.Should().Be(HttpStatusCode.OK);
455+
456+
string responseBody = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
457+
458+
responseBody.Should().BeJson("""
459+
{
460+
"type": "steeltoe",
461+
"_links": {
462+
"loggers": {
463+
"href": "http://localhost/some/prefix/cloudfoundryapplication/loggers",
464+
"templated": false
465+
},
466+
"self": {
467+
"href": "http://localhost/some/prefix/cloudfoundryapplication",
468+
"templated": false
469+
}
470+
}
471+
}
472+
""");
473+
}
474+
424475
[Theory]
425476
[InlineData("http://somehost:1234", "https://somehost:1234", "https")]
426477
[InlineData("http://somehost:443", "https://somehost", "https")]

src/Management/test/Endpoint.Test/Actuators/Hypermedia/HypermediaActuatorTest.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,44 @@ public async Task Can_use_slash_as_management_path()
370370
""");
371371
}
372372

373+
[Fact]
374+
public async Task Includes_base_path_from_UsePathBase_in_hypermedia_links()
375+
{
376+
WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create();
377+
builder.Services.AddHypermediaActuator(false);
378+
builder.Services.AddInfoActuator(false);
379+
await using WebApplication host = builder.Build();
380+
381+
host.UsePathBase("/some/prefix");
382+
host.UseRouting();
383+
host.UseActuatorEndpoints();
384+
385+
await host.StartAsync(TestContext.Current.CancellationToken);
386+
using HttpClient httpClient = host.GetTestClient();
387+
388+
HttpResponseMessage response = await httpClient.GetAsync(new Uri("http://localhost/some/prefix/actuator"), TestContext.Current.CancellationToken);
389+
390+
response.StatusCode.Should().Be(HttpStatusCode.OK);
391+
392+
string responseBody = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
393+
394+
responseBody.Should().BeJson("""
395+
{
396+
"type": "steeltoe",
397+
"_links": {
398+
"info": {
399+
"href": "http://localhost/some/prefix/actuator/info",
400+
"templated": false
401+
},
402+
"self": {
403+
"href": "http://localhost/some/prefix/actuator",
404+
"templated": false
405+
}
406+
}
407+
}
408+
""");
409+
}
410+
373411
[Theory]
374412
[InlineData("http://somehost:1234", "https://somehost:1234", "https")]
375413
[InlineData("http://somehost:443", "https://somehost", "https")]

0 commit comments

Comments
 (0)