< Summary

Information
Class: Elsa.ExternalAuthentication.Services.ExternalIdentityUserProvisioningService
Assembly: Elsa.ExternalAuthentication
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Services/ExternalIdentityUserProvisioningService.cs
Line coverage
70%
Covered lines: 46
Uncovered lines: 19
Coverable lines: 65
Total lines: 124
Line coverage: 70.7%
Branch coverage
55%
Covered branches: 20
Total branches: 36
Branch coverage: 55.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ResolveAsync()59.09%532260%
RemoveAsync(...)100%11100%
ExistsAsync()100%22100%
NormalizeUserNamePrefix(...)25%88100%
ResolveRoleIdsAsync()83.33%6683.33%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Services/ExternalIdentityUserProvisioningService.cs

#LineLine coverage
 1using Elsa.ExternalAuthentication.Models;
 2using Elsa.Extensions;
 3using Elsa.Identity.Contracts;
 4using Elsa.Identity.Entities;
 5using Elsa.Identity.Models;
 6using Elsa.Workflows;
 7
 8namespace Elsa.ExternalAuthentication.Services;
 9
 10/// <summary>
 11/// Applies the provider-independent user resolution and creation policy used by external identity provisioners.
 12/// </summary>
 12613public sealed class ExternalIdentityUserProvisioningService(
 12614    IUserStore userStore,
 12615    IUserProvider userProvider,
 12616    IRoleProvider roleProvider,
 12617    IIdentityGenerator identityGenerator)
 18{
 19    private const int MaximumUserNameAttempts = 10;
 20
 21    /// <summary>
 22    /// Resolves an explicitly selected user or creates a credential-less user from the supplied proposal.
 23    /// </summary>
 24    public async ValueTask<(User User, bool WasCreated)> ResolveAsync(
 25        ProvisioningRequest request,
 26        Func<string, bool>? tryReserveUserName = null,
 27        CancellationToken cancellationToken = default)
 28    {
 6329        if (!string.IsNullOrWhiteSpace(request.ExistingUserId))
 30        {
 4431            var existingUser = await userProvider.FindAsync(new()
 4432                                   { Id = request.ExistingUserId }, cancellationToken)
 4433                ?? throw new InvalidOperationException("The requested Elsa user does not exist.");
 4434            if (!string.Equals(existingUser.TenantId, request.TenantId, StringComparison.Ordinal))
 135                throw new InvalidOperationException("The requested Elsa user is outside the target tenant.");
 36
 4337            return (existingUser, false);
 38        }
 39
 1940        var proposal = request.Proposal ?? throw new InvalidOperationException("A user creation proposal is required for
 1941        var roleIds = await ResolveRoleIdsAsync(proposal.DefaultRoleIds, cancellationToken);
 1942        var prefix = NormalizeUserNamePrefix(proposal.UserNamePrefix);
 4043        for (var attempt = 0; attempt < MaximumUserNameAttempts; attempt++)
 44        {
 2045            var name = $"{prefix}-{identityGenerator.GenerateId()}";
 2046            if (tryReserveUserName is not null && !tryReserveUserName(name))
 47                continue;
 2048            if (await userProvider.FindAsync(new()
 2049                    { Name = name }, cancellationToken) is not null)
 50                continue;
 51
 1952            var user = new User
 1953            {
 1954                Id = identityGenerator.GenerateId(),
 1955                Name = name,
 1956                TenantId = request.TenantId,
 1957                HashedPassword = null,
 1958                HashedPasswordSalt = null,
 1959                Roles = roleIds.ToList()
 1960            };
 61
 62            try
 63            {
 1964                await userStore.SaveAsync(user, cancellationToken);
 1965                return (user, true);
 66            }
 067            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 68            {
 069                var persistedUser = await userStore.FindAsync(new()
 070                    { Id = user.Id }, CancellationToken.None);
 071                if (persistedUser is not null)
 072                    await userStore.DeleteAsync(new()
 073                        { Id = user.Id }, CancellationToken.None);
 074                throw;
 075            }
 076            catch
 77            {
 078                var persistedUser = await userProvider.FindAsync(new()
 079                    { Id = user.Id }, cancellationToken);
 080                if (persistedUser is not null)
 081                    return (persistedUser, true);
 082                if (await userProvider.FindAsync(new()
 083                        { Name = name }, cancellationToken) is null)
 084                    throw;
 85            }
 086        }
 87
 088        throw new InvalidOperationException("A unique Elsa user name could not be reserved for the external identity.");
 6289    }
 90
 91    /// <summary>
 92    /// Removes a user created by an operation that could not publish its external identity link.
 93    /// </summary>
 94    public Task RemoveAsync(User user, CancellationToken cancellationToken = default) =>
 495        userStore.DeleteAsync(new()
 496            { Id = user.Id }, cancellationToken);
 97
 98    /// <summary>
 99    /// Checks that the resolved user still exists in the source that supplied it.
 100    /// </summary>
 101    public async ValueTask<bool> ExistsAsync(User user, bool wasCreated, CancellationToken cancellationToken = default) 
 64102        wasCreated
 64103            ? await userStore.FindAsync(new()
 64104                { Id = user.Id }, cancellationToken) is not null
 64105            : await userProvider.FindAsync(new()
 64106                { Id = user.Id }, cancellationToken) is not null;
 107
 108    private static string NormalizeUserNamePrefix(string prefix)
 109    {
 171110        var normalized = new string((prefix ?? string.Empty).Trim().Where(character => char.IsAsciiLetterOrDigit(charact
 19111        return string.IsNullOrEmpty(normalized) ? "external" : normalized;
 112    }
 113
 114    private async ValueTask<IReadOnlyCollection<string>> ResolveRoleIdsAsync(IReadOnlyCollection<string>? roleIds, Cance
 115    {
 116        var requested = (roleIds ?? []).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.Ordinal).ToArr
 19117        if (requested.Length == 0)
 18118            return [];
 119        var found = (await roleProvider.FindByIdsAsync(requested, cancellationToken)).Select(x => x.Id).ToHashSet(String
 1120        if (!found.SetEquals(requested))
 0121            throw new InvalidOperationException("A configured default role no longer exists.");
 1122        return requested;
 19123    }
 124}