From 1f5cecb91685094819a3b8f485a4450743598b58 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 1 Apr 2024 21:01:36 +0800 Subject: [PATCH] More tests --- src/Aspire.Hosting/Dcp/ApplicationExecutor.cs | 14 ++- .../DistributedApplicationBuilder.cs | 22 +++-- .../DistributedApplicationLifecycle.cs | 4 +- src/Shared/KnownConfigNames.cs | 10 +- .../Dcp/ApplicationExecutorTests.cs | 52 ++++++++++- .../DistributedApplicationTests.cs | 93 +++++++++++++++++-- 6 files changed, 167 insertions(+), 28 deletions(-) diff --git a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs index 310c0b37c58..132fae6b458 100644 --- a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs +++ b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs @@ -780,11 +780,19 @@ private async Task>> GetDashboardEnvironmentVa KeyValuePair.Create(DashboardConfigNames.ResourceServiceUrlName.EnvVarName, resourceServiceUrl), KeyValuePair.Create(DashboardConfigNames.DashboardOtlpUrlName.EnvVarName, otlpEndpointUrl), KeyValuePair.Create(DashboardConfigNames.ResourceServiceAuthModeName.EnvVarName, "Unsecured"), - KeyValuePair.Create(DashboardConfigNames.DashboardFrontendAuthModeName.EnvVarName, "BrowserToken"), - KeyValuePair.Create(DashboardConfigNames.DashboardFrontendBrowserTokenName.EnvVarName, configuration["AppHost:BrowserToken"]!), }; - if (configuration["AppHost:OtlpApiKey"] is { } otlpApiKey) + if (configuration["AppHost:BrowserToken"] is { Length: > 0 } browserToken) + { + env.Add(KeyValuePair.Create(DashboardConfigNames.DashboardFrontendAuthModeName.EnvVarName, "BrowserToken")); + env.Add(KeyValuePair.Create(DashboardConfigNames.DashboardFrontendBrowserTokenName.EnvVarName, browserToken)); + } + else + { + env.Add(KeyValuePair.Create(DashboardConfigNames.DashboardFrontendAuthModeName.EnvVarName, "Unsecured")); + } + + if (configuration["AppHost:OtlpApiKey"] is { Length: > 0 } otlpApiKey) { env.Add(KeyValuePair.Create(DashboardConfigNames.DashboardOtlpAuthModeName.EnvVarName, "ApiKey")); env.Add(KeyValuePair.Create(DashboardConfigNames.DashboardOtlpPrimaryApiKeyName.EnvVarName, otlpApiKey)); diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index 0c338160625..940e418ab0f 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -28,8 +28,6 @@ public class DistributedApplicationBuilder : IDistributedApplicationBuilder private const string BuilderConstructingEventName = "DistributedApplicationBuilderConstructing"; private const string BuilderConstructedEventName = "DistributedApplicationBuilderConstructed"; - private const string DisableOtlpApiKeyAuthKey = "DOTNET_DISABLE_OTLP_API_KEY_AUTH"; - private readonly HostApplicationBuilder _innerBuilder; /// @@ -87,25 +85,29 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) // Make the app host directory available to the application via configuration ["AppHost:Directory"] = AppHostDirectory }); - if (!IsOtlpApiKeyAuthDisabled(_innerBuilder.Configuration)) + + if (!options.DisableDashboard && !IsDashboardUnsecured(_innerBuilder.Configuration)) { // Set a random API key for the OTLP exporter. // Passed to apps as a standard OTEL attribute to include in OTLP requests and the dashboard to validate. _innerBuilder.Configuration.AddInMemoryCollection( new Dictionary { - ["AppHost:OtlpApiKey"] = Guid.NewGuid().ToString() + ["AppHost:OtlpApiKey"] = TokenGenerator.GetToken() } ); - } - if (!options.DisableDashboard) - { + + if (_innerBuilder.Configuration[KnownConfigNames.DashboardFrontendBrowserToken] is not { Length: > 0 } browserToken) + { + browserToken = TokenGenerator.GetToken(); + } + // Set a random API key for the OTLP exporter. // Passed to apps as a standard OTEL attribute to include in OTLP requests and the dashboard to validate. _innerBuilder.Configuration.AddInMemoryCollection( new Dictionary { - ["AppHost:BrowserToken"] = Guid.NewGuid().ToString() + ["AppHost:BrowserToken"] = browserToken } ); } @@ -158,9 +160,9 @@ private void MapTransportOptionsFromCustomKeys(TransportOptions options) } } - private static bool IsOtlpApiKeyAuthDisabled(IConfiguration configuration) + private static bool IsDashboardUnsecured(IConfiguration configuration) { - return configuration.GetBool(DisableOtlpApiKeyAuthKey) ?? false; + return configuration.GetBool(KnownConfigNames.DashboardUnsecuredAllowAnonymous) ?? false; } private void ConfigurePublishingOptions(DistributedApplicationOptions options) diff --git a/src/Aspire.Hosting/DistributedApplicationLifecycle.cs b/src/Aspire.Hosting/DistributedApplicationLifecycle.cs index 334117282b5..3292b815609 100644 --- a/src/Aspire.Hosting/DistributedApplicationLifecycle.cs +++ b/src/Aspire.Hosting/DistributedApplicationLifecycle.cs @@ -21,9 +21,9 @@ public Task StartAsync(CancellationToken cancellationToken) public Task StartedAsync(CancellationToken cancellationToken) { - if (distributedApplicationOptions.DashboardEnabled) + if (distributedApplicationOptions.DashboardEnabled && configuration["AppHost:BrowserToken"] is { Length: > 0 } browserToken) { - LoggingHelpers.WriteDashboardUrl(logger, configuration["ASPNETCORE_URLS"], configuration["AppHost:BrowserToken"]); + LoggingHelpers.WriteDashboardUrl(logger, configuration["ASPNETCORE_URLS"], browserToken); } if (executionContext.IsRunMode) diff --git a/src/Shared/KnownConfigNames.cs b/src/Shared/KnownConfigNames.cs index 3e7efd5ac35..f18a929c347 100644 --- a/src/Shared/KnownConfigNames.cs +++ b/src/Shared/KnownConfigNames.cs @@ -5,8 +5,10 @@ namespace Aspire.Hosting; internal static class KnownConfigNames { - public static string AspNetCoreUrls = "ASPNETCORE_URLS"; - public static string AllowUnsecuredTransport = "ASPIRE_ALLOW_UNSECURED_TRANSPORT"; - public static string DashboardOtlpEndpointUrl = "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"; - public static string ResourceServiceEndpointUrl = "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL"; + public const string AspNetCoreUrls = "ASPNETCORE_URLS"; + public const string AllowUnsecuredTransport = "ASPIRE_ALLOW_UNSECURED_TRANSPORT"; + public const string DashboardOtlpEndpointUrl = "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"; + public const string DashboardFrontendBrowserToken = "DOTNET_DASHBOARD_FRONTEND_BROWSERTOKEN"; + public const string DashboardUnsecuredAllowAnonymous = "DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS"; + public const string ResourceServiceEndpointUrl = "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL"; } diff --git a/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs index 62783050d3b..0ff69cd516e 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs @@ -31,6 +31,54 @@ public async Task RunApplicationAsync_NoResources_DashboardStarted() Assert.Equal("aspire-dashboard", dashboard.Metadata.Name); } + [Fact] + public async Task RunApplicationAsync_AuthConfigured_EnvVarsPresent() + { + // Arrange + var distributedAppModel = new DistributedApplicationModel(new ResourceCollection()); + var kubernetesService = new MockKubernetesService(); + + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + + // Act + await appExecutor.RunApplicationAsync(); + + // Assert + var dashboard = Assert.IsType(Assert.Single(kubernetesService.CreatedResources)); + Assert.NotNull(dashboard.Spec.Env); + + Assert.Equal("BrowserToken", dashboard.Spec.Env.Single(e => e.Name == DashboardConfigNames.DashboardFrontendAuthModeName.EnvVarName).Value); + Assert.Equal("TestBrowserToken!", dashboard.Spec.Env.Single(e => e.Name == DashboardConfigNames.DashboardFrontendBrowserTokenName.EnvVarName).Value); + + Assert.Equal("ApiKey", dashboard.Spec.Env.Single(e => e.Name == DashboardConfigNames.DashboardOtlpAuthModeName.EnvVarName).Value); + Assert.Equal("TestOtlpApiKey!", dashboard.Spec.Env.Single(e => e.Name == DashboardConfigNames.DashboardOtlpPrimaryApiKeyName.EnvVarName).Value); + } + + [Fact] + public async Task RunApplicationAsync_AuthRemoved_EnvVarsUnsecured() + { + // Arrange + var distributedAppModel = new DistributedApplicationModel(new ResourceCollection()); + var kubernetesService = new MockKubernetesService(); + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(new Dictionary + { + ["DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"] = "http://localhost" + }); + + var appExecutor = CreateAppExecutor(distributedAppModel, configuration: builder.Build(), kubernetesService: kubernetesService); + + // Act + await appExecutor.RunApplicationAsync(); + + // Assert + var dashboard = Assert.IsType(Assert.Single(kubernetesService.CreatedResources)); + Assert.NotNull(dashboard.Spec.Env); + + Assert.Equal("Unsecured", dashboard.Spec.Env.Single(e => e.Name == DashboardConfigNames.DashboardFrontendAuthModeName.EnvVarName).Value); + Assert.Equal("Unsecured", dashboard.Spec.Env.Single(e => e.Name == DashboardConfigNames.DashboardOtlpAuthModeName.EnvVarName).Value); + } + [Fact] public async Task ContainersArePassedOtelServiceName() { @@ -63,7 +111,9 @@ private static ApplicationExecutor CreateAppExecutor( var builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(new Dictionary { - ["DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"] = "http://localhost" + ["DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"] = "http://localhost", + ["AppHost:BrowserToken"] = "TestBrowserToken!", + ["AppHost:OtlpApiKey"] = "TestOtlpApiKey!" }); configuration = builder.Build(); diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 73e2cc01932..d542ee0f217 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -272,12 +272,6 @@ public async Task SpecifyingEnvPortInEndpointFlowsToEnv() var nodeApp = await KubernetesHelper.GetResourceByNameAsync(kubernetes, "nodeapp", r => r.Status?.EffectiveEnv is not null, token); Assert.NotNull(nodeApp); - string? GetEnv(IEnumerable? envVars, string name) - { - Assert.NotNull(envVars); - return Assert.Single(envVars.Where(e => e.Name == name)).Value; - }; - Assert.Equal("redis:latest", redisContainer.Spec.Image); Assert.Equal("{{- portForServing \"redis0\" }}", GetEnv(redisContainer.Spec.Env, "REDIS_PORT")); Assert.Equal("6379", GetEnv(redisContainer.Status!.EffectiveEnv, "REDIS_PORT")); @@ -293,6 +287,89 @@ public async Task SpecifyingEnvPortInEndpointFlowsToEnv() Assert.NotEqual(0, int.Parse(nodeAppPortValue, CultureInfo.InvariantCulture)); await app.StopAsync(); + + static string? GetEnv(IEnumerable? envVars, string name) + { + Assert.NotNull(envVars); + return Assert.Single(envVars.Where(e => e.Name == name)).Value; + } + } + + [LocalOnlyFact("docker")] + public async Task StartAsync_DashboardAuthConfig_PassedToDashboardProcess() + { + var browserToken = "ThisIsATestToken"; + var args = new string[] { + "ASPNETCORE_URLS=http://localhost:0", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL=http://localhost:0", + $"DOTNET_DASHBOARD_FRONTEND_BROWSERTOKEN={browserToken}" + }; + using var testProgram = CreateTestProgram(args: args, disableDashboard: false); + + testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper)); + + await using var app = testProgram.Build(); + + var kubernetes = app.Services.GetRequiredService(); + + await app.StartAsync(); + + using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10)); + var token = cts.Token; + + var aspireDashboard = await KubernetesHelper.GetResourceByNameAsync(kubernetes, "aspire-dashboard", r => r.Status?.EffectiveEnv is not null, token); + Assert.NotNull(aspireDashboard); + + Assert.Equal("BrowserToken", GetEnv(aspireDashboard.Spec.Env, "DASHBOARD__FRONTEND__AUTHMODE")); + Assert.Equal("ThisIsATestToken", GetEnv(aspireDashboard.Spec.Env, "DASHBOARD__FRONTEND__BROWSERTOKEN")); + + Assert.Equal("ApiKey", GetEnv(aspireDashboard.Spec.Env, "DASHBOARD__OTLP__AUTHMODE")); + var keyBytes = Convert.FromHexString(GetEnv(aspireDashboard.Spec.Env, "DASHBOARD__OTLP__PRIMARYAPIKEY")!); + Assert.Equal(16, keyBytes.Length); + + await app.StopAsync(); + + static string? GetEnv(IEnumerable? envVars, string name) + { + Assert.NotNull(envVars); + return Assert.Single(envVars.Where(e => e.Name == name)).Value; + } + } + + [LocalOnlyFact("docker")] + public async Task StartAsync_UnsecuredAllowAnonymous_PassedToDashboardProcess() + { + var args = new string[] { + "ASPNETCORE_URLS=http://localhost:0", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL=http://localhost:0", + "DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true" + }; + using var testProgram = CreateTestProgram(args: args, disableDashboard: false); + + testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper)); + + await using var app = testProgram.Build(); + + var kubernetes = app.Services.GetRequiredService(); + + await app.StartAsync(); + + using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10)); + var token = cts.Token; + + var aspireDashboard = await KubernetesHelper.GetResourceByNameAsync(kubernetes, "aspire-dashboard", r => r.Status?.EffectiveEnv is not null, token); + Assert.NotNull(aspireDashboard); + + Assert.Equal("Unsecured", GetEnv(aspireDashboard.Spec.Env, "DASHBOARD__FRONTEND__AUTHMODE")); + Assert.Equal("Unsecured", GetEnv(aspireDashboard.Spec.Env, "DASHBOARD__OTLP__AUTHMODE")); + + await app.StopAsync(); + + static string? GetEnv(IEnumerable? envVars, string name) + { + Assert.NotNull(envVars); + return Assert.Single(envVars.Where(e => e.Name == name)).Value; + } } [LocalOnlyFact("docker")] @@ -716,6 +793,6 @@ public async Task AfterResourcesCreatedAsync(DistributedApplicationModel appMode } } - private static TestProgram CreateTestProgram(string[]? args = null, bool includeIntegrationServices = false, bool includeNodeApp = false) => - TestProgram.Create(args, includeIntegrationServices: includeIntegrationServices, includeNodeApp: includeNodeApp); + private static TestProgram CreateTestProgram(string[]? args = null, bool includeIntegrationServices = false, bool includeNodeApp = false, bool disableDashboard = true) => + TestProgram.Create(args, includeIntegrationServices: includeIntegrationServices, includeNodeApp: includeNodeApp, disableDashboard: disableDashboard); }