From eb07aa2cde1481ce041479ddb22ed4b0488996c9 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 27 Jul 2026 10:07:29 +0200 Subject: [PATCH] Fix external authentication connection contracts --- src/apps/Elsa.ModularServer.Web/Program.cs | 2 ++ ...lsaSecretsExternalAuthenticationFeature.cs | 23 +++++++++++++++++ .../Connections/ConnectionManagementModels.cs | 12 +++++++-- .../Connections/ConnectionManagementTests.cs | 17 +++++++++++-- ...cretsExternalAuthenticationFeatureTests.cs | 25 +++++++++++++++++++ 5 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 src/modules/Elsa.ExternalAuthentication.Secrets/ShellFeatures/ElsaSecretsExternalAuthenticationFeature.cs create mode 100644 test/unit/Elsa.ExternalAuthentication.UnitTests/Features/ElsaSecretsExternalAuthenticationFeatureTests.cs diff --git a/src/apps/Elsa.ModularServer.Web/Program.cs b/src/apps/Elsa.ModularServer.Web/Program.cs index 51428a442..b44a3c436 100644 --- a/src/apps/Elsa.ModularServer.Web/Program.cs +++ b/src/apps/Elsa.ModularServer.Web/Program.cs @@ -6,6 +6,7 @@ using Elsa.Dashboard.Api.ShellFeatures; using Elsa.Diagnostics.ConsoleLogs.Dashboard.ShellFeatures; using Elsa.Diagnostics.StructuredLogs.Dashboard.ShellFeatures; using Elsa.ExternalAuthentication.OpenIdConnect.ShellFeatures; +using Elsa.ExternalAuthentication.Secrets.ShellFeatures; using Elsa.ModularServer.Web; using Elsa.ModularServer.Web.Catalog; using Elsa.Platform.Integration.ShellFeatures; @@ -82,6 +83,7 @@ builder.AddShells(shells => shells typeof(DistributedRuntimeFeature), typeof(ElsaPlatformIntegrationFeature), typeof(OpenIdConnectExternalAuthenticationFeature), + typeof(ElsaSecretsExternalAuthenticationFeature), typeof(DashboardApiFeature), typeof(WorkflowRuntimeDashboardFeature), typeof(ConsoleLogsDashboardFeature), diff --git a/src/modules/Elsa.ExternalAuthentication.Secrets/ShellFeatures/ElsaSecretsExternalAuthenticationFeature.cs b/src/modules/Elsa.ExternalAuthentication.Secrets/ShellFeatures/ElsaSecretsExternalAuthenticationFeature.cs new file mode 100644 index 000000000..0753b227d --- /dev/null +++ b/src/modules/Elsa.ExternalAuthentication.Secrets/ShellFeatures/ElsaSecretsExternalAuthenticationFeature.cs @@ -0,0 +1,23 @@ +using CShells.Features; +using Elsa.ExternalAuthentication.ShellFeatures; +using Elsa.Platform.PackageManifest.Generator.Hints; +using Elsa.Secrets.ShellFeatures; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.ExternalAuthentication.Secrets.ShellFeatures; + +[ManifestFeatureCategory("Identity")] +[ManifestFeatureCategory("Security")] +[ShellFeature( + DisplayName = "Elsa Secrets External Authentication", + Description = "Provides managed External Authentication secret bindings backed by Elsa Secrets.", + DependsOn = [typeof(ExternalAuthenticationShellFeature), typeof(SecretsFeature)])] +[UsedImplicitly] +public sealed class ElsaSecretsExternalAuthenticationFeature : IShellFeature +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddElsaSecretsExternalAuthentication(); + } +} diff --git a/src/modules/Elsa.ExternalAuthentication/Endpoints/Connections/ConnectionManagementModels.cs b/src/modules/Elsa.ExternalAuthentication/Endpoints/Connections/ConnectionManagementModels.cs index eac404e03..e0063db3a 100644 --- a/src/modules/Elsa.ExternalAuthentication/Endpoints/Connections/ConnectionManagementModels.cs +++ b/src/modules/Elsa.ExternalAuthentication/Endpoints/Connections/ConnectionManagementModels.cs @@ -116,7 +116,7 @@ internal sealed class ConnectionResponse public PolicySelection? UnlinkedPolicy { get; init; } public IReadOnlyCollection PermissionGrantSources { get; init; } = []; public ClaimProjection ClaimProjection { get; init; } = ClaimProjection.Empty; - public UpstreamLogoutMode UpstreamLogoutMode { get; init; } + public string UpstreamLogoutMode { get; init; } = null!; public long Revision { get; init; } public string MaterialRevision { get; init; } = null!; public ConnectionObservationResponse? LatestObservation { get; init; } @@ -165,7 +165,7 @@ internal sealed class ConnectionResponse UnlinkedPolicy = effective.Connection.UnlinkedPolicy, PermissionGrantSources = effective.Connection.PermissionGrantSources.ToArray(), ClaimProjection = effective.Connection.ClaimProjection, - UpstreamLogoutMode = effective.Connection.UpstreamLogoutMode, + UpstreamLogoutMode = FormatUpstreamLogoutMode(effective.Connection.UpstreamLogoutMode), Revision = effective.Connection.Revision, MaterialRevision = effective.Connection.MaterialRevision, LatestObservation = observation is null @@ -179,6 +179,14 @@ internal sealed class ConnectionResponse observation.Summary) }; } + + private static string FormatUpstreamLogoutMode(UpstreamLogoutMode mode) => mode switch + { + Elsa.ExternalAuthentication.Models.UpstreamLogoutMode.Disabled => "disabled", + Elsa.ExternalAuthentication.Models.UpstreamLogoutMode.UserChoice => "user-choice", + Elsa.ExternalAuthentication.Models.UpstreamLogoutMode.Always => "always", + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "The upstream logout mode is not supported.") + }; } internal sealed record ConnectionObservationResponse(string Status, DateTimeOffset ObservedAt, string TestedMaterialRevision, bool IsStale, string Category, string Summary); diff --git a/test/integration/Elsa.ExternalAuthentication.IntegrationTests/Connections/ConnectionManagementTests.cs b/test/integration/Elsa.ExternalAuthentication.IntegrationTests/Connections/ConnectionManagementTests.cs index 4504dccf3..1519d76cc 100644 --- a/test/integration/Elsa.ExternalAuthentication.IntegrationTests/Connections/ConnectionManagementTests.cs +++ b/test/integration/Elsa.ExternalAuthentication.IntegrationTests/Connections/ConnectionManagementTests.cs @@ -162,6 +162,19 @@ public class ConnectionManagementTests : IAsyncLifetime Assert.False(restoredConnection.EnabledIntent); } + [Fact] + public async Task ConnectionResponseEmitsCanonicalUpstreamLogoutModeString() + { + var response = await _client!.PostAsJsonAsync( + "/external-authentication/connections", + CreateRequest("user-choice-logout", upstreamLogoutMode: "user-choice")); + var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.Equal(JsonValueKind.String, body.RootElement.GetProperty("upstreamLogoutMode").ValueKind); + Assert.Equal("user-choice", body.RootElement.GetProperty("upstreamLogoutMode").GetString()); + } + [Fact] public async Task ConfigurationConnectionIsReadOnlyAndBlocksSameScopeKeyCreation() { @@ -523,7 +536,7 @@ public class ConnectionManagementTests : IAsyncLifetime Assert.Equal(new[] { "workflow-user" }, _roleAuthorizationService.LastRequestedRoleIds); } - private static object CreateRequest(string key, object? scope = null, string displayName = "Contoso", object? settings = null, bool confirmUnsafeSettings = false, object? unlinkedPolicy = null) => new + private static object CreateRequest(string key, object? scope = null, string displayName = "Contoso", object? settings = null, bool confirmUnsafeSettings = false, object? unlinkedPolicy = null, string upstreamLogoutMode = "disabled") => new { key, scope = scope ?? new { kind = "host" }, @@ -533,7 +546,7 @@ public class ConnectionManagementTests : IAsyncLifetime displayName, order = 10, claimProjection = new { allowedClaimTypes = Array.Empty(), redactedClaimTypes = Array.Empty(), maximumClaimCount = 0, maximumValueLength = 0, maximumTotalBytes = 0 }, - upstreamLogoutMode = "disabled", + upstreamLogoutMode, confirmUnsafeSettings, unlinkedPolicy }; diff --git a/test/unit/Elsa.ExternalAuthentication.UnitTests/Features/ElsaSecretsExternalAuthenticationFeatureTests.cs b/test/unit/Elsa.ExternalAuthentication.UnitTests/Features/ElsaSecretsExternalAuthenticationFeatureTests.cs new file mode 100644 index 000000000..0cac2d465 --- /dev/null +++ b/test/unit/Elsa.ExternalAuthentication.UnitTests/Features/ElsaSecretsExternalAuthenticationFeatureTests.cs @@ -0,0 +1,25 @@ +using Elsa.ExternalAuthentication.Contracts; +using Elsa.ExternalAuthentication.Secrets.Services; +using Microsoft.Extensions.DependencyInjection; +using ElsaSecretsExternalAuthenticationShellFeature = Elsa.ExternalAuthentication.Secrets.ShellFeatures.ElsaSecretsExternalAuthenticationFeature; + +namespace Elsa.ExternalAuthentication.UnitTests.Features; + +public class ElsaSecretsExternalAuthenticationFeatureTests +{ + [Fact] + public void ShellFeatureRegistersTheManagedSecretBridge() + { + var services = new ServiceCollection(); + var feature = new ElsaSecretsExternalAuthenticationShellFeature(); + + feature.ConfigureServices(services); + + Assert.Contains(services, descriptor => + descriptor.ServiceType == typeof(ISecretBindingResolver) && + descriptor.ImplementationType == typeof(ElsaSecretBindingResolver)); + Assert.Contains(services, descriptor => + descriptor.ServiceType == typeof(IManagedSecretBindingWriter) && + descriptor.ImplementationType == typeof(ElsaSecretBindingResolver)); + } +}