diff --git a/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Program.cs b/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Program.cs index 1934185e924..b6623f591b4 100644 --- a/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Program.cs +++ b/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Program.cs @@ -4,7 +4,8 @@ var builder = DistributedApplication.CreateBuilder(args); builder.AddProject("web") - .WithExternalHttpEndpoints(); + .WithExternalHttpEndpoints() + .WithReplicas(2); #if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging diff --git a/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Properties/launchSettings.json b/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Properties/launchSettings.json index 3b13d714895..6ee205e25ab 100644 --- a/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Properties/launchSettings.json +++ b/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Properties/launchSettings.json @@ -11,7 +11,8 @@ "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "https://localhost:16175", "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:17037", - "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" + "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true", + "DOTNET_DASHBOARD_CORS_ALLOWED_ORIGINS": "*" } }, "http": { @@ -25,7 +26,8 @@ "DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "http://localhost:16175", "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:17037", "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true", - "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true" + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", + "DOTNET_DASHBOARD_CORS_ALLOWED_ORIGINS": "*" } }, "generate-manifest": { diff --git a/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs b/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs index 8c6aba5c720..69fa9f2b7a2 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs @@ -178,38 +178,19 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource) { context.EnvironmentVariables[DashboardConfigNames.DashboardOtlpHttpUrlName.EnvVarName] = otlpHttpEndpointUrl; - var model = context.ExecutionContext.ServiceProvider.GetRequiredService(); - var allResourceEndpoints = model.Resources - .Where(r => !string.Equals(r.Name, KnownResourceNames.AspireDashboard, StringComparisons.ResourceName)) - .SelectMany(r => r.Annotations) - .OfType() - .ToList(); - - var corsOrigins = new HashSet(StringComparers.UrlHost); - foreach (var endpoint in allResourceEndpoints) - { - if (endpoint.UriScheme is "http" or "https") - { - // Prefer allocated endpoint over EndpointAnnotation.Port. - var origin = endpoint.AllocatedEndpoint?.UriString; - var targetOrigin = (endpoint.TargetPort != null) - ? $"{endpoint.UriScheme}://localhost:{endpoint.TargetPort}" - : null; + // Use explicitly defined allowed origins if configured. + var allowedOrigins = configuration[KnownConfigNames.DashboardCorsAllowedOrigins]; - if (origin != null) - { - corsOrigins.Add(origin); - } - if (targetOrigin != null) - { - corsOrigins.Add(targetOrigin); - } - } + // If allowed origins are not configured then calculate allowed origins from endpoints. + if (string.IsNullOrEmpty(allowedOrigins)) + { + var model = context.ExecutionContext.ServiceProvider.GetRequiredService(); + allowedOrigins = GetAllowedOriginsFromResourceEndpoints(model); } - if (corsOrigins.Count > 0) + if (!string.IsNullOrEmpty(allowedOrigins)) { - context.EnvironmentVariables[DashboardConfigNames.DashboardOtlpCorsAllowedOriginsKeyName.EnvVarName] = string.Join(',', corsOrigins); + context.EnvironmentVariables[DashboardConfigNames.DashboardOtlpCorsAllowedOriginsKeyName.EnvVarName] = allowedOrigins; context.EnvironmentVariables[DashboardConfigNames.DashboardOtlpCorsAllowedHeadersKeyName.EnvVarName] = "*"; } } @@ -266,6 +247,44 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource) })); } + private static string? GetAllowedOriginsFromResourceEndpoints(DistributedApplicationModel model) + { + var allResourceEndpoints = model.Resources + .Where(r => !string.Equals(r.Name, KnownResourceNames.AspireDashboard, StringComparisons.ResourceName)) + .SelectMany(r => r.Annotations) + .OfType() + .ToList(); + + var corsOrigins = new HashSet(StringComparers.UrlHost); + foreach (var endpoint in allResourceEndpoints) + { + if (endpoint.UriScheme is "http" or "https") + { + // Prefer allocated endpoint over EndpointAnnotation.Port. + var origin = endpoint.AllocatedEndpoint?.UriString; + var targetOrigin = (endpoint.TargetPort != null) + ? $"{endpoint.UriScheme}://localhost:{endpoint.TargetPort}" + : null; + + if (origin != null) + { + corsOrigins.Add(origin); + } + if (targetOrigin != null) + { + corsOrigins.Add(targetOrigin); + } + } + } + + if (corsOrigins.Count > 0) + { + return string.Join(',', corsOrigins); + } + + return null; + } + private async Task WatchDashboardLogsAsync(CancellationToken cancellationToken) { var loggerCache = new ConcurrentDictionary(StringComparer.Ordinal); diff --git a/src/Shared/KnownConfigNames.cs b/src/Shared/KnownConfigNames.cs index 489f420343a..231002cc477 100644 --- a/src/Shared/KnownConfigNames.cs +++ b/src/Shared/KnownConfigNames.cs @@ -12,5 +12,6 @@ internal static class KnownConfigNames public const string DashboardFrontendBrowserToken = "DOTNET_DASHBOARD_FRONTEND_BROWSERTOKEN"; public const string DashboardResourceServiceClientApiKey = "DOTNET_DASHBOARD_RESOURCESERVICE_APIKEY"; public const string DashboardUnsecuredAllowAnonymous = "DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS"; + public const string DashboardCorsAllowedOrigins = "DOTNET_DASHBOARD_CORS_ALLOWED_ORIGINS"; public const string ResourceServiceEndpointUrl = "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL"; } diff --git a/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs index 7fa6c37ad36..08680f63e9c 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs @@ -279,8 +279,10 @@ public async Task DashboardResourceServiceUriIsSet() Assert.Equal("http://localhost:5000", config.Single(e => e.Key == DashboardConfigNames.ResourceServiceUrlName.EnvVarName).Value); } - [Fact] - public async Task DashboardResource_OtlpHttpEndpoint_CorsEnvVarSet() + [Theory] + [InlineData("*")] + [InlineData(null)] + public async Task DashboardResource_OtlpHttpEndpoint_CorsEnvVarSet(string? explicitCorsAllowedOrigins) { // Arrange using var builder = TestDistributedApplicationBuilder.Create( @@ -296,7 +298,8 @@ public async Task DashboardResource_OtlpHttpEndpoint_CorsEnvVarSet() builder.Configuration.AddInMemoryCollection(new Dictionary { ["ASPNETCORE_URLS"] = "http://localhost", - ["DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"] = "http://localhost" + ["DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"] = "http://localhost", + ["DOTNET_DASHBOARD_CORS_ALLOWED_ORIGINS"] = explicitCorsAllowedOrigins }); using var app = builder.Build(); @@ -314,12 +317,15 @@ public async Task DashboardResource_OtlpHttpEndpoint_CorsEnvVarSet() var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, app.Services); - Assert.Equal("http://localhost:8081,http://localhost:58080", config.Single(e => e.Key == DashboardConfigNames.DashboardOtlpCorsAllowedOriginsKeyName.EnvVarName).Value); + var expectedAllowedOrigins = !string.IsNullOrEmpty(explicitCorsAllowedOrigins) ? explicitCorsAllowedOrigins : "http://localhost:8081,http://localhost:58080"; + Assert.Equal(expectedAllowedOrigins, config.Single(e => e.Key == DashboardConfigNames.DashboardOtlpCorsAllowedOriginsKeyName.EnvVarName).Value); Assert.Equal("*", config.Single(e => e.Key == DashboardConfigNames.DashboardOtlpCorsAllowedHeadersKeyName.EnvVarName).Value); } - [Fact] - public async Task DashboardResource_OtlpGrpcEndpoint_CorsEnvVarNotSet() + [Theory] + [InlineData("*")] + [InlineData(null)] + public async Task DashboardResource_OtlpGrpcEndpoint_CorsEnvVarNotSet(string? explicitCorsAllowedOrigins) { // Arrange using var builder = TestDistributedApplicationBuilder.Create( @@ -335,7 +341,8 @@ public async Task DashboardResource_OtlpGrpcEndpoint_CorsEnvVarNotSet() builder.Configuration.AddInMemoryCollection(new Dictionary { ["ASPNETCORE_URLS"] = "http://localhost", - ["DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"] = "http://localhost" + ["DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"] = "http://localhost", + ["DOTNET_DASHBOARD_CORS_ALLOWED_ORIGINS"] = explicitCorsAllowedOrigins }); using var app = builder.Build();