| | | 1 | | using System.Diagnostics; |
| | | 2 | | using System.Security.Claims; |
| | | 3 | | using Elsa.Common; |
| | | 4 | | using Elsa.ExternalAuthentication.Contracts; |
| | | 5 | | using Elsa.ExternalAuthentication.Models; |
| | | 6 | | using Elsa.ExternalAuthentication.Notifications; |
| | | 7 | | |
| | | 8 | | namespace Elsa.ExternalAuthentication.Services; |
| | | 9 | | |
| | | 10 | | /// <summary>Runs an explicit adapter test and stores only its latest redacted outcome.</summary> |
| | | 11 | | public sealed class ConnectionTestService( |
| | | 12 | | IdentityProviderConnectionManagementService management, |
| | | 13 | | IExternalAuthenticationAdapterRegistry adapters, |
| | | 14 | | IEnumerable<ISecretBindingResolver> secretBindingResolvers, |
| | | 15 | | IConnectionObservationStore observations, |
| | | 16 | | ISystemClock clock, |
| | | 17 | | ExternalAuthenticationSecurityNotifier notifier) |
| | | 18 | | { |
| | | 19 | | private readonly IReadOnlyDictionary<string, ISecretBindingResolver> _resolvers = secretBindingResolvers.ToDictionar |
| | | 20 | | |
| | | 21 | | public async ValueTask<ConnectionTestOperationResult> TestAsync(string connectionId, long expectedRevision, string t |
| | | 22 | | { |
| | | 23 | | var lookup = await management.FindAsync(connectionId, tenantId, cancellationToken); |
| | | 24 | | if (lookup is not ManagementConnectionLookupResult.Found(var connection)) |
| | | 25 | | return new ConnectionTestOperationResult.NotFound(); |
| | | 26 | | if (connection.Connection.Revision != expectedRevision) |
| | | 27 | | return new ConnectionTestOperationResult.PreconditionFailed(connection.Connection.Revision); |
| | | 28 | | if (!adapters.TryGet(connection.Connection.AdapterType, out var adapter)) |
| | | 29 | | return new ConnectionTestOperationResult.Unavailable(); |
| | | 30 | | |
| | | 31 | | var timer = Stopwatch.StartNew(); |
| | | 32 | | ConnectionTestResult test; |
| | | 33 | | IReadOnlyDictionary<string, ResolvedSecretBinding> secrets = new Dictionary<string, ResolvedSecretBinding>(); |
| | | 34 | | try |
| | | 35 | | { |
| | | 36 | | secrets = await ResolveSecretsAsync(connection.Connection.SecretBindings, cancellationToken); |
| | | 37 | | test = await adapter.TestAsync(new ConnectionTestContext(connection, secrets, clock), cancellationToken); |
| | | 38 | | } |
| | | 39 | | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) |
| | | 40 | | { |
| | | 41 | | throw; |
| | | 42 | | } |
| | | 43 | | catch |
| | | 44 | | { |
| | | 45 | | test = new ConnectionTestResult(ConnectionObservationStatus.Failed, "unavailable", "The provider test could |
| | | 46 | | } |
| | | 47 | | finally |
| | | 48 | | { |
| | | 49 | | timer.Stop(); |
| | | 50 | | foreach (var secret in secrets.Values) |
| | | 51 | | secret.Value.Dispose(); |
| | | 52 | | } |
| | | 53 | | |
| | | 54 | | var observation = new ConnectionObservation( |
| | | 55 | | connection.Connection.Id, |
| | | 56 | | connection.Connection.MaterialRevision, |
| | | 57 | | clock.UtcNow, |
| | | 58 | | test.Status, |
| | | 59 | | SafeCategory(test.Category), |
| | | 60 | | timer.Elapsed, |
| | | 61 | | SafeSummary(test.Summary), |
| | | 62 | | test.Warnings.Select(SafeSummary).ToArray(), |
| | | 63 | | Guid.NewGuid().ToString("N")); |
| | | 64 | | await observations.SaveLatestAsync(observation, cancellationToken); |
| | | 65 | | await notifier.PublishAsync(new IdentityProviderConnectionTested( |
| | | 66 | | ExternalAuthenticationSecurityNotifier.Context( |
| | | 67 | | ActorId(actor), |
| | | 68 | | tenantId, |
| | | 69 | | connection.Connection.Id, |
| | | 70 | | null, |
| | | 71 | | observation.Status == ConnectionObservationStatus.Failed ? SecurityEventOutcome.Failed : SecurityEventOu |
| | | 72 | | "Identity provider connection test completed."), |
| | | 73 | | observation.TestedMaterialRevision, |
| | | 74 | | observation.Status.ToString().ToLowerInvariant(), |
| | | 75 | | observation.Category, |
| | | 76 | | observation.Duration), cancellationToken); |
| | | 77 | | return new ConnectionTestOperationResult.Completed(observation); |
| | | 78 | | } |
| | | 79 | | |
| | | 80 | | private async ValueTask<IReadOnlyDictionary<string, ResolvedSecretBinding>> ResolveSecretsAsync(IDictionary<string, |
| | | 81 | | { |
| | | 82 | | var result = new Dictionary<string, ResolvedSecretBinding>(StringComparer.Ordinal); |
| | | 83 | | try |
| | | 84 | | { |
| | | 85 | | foreach (var (name, binding) in bindings) |
| | | 86 | | { |
| | | 87 | | if (!_resolvers.TryGetValue(binding.ResolverType, out var resolver)) |
| | | 88 | | throw new InvalidOperationException("The secret binding resolver is unavailable."); |
| | | 89 | | result[name] = await resolver.ResolveAsync(binding, cancellationToken); |
| | | 90 | | } |
| | | 91 | | return result; |
| | | 92 | | } |
| | | 93 | | catch |
| | | 94 | | { |
| | | 95 | | foreach (var secret in result.Values) |
| | | 96 | | secret.Value.Dispose(); |
| | | 97 | | throw; |
| | | 98 | | } |
| | | 99 | | } |
| | | 100 | | |
| | | 101 | | private static string? ActorId(ClaimsPrincipal actor) => actor.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? actor. |
| | | 102 | | // Adapter messages are already contractually safe, but cap them at a predictable diagnostic size. |
| | | 103 | | private static string SafeSummary(string value) => string.IsNullOrWhiteSpace(value) ? "No additional details are ava |
| | | 104 | | private static string SafeCategory(string value) => string.IsNullOrWhiteSpace(value) ? "unknown" : value.Length <= 1 |
| | | 105 | | } |
| | | 106 | | |
| | | 107 | | public abstract record ConnectionTestOperationResult |
| | | 108 | | { |
| | 0 | 109 | | private ConnectionTestOperationResult() { } |
| | 0 | 110 | | public sealed record Completed(ConnectionObservation Observation) : ConnectionTestOperationResult; |
| | | 111 | | public sealed record NotFound : ConnectionTestOperationResult; |
| | 0 | 112 | | public sealed record PreconditionFailed(long CurrentRevision) : ConnectionTestOperationResult; |
| | | 113 | | public sealed record Unavailable : ConnectionTestOperationResult; |
| | | 114 | | } |