| | | 1 | | using Elsa.Common; |
| | | 2 | | using Elsa.Common.Models; |
| | | 3 | | using Elsa.ExternalAuthentication.Contracts; |
| | | 4 | | using Elsa.ExternalAuthentication.Models; |
| | | 5 | | using Elsa.Identity.Contracts; |
| | | 6 | | using Elsa.Identity.Entities; |
| | | 7 | | using Elsa.Workflows; |
| | | 8 | | |
| | | 9 | | namespace Elsa.ExternalAuthentication.Services; |
| | | 10 | | |
| | | 11 | | /// <summary> |
| | | 12 | | /// Provides a replaceable single-node implementation of atomic external identity linking and just-in-time provisioning. |
| | | 13 | | /// Durable, multi-node hosts should replace this service with a transactional provisioner. |
| | | 14 | | /// </summary> |
| | 89 | 15 | | public sealed class InMemoryExternalIdentityProvisioner( |
| | 89 | 16 | | IUserStore userStore, |
| | 89 | 17 | | IUserProvider userProvider, |
| | 89 | 18 | | IRoleProvider roleProvider, |
| | 89 | 19 | | IIdentityGenerator identityGenerator, |
| | 89 | 20 | | ISystemClock clock, |
| | 89 | 21 | | IExternalAuthenticationHandleHasher handleHasher, |
| | 89 | 22 | | InMemoryExternalIdentityProvisionerState state) : IExternalIdentityProvisioner, IExternalIdentityLinkManagementStore |
| | | 23 | | { |
| | 89 | 24 | | private readonly ExternalIdentityUserProvisioningService _userProvisioningService = new(userStore, userProvider, rol |
| | | 25 | | |
| | | 26 | | public async ValueTask<ExternalIdentityLink?> FindLinkAsync(string tenantId, string connectionKey, ExternalIdentity |
| | | 27 | | { |
| | 12 | 28 | | cancellationToken.ThrowIfCancellationRequested(); |
| | 12 | 29 | | var key = new ExternalIdentityKey(tenantId, ConnectionRevisionCalculator.NormalizeKey(connectionKey), identity.I |
| | | 30 | | |
| | 12 | 31 | | await state.Mutex.WaitAsync(cancellationToken); |
| | | 32 | | try |
| | | 33 | | { |
| | 12 | 34 | | return state.Links.TryGetValue(key, out var link) ? link : null; |
| | | 35 | | } |
| | | 36 | | finally |
| | | 37 | | { |
| | 12 | 38 | | state.Mutex.Release(); |
| | | 39 | | } |
| | 12 | 40 | | } |
| | | 41 | | |
| | | 42 | | public async ValueTask<ProvisioningResult> CreateLinkOrGetExistingAsync(ProvisioningRequest request, CancellationTok |
| | | 43 | | { |
| | 46 | 44 | | ArgumentNullException.ThrowIfNull(request); |
| | 46 | 45 | | cancellationToken.ThrowIfCancellationRequested(); |
| | 46 | 46 | | var subjectHash = handleHasher.Hash(request.Identity.Subject); |
| | 46 | 47 | | var key = new ExternalIdentityKey(request.TenantId, ConnectionRevisionCalculator.NormalizeKey(request.Connection |
| | | 48 | | |
| | 46 | 49 | | await state.Mutex.WaitAsync(cancellationToken); |
| | | 50 | | try |
| | | 51 | | { |
| | 46 | 52 | | if (state.Links.TryGetValue(key, out var existingLink)) |
| | 18 | 53 | | return new(existingLink.UserId, existingLink, false); |
| | | 54 | | |
| | 28 | 55 | | var (user, wasCreated) = await _userProvisioningService.ResolveAsync(request, state.ReservedUserNames.Add, c |
| | 27 | 56 | | var link = new ExternalIdentityLink( |
| | 27 | 57 | | identityGenerator.GenerateId(), |
| | 27 | 58 | | request.TenantId, |
| | 27 | 59 | | ConnectionRevisionCalculator.NormalizeKey(request.ConnectionKey), |
| | 27 | 60 | | request.Identity.Issuer, |
| | 27 | 61 | | subjectHash, |
| | 27 | 62 | | null, |
| | 27 | 63 | | user.Id, |
| | 27 | 64 | | clock.UtcNow, |
| | 27 | 65 | | null); |
| | 27 | 66 | | state.Links[key] = link; |
| | 27 | 67 | | if (!await _userProvisioningService.ExistsAsync(user, wasCreated, CancellationToken.None)) |
| | | 68 | | { |
| | 1 | 69 | | state.Links.Remove(key); |
| | 1 | 70 | | throw new InvalidOperationException("The Elsa user was deleted while its external identity link was bein |
| | | 71 | | } |
| | 26 | 72 | | return new(user.Id, link, wasCreated, true); |
| | | 73 | | } |
| | | 74 | | finally |
| | | 75 | | { |
| | 46 | 76 | | state.Mutex.Release(); |
| | | 77 | | } |
| | 44 | 78 | | } |
| | | 79 | | |
| | | 80 | | public async ValueTask<bool> RecordSuccessfulSignInAsync( |
| | | 81 | | string tenantId, |
| | | 82 | | string connectionKey, |
| | | 83 | | ExternalIdentity identity, |
| | | 84 | | string userId, |
| | | 85 | | DateTimeOffset signedInAt, |
| | | 86 | | CancellationToken cancellationToken = default) |
| | | 87 | | { |
| | 5 | 88 | | cancellationToken.ThrowIfCancellationRequested(); |
| | 5 | 89 | | var key = new ExternalIdentityKey(tenantId, ConnectionRevisionCalculator.NormalizeKey(connectionKey), identity.I |
| | | 90 | | |
| | 5 | 91 | | await state.Mutex.WaitAsync(cancellationToken); |
| | | 92 | | try |
| | | 93 | | { |
| | 5 | 94 | | if (!state.Links.TryGetValue(key, out var link) || !string.Equals(link.UserId, userId, StringComparison.Ordi |
| | 0 | 95 | | return false; |
| | | 96 | | |
| | 5 | 97 | | if (link.LastSignedInAt is null || link.LastSignedInAt < signedInAt) |
| | 5 | 98 | | state.Links[key] = link with { LastSignedInAt = signedInAt }; |
| | 5 | 99 | | return true; |
| | | 100 | | } |
| | | 101 | | finally |
| | | 102 | | { |
| | 5 | 103 | | state.Mutex.Release(); |
| | | 104 | | } |
| | 5 | 105 | | } |
| | | 106 | | |
| | | 107 | | public async ValueTask<ExternalIdentityLinkReplaceResult> ReplaceAsync(ExternalIdentityLinkReplaceRequest request, C |
| | | 108 | | { |
| | 8 | 109 | | ArgumentNullException.ThrowIfNull(request); |
| | 8 | 110 | | cancellationToken.ThrowIfCancellationRequested(); |
| | 8 | 111 | | var normalizedConnectionKey = ConnectionRevisionCalculator.NormalizeKey(request.ConnectionKey); |
| | 8 | 112 | | var replacementKey = new ExternalIdentityKey(request.TenantId, normalizedConnectionKey, request.Identity.Issuer, |
| | | 113 | | |
| | 8 | 114 | | await state.Mutex.WaitAsync(cancellationToken); |
| | | 115 | | try |
| | | 116 | | { |
| | 8 | 117 | | var oldEntry = state.Links.FirstOrDefault(x => |
| | 18 | 118 | | string.Equals(x.Value.Id, request.LinkId, StringComparison.Ordinal) && |
| | 18 | 119 | | string.Equals(x.Value.TenantId, request.TenantId, StringComparison.Ordinal)); |
| | 8 | 120 | | if (oldEntry.Equals(default(KeyValuePair<ExternalIdentityKey, ExternalIdentityLink>))) |
| | 1 | 121 | | return new ExternalIdentityLinkReplaceResult.NotFound(); |
| | | 122 | | |
| | 7 | 123 | | if (state.Links.TryGetValue(replacementKey, out var conflictingLink) && |
| | 7 | 124 | | !string.Equals(conflictingLink.Id, oldEntry.Value.Id, StringComparison.Ordinal)) |
| | 2 | 125 | | return new ExternalIdentityLinkReplaceResult.Conflict(oldEntry.Value, conflictingLink); |
| | | 126 | | |
| | 5 | 127 | | var (user, _) = await _userProvisioningService.ResolveAsync( |
| | 5 | 128 | | new(request.TenantId, normalizedConnectionKey, request.Identity, null, request.UserId), |
| | 5 | 129 | | cancellationToken: cancellationToken); |
| | 5 | 130 | | var replacement = new ExternalIdentityLink( |
| | 5 | 131 | | identityGenerator.GenerateId(), |
| | 5 | 132 | | request.TenantId, |
| | 5 | 133 | | normalizedConnectionKey, |
| | 5 | 134 | | request.Identity.Issuer, |
| | 5 | 135 | | replacementKey.SubjectHash, |
| | 5 | 136 | | null, |
| | 5 | 137 | | user.Id, |
| | 5 | 138 | | clock.UtcNow, |
| | 5 | 139 | | null); |
| | 5 | 140 | | state.Links.Remove(oldEntry.Key); |
| | 5 | 141 | | state.Links[replacementKey] = replacement; |
| | 5 | 142 | | if (!await _userProvisioningService.ExistsAsync(user, false, CancellationToken.None)) |
| | | 143 | | { |
| | 1 | 144 | | state.Links.Remove(replacementKey); |
| | 1 | 145 | | state.Links[oldEntry.Key] = oldEntry.Value; |
| | 1 | 146 | | var previousUser = new User { Id = oldEntry.Value.UserId, TenantId = oldEntry.Value.TenantId }; |
| | 1 | 147 | | if (!await _userProvisioningService.ExistsAsync(previousUser, false, CancellationToken.None)) |
| | 1 | 148 | | state.Links.Remove(oldEntry.Key); |
| | 1 | 149 | | throw new InvalidOperationException("The Elsa user was deleted while its external identity link was bein |
| | | 150 | | } |
| | 4 | 151 | | return new ExternalIdentityLinkReplaceResult.Success(oldEntry.Value, replacement); |
| | | 152 | | } |
| | | 153 | | finally |
| | | 154 | | { |
| | 8 | 155 | | state.Mutex.Release(); |
| | | 156 | | } |
| | 7 | 157 | | } |
| | | 158 | | |
| | | 159 | | public async ValueTask<Page<ExternalIdentityLink>> FindAsync(ExternalIdentityLinkFilter filter, CancellationToken ca |
| | | 160 | | { |
| | 14 | 161 | | ArgumentNullException.ThrowIfNull(filter); |
| | 14 | 162 | | cancellationToken.ThrowIfCancellationRequested(); |
| | | 163 | | |
| | 14 | 164 | | await state.Mutex.WaitAsync(cancellationToken); |
| | | 165 | | try |
| | | 166 | | { |
| | 14 | 167 | | var links = state.Links.Values |
| | 17 | 168 | | .Where(x => string.Equals(x.TenantId, filter.TenantId, StringComparison.Ordinal)) |
| | 15 | 169 | | .Where(x => filter.UserId is null || string.Equals(x.UserId, filter.UserId, StringComparison.Ordinal)) |
| | 15 | 170 | | .Where(x => filter.ConnectionKey is null || string.Equals(x.ConnectionKey, ConnectionRevisionCalculator. |
| | 10 | 171 | | .OrderBy(x => x.CreatedAt) |
| | 10 | 172 | | .ThenBy(x => x.Id, StringComparer.Ordinal) |
| | 14 | 173 | | .ToArray(); |
| | 14 | 174 | | return Page.Of<ExternalIdentityLink>(links, links.Length); |
| | | 175 | | } |
| | | 176 | | finally |
| | | 177 | | { |
| | 14 | 178 | | state.Mutex.Release(); |
| | | 179 | | } |
| | 14 | 180 | | } |
| | | 181 | | |
| | | 182 | | public async ValueTask<bool> DeleteAsync(string tenantId, string linkId, CancellationToken cancellationToken = defau |
| | | 183 | | { |
| | 1 | 184 | | cancellationToken.ThrowIfCancellationRequested(); |
| | | 185 | | |
| | 1 | 186 | | await state.Mutex.WaitAsync(cancellationToken); |
| | | 187 | | try |
| | | 188 | | { |
| | 2 | 189 | | var entry = state.Links.FirstOrDefault(x => string.Equals(x.Value.Id, linkId, StringComparison.Ordinal) && s |
| | 1 | 190 | | return !entry.Equals(default(KeyValuePair<ExternalIdentityKey, ExternalIdentityLink>)) && state.Links.Remove |
| | | 191 | | } |
| | | 192 | | finally |
| | | 193 | | { |
| | 1 | 194 | | state.Mutex.Release(); |
| | | 195 | | } |
| | 1 | 196 | | } |
| | | 197 | | |
| | | 198 | | } |