Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ private bool ShouldShowUnsecuredMcpMessage()
private bool ShouldShowUnsecuredApiMessage()
{
// Only show warning if API is enabled and unsecured
return Options.CurrentValue.Api.Enabled == true &&
return Options.CurrentValue.Api.Enabled.GetValueOrDefault() &&
Options.CurrentValue.Api.AuthMode == ApiAuthMode.Unsecured;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Configuration/DashboardOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public sealed class ApiOptions
/// <summary>
/// Gets or sets whether the Telemetry HTTP API is enabled.
/// When false, the /api/telemetry/* endpoints are not registered.
/// Defaults to true.
/// Defaults to false.
/// </summary>
public bool? Enabled { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public void PostConfigure(string? name, DashboardOptions options)

options.AI.Disabled = _configuration.GetBool(DashboardConfigNames.DashboardAIDisabledName.ConfigKey);

options.Api.Enabled ??= _configuration.GetBool(DashboardConfigNames.DashboardAspireApiEnabledName.ConfigKey);

// Normalize API keys: Api is canonical, falls back to Mcp if not set.
// Api -> Mcp fallback only (not bidirectional).
if (string.IsNullOrEmpty(options.Mcp.PrimaryApiKey) && !string.IsNullOrEmpty(options.Api.PrimaryApiKey))
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/DashboardEndpointsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ public static void MapDashboardMcp(this IEndpointRouteBuilder endpoints, Dashboa

public static void MapTelemetryApi(this IEndpointRouteBuilder endpoints, DashboardOptions dashboardOptions)
{
// Check if API is disabled (defaults to enabled if not specified)
if (dashboardOptions.Api.Enabled == false)
// Check if API is enabled (defaults to disabled if not specified)
if (!dashboardOptions.Api.Enabled.GetValueOrDefault())
{
return;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/DashboardWebApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ public DashboardWebApplication(

// Only show API security warning if API is enabled and unsecured
// API runs on the frontend endpoint (no separate accessor needed)
if (_dashboardOptionsMonitor.CurrentValue.Api.Enabled == true &&
if (_dashboardOptionsMonitor.CurrentValue.Api.Enabled.GetValueOrDefault() &&
_dashboardOptionsMonitor.CurrentValue.Api.AuthMode == ApiAuthMode.Unsecured)
{
_logger.LogWarning("Dashboard API is unsecured. Untrusted apps can access sensitive telemetry data.");
Expand Down
3 changes: 3 additions & 0 deletions src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,9 @@ internal async Task ConfigureEnvironmentVariables(EnvironmentCallbackContext con
context.EnvironmentVariables[DashboardConfigNames.DashboardApiAuthModeName.EnvVarName] = "Unsecured";
}

// Enable dashboard API
context.EnvironmentVariables[DashboardConfigNames.DashboardAspireApiEnabledName.EnvVarName] = "true";

// Configure dashboard to show CLI MCP instructions when running with an AppHost (not in standalone mode)
context.EnvironmentVariables[DashboardConfigNames.DashboardMcpUseCliMcpName.EnvVarName] = "true";

Expand Down
1 change: 1 addition & 0 deletions src/Shared/DashboardConfigNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal static class DashboardConfigNames
public static readonly ConfigName DashboardConfigFilePathName = new(KnownConfigNames.DashboardConfigFilePath);
public static readonly ConfigName DashboardFileConfigDirectoryName = new(KnownConfigNames.DashboardFileConfigDirectory);
public static readonly ConfigName DashboardAIDisabledName = new(KnownConfigNames.DashboardAIDisabled);
public static readonly ConfigName DashboardAspireApiEnabledName = new(KnownConfigNames.DashboardApiEnabled);
public static readonly ConfigName ResourceServiceUrlName = new(KnownConfigNames.ResourceServiceEndpointUrl);
public static readonly ConfigName ForwardedHeaders = new(KnownConfigNames.DashboardForwardedHeadersEnabled);

Expand Down
1 change: 1 addition & 0 deletions src/Shared/KnownConfigNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal static class KnownConfigNames
public const string DashboardConfigFilePath = "ASPIRE_DASHBOARD_CONFIG_FILE_PATH";
public const string DashboardFileConfigDirectory = "ASPIRE_DASHBOARD_FILE_CONFIG_DIRECTORY";
public const string DashboardAIDisabled = "ASPIRE_DASHBOARD_AI_DISABLED";
public const string DashboardApiEnabled = "ASPIRE_DASHBOARD_API_ENABLED";
public const string DashboardForwardedHeadersEnabled = "ASPIRE_DASHBOARD_FORWARDEDHEADERS_ENABLED";

public const string ShowDashboardResources = "ASPIRE_SHOW_DASHBOARD_RESOURCES";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ public async Task LogOutput_NoToken_GeneratedTokenLogged()
Assert.Equal(LogLevel.Warning, w.LogLevel);
},
w =>
{
Assert.Equal("Dashboard API is unsecured. Untrusted apps can access sensitive telemetry data.", LogTestHelpers.GetValue(w, "{OriginalFormat}"));
Assert.Equal(LogLevel.Warning, w.LogLevel);
},
w =>
{
Assert.Equal("Login to the dashboard at {DashboardLoginUrl}", LogTestHelpers.GetValue(w, "{OriginalFormat}"));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public static DashboardWebApplication CreateDashboardWebApplication(
[DashboardConfigNames.DashboardMcpUrlName.ConfigKey] = "http://127.0.0.1:0",
[DashboardConfigNames.DashboardOtlpAuthModeName.ConfigKey] = nameof(OtlpAuthMode.Unsecured),
[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = nameof(FrontendAuthMode.Unsecured),
[DashboardConfigNames.DashboardApiEnabledName.ConfigKey] = "true",
// Allow the requirement of HTTPS communication with the OpenIdConnect authority to be relaxed during tests.
["Authentication:Schemes:OpenIdConnect:RequireHttpsMetadata"] = "false"
};
Expand Down
62 changes: 62 additions & 0 deletions tests/Aspire.Dashboard.Tests/Integration/StartupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,11 @@ public async Task LogOutput_DynamicPort_PortResolvedInLogs()
{
Assert.Equal("MCP server is unsecured. Untrusted apps can access sensitive information.", LogTestHelpers.GetValue(w, "{OriginalFormat}"));
Assert.Equal(LogLevel.Warning, w.LogLevel);
},
w =>
{
Assert.Equal("Dashboard API is unsecured. Untrusted apps can access sensitive telemetry data.", LogTestHelpers.GetValue(w, "{OriginalFormat}"));
Assert.Equal(LogLevel.Warning, w.LogLevel);
});
}

Expand Down Expand Up @@ -765,6 +770,11 @@ public async Task LogOutput_NoOtlpEndpoints_NoOtlpLogs()
{
Assert.Equal("MCP server is unsecured. Untrusted apps can access sensitive information.", LogTestHelpers.GetValue(w, "{OriginalFormat}"));
Assert.Equal(LogLevel.Warning, w.LogLevel);
},
w =>
{
Assert.Equal("Dashboard API is unsecured. Untrusted apps can access sensitive telemetry data.", LogTestHelpers.GetValue(w, "{OriginalFormat}"));
Assert.Equal(LogLevel.Warning, w.LogLevel);
});
}

Expand All @@ -790,6 +800,53 @@ public async Task LogOutput_McpDisabled_NoMcpWarningLog()
Assert.DoesNotContain(l, w => LogTestHelpers.GetValue(w, "{OriginalFormat}")?.ToString()?.Contains("MCP server is unsecured") == true);
}

[Theory]
[InlineData(null, HttpStatusCode.NotFound)]
[InlineData(true, HttpStatusCode.OK)]
[InlineData(false, HttpStatusCode.NotFound)]
public async Task ApiEnabled_ReturnsExpectedStatusAndWarning(bool? enabled, HttpStatusCode expectedStatusCode)
{
const string ApiUnsecuredWarning = "Dashboard API is unsecured. Untrusted apps can access sensitive telemetry data.";

// Arrange
var testSink = new TestSink();
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(testOutputHelper,
additionalConfiguration: config =>
{
if (enabled is not null)
{
config[DashboardConfigNames.DashboardApiEnabledName.ConfigKey] = enabled.Value.ToString();
}
else
{
config.Remove(DashboardConfigNames.DashboardApiEnabledName.ConfigKey);
}
},
testSink: testSink);
await app.StartAsync().DefaultTimeout();

using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");

// Act
var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();

// Assert
Assert.Equal(expectedStatusCode, response.StatusCode);

var warnings = testSink.Writes
.Where(w => w.LoggerName == typeof(DashboardWebApplication).FullName && w.LogLevel >= LogLevel.Warning)
.ToList();

if (enabled == true)
{
Assert.Contains(warnings, w => LogTestHelpers.GetValue(w, "{OriginalFormat}")?.ToString() == ApiUnsecuredWarning);
}
else
{
Assert.DoesNotContain(warnings, w => LogTestHelpers.GetValue(w, "{OriginalFormat}")?.ToString() == ApiUnsecuredWarning);
}
}

[Fact]
public async Task LogOutput_LocalhostAddress_LocalhostInLogOutput()
{
Expand Down Expand Up @@ -885,6 +942,11 @@ await ServerRetryHelper.BindPortsWithRetry(async ports =>
{
Assert.Equal("MCP server is unsecured. Untrusted apps can access sensitive information.", LogTestHelpers.GetValue(w, "{OriginalFormat}"));
Assert.Equal(LogLevel.Warning, w.LogLevel);
},
w =>
{
Assert.Equal("Dashboard API is unsecured. Untrusted apps can access sensitive telemetry data.", LogTestHelpers.GetValue(w, "{OriginalFormat}"));
Assert.Equal(LogLevel.Warning, w.LogLevel);
});
}

Expand Down
19 changes: 0 additions & 19 deletions tests/Aspire.Dashboard.Tests/Integration/TelemetryApiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,25 +181,6 @@ public async Task GetSpans_WithWrongApiKey_ReturnsUnauthorized()
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}

[Fact]
public async Task GetSpans_ApiDisabled_Returns404()
{
// Arrange - disable the Telemetry API explicitly
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
{
config[DashboardConfigNames.DashboardApiEnabledName.ConfigKey] = "false";
});
await app.StartAsync().DefaultTimeout();

using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");

// Act
var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();

// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

[Fact]
public async Task GetLogs_UnsecuredMode_Returns200()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ public async Task DashboardDoesNotAddResource_ConfiguresExistingDashboard(string
.ToList();

Assert.Collection(config,
e =>
{
Assert.Equal(KnownConfigNames.DashboardApiEnabled, e.Key);
Assert.Equal("true", e.Value);
},
e =>
{
Assert.Equal(KnownConfigNames.DashboardMcpEndpointUrl, e.Key);
Expand Down
Loading