|
// 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;
using System.Text;
using Aspire.Hosting;
namespace Aspire.Dashboard.Configuration;
public sealed class DashboardOptions
{
public string? ApplicationName { get; set; }
public OtlpOptions Otlp { get; set; } = new();
public FrontendOptions Frontend { get; set; } = new();
public ResourceServiceClientOptions ResourceServiceClient { get; set; } = new();
public TelemetryLimitOptions TelemetryLimits { get; set; } = new();
}
// Don't set values after validating/parsing options.
public sealed class ResourceServiceClientOptions
{
private Uri? _parsedUrl;
private byte[]? _apiKeyBytes;
public string? Url { get; set; }
public ResourceClientAuthMode? AuthMode { get; set; }
public ResourceServiceClientCertificateOptions ClientCertificate { get; set; } = new();
public string? ApiKey { get; set; }
public Uri? GetUri() => _parsedUrl;
internal byte[] GetApiKeyBytes() => _apiKeyBytes ?? throw new InvalidOperationException($"{nameof(ApiKey)} is not available.");
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;
}
}
_apiKeyBytes = ApiKey != null ? Encoding.UTF8.GetBytes(ApiKey) : null;
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 AllowedCertificateRule
{
public string? Thumbprint { get; set; }
}
// Don't set values after validating/parsing options.
public sealed class OtlpOptions
{
private BindingAddress? _parsedGrpcEndpointAddress;
private BindingAddress? _parsedHttpEndpointAddress;
private byte[]? _primaryApiKeyBytes;
private byte[]? _secondaryApiKeyBytes;
public string? PrimaryApiKey { get; set; }
public string? SecondaryApiKey { get; set; }
public OtlpAuthMode? AuthMode { get; set; }
public string? GrpcEndpointUrl { get; set; }
public string? HttpEndpointUrl { get; set; }
public List<AllowedCertificateRule> AllowedCertificates { get; set; } = new();
public BindingAddress? GetGrpcEndpointAddress()
{
return _parsedGrpcEndpointAddress;
}
public BindingAddress? GetHttpEndpointAddress()
{
return _parsedHttpEndpointAddress;
}
public byte[] GetPrimaryApiKeyBytes()
{
Debug.Assert(_primaryApiKeyBytes is not null, "Should have been parsed during validation.");
return _primaryApiKeyBytes;
}
public byte[]? GetSecondaryApiKeyBytes() => _secondaryApiKeyBytes;
public OtlpCors Cors { get; set; } = new();
internal bool TryParseOptions([NotNullWhen(false)] out string? errorMessage)
{
if (string.IsNullOrEmpty(GrpcEndpointUrl) && string.IsNullOrEmpty(HttpEndpointUrl))
{
errorMessage = $"Neither OTLP/gRPC or OTLP/HTTP endpoint URLs are configured. Specify either a {DashboardConfigNames.DashboardOtlpGrpcUrlName.EnvVarName} or {DashboardConfigNames.DashboardOtlpHttpUrlName.EnvVarName} value.";
return false;
}
if (!string.IsNullOrEmpty(GrpcEndpointUrl) && !OptionsHelpers.TryParseBindingAddress(GrpcEndpointUrl, out _parsedGrpcEndpointAddress))
{
errorMessage = $"Failed to parse OTLP gRPC endpoint URL '{GrpcEndpointUrl}'.";
return false;
}
if (!string.IsNullOrEmpty(HttpEndpointUrl) && !OptionsHelpers.TryParseBindingAddress(HttpEndpointUrl, out _parsedHttpEndpointAddress))
{
errorMessage = $"Failed to parse OTLP HTTP endpoint URL '{HttpEndpointUrl}'.";
return false;
}
if (string.IsNullOrEmpty(HttpEndpointUrl) && !string.IsNullOrEmpty(Cors.AllowedOrigins))
{
errorMessage = $"CORS configured without an OTLP HTTP endpoint. Either remove CORS configuration or specify a {DashboardConfigNames.DashboardOtlpHttpUrlName.EnvVarName} value.";
return false;
}
_primaryApiKeyBytes = PrimaryApiKey != null ? Encoding.UTF8.GetBytes(PrimaryApiKey) : null;
_secondaryApiKeyBytes = SecondaryApiKey != null ? Encoding.UTF8.GetBytes(SecondaryApiKey) : null;
errorMessage = null;
return true;
}
}
public sealed class OtlpCors
{
public string? AllowedOrigins { get; set; }
public string? AllowedHeaders { get; set; }
[MemberNotNullWhen(true, nameof(AllowedOrigins))]
public bool IsCorsEnabled => !string.IsNullOrEmpty(AllowedOrigins);
}
// Don't set values after validating/parsing options.
public sealed class FrontendOptions
{
private List<BindingAddress>? _parsedEndpointAddresses;
private byte[]? _browserTokenBytes;
public string? EndpointUrls { get; set; }
public FrontendAuthMode? AuthMode { get; set; }
public string? BrowserToken { get; set; }
/// <summary>
/// Gets and sets an optional limit on the number of console log messages to be retained in the viewer.
/// </summary>
/// <remarks>
/// The viewer will retain at most this number of log messages. When the limit is reached, the oldest messages will be removed.
/// Defaults to 10,000, which matches the default used in the app host's circular buffer, on the publish side.
/// </remarks>
public int MaxConsoleLogCount { get; set; } = 10_000;
public OpenIdConnectOptions OpenIdConnect { get; set; } = new();
public byte[]? GetBrowserTokenBytes() => _browserTokenBytes;
public IReadOnlyList<BindingAddress> GetEndpointAddresses()
{
Debug.Assert(_parsedEndpointAddresses is not null, "Should have been parsed during validation.");
return _parsedEndpointAddresses;
}
internal bool TryParseOptions([NotNullWhen(false)] out string? errorMessage)
{
if (string.IsNullOrEmpty(EndpointUrls))
{
errorMessage = $"One or more frontend endpoint URLs are not configured. Specify an {DashboardConfigNames.DashboardFrontendUrlName.ConfigKey} value.";
return false;
}
else
{
var parts = EndpointUrls.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var addresses = new List<BindingAddress>(parts.Length);
foreach (var part in parts)
{
if (OptionsHelpers.TryParseBindingAddress(part, out var bindingAddress))
{
addresses.Add(bindingAddress);
}
else
{
errorMessage = $"Failed to parse frontend endpoint URLs '{EndpointUrls}'.";
return false;
}
}
_parsedEndpointAddresses = addresses;
}
_browserTokenBytes = BrowserToken != null ? Encoding.UTF8.GetBytes(BrowserToken) : null;
errorMessage = null;
return true;
}
}
public static class OptionsHelpers
{
public static bool TryParseBindingAddress(string address, [NotNullWhen(true)] out BindingAddress? bindingAddress)
{
try
{
bindingAddress = BindingAddress.Parse(address);
return true;
}
catch
{
bindingAddress = null;
return false;
}
}
}
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;
}
// Don't set values after validating/parsing options.
public sealed class OpenIdConnectOptions
{
private string[]? _nameClaimTypes;
private string[]? _usernameClaimTypes;
public string NameClaimType { get; set; } = "name";
public string UsernameClaimType { get; set; } = "preferred_username";
/// <summary>
/// Gets the optional name of a claim that users authenticated via OpenID Connect are required to have.
/// If specified, users without this claim will be rejected. If <see cref="RequiredClaimValue"/>
/// is also specified, then the value of this claim must also match <see cref="RequiredClaimValue"/>.
/// </summary>
public string RequiredClaimType { get; set; } = "";
/// <summary>
/// Gets the optional value of the <see cref="RequiredClaimType"/> claim for users authenticated via
/// OpenID Connect. If specified, users not having this value for the corresponding claim type are
/// rejected.
/// </summary>
public string RequiredClaimValue { get; set; } = "";
public string[] GetNameClaimTypes()
{
Debug.Assert(_nameClaimTypes is not null, "Should have been parsed during validation.");
return _nameClaimTypes;
}
public string[] GetUsernameClaimTypes()
{
Debug.Assert(_usernameClaimTypes is not null, "Should have been parsed during validation.");
return _usernameClaimTypes;
}
internal bool TryParseOptions([NotNullWhen(false)] out IEnumerable<string>? errorMessages)
{
List<string>? messages = null;
if (string.IsNullOrWhiteSpace(NameClaimType))
{
messages ??= [];
messages.Add("OpenID Connect claim type for name not configured. Specify a Dashboard:Frontend:OpenIdConnect:NameClaimType value.");
}
else
{
_nameClaimTypes = NameClaimType.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
if (string.IsNullOrWhiteSpace(UsernameClaimType))
{
messages ??= [];
messages.Add("OpenID Connect claim type for username not configured. Specify a Dashboard:Frontend:OpenIdConnect:UsernameClaimType value.");
}
else
{
_usernameClaimTypes = UsernameClaimType.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
errorMessages = messages;
return messages is null;
}
}
|