Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/8.0-preview5] Organize dashboard config to use strongly typed options, support primary/secondary API keys and rotation #3179

Merged
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
1 change: 1 addition & 0 deletions src/Aspire.Dashboard/Aspire.Dashboard.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -185,5 +185,6 @@
<Compile Include="$(SharedDir)Model\KnownProperties.cs" Link="Utils\KnownProperties.cs" />
<Compile Include="$(SharedDir)Model\KnownResourceTypes.cs" Link="Utils\KnownResourceTypes.cs" />
<Compile Include="$(SharedDir)CircularBuffer.cs" Link="Otlp\Storage\CircularBuffer.cs" />
<Compile Include="$(SharedDir)DashboardConfigNames.cs" Link="Utils\DashboardConfigNames.cs" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Encodings.Web;
using Aspire.Dashboard.Configuration;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;

Expand All @@ -11,22 +12,30 @@ public class OtlpApiKeyAuthenticationHandler : AuthenticationHandler<OtlpApiKeyA
{
public const string ApiKeyHeaderName = "x-otlp-api-key";

public OtlpApiKeyAuthenticationHandler(IOptionsMonitor<OtlpApiKeyAuthenticationHandlerOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder)
private readonly IOptionsMonitor<DashboardOptions> _dashboardOptions;

public OtlpApiKeyAuthenticationHandler(IOptionsMonitor<DashboardOptions> dashboardOptions, IOptionsMonitor<OtlpApiKeyAuthenticationHandlerOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder)
{
_dashboardOptions = dashboardOptions;
}

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (string.IsNullOrEmpty(Options.OtlpApiKey))
var options = _dashboardOptions.CurrentValue.Otlp;

if (string.IsNullOrEmpty(options.PrimaryApiKey))
{
throw new InvalidOperationException("OTLP API key is not configured.");
}

if (Context.Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKey))
{
if (Options.OtlpApiKey != apiKey)
if (options.PrimaryApiKey != apiKey)
{
return Task.FromResult(AuthenticateResult.Fail("Incoming API key doesn't match required API key."));
if (string.IsNullOrEmpty(options.SecondaryApiKey) || options.SecondaryApiKey != apiKey)
{
return Task.FromResult(AuthenticateResult.Fail($"Incoming API key from '{ApiKeyHeaderName}' header doesn't match configured API key."));
}
}
}
else
Expand All @@ -45,5 +54,4 @@ public static class OtlpApiKeyAuthenticationDefaults

