diff --git a/src/Aspire.Hosting/AspireEventSource.cs b/src/Aspire.Hosting/AspireEventSource.cs index cdea12bded7..9ff1b6a6c3e 100644 --- a/src/Aspire.Hosting/AspireEventSource.cs +++ b/src/Aspire.Hosting/AspireEventSource.cs @@ -370,4 +370,22 @@ public void StartResourceStop(string kind, string resourceName) WriteEvent(42, kind, resourceName); } } + + [Event(43, Level = EventLevel.Informational, Message = "Development certificate trust check is starting...")] + public void DevelopmentCertificateTrustCheckStart() + { + if (IsEnabled()) + { + WriteEvent(43); + } + } + + [Event(44, Level = EventLevel.Informational, Message = "Development certificate trust check completed")] + public void DevelopmentCertificateTrustCheckStop() + { + if (IsEnabled()) + { + WriteEvent(44); + } + } } diff --git a/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs b/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs index 79bca25c326..a991993ff92 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only +#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only using System.Collections.Concurrent; using System.Diagnostics; @@ -424,6 +425,7 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource) if (otlpGrpcEndpointUrl != null) { var address = BindingAddress.Parse(otlpGrpcEndpointUrl); + dashboardResource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, name: OtlpGrpcEndpointName, uriScheme: address.Scheme, port: address.Port, isProxied: true, transport: "http2") { TargetHost = address.Host @@ -433,6 +435,7 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource) if (otlpHttpEndpointUrl != null) { var address = BindingAddress.Parse(otlpHttpEndpointUrl); + dashboardResource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, name: OtlpHttpEndpointName, uriScheme: address.Scheme, port: address.Port, isProxied: true) { TargetHost = address.Host @@ -442,12 +445,45 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource) if (mcpEndpointUrl != null) { var address = BindingAddress.Parse(mcpEndpointUrl); + dashboardResource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, name: McpEndpointName, uriScheme: address.Scheme, port: address.Port, isProxied: true) { TargetHost = address.Host }); } + // Determine whether any HTTPS endpoints are configured + var hasHttpsEndpoint = dashboardResource.TryGetAnnotationsOfType(out var endpoints) && endpoints.Any(e => e.UriScheme is "https"); + + if (hasHttpsEndpoint && + !dashboardResource.HasAnnotationOfType()) + { + // If the dashboard has an HTTPS endpoint and we haven't already applied an HTTPS certificate configuration (no HttpsCertificateConfigurationCallbackAnnotation), + // apply a default configuration with a valid trusted dev cert instance. + var developerCertificateService = executionContext.ServiceProvider.GetRequiredService(); + var trustDeveloperCertificate = developerCertificateService.TrustCertificate; + if (dashboardResource.TryGetLastAnnotation(out var certificateAuthorityAnnotation)) + { + trustDeveloperCertificate = certificateAuthorityAnnotation.TrustDeveloperCertificates.GetValueOrDefault(trustDeveloperCertificate); + } + + if (trustDeveloperCertificate) + { + dashboardResource.Annotations.Add(new HttpsCertificateConfigurationCallbackAnnotation(ctx => + { + // Ensure we use a trusted developer certificate (Kestrel selects the latest certificate, which may not be trusted after an SDK update). + // There can be issues referencing an exported PEM key pair on MacOS, so we the PFX version of the certificate here. + ctx.EnvironmentVariables["Kestrel__Certificates__Default__Path"] = ctx.PfxPath; + if (ctx.Password is not null) + { + ctx.EnvironmentVariables["Kestrel__Certificates__Default__Password"] = ctx.Password; + } + + return Task.CompletedTask; + })); + } + } + dashboardResource.Annotations.Add(new ResourceUrlsCallbackAnnotation(c => { var browserToken = options.DashboardToken; diff --git a/src/Aspire.Hosting/Dcp/DcpHost.cs b/src/Aspire.Hosting/Dcp/DcpHost.cs index 19bb2e34b79..15a82b5fd7c 100644 --- a/src/Aspire.Hosting/Dcp/DcpHost.cs +++ b/src/Aspire.Hosting/Dcp/DcpHost.cs @@ -10,12 +10,15 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Dcp.Process; using Aspire.Hosting.Resources; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Aspire.Hosting.Dcp; #pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. internal sealed class DcpHost { @@ -29,6 +32,9 @@ internal sealed class DcpHost private readonly IInteractionService _interactionService; private readonly Locations _locations; private readonly TimeProvider _timeProvider; + private readonly IDeveloperCertificateService _developerCertificateService; + private readonly IFileSystemService _fileSystemService; + private readonly IConfiguration _configuration; private readonly CancellationTokenSource _shutdownCts = new(); private Task? _logProcessorTask; @@ -48,7 +54,10 @@ public DcpHost( IInteractionService interactionService, Locations locations, DistributedApplicationModel applicationModel, - TimeProvider timeProvider) + TimeProvider timeProvider, + IDeveloperCertificateService developerCertificateService, + IFileSystemService fileSystemService, + IConfiguration configuration) { _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(); @@ -58,11 +67,15 @@ public DcpHost( _locations = locations; _applicationModel = applicationModel; _timeProvider = timeProvider; + _developerCertificateService = developerCertificateService; + _fileSystemService = fileSystemService; + _configuration = configuration; } public async Task StartAsync(CancellationToken cancellationToken) { await EnsureDcpContainerRuntimeAsync(cancellationToken).ConfigureAwait(false); + await EnsureDevelopmentCertificateTrustAsync(cancellationToken).ConfigureAwait(false); EnsureDcpHostRunning(); } @@ -122,6 +135,63 @@ internal async Task EnsureDcpContainerRuntimeAsync(CancellationToken cancellatio } } + internal async Task EnsureDevelopmentCertificateTrustAsync(CancellationToken cancellationToken) + { + AspireEventSource.Instance.DevelopmentCertificateTrustCheckStart(); + + try + { + // If no resources use HTTPS/TLS, there's no need to warn about untrusted dev certificates. + if (!_applicationModel.Resources.Any(ResourceUsesTls)) + { + return; + } + + // Check and warn if no trusted dev certs exist, or if a newer untrusted cert was detected + var hasNewerUntrustedCert = _developerCertificateService.LatestCertificateIsUntrusted; + var hasNoTrustedCerts = _developerCertificateService.Certificates.Count == 0; + + if (hasNoTrustedCerts || hasNewerUntrustedCert) + { + string title; + string message; + + if (hasNoTrustedCerts) + { + title = InteractionStrings.NoDeveloperCertificateTrustedTitle; + message = InteractionStrings.NoDeveloperCertificateTrustedMessage; + _logger.LogWarning("No trusted Aspire development certificate was found. See https://aka.ms/aspire/devcerts for more information."); + } + else + { + title = InteractionStrings.DeveloperCertificateNotFullyTrustedTitle; + message = InteractionStrings.DeveloperCertificateNotFullyTrustedMessage; + _logger.LogWarning("The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information."); + } + + // Check if the interaction service is available (dashboard enabled) + if (!_interactionService.IsAvailable) + { + return; + } + + // Send notification to the dashboard + _ = _interactionService.PromptNotificationAsync( + title: title, + message: message, + options: new NotificationInteractionOptions + { + Intent = MessageIntent.Error, + }, + cancellationToken: cancellationToken); + } + } + finally + { + AspireEventSource.Instance.DevelopmentCertificateTrustCheckStop(); + } + } + public async Task StopAsync() { _shutdownCts.Cancel(); @@ -474,6 +544,38 @@ private static bool IsContainerRuntimeHealthy(DcpInfo dcpInfo) var running = dcpInfo.Containers?.Running ?? false; return installed && running; } + + /// + /// Determines whether a resource uses HTTPS/TLS by checking for HTTPS endpoint annotations + /// or active HTTPS certificate configuration callbacks that haven't been disabled. + /// + private static bool ResourceUsesTls(IResource resource) + { + // Check if the resource has any HTTPS endpoints + if (resource.Annotations.OfType().Any(e => e.UriScheme is "https")) + { + return true; + } + + // Check if the resource has an HTTPS certificate configuration callback that hasn't been + // disabled via WithoutHttpsCertificate(). HttpsCertificateAnnotation has no effect without + // HttpsCertificateConfigurationCallbackAnnotation, so it's only checked as a filter here. + if (resource.Annotations.OfType().Any()) + { + // The callback is present. Check if it's been disabled by WithoutHttpsCertificate() + // which sets UseDeveloperCertificate = false and Certificate = null. + if (resource.TryGetLastAnnotation(out var certAnnotation) + && certAnnotation.UseDeveloperCertificate is false or null + && certAnnotation.Certificate is null) + { + return false; + } + + return true; + } + + return false; + } } #pragma warning restore ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/src/Aspire.Hosting/DeveloperCertificateService.cs b/src/Aspire.Hosting/DeveloperCertificateService.cs index bb8a570272a..6358a00b41c 100644 --- a/src/Aspire.Hosting/DeveloperCertificateService.cs +++ b/src/Aspire.Hosting/DeveloperCertificateService.cs @@ -1,4 +1,5 @@ #pragma warning disable ASPIRECERTIFICATES001 +#pragma warning disable ASPIREFILESYSTEM001 // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -16,50 +17,114 @@ internal class DeveloperCertificateService : IDeveloperCertificateService private readonly Lazy> _certificates; private readonly Lazy _supportsContainerTrust; private readonly Lazy _supportsTlsTermination; + private bool _latestCertificateIsUntrusted; public DeveloperCertificateService(ILogger logger, IConfiguration configuration, DistributedApplicationOptions options) { + TrustCertificate = configuration.GetBool(KnownConfigNames.DeveloperCertificateDefaultTrust) ?? + options.TrustDeveloperCertificate ?? + true; + _certificates = new Lazy>(() => { try { - var devCerts = new List(); - using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadOnly); + var now = DateTimeOffset.Now; + + // Get all valid ASP.NET Core development certificates. + // Use .Where() instead of .Find() to preserve the original keychain-backed certificate + // instances on macOS. Find() clones certificates which can invalidate keychain handles. + var validCerts = FindDevCertificates(store, now).ToList(); + + // If any certificate has a Subject Key Identifier extension, exclude certificates without it + if (validCerts.Any(c => c.HasSubjectKeyIdentifier())) + { + validCerts = validCerts.Where(c => c.HasSubjectKeyIdentifier()).ToList(); + } + // Order by version and expiration date descending to get the most recent, highest version first. // OpenSSL will only check the first self-signed certificate in the bundle that matches a given domain, // so we want to ensure the certificate that will be used by ASP.NET Core is the first one in the bundle. // Match the ordering logic ASP.NET Core uses, including DateTimeOffset.Now for current time: https://github.com/dotnet/aspnetcore/blob/0aefdae365ff9b73b52961acafd227309524ce3c/src/Shared/CertificateGeneration/CertificateManager.cs#L122 - var now = DateTimeOffset.Now; - - // Get all valid ASP.NET Core development certificates - var validCerts = store.Certificates - .Where(c => c.IsAspNetCoreDevelopmentCertificate()) - .Where(c => c.NotBefore <= now && now <= c.NotAfter) + var bestCerts = validCerts + .GroupBy(c => c.Extensions.OfType().FirstOrDefault()?.SubjectKeyIdentifier) + .SelectMany(g => g.OrderByVersion().Take(1)) + .OrderByVersion() .ToList(); - - // If any certificate has a Subject Key Identifier extension, exclude certificates without it - if (validCerts.Any(c => c.HasSubjectKeyIdentifier())) + + // Partition into trusted and untrusted using a single X509Chain instance. + // RevocationMode is set to NoCheck since revocation doesn't apply to self-signed dev certs. + using var chain = new X509Chain(); + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + // On Windows, chain.Build() can succeed even when the certificate isn't in the + // trusted root store. Open the CurrentUser Root store so we can verify membership. + X509Certificate2Collection? rootCerts = null; + if (OperatingSystem.IsWindows()) { - validCerts = validCerts.Where(c => c.HasSubjectKeyIdentifier()).ToList(); + using var rootStore = new X509Store(StoreName.Root, StoreLocation.CurrentUser); + rootStore.Open(OpenFlags.ReadOnly); + rootCerts = rootStore.Certificates; + } + + // Find the dev certs that are trusted + var trustedCerts = new List(); + foreach (var cert in bestCerts) + { + try + { + if (!chain.Build(cert)) + { + continue; + } + + // On Windows, also verify the certificate exists in the root store + if (rootCerts is not null && + !rootCerts.Any(rc => rc.RawDataMemory.Span.SequenceEqual(cert.RawDataMemory.Span))) + { + continue; + } + + trustedCerts.Add(cert); + } + finally + { + // Reset the chain for the next certificate regardless of branch taken. + chain.Reset(); + } + } + + // Dispose root store certificates after use + if (rootCerts is not null) + { + foreach (var rc in rootCerts) + { + rc.Dispose(); + } } - - // Take the highest version valid certificate for each unique SKI - devCerts.AddRange( - validCerts - .GroupBy(c => c.Extensions.OfType().FirstOrDefault()?.SubjectKeyIdentifier) - .SelectMany(g => g.OrderByDescending(c => c.GetCertificateVersion()).ThenByDescending(c => c.NotAfter).Take(1)) - .OrderByDescending(c => c.GetCertificateVersion()).ThenByDescending(c => c.NotAfter)); - - if (devCerts.Count == 0) + + // Flag if the newest/highest-version cert is not trusted + if (bestCerts.Count > 0 && + (trustedCerts.Count == 0 || trustedCerts[0].Thumbprint != bestCerts[0].Thumbprint)) + { + _latestCertificateIsUntrusted = true; + } + + // Release the unused certificates + foreach (var unusedCert in validCerts.Except(trustedCerts)) + { + unusedCert.Dispose(); + } + + if (trustedCerts.Count == 0) { - logger.LogInformation("No ASP.NET Core developer certificates found in the CurrentUser/My certificate store."); return ImmutableList.Empty; } - return devCerts.ToImmutableList(); + return trustedCerts.ToImmutableList(); } catch (Exception ex) { @@ -82,15 +147,10 @@ public DeveloperCertificateService(ILogger logger, return supportsTlsTermination; }); - // Environment variable config > DistributedApplicationOptions > default true - TrustCertificate = configuration.GetBool(KnownConfigNames.DeveloperCertificateDefaultTrust) ?? - options.TrustDeveloperCertificate ?? - true; - // By default, only use for server authentication if trust is also enabled (and a developer certificate with a private key is available) UseForHttps = (configuration.GetBool(KnownConfigNames.DeveloperCertificateDefaultHttpsTermination) ?? options.DeveloperCertificateDefaultHttpsTerminationEnabled ?? - true ) && TrustCertificate && _supportsTlsTermination.Value; + true) && TrustCertificate && _supportsTlsTermination.Value; } /// @@ -104,4 +164,29 @@ public DeveloperCertificateService(ILogger logger, /// public bool UseForHttps { get; } + + /// + /// Gets a value indicating whether a newer ASP.NET Core development certificate was detected + /// that is not in the trusted set. This is true when the highest-version/most-recent dev cert + /// is not trusted, even though older trusted certs may exist. + /// + internal bool LatestCertificateIsUntrusted + { + get + { + _ = _certificates.Value; // Ensure certificates have been evaluated + return _latestCertificateIsUntrusted; + } + } + + /// + /// Finds ASP.NET Core development certificates in the store, filtered by date validity and private key presence. + /// + private static IEnumerable FindDevCertificates(X509Store store, DateTimeOffset now) + { + return store.Certificates + .Where(c => c.IsAspNetCoreDevelopmentCertificate()) + .Where(c => c.NotBefore <= now && now <= c.NotAfter) + .Where(c => c.HasPrivateKey); + } } diff --git a/src/Aspire.Hosting/IDeveloperCertificateService.cs b/src/Aspire.Hosting/IDeveloperCertificateService.cs index 05fc9786069..fd83b1d7868 100644 --- a/src/Aspire.Hosting/IDeveloperCertificateService.cs +++ b/src/Aspire.Hosting/IDeveloperCertificateService.cs @@ -35,4 +35,11 @@ public interface IDeveloperCertificateService /// Indicates whether the default behavior is to attempt to trust the developer certificate(s) at runtime. /// bool TrustCertificate { get; } + + /// + /// Gets a value indicating whether a newer ASP.NET Core development certificate was detected + /// that is not in the trusted set. This is true when the highest-version/most-recent dev cert + /// is not trusted, even though older trusted certs may exist. + /// + bool LatestCertificateIsUntrusted => false; } diff --git a/src/Aspire.Hosting/Resources/InteractionStrings.Designer.cs b/src/Aspire.Hosting/Resources/InteractionStrings.Designer.cs index ceafadbcbca..ab0cea77d35 100644 --- a/src/Aspire.Hosting/Resources/InteractionStrings.Designer.cs +++ b/src/Aspire.Hosting/Resources/InteractionStrings.Designer.cs @@ -10,8 +10,8 @@ namespace Aspire.Hosting.Resources { using System; - - + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -23,15 +23,15 @@ namespace Aspire.Hosting.Resources { [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class InteractionStrings { - + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal InteractionStrings() { } - + /// /// Returns the cached ResourceManager instance used by this class. /// @@ -45,7 +45,7 @@ internal InteractionStrings() { return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. @@ -59,7 +59,7 @@ internal InteractionStrings() { resourceCulture = value; } } - + /// /// Looks up a localized string similar to Ensure that Docker is running and that the Docker daemon is accessible. If Resource Saver mode is enabled, containers may not run.. /// @@ -68,7 +68,7 @@ internal static string ContainerRuntimeDockerAdvice { return ResourceManager.GetString("ContainerRuntimeDockerAdvice", resourceCulture); } } - + /// /// Looks up a localized string similar to Ensure that the container runtime is running.. /// @@ -77,7 +77,7 @@ internal static string ContainerRuntimeGenericAdvice { return ResourceManager.GetString("ContainerRuntimeGenericAdvice", resourceCulture); } } - + /// /// Looks up a localized string similar to Learn more. /// @@ -86,7 +86,7 @@ internal static string ContainerRuntimeLinkText { return ResourceManager.GetString("ContainerRuntimeLinkText", resourceCulture); } } - + /// /// Looks up a localized string similar to Container runtime could not be found. See https://aka.ms/dotnet/aspire/containers for more details on supported container runtimes.. /// @@ -95,7 +95,7 @@ internal static string ContainerRuntimeNotInstalledMessage { return ResourceManager.GetString("ContainerRuntimeNotInstalledMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Container runtime not installed. /// @@ -104,7 +104,43 @@ internal static string ContainerRuntimeNotInstalledTitle { return ResourceManager.GetString("ContainerRuntimeNotInstalledTitle", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Development certificate not fully trusted. + /// + internal static string DeveloperCertificateNotFullyTrustedTitle { + get { + return ResourceManager.GetString("DeveloperCertificateNotFullyTrustedTitle", resourceCulture); + } + } + + /// + /// The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information.. + /// + internal static string DeveloperCertificateNotFullyTrustedMessage { + get { + return ResourceManager.GetString("DeveloperCertificateNotFullyTrustedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No trusted development certificate. + /// + internal static string NoDeveloperCertificateTrustedTitle { + get { + return ResourceManager.GetString("NoDeveloperCertificateTrustedTitle", resourceCulture); + } + } + + /// + /// No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information.. + /// + internal static string NoDeveloperCertificateTrustedMessage { + get { + return ResourceManager.GetString("NoDeveloperCertificateTrustedMessage", resourceCulture); + } + } + /// /// Looks up a localized string similar to Ensure that Podman is running.. /// @@ -113,7 +149,7 @@ internal static string ContainerRuntimePodmanAdvice { return ResourceManager.GetString("ContainerRuntimePodmanAdvice", resourceCulture); } } - + /// /// Looks up a localized string similar to Container runtime '{0}' was found but appears to be unhealthy. . /// @@ -122,7 +158,7 @@ internal static string ContainerRuntimeUnhealthyMessage { return ResourceManager.GetString("ContainerRuntimeUnhealthyMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Container runtime unhealthy. /// @@ -131,7 +167,7 @@ internal static string ContainerRuntimeUnhealthyTitle { return ResourceManager.GetString("ContainerRuntimeUnhealthyTitle", resourceCulture); } } - + /// /// Looks up a localized string similar to Are you sure you want to delete the value for parameter `{0}`? . /// @@ -140,7 +176,7 @@ internal static string DeleteParameterMessage { return ResourceManager.GetString("DeleteParameterMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Are you sure you want to delete the value for parameter `{0}`? /// @@ -151,7 +187,7 @@ internal static string DeleteParameterMessageWithUserSecrets { return ResourceManager.GetString("DeleteParameterMessageWithUserSecrets", resourceCulture); } } - + /// /// Looks up a localized string similar to Delete. /// @@ -160,7 +196,7 @@ internal static string DeleteParameterPrimaryButtonText { return ResourceManager.GetString("DeleteParameterPrimaryButtonText", resourceCulture); } } - + /// /// Looks up a localized string similar to Delete parameter value. /// @@ -169,7 +205,7 @@ internal static string DeleteParameterTitle { return ResourceManager.GetString("DeleteParameterTitle", resourceCulture); } } - + /// /// Looks up a localized string similar to There are unresolved parameters that need to be set. Please provide values for them.. /// @@ -178,7 +214,7 @@ internal static string ParametersBarMessage { return ResourceManager.GetString("ParametersBarMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Enter values. /// @@ -187,7 +223,7 @@ internal static string ParametersBarPrimaryButtonText { return ResourceManager.GetString("ParametersBarPrimaryButtonText", resourceCulture); } } - + /// /// Looks up a localized string similar to Unresolved parameters. /// @@ -196,7 +232,7 @@ internal static string ParametersBarTitle { return ResourceManager.GetString("ParametersBarTitle", resourceCulture); } } - + /// /// Looks up a localized string similar to Delete from user secrets. /// @@ -205,7 +241,7 @@ internal static string ParametersInputsDeleteLabel { return ResourceManager.GetString("ParametersInputsDeleteLabel", resourceCulture); } } - + /// /// Looks up a localized string similar to Please provide values for the unresolved parameters. Parameters can be saved to [user secrets](https://aka.ms/aspire/user-secrets) for future use.. /// @@ -214,7 +250,7 @@ internal static string ParametersInputsMessage { return ResourceManager.GetString("ParametersInputsMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Please provide values for the unresolved parameters.. /// @@ -223,7 +259,7 @@ internal static string ParametersInputsMessagePublishMode { return ResourceManager.GetString("ParametersInputsMessagePublishMode", resourceCulture); } } - + /// /// Looks up a localized string similar to Enter value for {0}. /// @@ -232,7 +268,7 @@ internal static string ParametersInputsParameterPlaceholder { return ResourceManager.GetString("ParametersInputsParameterPlaceholder", resourceCulture); } } - + /// /// Looks up a localized string similar to Save. /// @@ -241,7 +277,7 @@ internal static string ParametersInputsPrimaryButtonText { return ResourceManager.GetString("ParametersInputsPrimaryButtonText", resourceCulture); } } - + /// /// Looks up a localized string similar to Save to user secrets. /// @@ -250,7 +286,7 @@ internal static string ParametersInputsRememberLabel { return ResourceManager.GetString("ParametersInputsRememberLabel", resourceCulture); } } - + /// /// Looks up a localized string similar to Requires a `UserSecretsId` to be configured in the AppHost project file. Run `aspire secret set` to initialize user secrets, or see [user secrets](https://aka.ms/aspire/user-secrets) for more information.. /// @@ -259,7 +295,7 @@ internal static string ParametersInputsRememberDescriptionNotConfigured { return ResourceManager.GetString("ParametersInputsRememberDescriptionNotConfigured", resourceCulture); } } - + /// /// Looks up a localized string similar to Set unresolved parameters. /// @@ -268,7 +304,7 @@ internal static string ParametersInputsTitle { return ResourceManager.GetString("ParametersInputsTitle", resourceCulture); } } - + /// /// Looks up a localized string similar to Please provide a value for the parameter. The parameter can be saved to [user secrets](https://aka.ms/aspire/user-secrets) for future use. /// @@ -279,7 +315,7 @@ internal static string SetParameterMessage { return ResourceManager.GetString("SetParameterMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Set parameter. /// @@ -288,7 +324,7 @@ internal static string SetParameterTitle { return ResourceManager.GetString("SetParameterTitle", resourceCulture); } } - + /// /// Looks up a localized string similar to Upgrade instructions. /// @@ -297,7 +333,7 @@ internal static string VersionCheckLinkText { return ResourceManager.GetString("VersionCheckLinkText", resourceCulture); } } - + /// /// Looks up a localized string similar to Aspire {0} is available.. /// @@ -306,7 +342,7 @@ internal static string VersionCheckMessage { return ResourceManager.GetString("VersionCheckMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Ignore. /// @@ -315,7 +351,7 @@ internal static string VersionCheckPrimaryButtonText { return ResourceManager.GetString("VersionCheckPrimaryButtonText", resourceCulture); } } - + /// /// Looks up a localized string similar to Update now. /// diff --git a/src/Aspire.Hosting/Resources/InteractionStrings.resx b/src/Aspire.Hosting/Resources/InteractionStrings.resx index f32c14b1f73..ef574ec505a 100644 --- a/src/Aspire.Hosting/Resources/InteractionStrings.resx +++ b/src/Aspire.Hosting/Resources/InteractionStrings.resx @@ -1,17 +1,17 @@ - @@ -184,6 +184,18 @@ Container runtime could not be found. See https://aka.ms/dotnet/aspire/containers for more details on supported container runtimes. + + Development certificate not fully trusted + + + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + + + No trusted development certificate + + + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + Set parameter diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.cs.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.cs.xlf index 5066bdee48c..a76ba1344e8 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.cs.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.cs.xlf @@ -66,6 +66,26 @@ Hodnota je aktuálně uložena v [tajných klíčích uživatele](https://learn. Odstranit hodnotu parametru + + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + + + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + + + + No trusted development certificate + No trusted development certificate + + There are unresolved parameters that need to be set. Please provide values for them. Existují nevyřešené parametry, které je třeba nastavit. Zadejte pro ně hodnoty. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.de.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.de.xlf index 2626b66e6b4..a4afd72f061 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.de.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.de.xlf @@ -66,6 +66,26 @@ Der Wert wird derzeit in [Benutzergeheimnissen](https://learn.microsoft.com/aspn Parameterwert löschen + + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + + + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + + + + No trusted development certificate + No trusted development certificate + + There are unresolved parameters that need to be set. Please provide values for them. Es gibt nicht aufgelöste Parameter, die festgelegt werden müssen. Geben Sie Werte für diese an. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.es.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.es.xlf index 46cfac962da..6d44c602afc 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.es.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.es.xlf @@ -66,6 +66,26 @@ El valor se guarda actualmente en [secretos de usuario](https://learn.microsoft. Eliminar valor de parámetro + + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + + + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + + + + No trusted development certificate + No trusted development certificate + + There are unresolved parameters that need to be set. Please provide values for them. Hay parámetros sin resolver que deben configurarse. Proporcione valores para ellos. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.fr.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.fr.xlf index 13295e25eb1..ab22b24bd09 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.fr.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.fr.xlf @@ -66,6 +66,26 @@ La valeur est actuellement enregistrée dans [secrets utilisateur](https://learn Supprimer la valeur du paramètre + + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + + + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + + + + No trusted development certificate + No trusted development certificate + + There are unresolved parameters that need to be set. Please provide values for them. Vous devez définir des paramètres non résolus. Veuillez leur fournir des valeurs. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.it.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.it.xlf index 3fea295d9c5..c2040c9bcd7 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.it.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.it.xlf @@ -66,6 +66,26 @@ Il valore è attualmente salvato nei [segreti utente](https://learn.microsoft.co Elimina valore parametro + + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + + + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + + + + No trusted development certificate + No trusted development certificate + + There are unresolved parameters that need to be set. Please provide values for them. Sono presenti parametri non risolti che devono essere impostati. Specifica i valori per questi parametri. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ja.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ja.xlf index 9a74d0f9616..7432c52fc74 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ja.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ja.xlf @@ -66,6 +66,26 @@ The value is currently saved in [user secrets](https://aka.ms/aspire/user-secret パラメーター値の削除 + + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + + + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + + + + No trusted development certificate + No trusted development certificate + + There are unresolved parameters that need to be set. Please provide values for them. 設定する必要のある未解決のパラメーターがあります。それらの値を指定してください。 diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ko.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ko.xlf index 898c34d3b50..7730cea51a1 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ko.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ko.xlf @@ -66,6 +66,26 @@ The value is currently saved in [user secrets](https://aka.ms/aspire/user-secret 매개 변수 값 삭제 + + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + + + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + + + + No trusted development certificate + No trusted development certificate + + There are unresolved parameters that need to be set. Please provide values for them. 설정해야 하는 해결되지 않은 매개 변수가 있습니다. 값을 입력하세요. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pl.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pl.xlf index 5d5533fe194..8a8191ec2a5 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pl.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pl.xlf @@ -66,6 +66,26 @@ Wartość jest obecnie zapisana we [wpisach tajnych użytkownika](https://learn. Usuń wartość parametru + + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + + + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + + + + No trusted development certificate + No trusted development certificate + + There are unresolved parameters that need to be set. Please provide values for them. Istnieją nierozpoznane parametry, które należy ustawić. Podaj dla nich wartości. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pt-BR.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pt-BR.xlf index 2834a604029..13f69fc3542 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pt-BR.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pt-BR.xlf @@ -66,6 +66,26 @@ O valor está atualmente salvo em [segredos do usuário](https://learn.microsof Excluir valor do parâmetro + + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + + + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + + + + No trusted development certificate + No trusted development certificate + + There are unresolved parameters that need to be set. Please provide values for them. Há parâmetros não resolvidos que precisam ser definidos. Insira valores para eles. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ru.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ru.xlf index fb48a6b5fda..a3a765a25a2 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ru.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ru.xlf @@ -66,6 +66,26 @@ The value is currently saved in [user secrets](https://aka.ms/aspire/user-secret Удалить значение параметра + + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + + + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + + + + No trusted development certificate + No trusted development certificate + + There are unresolved parameters that need to be set. Please provide values for them. Существуют неразрешенные параметры, которые необходимо настроить. Укажите значения для них. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.tr.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.tr.xlf index 34fd4ff2217..b8b32400324 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.tr.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.tr.xlf @@ -66,6 +66,26 @@ Değer şu anda [kullanıcı gizli dizinleri](https://learn.microsoft.com/aspnet Parametre değerini sil + + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + + + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + + + + No trusted development certificate + No trusted development certificate + + There are unresolved parameters that need to be set. Please provide values for them. Ayarlanması gereken çözülmemiş parametreler var. Lütfen bunlara değerler girin. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hans.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hans.xlf index 5f0dbcaeeea..7a0c2b73cf0 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hans.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hans.xlf @@ -66,6 +66,26 @@ The value is currently saved in [user secrets](https://aka.ms/aspire/user-secret 删除参数值 + + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + + + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + + + + No trusted development certificate + No trusted development certificate + + There are unresolved parameters that need to be set. Please provide values for them. 有未解析的参数需要设置。请为这些参数提供值。 diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hant.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hant.xlf index c17ca68b8fa..5e322c9ef56 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hant.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hant.xlf @@ -66,6 +66,26 @@ The value is currently saved in [user secrets](https://aka.ms/aspire/user-secret 刪除參數值 + + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + + + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + No trusted development certificate was found. See https://aka.ms/aspire/devcerts for more information. + + + + No trusted development certificate + No trusted development certificate + + There are unresolved parameters that need to be set. Please provide values for them. 有些未解析的參數需要設定。請提供它們的值。 diff --git a/src/Shared/X509Certificate2Extensions.cs b/src/Shared/X509Certificate2Extensions.cs index bc4c42acd80..91bd6bb5d58 100644 --- a/src/Shared/X509Certificate2Extensions.cs +++ b/src/Shared/X509Certificate2Extensions.cs @@ -46,8 +46,7 @@ public static byte GetCertificateVersion(this X509Certificate2 certificate) ArgumentNullException.ThrowIfNull(certificate); var byteArray = certificate.Extensions.OfType() - .Where(e => string.Equals(AspNetHttpsOid, e.Oid?.Value, StringComparison.Ordinal)) - .Single() + .Single(e => string.Equals(AspNetHttpsOid, e.Oid?.Value, StringComparison.Ordinal)) .RawData; if ((byteArray.Length == AspNetHttpsOidFriendlyName.Length && byteArray[0] == (byte)'A') || byteArray.Length == 0) @@ -117,4 +116,17 @@ public static bool HasSubjectKeyIdentifier(this X509Certificate2 certificate) return certificate.Extensions.OfType().Any(ski => !string.IsNullOrEmpty(ski.SubjectKeyIdentifier)); } + + /// + /// Orders certificates by version descending, then by expiration date descending, + /// matching the ordering logic ASP.NET Core uses. + /// + /// The certificates to order. + /// The certificates ordered by version descending, then by expiration date descending. + public static IOrderedEnumerable OrderByVersion(this IEnumerable certificates) + { + return certificates + .OrderByDescending(c => c.GetCertificateVersion()) + .ThenByDescending(c => c.NotAfter); + } } diff --git a/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs index 14d479bba23..2df7c8ee0d4 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs @@ -1,3 +1,5 @@ +#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -572,13 +574,19 @@ private static DashboardEventHandlers CreateHook( codespacesOptions ??= Options.Create(new CodespacesOptions()); dashboardOptions ??= Options.Create(new DashboardOptions { DashboardPath = "test.dll" }); var rewriter = new CodespacesUrlRewriter(codespacesOptions); + var executionContextServiceProvider = new TestServiceProvider(configuration) + .AddService(new TestDeveloperCertificateService([], supportsContainerTrust: true, trustCertificate: true, tlsTerminate: true)); + var executionContext = new DistributedApplicationExecutionContext(new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run) + { + ServiceProvider = executionContextServiceProvider + }); return new DashboardEventHandlers( configuration, dashboardOptions, distributedApplicationLogger ?? NullLogger.Instance, new TestDashboardEndpointProvider(), - new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run), + executionContext, resourceNotificationService, resourceLoggerService, loggerFactory ?? NullLoggerFactory.Instance, diff --git a/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs index cb346a339cb..cb948fc505c 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs @@ -14,6 +14,8 @@ namespace Aspire.Hosting.Tests.Dashboard; +#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only + [Trait("Partition", "3")] public class DashboardResourceTests(ITestOutputHelper testOutputHelper) { @@ -462,6 +464,33 @@ public async Task DashboardResource_OtlpGrpcEndpoint_CorsEnvVarNotSet(string? ex Assert.DoesNotContain(config, e => e.Key == DashboardConfigNames.DashboardOtlpCorsAllowedHeadersKeyName.EnvVarName); } + [Fact] + public async Task DashboardResource_HttpsEndpoint_ConfiguresKestrelCertificateCallback() + { + using var builder = TestDistributedApplicationBuilder.Create(options => + { + options.DisableDashboard = false; + options.TrustDeveloperCertificate = true; + }, testOutputHelper: testOutputHelper); + + builder.Configuration.Sources.Clear(); + + builder.Configuration.AddInMemoryCollection(new Dictionary + { + [KnownConfigNames.AspNetCoreUrls] = "https://localhost", + [KnownConfigNames.DashboardOtlpGrpcEndpointUrl] = "http://localhost" + }); + + using var app = builder.Build(); + + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); + + var model = app.Services.GetRequiredService(); + var dashboard = Assert.Single(model.Resources, r => r.Name == "aspire-dashboard"); + + Assert.True(dashboard.HasAnnotationOfType()); + } + [Fact] public async Task DashboardIsNotAddedInPublishMode() { diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpHostNotificationTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpHostNotificationTests.cs index 6915a440c42..53ed48f8f63 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpHostNotificationTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpHostNotificationTests.cs @@ -2,18 +2,24 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; using Aspire.Hosting.Dcp; using Aspire.Hosting.Resources; +using Aspire.Hosting.Tests.Utils; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; using Microsoft.Extensions.Time.Testing; namespace Aspire.Hosting.Tests.Dcp; #pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. [Trait("Partition", "4")] public sealed class DcpHostNotificationTests @@ -36,6 +42,10 @@ public void DcpHost_WithIInteractionService_CanBeConstructed() var applicationModel = new DistributedApplicationModel(new ResourceCollection()); var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([], false, false, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); + // Act & Assert - should not throw var dcpHost = new DcpHost( loggerFactory, @@ -44,7 +54,10 @@ public void DcpHost_WithIInteractionService_CanBeConstructed() interactionService, locations, applicationModel, - timeProvider); + timeProvider, + developerCertificateService, + fileSystemService, + configuration); Assert.NotNull(dcpHost); } @@ -74,6 +87,9 @@ public async Task DcpHost_WithUnhealthyContainerRuntime_ShowsNotification() var interactionService = new TestInteractionService { IsAvailable = true }; var locations = CreateTestLocations(); var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([], false, false, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); var dcpHost = new DcpHost( loggerFactory, @@ -82,7 +98,10 @@ public async Task DcpHost_WithUnhealthyContainerRuntime_ShowsNotification() interactionService, locations, applicationModel, - timeProvider); + timeProvider, + developerCertificateService, + fileSystemService, + configuration); // Act await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout(); @@ -98,6 +117,52 @@ public async Task DcpHost_WithUnhealthyContainerRuntime_ShowsNotification() Assert.Equal(MessageIntent.Error, notificationOptions.Intent); } + [Fact] + public async Task DcpHost_WithUntrustedDeveloperCertificate_ShowsNotificationAndLogsWarning() + { + // Arrange + using var certificate = CreateUntrustedCertificate(); + var testSink = new TestSink(); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddProvider(new TestLoggerProvider(testSink)); + }); + var dcpOptions = Options.Create(new DcpOptions()); + var dependencyCheckService = new TestDcpDependencyCheckService(); + var interactionService = new TestInteractionService { IsAvailable = true }; + var locations = CreateTestLocations(); + var applicationModel = CreateApplicationModelWithHttpsEndpoint(); + var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false, latestCertificateIsUntrusted: true); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); + + var dcpHost = new DcpHost( + loggerFactory, + dcpOptions, + dependencyCheckService, + interactionService, + locations, + applicationModel, + timeProvider, + developerCertificateService, + fileSystemService, + configuration); + + // Act + await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout(); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token); + + // Assert + Assert.Equal(InteractionStrings.DeveloperCertificateNotFullyTrustedTitle, interaction.Title); + Assert.Equal(InteractionStrings.DeveloperCertificateNotFullyTrustedMessage, interaction.Message); + var notificationOptions = Assert.IsType(interaction.Options); + Assert.Equal(MessageIntent.Error, notificationOptions.Intent); + Assert.Contains(testSink.Writes, w => w.LogLevel == LogLevel.Warning && w.Message is not null && w.Message.Contains("aka.ms/aspire/devcerts", StringComparison.Ordinal)); + } + [Fact] public async Task DcpHost_WithHealthyContainerRuntime_DoesNotShowNotification() { @@ -123,6 +188,9 @@ public async Task DcpHost_WithHealthyContainerRuntime_DoesNotShowNotification() var interactionService = new TestInteractionService { IsAvailable = true }; var locations = CreateTestLocations(); var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([], false, false, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); var dcpHost = new DcpHost( loggerFactory, @@ -131,7 +199,10 @@ public async Task DcpHost_WithHealthyContainerRuntime_DoesNotShowNotification() interactionService, locations, applicationModel, - timeProvider); + timeProvider, + developerCertificateService, + fileSystemService, + configuration); // Act await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout(); @@ -178,6 +249,9 @@ public async Task DcpHost_WithDashboardDisabled_DoesNotShowNotification() var interactionService = new TestInteractionService { IsAvailable = false }; // Dashboard disabled var locations = CreateTestLocations(); var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([], false, false, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); var dcpHost = new DcpHost( loggerFactory, @@ -186,7 +260,10 @@ public async Task DcpHost_WithDashboardDisabled_DoesNotShowNotification() interactionService, locations, applicationModel, - timeProvider); + timeProvider, + developerCertificateService, + fileSystemService, + configuration); // Act await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout(); @@ -233,6 +310,9 @@ public async Task DcpHost_WithPodmanUnhealthy_ShowsCorrectMessage() var interactionService = new TestInteractionService { IsAvailable = true }; var locations = CreateTestLocations(); var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([], false, false, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); var dcpHost = new DcpHost( loggerFactory, @@ -241,7 +321,10 @@ public async Task DcpHost_WithPodmanUnhealthy_ShowsCorrectMessage() interactionService, locations, applicationModel, - timeProvider); + timeProvider, + developerCertificateService, + fileSystemService, + configuration); // Act await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout(); @@ -283,6 +366,9 @@ public async Task DcpHost_WithUnhealthyContainerRuntime_NotificationCancelledWhe var interactionService = new TestInteractionService { IsAvailable = true }; var locations = CreateTestLocations(); var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([], false, false, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); var dcpHost = new DcpHost( loggerFactory, @@ -291,7 +377,10 @@ public async Task DcpHost_WithUnhealthyContainerRuntime_NotificationCancelledWhe interactionService, locations, applicationModel, - timeProvider); + timeProvider, + developerCertificateService, + fileSystemService, + configuration); // Act await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout(); @@ -351,6 +440,9 @@ public async Task DcpHost_WithContainerRuntimeNotInstalled_ShowsNotificationWith var interactionService = new TestInteractionService { IsAvailable = true }; var locations = CreateTestLocations(); var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([], false, false, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); var dcpHost = new DcpHost( loggerFactory, @@ -359,7 +451,10 @@ public async Task DcpHost_WithContainerRuntimeNotInstalled_ShowsNotificationWith interactionService, locations, applicationModel, - timeProvider); + timeProvider, + developerCertificateService, + fileSystemService, + configuration); // Act await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout(); @@ -389,6 +484,377 @@ private static DistributedApplication CreateAppWithContainers() return builder.Build(); } + private static DistributedApplicationModel CreateApplicationModelWithHttpsEndpoint() + { + var resource = new ContainerResource("test-resource"); + resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "https", name: "https")); + return new DistributedApplicationModel(new ResourceCollection([resource])); + } + + private static DistributedApplicationModel CreateApplicationModelWithHttpEndpoint() + { + var resource = new ContainerResource("test-resource"); + resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http")); + return new DistributedApplicationModel(new ResourceCollection([resource])); + } + + private static X509Certificate2 CreateUntrustedCertificate() + { + var searchPaths = new[] + { + Path.Combine(Directory.GetCurrentDirectory(), "tests", "Shared", "TestCertificates", "testCert.pfx"), + Path.Combine(AppContext.BaseDirectory, "shared", "TestCertificates", "testCert.pfx"), + Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "tests", "Shared", "TestCertificates", "testCert.pfx")) + }; + + foreach (var path in searchPaths) + { + if (File.Exists(path)) + { + return new X509Certificate2(path, "testPassword"); + } + } + + throw new FileNotFoundException("Could not locate test certificate file 'testCert.pfx' in expected locations."); + } + + [Fact] + public async Task DcpHost_WithNoHttpsResources_DoesNotShowCertificateWarning() + { + // Arrange - only HTTP endpoints, no HTTPS + using var certificate = CreateUntrustedCertificate(); + var testSink = new TestSink(); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddProvider(new TestLoggerProvider(testSink)); + }); + var dcpOptions = Options.Create(new DcpOptions()); + var dependencyCheckService = new TestDcpDependencyCheckService(); + var interactionService = new TestInteractionService { IsAvailable = true }; + var locations = CreateTestLocations(); + var applicationModel = CreateApplicationModelWithHttpEndpoint(); + var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); + + var dcpHost = new DcpHost( + loggerFactory, + dcpOptions, + dependencyCheckService, + interactionService, + locations, + applicationModel, + timeProvider, + developerCertificateService, + fileSystemService, + configuration); + + // Act + await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout(); + + // Assert - no notification or warning should be shown + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); + var hasInteraction = false; + try + { + await interactionService.Interactions.Reader.ReadAsync(cts.Token); + hasInteraction = true; + } + catch (OperationCanceledException) + { + // Expected - no notification + } + + Assert.False(hasInteraction); + Assert.DoesNotContain(testSink.Writes, w => w.LogLevel == LogLevel.Warning); + } + + [Fact] + public async Task DcpHost_WithNoResources_DoesNotShowCertificateWarning() + { + // Arrange - empty resource model + using var certificate = CreateUntrustedCertificate(); + var testSink = new TestSink(); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddProvider(new TestLoggerProvider(testSink)); + }); + var dcpOptions = Options.Create(new DcpOptions()); + var dependencyCheckService = new TestDcpDependencyCheckService(); + var interactionService = new TestInteractionService { IsAvailable = true }; + var locations = CreateTestLocations(); + var applicationModel = new DistributedApplicationModel(new ResourceCollection()); + var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); + + var dcpHost = new DcpHost( + loggerFactory, + dcpOptions, + dependencyCheckService, + interactionService, + locations, + applicationModel, + timeProvider, + developerCertificateService, + fileSystemService, + configuration); + + // Act + await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout(); + + // Assert - no notification or warning should be shown for empty model + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); + var hasInteraction = false; + try + { + await interactionService.Interactions.Reader.ReadAsync(cts.Token); + hasInteraction = true; + } + catch (OperationCanceledException) + { + // Expected - no notification + } + + Assert.False(hasInteraction); + Assert.DoesNotContain(testSink.Writes, w => w.LogLevel == LogLevel.Warning); + } + + [Fact] + public async Task DcpHost_WithHttpsCertificateConfigCallback_ShowsCertificateWarning() + { + // Arrange - resource has HttpsCertificateConfigurationCallbackAnnotation but no HTTPS endpoint + using var certificate = CreateUntrustedCertificate(); + var testSink = new TestSink(); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddProvider(new TestLoggerProvider(testSink)); + }); + var dcpOptions = Options.Create(new DcpOptions()); + var dependencyCheckService = new TestDcpDependencyCheckService(); + var interactionService = new TestInteractionService { IsAvailable = true }; + var locations = CreateTestLocations(); + + var resource = new ContainerResource("test-resource"); + resource.Annotations.Add(new HttpsCertificateConfigurationCallbackAnnotation(_ => Task.CompletedTask)); + var applicationModel = new DistributedApplicationModel(new ResourceCollection([resource])); + + var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false, latestCertificateIsUntrusted: true); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var appHostDirectory = Path.Combine(Path.GetTempPath(), "aspire-apphost-test"); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AppHost:Directory"] = appHostDirectory + }) + .Build(); + + var dcpHost = new DcpHost( + loggerFactory, + dcpOptions, + dependencyCheckService, + interactionService, + locations, + applicationModel, + timeProvider, + developerCertificateService, + fileSystemService, + configuration); + + // Act + await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout(); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token); + + // Assert - warning should be shown because HttpsCertificateConfigurationCallbackAnnotation indicates TLS + Assert.Equal(InteractionStrings.DeveloperCertificateNotFullyTrustedTitle, interaction.Title); + Assert.Contains(testSink.Writes, w => w.LogLevel == LogLevel.Warning); + } + + [Fact] + public async Task DcpHost_WithHttpsCertificateConfigCallbackDisabledByWithoutHttpsCertificate_DoesNotShowCertificateWarning() + { + // Arrange - resource has HttpsCertificateConfigurationCallbackAnnotation but disabled by WithoutHttpsCertificate + using var certificate = CreateUntrustedCertificate(); + var testSink = new TestSink(); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddProvider(new TestLoggerProvider(testSink)); + }); + var dcpOptions = Options.Create(new DcpOptions()); + var dependencyCheckService = new TestDcpDependencyCheckService(); + var interactionService = new TestInteractionService { IsAvailable = true }; + var locations = CreateTestLocations(); + + var resource = new ContainerResource("test-resource"); + resource.Annotations.Add(new HttpsCertificateConfigurationCallbackAnnotation(_ => Task.CompletedTask)); + // This is the state set by WithoutHttpsCertificate() + resource.Annotations.Add(new HttpsCertificateAnnotation + { + Certificate = null, + UseDeveloperCertificate = false, + }); + var applicationModel = new DistributedApplicationModel(new ResourceCollection([resource])); + + var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); + + var dcpHost = new DcpHost( + loggerFactory, + dcpOptions, + dependencyCheckService, + interactionService, + locations, + applicationModel, + timeProvider, + developerCertificateService, + fileSystemService, + configuration); + + // Act + await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout(); + + // Assert - no warning because TLS was explicitly disabled + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); + var hasInteraction = false; + try + { + await interactionService.Interactions.Reader.ReadAsync(cts.Token); + hasInteraction = true; + } + catch (OperationCanceledException) + { + // Expected - no notification + } + + Assert.False(hasInteraction); + Assert.DoesNotContain(testSink.Writes, w => w.LogLevel == LogLevel.Warning); + } + + [Fact] + public async Task DcpHost_WithHttpsCertificateAnnotationOnly_DoesNotShowCertificateWarning() + { + // Arrange - resource has HttpsCertificateAnnotation but NO HttpsCertificateConfigurationCallbackAnnotation + // HttpsCertificateAnnotation alone has no effect without the callback annotation + using var certificate = CreateUntrustedCertificate(); + var testSink = new TestSink(); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddProvider(new TestLoggerProvider(testSink)); + }); + var dcpOptions = Options.Create(new DcpOptions()); + var dependencyCheckService = new TestDcpDependencyCheckService(); + var interactionService = new TestInteractionService { IsAvailable = true }; + var locations = CreateTestLocations(); + + var resource = new ContainerResource("test-resource"); + resource.Annotations.Add(new HttpsCertificateAnnotation + { + UseDeveloperCertificate = true, + }); + var applicationModel = new DistributedApplicationModel(new ResourceCollection([resource])); + + var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); + + var dcpHost = new DcpHost( + loggerFactory, + dcpOptions, + dependencyCheckService, + interactionService, + locations, + applicationModel, + timeProvider, + developerCertificateService, + fileSystemService, + configuration); + + // Act + await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout(); + + // Assert - no warning because HttpsCertificateAnnotation alone has no effect + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); + var hasInteraction = false; + try + { + await interactionService.Interactions.Reader.ReadAsync(cts.Token); + hasInteraction = true; + } + catch (OperationCanceledException) + { + // Expected - no notification + } + + Assert.False(hasInteraction); + Assert.DoesNotContain(testSink.Writes, w => w.LogLevel == LogLevel.Warning); + } + + [Fact] + public async Task DcpHost_WithHttpsCertificateConfigCallbackAndDevCert_ShowsCertificateWarning() + { + // Arrange - resource has both HttpsCertificateConfigurationCallbackAnnotation and HttpsCertificateAnnotation + // with UseDeveloperCertificate = true (not disabled) + using var certificate = CreateUntrustedCertificate(); + var testSink = new TestSink(); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddProvider(new TestLoggerProvider(testSink)); + }); + var dcpOptions = Options.Create(new DcpOptions()); + var dependencyCheckService = new TestDcpDependencyCheckService(); + var interactionService = new TestInteractionService { IsAvailable = true }; + var locations = CreateTestLocations(); + + var resource = new ContainerResource("test-resource"); + resource.Annotations.Add(new HttpsCertificateConfigurationCallbackAnnotation(_ => Task.CompletedTask)); + resource.Annotations.Add(new HttpsCertificateAnnotation + { + UseDeveloperCertificate = true, + }); + var applicationModel = new DistributedApplicationModel(new ResourceCollection([resource])); + + var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false, latestCertificateIsUntrusted: true); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var appHostDirectory = Path.Combine(Path.GetTempPath(), "aspire-apphost-test"); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AppHost:Directory"] = appHostDirectory + }) + .Build(); + + var dcpHost = new DcpHost( + loggerFactory, + dcpOptions, + dependencyCheckService, + interactionService, + locations, + applicationModel, + timeProvider, + developerCertificateService, + fileSystemService, + configuration); + + // Act + await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout(); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token); + + // Assert - warning should be shown because TLS is active + Assert.Equal(InteractionStrings.DeveloperCertificateNotFullyTrustedTitle, interaction.Title); + Assert.Contains(testSink.Writes, w => w.LogLevel == LogLevel.Warning); + } + private sealed class TestDcpDependencyCheckService : IDcpDependencyCheckService { public DcpInfo? DcpInfoResult { get; set; } = new DcpInfo @@ -411,3 +877,4 @@ private sealed class TestDcpDependencyCheckService : IDcpDependencyCheckService } #pragma warning restore ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning restore ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/tests/Aspire.Hosting.Tests/Utils/TestDeveloperCertificateService.cs b/tests/Aspire.Hosting.Tests/Utils/TestDeveloperCertificateService.cs index 6141e1251e7..df33e59e644 100644 --- a/tests/Aspire.Hosting.Tests/Utils/TestDeveloperCertificateService.cs +++ b/tests/Aspire.Hosting.Tests/Utils/TestDeveloperCertificateService.cs @@ -7,7 +7,7 @@ namespace Aspire.Hosting.Tests.Utils; #pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. -public sealed class TestDeveloperCertificateService(List certificates, bool supportsContainerTrust, bool trustCertificate, bool tlsTerminate) : IDeveloperCertificateService +public sealed class TestDeveloperCertificateService(List certificates, bool supportsContainerTrust, bool trustCertificate, bool tlsTerminate, bool latestCertificateIsUntrusted = false) : IDeveloperCertificateService #pragma warning restore ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. { /// @@ -21,4 +21,7 @@ public sealed class TestDeveloperCertificateService(List certi /// public bool UseForHttps => !OperatingSystem.IsMacOS() && tlsTerminate && trustCertificate; + + /// + public bool LatestCertificateIsUntrusted => latestCertificateIsUntrusted; } diff --git a/tests/Shared/TemplatesTesting/BuildEnvironment.cs b/tests/Shared/TemplatesTesting/BuildEnvironment.cs index fff69c247de..4cfece7b90a 100644 --- a/tests/Shared/TemplatesTesting/BuildEnvironment.cs +++ b/tests/Shared/TemplatesTesting/BuildEnvironment.cs @@ -182,8 +182,9 @@ public BuildEnvironment(bool useSystemDotNet = false, string sdkDirName = "dotne if (OperatingSystem.IsMacOS()) { - // Disable default developer certificate server authentication in MacOS due to test performance issues - EnvVars["ASPIRE_DEVELOPER_CERTIFICATE_DEFAULT_SERVER_AUTHENTICATION"] = "false"; + // Disable developer certificate trust and HTTPS termination in macOS template tests to avoid keychain prompts. + EnvVars["ASPIRE_DEVELOPER_CERTIFICATE_DEFAULT_TRUST"] = "false"; + EnvVars["ASPIRE_DEVELOPER_CERTIFICATE_DEFAULT_HTTPS_TERMINATION"] = "false"; } DotNet = Path.Combine(sdkForTemplatePath!, "dotnet");