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