public sealed class OtlpApiKeyAuthenticationHandlerOptions : AuthenticationSchemeOptions
{
public string? OtlpApiKey { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,24 @@
using System.Text.Encodings.Web;
using Aspire.Dashboard.Authentication.OtlpApiKey;
using Aspire.Dashboard.Authentication.OtlpConnection;
using Aspire.Dashboard.Configuration;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Certificate;
using Microsoft.Extensions.Options;

namespace Aspire.Dashboard.Authentication;

public sealed class OtlpCompositeAuthenticationHandler(
IOptionsMonitor<DashboardOptions> dashboardOptions,
IOptionsMonitor<OtlpCompositeAuthenticationHandlerOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: AuthenticationHandler<OtlpCompositeAuthenticationHandlerOptions>(options, logger, encoder)
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var options = dashboardOptions.CurrentValue;

foreach (var scheme in GetRelevantAuthenticationSchemes())
{
var result = await Context.AuthenticateAsync(scheme).ConfigureAwait(false);
Expand All @@ -37,11 +41,11 @@ IEnumerable<string> GetRelevantAuthenticationSchemes()
{
yield return OtlpConnectionAuthenticationDefaults.AuthenticationScheme;

if (Options.OtlpAuthMode is OtlpAuthMode.ApiKey)
if (options.Otlp.AuthMode is OtlpAuthMode.ApiKey)
{
yield return OtlpApiKeyAuthenticationDefaults.AuthenticationScheme;
}
else if (Options.OtlpAuthMode is OtlpAuthMode.ClientCertificate)
else if (options.Otlp.AuthMode is OtlpAuthMode.ClientCertificate)
{
yield return CertificateAuthenticationDefaults.AuthenticationScheme;
}
Expand All @@ -56,5 +60,4 @@ public static class OtlpCompositeAuthenticationDefaults

public sealed class OtlpCompositeAuthenticationHandlerOptions : AuthenticationSchemeOptions
{
public OtlpAuthMode OtlpAuthMode { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Dashboard.Configuration;

public enum DashboardClientCertificateSource
{
File,
KeyStore
}
141 changes: 141 additions & 0 deletions src/Aspire.Dashboard/Configuration/DashboardOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography.X509Certificates;

namespace Aspire.Dashboard.Configuration;

public sealed class DashboardOptions
{
public string? ApplicationName { get; set; }
public OtlpOptions Otlp { get; set; } = new OtlpOptions();
public FrontendOptions Frontend { get; set; } = new FrontendOptions();
public ResourceServiceClientOptions ResourceServiceClient { get; set; } = new ResourceServiceClientOptions();
public TelemetryLimitOptions TelemetryLimits { get; set; } = new TelemetryLimitOptions();
}

public sealed class ResourceServiceClientOptions
{
private Uri? _parsedUrl;

public string? Url { get; set; }
public ResourceClientAuthMode? AuthMode { get; set; }
public ResourceServiceClientCertificateOptions ClientCertificates { get; set; } = new ResourceServiceClientCertificateOptions();

public Uri? GetUri() => _parsedUrl;

internal bool TryParseOptions([NotNullWhen(false)] out string? errorMessage)
{
if (!string.IsNullOrEmpty(Url))
{
if (!Uri.TryCreate(Url, UriKind.Absolute, out _parsedUrl))
{
errorMessage = $"Failed to parse resource service client endpoint URL '{Url}'.";
return false;
}
}

errorMessage = null;
return true;
}
}

public sealed class ResourceServiceClientCertificateOptions
{
public DashboardClientCertificateSource? Source { get; set; }
public string? FilePath { get; set; }
public string? Password { get; set; }
public string? Subject { get; set; }
public string? Store { get; set; }
public StoreLocation? Location { get; set; }
}

public sealed class OtlpOptions
{
private Uri? _parsedEndpointUrl;

public string? PrimaryApiKey { get; set; }
public string? SecondaryApiKey { get; set; }
public OtlpAuthMode? AuthMode { get; set; }
public string? EndpointUrl { get; set; }

public Uri GetEndpointUri()
{
Debug.Assert(_parsedEndpointUrl is not null, "Should have been parsed during validation.");
return _parsedEndpointUrl;
}

internal bool TryParseOptions([NotNullWhen(false)] out string? errorMessage)
{
if (string.IsNullOrEmpty(EndpointUrl))
{
errorMessage = "OTLP endpoint URL is not configured. Specify a Dashboard:Otlp:EndpointUrl value.";
return false;
}
else
{
if (!Uri.TryCreate(EndpointUrl, UriKind.Absolute, out _parsedEndpointUrl))
{
errorMessage = $"Failed to parse OTLP endpoint URL '{EndpointUrl}'.";
return false;
}
}

errorMessage = null;
return true;
}
}

public sealed class FrontendOptions
{
private List<Uri>? _parsedEndpointUrls;

public string? EndpointUrls { get; set; }
public FrontendAuthMode? AuthMode { get; set; }

public IReadOnlyList<Uri> GetEndpointUris()
{
Debug.Assert(_parsedEndpointUrls is not null, "Should have been parsed during validation.");
return _parsedEndpointUrls;
}

internal bool TryParseOptions([NotNullWhen(false)] out string? errorMessage)
{
if (string.IsNullOrEmpty(EndpointUrls))
{
errorMessage = "One or more frontend endpoint URLs are not configured. Specify a Dashboard:Frontend:EndpointUrls value.";
return false;
}
else
{
var parts = EndpointUrls.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var uris = new List<Uri>(parts.Length);
foreach (var part in parts)
{
if (!Uri.TryCreate(part, UriKind.Absolute, out var uri))
{
errorMessage = $"Failed to parse frontend endpoint URLs '{EndpointUrls}'.";
return false;
}

uris.Add(uri);
}
_parsedEndpointUrls = uris;
}

errorMessage = null;
return true;
}
}

public sealed class TelemetryLimitOptions
{
public int MaxLogCount { get; set; } = 10_000;
public int MaxTraceCount { get; set; } = 10_000;
public int MaxMetricsCount { get; set; } = 50_000; // Allows for 1 metric point per second for over 12 hours.
public int MaxAttributeCount { get; set; } = 128;
public int MaxAttributeLength { get; set; } = int.MaxValue;
public int MaxSpanEventCount { get; set; } = int.MaxValue;
}
10 changes: 10 additions & 0 deletions src/Aspire.Dashboard/Configuration/FrontendAuthMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Dashboard.Configuration;

public enum FrontendAuthMode
{
Unsecured,
OpenIdConnect
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Dashboard.Authentication;
namespace Aspire.Dashboard.Configuration;

public enum OtlpAuthMode
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting;
using Microsoft.Extensions.Options;

namespace Aspire.Dashboard.Configuration;

public sealed class PostConfigureDashboardOptions : IPostConfigureOptions<DashboardOptions>
{
private readonly IConfiguration _configuration;

public PostConfigureDashboardOptions(IConfiguration configuration)
{
_configuration = configuration;
}

public void PostConfigure(string? name, DashboardOptions options)
{
// Copy aliased config values to the strongly typed options.
if (_configuration[DashboardConfigNames.DashboardOtlpUrlName.ConfigKey] is { Length: > 0 } otlpUrl)
{
options.Otlp.EndpointUrl = otlpUrl;
}
if (_configuration[DashboardConfigNames.DashboardFrontendUrlName.ConfigKey] is { Length: > 0 } frontendUrls)
{
options.Frontend.EndpointUrls = frontendUrls;
}
if (_configuration[DashboardConfigNames.ResourceServiceUrlName.ConfigKey] is { Length: > 0 } resourceServiceUrl)
{
options.ResourceServiceClient.Url = resourceServiceUrl;
}
if (_configuration.GetBool(DashboardConfigNames.DashboardInsecureAllowAnonymousName.ConfigKey) ?? false)
{
options.Frontend.AuthMode = FrontendAuthMode.Unsecured;
options.Otlp.AuthMode = OtlpAuthMode.Unsecured;
}
}
}
10 changes: 10 additions & 0 deletions src/Aspire.Dashboard/Configuration/ResourceClientAuthMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Dashboard.Configuration;

public enum ResourceClientAuthMode
{
Unsecured,
Certificate
}
Loading