Skip to content
Closed
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
3 changes: 2 additions & 1 deletion docs/specs/dashboard-http-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ The API can be enabled/disabled and configured via `Dashboard:Api` settings:

| Setting | Values | Default | Description |
|---------|--------|---------|-------------|
| `Enabled` | `true`, `false` | `true` | Whether the Telemetry HTTP API is enabled |
| `Enabled` | `true`, `false` | `true` when frontend is unsecured or an API key is configured; `false` when the frontend requires authentication and no API key is set | Whether the Telemetry HTTP API is enabled |
| `AuthMode` | `ApiKey`, `Unsecured` | `Unsecured` | Authentication mode for the API |
| `PrimaryApiKey` | string | - | API key for authentication (required when `AuthMode=ApiKey`) |
| `SecondaryApiKey` | string | - | Optional secondary API key for key rotation |
Expand All @@ -58,6 +58,7 @@ The API can be enabled/disabled and configured via `Dashboard:Api` settings:

- The API shares the same port as the Dashboard frontend (default: 18888).
- Hosters may set `Enabled: false` to disable the API for security.
- When the frontend requires authentication (e.g., OpenID Connect) and no API key is configured, the API is disabled by default. Set `Enabled: true` explicitly to override.
- API keys are shared with MCP configuration—setting either configures both.
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The note "API keys are shared with MCP configuration—setting either configures both" doesn’t match the current implementation in PostConfigureDashboardOptions (it only copies Api -> Mcp, not the other direction). With the updated defaulting rules for Api:Enabled, this doc can further confuse hosters who set only MCP keys and expect the API to be treated as keyed. Update the note to reflect the one-way fallback (or implement bidirectional behavior).

Suggested change
- API keys are shared with MCP configuration—setting either configures both.
- MCP configuration falls back to the Dashboard API keys when MCP keys are not set; configuring MCP keys does not configure Dashboard API keys or affect `Api:Enabled`.

Copilot uses AI. Check for mistakes.
- When running in unsecured mode, a warning is logged on first API request.

Expand Down
6 changes: 4 additions & 2 deletions src/Aspire.Dashboard/Configuration/DashboardOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,10 @@ 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.
/// When <see langword="false"/>, the /api/telemetry/* endpoints are not registered.
/// Defaults to <see langword="true"/> when the dashboard frontend is unsecured or an API key is configured.
/// Defaults to <see langword="false"/> when the frontend requires authentication (e.g., OpenID Connect
/// or BrowserToken) and no API key is configured, to prevent unauthenticated access to telemetry data.
/// </summary>
public bool? Enabled { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ public void PostConfigure(string? name, DashboardOptions options)
// If an API key is configured, default to ApiKey auth mode instead of Unsecured.
options.Mcp.AuthMode ??= string.IsNullOrEmpty(options.Mcp.PrimaryApiKey) ? McpAuthMode.Unsecured : McpAuthMode.ApiKey;
options.Api.AuthMode ??= string.IsNullOrEmpty(options.Api.PrimaryApiKey) ? ApiAuthMode.Unsecured : ApiAuthMode.ApiKey;

// When the frontend requires authentication (e.g. OpenID Connect or BrowserToken) but the API
// has no key configured (and would therefore default to Unsecured), disable the API by default
// to prevent unauthenticated access to telemetry data on the same port.
if (options.Api.Enabled is null &&
options.Frontend.AuthMode is not FrontendAuthMode.Unsecured &&
options.Api.AuthMode is ApiAuthMode.Unsecured)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new default-disable condition is based on options.Api.AuthMode is ApiAuthMode.Unsecured, but the comment/doc say this should happen specifically when no API key is configured. As written, this can also disable the API when a key is configured but Api:AuthMode is explicitly set to Unsecured, which contradicts the intended rule and the updated docs. Consider checking key presence directly (e.g., string.IsNullOrEmpty(options.Api.PrimaryApiKey) (and/or secondary) in addition to Enabled is null and frontend auth mode) rather than relying on AuthMode alone.

Suggested change
options.Api.AuthMode is ApiAuthMode.Unsecured)
string.IsNullOrEmpty(options.Api.PrimaryApiKey) &&
string.IsNullOrEmpty(options.Api.SecondaryApiKey))

Copilot uses AI. Check for mistakes.
{
options.Api.Enabled = false;
}
}

