From 7c6525133ad8c57c34f97a2e6f73bdbbeff955f3 Mon Sep 17 00:00:00 2001 From: Eddie Wassef Date: Tue, 17 Dec 2024 21:38:08 -0600 Subject: [PATCH 1/4] Adding basepath to the config This will allow the aspire app to handle running behind a reverse proxy such as a k8s ingress on a subpath https://github.com/dotnet/aspire/issues/4159 https://github.com/dotnet/aspire/issues/4528 https://github.com/dotnet/aspire/issues/5134 --- .../Configuration/DashboardOptions.cs | 1 + .../Configuration/ValidateDashboardOptions.cs | 15 ++++++++++ .../DashboardEndpointsBuilder.cs | 12 ++++---- .../DashboardWebApplication.cs | 6 +++- .../Model/ValidateTokenMiddleware.cs | 2 +- src/Aspire.Dashboard/README.md | 2 ++ src/Aspire.Dashboard/Utils/DashboardUrls.cs | 28 +++++++++++-------- src/Shared/LoggingHelpers.cs | 3 +- 8 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs index 110a45be09..acf4f87eaf 100644 --- a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs +++ b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs @@ -12,6 +12,7 @@ namespace Aspire.Dashboard.Configuration; public sealed class DashboardOptions { public string? ApplicationName { get; set; } + public string PathBase { get; set; } = "/"; public OtlpOptions Otlp { get; set; } = new(); public FrontendOptions Frontend { get; set; } = new(); public ResourceServiceClientOptions ResourceServiceClient { get; set; } = new(); diff --git a/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs b/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs index 2ab97f3c60..9cced29276 100644 --- a/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs +++ b/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs @@ -144,6 +144,21 @@ public ValidateOptionsResult Validate(string? name, DashboardOptions options) } } + // Validate the Path base and make sure it starts and ends with a forward slash. + + if (options.PathBase != null) + { + if (!options.PathBase.StartsWith("/", StringComparison.Ordinal)) + { + errorMessages.Add($"{nameof(DashboardOptions.PathBase)} must start with a forward slash."); + } + + if (!options.PathBase.EndsWith("/", StringComparison.Ordinal)) + { + errorMessages.Add($"{nameof(DashboardOptions.PathBase)} must end with a forward slash."); + } + } + return errorMessages.Count > 0 ? ValidateOptionsResult.Fail(errorMessages) : ValidateOptionsResult.Success; diff --git a/src/Aspire.Dashboard/DashboardEndpointsBuilder.cs b/src/Aspire.Dashboard/DashboardEndpointsBuilder.cs index 9b8af28362..1d2394d49d 100644 --- a/src/Aspire.Dashboard/DashboardEndpointsBuilder.cs +++ b/src/Aspire.Dashboard/DashboardEndpointsBuilder.cs @@ -3,7 +3,7 @@ using Aspire.Dashboard.Configuration; using Aspire.Dashboard.Model; -using Aspire.Dashboard.Utils; +using Aspire.Dashboard.Utils; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Localization; @@ -18,28 +18,28 @@ public static void MapDashboardApi(this IEndpointRouteBuilder endpoints, Dashboa { if (dashboardOptions.Frontend.AuthMode == FrontendAuthMode.BrowserToken) { - endpoints.MapPost("/api/validatetoken", async (string token, HttpContext httpContext, IOptionsMonitor dashboardOptions) => + endpoints.MapPost($"{DashboardUrls.BasePath}api/validatetoken", async (string token, HttpContext httpContext, IOptionsMonitor dashboardOptions) => { return await ValidateTokenMiddleware.TryAuthenticateAsync(token, httpContext, dashboardOptions).ConfigureAwait(false); }); #if DEBUG // Available in local debug for testing. - endpoints.MapGet("/api/signout", async (HttpContext httpContext) => + endpoints.MapGet($"{DashboardUrls.BasePath}api/signout", async (HttpContext httpContext) => { await Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.SignOutAsync( httpContext, CookieAuthenticationDefaults.AuthenticationScheme).ConfigureAwait(false); - httpContext.Response.Redirect("/"); + httpContext.Response.Redirect($"{DashboardUrls.BasePath}"); }); #endif } else if (dashboardOptions.Frontend.AuthMode == FrontendAuthMode.OpenIdConnect) { - endpoints.MapPost("/authentication/logout", () => TypedResults.SignOut(authenticationSchemes: [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme])); + endpoints.MapPost($"{DashboardUrls.BasePath}authentication/logout", () => TypedResults.SignOut(authenticationSchemes: [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme])); } - endpoints.MapGet("/api/set-language", async (string? language, string? redirectUrl, [FromHeader(Name = "Accept-Language")] string? acceptLanguage, HttpContext httpContext) => + endpoints.MapGet($"{DashboardUrls.BasePath}api/set-language", async (string? language, string? redirectUrl, [FromHeader(Name = "Accept-Language")] string? acceptLanguage, HttpContext httpContext) => { if (string.IsNullOrEmpty(redirectUrl)) { diff --git a/src/Aspire.Dashboard/DashboardWebApplication.cs b/src/Aspire.Dashboard/DashboardWebApplication.cs index 24f1a7a7df..af48927c8a 100644 --- a/src/Aspire.Dashboard/DashboardWebApplication.cs +++ b/src/Aspire.Dashboard/DashboardWebApplication.cs @@ -172,6 +172,7 @@ public DashboardWebApplication( } else { + DashboardUrls.SetBasePath(dashboardOptions.PathBase); _validationFailures = Array.Empty(); } @@ -369,7 +370,7 @@ public DashboardWebApplication( // Configure the HTTP request pipeline. if (!_app.Environment.IsDevelopment()) { - _app.UseExceptionHandler("/error"); + _app.UseExceptionHandler($"{DashboardUrls.BasePath}error"); if (isAllHttps) { _app.UseHsts(); @@ -416,6 +417,9 @@ public DashboardWebApplication( _app.MapGrpcService(); _app.MapDashboardApi(dashboardOptions); + + _app.UsePathBase(dashboardOptions.PathBase); + } private ILogger GetLogger() diff --git a/src/Aspire.Dashboard/Model/ValidateTokenMiddleware.cs b/src/Aspire.Dashboard/Model/ValidateTokenMiddleware.cs index e741099f7b..8a93cb32e2 100644 --- a/src/Aspire.Dashboard/Model/ValidateTokenMiddleware.cs +++ b/src/Aspire.Dashboard/Model/ValidateTokenMiddleware.cs @@ -26,7 +26,7 @@ public ValidateTokenMiddleware(RequestDelegate next, IOptionsMonitor BasePath = pathBase; } diff --git a/src/Shared/LoggingHelpers.cs b/src/Shared/LoggingHelpers.cs index f6798a12c3..e465742d4c 100644 --- a/src/Shared/LoggingHelpers.cs +++ b/src/Shared/LoggingHelpers.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.Utils; +using Aspire.Dashboard.Utils; using Microsoft.Extensions.Logging; namespace Aspire.Hosting; @@ -20,7 +21,7 @@ public static void WriteDashboardUrl(ILogger logger, string? dashboardUrls, stri var message = !isContainer ? "Login to the dashboard at {DashboardLoginUrl}" : "Login to the dashboard at {DashboardLoginUrl}. The URL may need changes depending on how network access to the container is configured."; - logger.LogInformation(message, $"{firstDashboardUrl.GetLeftPart(UriPartial.Authority)}/login?t={token}"); + logger.LogInformation(message, $"{firstDashboardUrl.GetLeftPart(UriPartial.Authority)}{DashboardUrls.BasePath}login?t={token}"); } } } From c4c49760d33de02a7d27d91362abcfa760e4dffb Mon Sep 17 00:00:00 2001 From: Eddie Wassef Date: Wed, 18 Dec 2024 10:28:44 -0600 Subject: [PATCH 2/4] updating the logging helpers as they dont have refernce to the utility --- src/Aspire.Dashboard/DashboardWebApplication.cs | 2 +- src/Shared/LoggingHelpers.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Dashboard/DashboardWebApplication.cs b/src/Aspire.Dashboard/DashboardWebApplication.cs index af48927c8a..560dff6f74 100644 --- a/src/Aspire.Dashboard/DashboardWebApplication.cs +++ b/src/Aspire.Dashboard/DashboardWebApplication.cs @@ -336,7 +336,7 @@ public DashboardWebApplication( // https://learn.microsoft.com/dotnet/core/tools/dotnet-environment-variables#dotnet_running_in_container-and-dotnet_running_in_containers var isContainer = _app.Configuration.GetBool("DOTNET_RUNNING_IN_CONTAINER") ?? false; - LoggingHelpers.WriteDashboardUrl(_logger, frontendEndpointInfo.GetResolvedAddress(replaceIPAnyWithLocalhost: true), options.Frontend.BrowserToken, isContainer); + LoggingHelpers.WriteDashboardUrl(_logger, frontendEndpointInfo.GetResolvedAddress(replaceIPAnyWithLocalhost: true) + DashboardUrls.BasePath, options.Frontend.BrowserToken, isContainer); } } }); diff --git a/src/Shared/LoggingHelpers.cs b/src/Shared/LoggingHelpers.cs index e465742d4c..d22ce6978e 100644 --- a/src/Shared/LoggingHelpers.cs +++ b/src/Shared/LoggingHelpers.cs @@ -21,7 +21,7 @@ public static void WriteDashboardUrl(ILogger logger, string? dashboardUrls, stri var message = !isContainer ? "Login to the dashboard at {DashboardLoginUrl}" : "Login to the dashboard at {DashboardLoginUrl}. The URL may need changes depending on how network access to the container is configured."; - logger.LogInformation(message, $"{firstDashboardUrl.GetLeftPart(UriPartial.Authority)}{DashboardUrls.BasePath}login?t={token}"); + logger.LogInformation(message, $"{firstDashboardUrl.GetLeftPart(UriPartial.Path)}login?t={token}"); } } } From 4836ed3898fa909cb23fb14071930ba04f1abe6e Mon Sep 17 00:00:00 2001 From: Eddie Wassef Date: Sun, 22 Dec 2024 21:59:03 -0600 Subject: [PATCH 3/4] Adding tests --- .../DashboardOptionsTests.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/Aspire.Dashboard.Tests/DashboardOptionsTests.cs b/tests/Aspire.Dashboard.Tests/DashboardOptionsTests.cs index 64666c3d07..e63409dfab 100644 --- a/tests/Aspire.Dashboard.Tests/DashboardOptionsTests.cs +++ b/tests/Aspire.Dashboard.Tests/DashboardOptionsTests.cs @@ -276,4 +276,56 @@ public void OpenIdConnectOptions_NoUserNameClaimType() } #endregion + + #region Path Base options + + [Fact] + public void PathBaseOptions_InvalidPathBase() + { + var options = GetValidOptions(); + options.PathBase = "invalid"; + var result = new ValidateDashboardOptions().Validate(null, options); + Assert.False(result.Succeeded); + Assert.Equal("PathBase must start with a forward slash.; PathBase must end with a forward slash.", result.FailureMessage); + } + + [Fact] + public void PathBaseOptions_InvalidPathBase_Missing_TrailingSlash() + { + var options = GetValidOptions(); + options.PathBase = "/invalid"; + var result = new ValidateDashboardOptions().Validate(null, options); + Assert.False(result.Succeeded); + Assert.Equal("PathBase must end with a forward slash.", result.FailureMessage); + } + + [Fact] + public void PathBaseOptions_ValidPathBase() + { + var options = GetValidOptions(); + options.PathBase = "/valid/"; + var result = new ValidateDashboardOptions().Validate(null, options); + Assert.True(result.Succeeded); + } + + [Fact] + public void PathBaseOptions_Not_Set_PathBase_Defaults_To_Slash() + { + var options = GetValidOptions(); + Assert.Equal("/", options.PathBase); + var result = new ValidateDashboardOptions().Validate(null, options); + + Assert.True(result.Succeeded); + } + [Fact] + public void PathBaseOptions_InvalidPathBase_Missing_LeadingSlash() + { + var options = GetValidOptions(); + options.PathBase = "invalid/"; + var result = new ValidateDashboardOptions().Validate(null, options); + Assert.False(result.Succeeded); + Assert.Equal("PathBase must start with a forward slash.", result.FailureMessage); + } + + #endregion } From 7240866fd575fcffe7edeb5fe542a7af4e24710f Mon Sep 17 00:00:00 2001 From: Eddie Date: Tue, 24 Dec 2024 09:21:50 -0600 Subject: [PATCH 4/4] Update README.md rerunning the PR checks --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a83d15edc1..fc1d8d4149 100644 --- a/README.md +++ b/README.md @@ -58,4 +58,4 @@ This project has adopted the code of conduct defined by the [Contributor Covenan ## License -The code in this repo is licensed under the [MIT](LICENSE.TXT) license. +The code in this repo is licensed under the [MIT](LICENSE.TXT) license.