| | | 1 | | using Elsa.Common; |
| | | 2 | | using Elsa.ExternalAuthentication.Contracts; |
| | | 3 | | using Elsa.ExternalAuthentication.Models; |
| | | 4 | | |
| | | 5 | | namespace Elsa.ExternalAuthentication.Services; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// Resolves adapter-specific structural validity without invoking provider test endpoints. |
| | | 9 | | /// </summary> |
| | 23 | 10 | | public sealed class IdentityProviderConnectionValidityAssessor( |
| | 23 | 11 | | IExternalAuthenticationAdapterRegistry adapters, |
| | 23 | 12 | | IAdapterSettingsMigrationService settingsMigrations, |
| | 23 | 13 | | IEnumerable<ISecretBindingResolver> secretBindingResolvers, |
| | 23 | 14 | | ISystemClock clock) : IIdentityProviderConnectionValidityAssessor |
| | | 15 | | { |
| | 23 | 16 | | private readonly IReadOnlyDictionary<string, ISecretBindingResolver> _secretBindingResolvers = |
| | 45 | 17 | | secretBindingResolvers.ToDictionary(x => x.Type, StringComparer.Ordinal); |
| | | 18 | | |
| | | 19 | | public async ValueTask<EffectiveIdentityProviderConnection> AssessAsync( |
| | | 20 | | EffectiveIdentityProviderConnection effective, |
| | | 21 | | CancellationToken cancellationToken = default) |
| | | 22 | | { |
| | 44 | 23 | | if (effective.Validity == ConnectionValidity.Invalid) |
| | 0 | 24 | | return effective; |
| | | 25 | | |
| | | 26 | | try |
| | | 27 | | { |
| | 44 | 28 | | return await AssessCoreAsync(effective, cancellationToken); |
| | | 29 | | } |
| | 0 | 30 | | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) |
| | | 31 | | { |
| | 0 | 32 | | throw; |
| | | 33 | | } |
| | 1 | 34 | | catch |
| | | 35 | | { |
| | | 36 | | // A broken adapter or secret backend must not prevent every other sign-in method from being discovered. |
| | 1 | 37 | | return effective with { Validity = ConnectionValidity.Invalid }; |
| | | 38 | | } |
| | 44 | 39 | | } |
| | | 40 | | |
| | | 41 | | private async ValueTask<EffectiveIdentityProviderConnection> AssessCoreAsync( |
| | | 42 | | EffectiveIdentityProviderConnection effective, |
| | | 43 | | CancellationToken cancellationToken) |
| | | 44 | | { |
| | | 45 | | |
| | 44 | 46 | | var connection = IdentityProviderConnectionCloner.Clone(effective.Connection); |
| | 44 | 47 | | if (!adapters.TryGet(connection.AdapterType, out var adapter)) |
| | 1 | 48 | | return effective with { Validity = ConnectionValidity.Invalid }; |
| | | 49 | | |
| | | 50 | | try |
| | | 51 | | { |
| | 43 | 52 | | var migration = await settingsMigrations.MigrateAsync( |
| | 43 | 53 | | connection.AdapterType, |
| | 43 | 54 | | connection.AdapterSettingsVersion, |
| | 43 | 55 | | connection.AdapterSettings, |
| | 43 | 56 | | cancellationToken); |
| | 43 | 57 | | connection.AdapterSettingsVersion = migration.SettingsVersion; |
| | 43 | 58 | | connection.AdapterSettings = migration.Settings; |
| | 43 | 59 | | } |
| | 0 | 60 | | catch (InvalidOperationException) |
| | | 61 | | { |
| | 0 | 62 | | return effective with { Validity = ConnectionValidity.Invalid }; |
| | | 63 | | } |
| | | 64 | | |
| | 43 | 65 | | var descriptor = adapter.Describe(); |
| | 43 | 66 | | var declaredSecrets = descriptor.Fields |
| | 84 | 67 | | .Where(x => x.IsSecretBinding) |
| | 86 | 68 | | .ToDictionary(x => x.Name, StringComparer.Ordinal); |
| | 47 | 69 | | if (connection.SecretBindings.Keys.Any(x => !declaredSecrets.ContainsKey(x))) |
| | 0 | 70 | | return effective with { Validity = ConnectionValidity.Invalid }; |
| | | 71 | | |
| | 43 | 72 | | var secretStates = await GetSecretStatesAsync(connection, cancellationToken); |
| | 42 | 73 | | if (declaredSecrets.Values |
| | 42 | 74 | | .Where(x => x.IsRequired) |
| | 44 | 75 | | .Any(field => !secretStates.TryGetValue(field.Name, out var state) || !state.IsConfigured || !state.IsResolv |
| | 2 | 76 | | return effective with { Validity = ConnectionValidity.Invalid }; |
| | | 77 | | |
| | 40 | 78 | | var validation = await adapter.ValidateAsync(new ConnectionValidationContext( |
| | 40 | 79 | | effective with { Connection = connection, Validity = ConnectionValidity.Unknown }, |
| | 40 | 80 | | new Dictionary<string, ResolvedSecretBinding>(), |
| | 40 | 81 | | clock), cancellationToken); |
| | 40 | 82 | | return effective with { Validity = validation.IsValid ? ConnectionValidity.Valid : ConnectionValidity.Invalid }; |
| | 43 | 83 | | } |
| | | 84 | | |
| | | 85 | | private async ValueTask<IReadOnlyDictionary<string, SecretBindingState>> GetSecretStatesAsync( |
| | | 86 | | IdentityProviderConnection connection, |
| | | 87 | | CancellationToken cancellationToken) |
| | | 88 | | { |
| | 43 | 89 | | var states = new Dictionary<string, SecretBindingState>(StringComparer.Ordinal); |
| | 93 | 90 | | foreach (var (name, binding) in connection.SecretBindings) |
| | | 91 | | { |
| | 4 | 92 | | states[name] = _secretBindingResolvers.TryGetValue(binding.ResolverType, out var resolver) |
| | 4 | 93 | | ? await resolver.GetStateAsync(binding, cancellationToken) |
| | 4 | 94 | | : new SecretBindingState(false, false); |
| | | 95 | | } |
| | | 96 | | |
| | 42 | 97 | | return states; |
| | 42 | 98 | | } |
| | | 99 | | } |