Fix external authentication connection contracts

This commit is contained in:
Sipke Schoorstra 2026-07-27 10:07:29 +02:00
parent 75a3216ae8
commit eb07aa2cde
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
5 changed files with 75 additions and 4 deletions

View file

@ -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),

View file

@ -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();
}
}

View file

@ -116,7 +116,7 @@ internal sealed class ConnectionResponse
public PolicySelection? UnlinkedPolicy { get; init; }
public IReadOnlyCollection<GrantSourceSelection> 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);

View file

@ -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<string>(), redactedClaimTypes = Array.Empty<string>(), maximumClaimCount = 0, maximumValueLength = 0, maximumTotalBytes = 0 },
upstreamLogoutMode = "disabled",
upstreamLogoutMode,
confirmUnsafeSettings,
unlinkedPolicy
};

View file

@ -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));
}
}