| | | 1 | | using Elsa.ExternalAuthentication.Contracts; |
| | | 2 | | using Elsa.ExternalAuthentication.Models; |
| | | 3 | | |
| | | 4 | | namespace Elsa.ExternalAuthentication.Services; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Builds the read-through effective connection registry from configuration and optional persisted sources. |
| | | 8 | | /// </summary> |
| | 10 | 9 | | public sealed class DefaultIdentityProviderConnectionRegistry( |
| | 10 | 10 | | IEnumerable<IIdentityProviderConnectionSource> sources, |
| | 10 | 11 | | ConnectionRevisionCalculator revisionCalculator) : IIdentityProviderConnectionRegistry |
| | | 12 | | { |
| | 10 | 13 | | private readonly IReadOnlyList<IIdentityProviderConnectionSource> _sources = sources |
| | 10 | 14 | | .OrderBy(x => GetOwnershipPriority(x.Ownership)) |
| | 10 | 15 | | .ThenBy(x => x.Name, StringComparer.Ordinal) |
| | 10 | 16 | | .ToArray(); |
| | | 17 | | |
| | | 18 | | public async ValueTask<EffectiveConnectionRegistry> GetAsync(string targetTenantId, CancellationToken cancellationTo |
| | | 19 | | { |
| | 18 | 20 | | var scopes = GetApplicableScopes(); |
| | 18 | 21 | | var snapshots = new List<(IIdentityProviderConnectionSource Source, ConnectionSourceSnapshot Snapshot)>(); |
| | | 22 | | |
| | 72 | 23 | | foreach (var scope in scopes) |
| | 82 | 24 | | foreach (var source in _sources) |
| | | 25 | | { |
| | 23 | 26 | | var snapshot = await source.GetSnapshotAsync(scope, cancellationToken); |
| | 23 | 27 | | if (snapshot.Scope != scope) |
| | 0 | 28 | | throw new InvalidOperationException($"Connection source '{source.Name}' returned a snapshot for a scope |
| | | 29 | | |
| | 23 | 30 | | snapshots.Add((source, snapshot)); |
| | 23 | 31 | | } |
| | | 32 | | |
| | 18 | 33 | | var candidates = snapshots |
| | | 34 | | .SelectMany(x => x.Snapshot.Connections.Select(connection => new Candidate(x.Source, x.Snapshot.Scope, conne |
| | | 35 | | .Where(x => IsInScope(x.Connection, x.Scope)) |
| | | 36 | | .OrderBy(x => ConnectionRevisionCalculator.NormalizeKey(x.Connection.Key), StringComparer.Ordinal) |
| | | 37 | | .ThenBy(x => GetOwnershipPriority(x.Source.Ownership)) |
| | | 38 | | .ThenBy(x => x.Source.Name, StringComparer.Ordinal) |
| | | 39 | | .ThenBy(x => x.Connection.Id, StringComparer.Ordinal) |
| | 18 | 40 | | .ToArray(); |
| | | 41 | | |
| | 18 | 42 | | var connections = new List<EffectiveIdentityProviderConnection>(candidates.Length); |
| | | 43 | | foreach (var group in candidates.GroupBy(x => ConnectionRevisionCalculator.NormalizeKey(x.Connection.Key), Strin |
| | | 44 | | { |
| | 40018 | 45 | | var candidatesForKey = group.ToArray(); |
| | | 46 | | var hasInheritedScopeCollision = candidatesForKey.Select(x => x.Scope).Distinct().Skip(1).Any(); |
| | | 47 | | |
| | | 48 | | var explicitOverride = candidatesForKey.FirstOrDefault(x => x.Source.Ownership == ConnectionSourceOwnership. |
| | | 49 | | var preferred = explicitOverride ?? candidatesForKey.FirstOrDefault(x => x.Source.Ownership == ConnectionSou |
| | 40018 | 50 | | var preferredReference = ToReference(preferred); |
| | 40018 | 51 | | var shadowedReferences = hasInheritedScopeCollision |
| | 40018 | 52 | | ? [] |
| | 40018 | 53 | | : candidatesForKey |
| | 40022 | 54 | | .Where(candidate => !ReferenceEquals(candidate, preferred) && !candidate.Connection.ArchivedAt.HasVa |
| | 40018 | 55 | | .Select(ToReference) |
| | 40018 | 56 | | .ToArray(); |
| | | 57 | | |
| | 160080 | 58 | | for (var index = 0; index < candidatesForKey.Length; index++) |
| | | 59 | | { |
| | 40022 | 60 | | var candidate = candidatesForKey[index]; |
| | 40022 | 61 | | var isArchived = candidate.Connection.ArchivedAt.HasValue; |
| | 40022 | 62 | | var isShadowed = !isArchived && !hasInheritedScopeCollision && !ReferenceEquals(candidate, preferred); |
| | 40022 | 63 | | connections.Add(new EffectiveIdentityProviderConnection( |
| | 40022 | 64 | | candidate.Connection, |
| | 40022 | 65 | | candidate.Source.Ownership, |
| | 40022 | 66 | | candidate.Scope, |
| | 40022 | 67 | | hasInheritedScopeCollision ? ConnectionValidity.Invalid : ConnectionValidity.Unknown, |
| | 40022 | 68 | | isShadowed, |
| | 40022 | 69 | | candidate.Source.Name) |
| | 40022 | 70 | | { |
| | 40022 | 71 | | ShadowedBy = isShadowed ? preferredReference : null, |
| | 40022 | 72 | | Shadows = isShadowed || isArchived ? [] : shadowedReferences |
| | 40022 | 73 | | }); |
| | | 74 | | } |
| | | 75 | | } |
| | | 76 | | |
| | 18 | 77 | | var orderedConnections = connections |
| | | 78 | | .OrderBy(x => x.Connection.DisplayOrder) |
| | | 79 | | .ThenBy(x => ConnectionRevisionCalculator.NormalizeKey(x.Connection.Key), StringComparer.Ordinal) |
| | | 80 | | .ThenBy(x => x.Connection.Id, StringComparer.Ordinal) |
| | 18 | 81 | | .ToArray(); |
| | | 82 | | |
| | | 83 | | var version = revisionCalculator.CalculateRegistryVersion(snapshots.Select(x => (x.Source.Name, x.Source.Ownersh |
| | 18 | 84 | | var loginMethods = CreateLoginMethods(orderedConnections); |
| | 18 | 85 | | return new EffectiveConnectionRegistry(orderedConnections, loginMethods, version); |
| | 18 | 86 | | } |
| | | 87 | | |
| | | 88 | | public async ValueTask<EffectiveIdentityProviderConnection?> FindByKeyAsync(string targetTenantId, string key, Cance |
| | | 89 | | { |
| | 3 | 90 | | var registry = await GetAsync(targetTenantId, cancellationToken); |
| | 3 | 91 | | var normalizedKey = ConnectionRevisionCalculator.NormalizeKey(key); |
| | 3 | 92 | | return registry.Connections.FirstOrDefault(x => |
| | 1547 | 93 | | IsAvailableForAuthentication(x) && |
| | 1547 | 94 | | string.Equals(ConnectionRevisionCalculator.NormalizeKey(x.Connection.Key), normalizedKey, StringComparison.O |
| | 3 | 95 | | } |
| | | 96 | | |
| | | 97 | | public async ValueTask<EffectiveIdentityProviderConnection?> FindByIdAsync(string targetTenantId, string connectionI |
| | | 98 | | { |
| | 1 | 99 | | var registry = await GetAsync(targetTenantId, cancellationToken); |
| | 1542 | 100 | | return registry.Connections.FirstOrDefault(x => string.Equals(x.Connection.Id, connectionId, StringComparison.Or |
| | 1 | 101 | | } |
| | | 102 | | |
| | | 103 | | private static IReadOnlyCollection<LoginMethod> CreateLoginMethods(IReadOnlyCollection<EffectiveIdentityProviderConn |
| | | 104 | | { |
| | 18 | 105 | | var available = connections |
| | 18 | 106 | | .Where(IsAvailableForAuthentication) |
| | 18 | 107 | | .ToArray(); |
| | 18 | 108 | | var configuredPreferred = available |
| | 40017 | 109 | | .Where(x => x.Ownership == ConnectionSourceOwnership.Configuration && x.Connection.IsPreferred) |
| | 6 | 110 | | .OrderBy(x => x.Connection.DisplayOrder) |
| | 6 | 111 | | .ThenBy(x => ConnectionRevisionCalculator.NormalizeKey(x.Connection.Key), StringComparer.Ordinal) |
| | 6 | 112 | | .ThenBy(x => x.Connection.Id, StringComparer.Ordinal) |
| | 18 | 113 | | .FirstOrDefault(); |
| | 18 | 114 | | if (configuredPreferred is not null) |
| | 6 | 115 | | return ToLoginMethods(available, configuredPreferred.Connection.Id); |
| | | 116 | | |
| | 12 | 117 | | var preferredConnectionId = available |
| | 14 | 118 | | .Where(x => x.Connection.IsPreferred) |
| | 1 | 119 | | .OrderBy(x => x.Connection.DisplayOrder) |
| | 1 | 120 | | .ThenBy(x => ConnectionRevisionCalculator.NormalizeKey(x.Connection.Key), StringComparer.Ordinal) |
| | 1 | 121 | | .ThenBy(x => x.Connection.Id, StringComparer.Ordinal) |
| | 1 | 122 | | .Select(x => x.Connection.Id) |
| | 12 | 123 | | .FirstOrDefault(); |
| | | 124 | | |
| | 12 | 125 | | return ToLoginMethods(available, preferredConnectionId); |
| | | 126 | | } |
| | | 127 | | |
| | 18 | 128 | | private static IReadOnlyCollection<LoginMethod> ToLoginMethods(IEnumerable<EffectiveIdentityProviderConnection> conn |
| | 40017 | 129 | | .OrderBy(x => x.Connection.DisplayOrder) |
| | 40017 | 130 | | .ThenBy(x => ConnectionRevisionCalculator.NormalizeKey(x.Connection.Key), StringComparer.Ordinal) |
| | 40017 | 131 | | .ThenBy(x => x.Connection.Id, StringComparer.Ordinal) |
| | 40017 | 132 | | .Select(x => new LoginMethod( |
| | 40017 | 133 | | x.Connection.Id, |
| | 40017 | 134 | | x.Connection.Key, |
| | 40017 | 135 | | LoginMethodKind.External, |
| | 40017 | 136 | | x.Connection.DisplayName, |
| | 40017 | 137 | | x.Connection.IconId, |
| | 40017 | 138 | | x.Connection.DisplayOrder, |
| | 40017 | 139 | | string.Equals(x.Connection.Id, preferredConnectionId, StringComparison.Ordinal), |
| | 40017 | 140 | | new Uri($"/external-authentication/authorize/{Uri.EscapeDataString(x.Connection.Key)}", UriKind.Relative |
| | 18 | 141 | | .ToArray(); |
| | | 142 | | |
| | 18 | 143 | | private static IReadOnlyList<ConnectionScope> GetApplicableScopes() => [ConnectionScope.Host]; |
| | | 144 | | |
| | 40022 | 145 | | private static bool IsInScope(IdentityProviderConnection connection, ConnectionScope scope) => string.Equals(connect |
| | | 146 | | private static bool IsAvailableForAuthentication(EffectiveIdentityProviderConnection connection) => |
| | 41566 | 147 | | !connection.IsShadowed && |
| | 41566 | 148 | | connection.Validity != ConnectionValidity.Invalid && |
| | 41566 | 149 | | connection.Connection.IsEnabled && |
| | 41566 | 150 | | !connection.Connection.ArchivedAt.HasValue; |
| | 40024 | 151 | | private static int GetOwnershipPriority(ConnectionSourceOwnership ownership) => ownership == ConnectionSourceOwnersh |
| | | 152 | | private static IdentityProviderConnectionReference ToReference(Candidate candidate) => |
| | 40020 | 153 | | new(candidate.Connection.Id, candidate.Connection.DisplayName, candidate.Source.Ownership); |
| | | 154 | | |
| | 760394 | 155 | | private sealed record Candidate(IIdentityProviderConnectionSource Source, ConnectionScope Scope, IdentityProviderCon |
| | | 156 | | } |