if (options.Frontend.AuthMode == FrontendAuthMode.BrowserToken && string.IsNullOrEmpty(options.Frontend.BrowserToken))
Expand Down
80 changes: 80 additions & 0 deletions tests/Aspire.Dashboard.Tests/Integration/TelemetryApiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,86 @@ public async Task Configuration_ApiKeyExplicit_OverridesMcp()
Assert.Equal(apiKey.Length, options.Api.GetPrimaryApiKeyBytesOrNull()!.Length);
}

[Fact]
public async Task Configuration_ApiDefaultsToDisabled_WhenFrontendIsBrowserToken()
{
// Arrange - BrowserToken frontend with no API key configured
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
{
config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString();
// Don't set any Api config — no API key
});
await app.StartAsync().DefaultTimeout();

// Assert - API should default to disabled when frontend has auth but no API key
var options = app.Services.GetRequiredService<IOptionsMonitor<DashboardOptions>>().CurrentValue;
Assert.False(options.Api.Enabled);
}
Comment on lines +96 to +99
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DashboardOptions.Api.Enabled is a nullable bool?, so using Assert.False(options.Api.Enabled) is ambiguous (and may not compile depending on Assert overloads) and can unintentionally treat null the same as false. Since this test is validating the new behavior of explicitly defaulting the API to disabled, assert the exact nullable value (e.g., that it is false).

Copilot uses AI. Check for mistakes.

[Fact]
public async Task Configuration_ApiStaysEnabled_WhenExplicitlyEnabled()
{
// Arrange - BrowserToken frontend, explicitly enable API with API key auth
var apiKey = "TestKey123!";
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
{
config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString();
config[DashboardConfigNames.DashboardApiEnabledName.ConfigKey] = "true";
config[DashboardConfigNames.DashboardApiAuthModeName.ConfigKey] = ApiAuthMode.ApiKey.ToString();
config[DashboardConfigNames.DashboardApiPrimaryApiKeyName.ConfigKey] = apiKey;
});
await app.StartAsync().DefaultTimeout();

// Assert - API should be enabled because explicitly configured
var options = app.Services.GetRequiredService<IOptionsMonitor<DashboardOptions>>().CurrentValue;
Assert.True(options.Api.Enabled);
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DashboardOptions.Api.Enabled is bool?. Assert.True(options.Api.Enabled) can be brittle/unclear for nullable values and may not be asserting what you intend if Enabled is ever null. Since this case sets Dashboard:Api:Enabled explicitly, assert that the value is exactly true (nullable comparison) to ensure the test is validating the explicit override behavior.

Suggested change
Assert.True(options.Api.Enabled);
Assert.Equal(true, options.Api.Enabled);

Copilot uses AI. Check for mistakes.

using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
httpClient.DefaultRequestHeaders.TryAddWithoutValidation(ApiAuthenticationHandler.ApiKeyHeaderName, apiKey);
var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

[Fact]
public async Task Configuration_ApiStaysEnabled_WhenApiKeyConfigured()
{
// Arrange - BrowserToken frontend with API key configured (should auto-enable ApiKey auth mode)
var apiKey = "TestKey123!";
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
{
config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString();
config[DashboardConfigNames.DashboardApiPrimaryApiKeyName.ConfigKey] = apiKey;
});
await app.StartAsync().DefaultTimeout();

// Assert - API should be enabled because an API key is configured (AuthMode defaults to ApiKey)
var options = app.Services.GetRequiredService<IOptionsMonitor<DashboardOptions>>().CurrentValue;
Assert.Equal(ApiAuthMode.ApiKey, options.Api.AuthMode);
Assert.NotEqual(false, options.Api.Enabled);
}

[Fact]
public async Task Configuration_ApiStaysEnabled_WhenFrontendUnsecured()
{
// Arrange - Unsecured frontend with no API key configured
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
{
config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
// Don't set any Api config
});
await app.StartAsync().DefaultTimeout();

// Assert - API should remain enabled when frontend is also unsecured
var options = app.Services.GetRequiredService<IOptionsMonitor<DashboardOptions>>().CurrentValue;
Assert.NotEqual(false, options.Api.Enabled);
Assert.Equal(ApiAuthMode.Unsecured, options.Api.AuthMode);

// Verify endpoints work
using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

#endregion

[Fact]
Expand Down