< 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
73%
Covered lines: 41
Uncovered lines: 15
Coverable lines: 56
Total lines: 115
Line coverage: 73.2%
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%442264.1%
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 UserFilter { Id = request.ExistingUserId }, cancellation
 4432                ?? throw new InvalidOperationException("The requested Elsa user does not exist.");
 4433            if (!string.Equals(existingUser.TenantId, request.TenantId, StringComparison.Ordinal))
 134                throw new InvalidOperationException("The requested Elsa user is outside the target tenant.");
 35
 4336            return (existingUser, false);
 37        }
 38
 1939        var proposal = request.Proposal ?? throw new InvalidOperationException("A user creation proposal is required for
 1940        var roleIds = await ResolveRoleIdsAsync(proposal.DefaultRoleIds, cancellationToken);
 1941        var prefix = NormalizeUserNamePrefix(proposal.UserNamePrefix);
 4042        for (var attempt = 0; attempt < MaximumUserNameAttempts; attempt++)
 43        {
 2044            var name = $"{prefix}-{identityGenerator.GenerateId()}";
 2045            if (tryReserveUserName is not null && !tryReserveUserName(name))
 46                continue;
 2047            if (await userProvider.FindAsync(new UserFilter { Name = name }, cancellationToken) is not null)
 48                continue;
 49
 1950            var user = new User
 1951            {
 1952                Id = identityGenerator.GenerateId(),
 1953                Name = name,
 1954                TenantId = request.TenantId,
 1955                HashedPassword = null,
 1956                HashedPasswordSalt = null,
 1957                Roles = roleIds.ToList()
 1958            };
 59
 60            try
 61            {
 1962                await userStore.SaveAsync(user, cancellationToken);
 1963                return (user, true);
 64            }
 065            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 66            {
 067                var persistedUser = await userStore.FindAsync(new UserFilter { Id = user.Id }, CancellationToken.None);
 068                if (persistedUser is not null)
 069                    await userStore.DeleteAsync(new UserFilter { Id = user.Id }, CancellationToken.None);
 070                throw;
 071            }
 072            catch
 73            {
 074                var persistedUser = await userProvider.FindAsync(new UserFilter { Id = user.Id }, cancellationToken);
 075                if (persistedUser is not null)
 076                    return (persistedUser, true);
 077                if (await userProvider.FindAsync(new UserFilter { Name = name }, cancellationToken) is null)
 078                    throw;
 79            }
 080        }
 81
 082        throw new InvalidOperationException("A unique Elsa user name could not be reserved for the external identity.");
 6283    }
 84
 85    /// <summary>
 86    /// Removes a user created by an operation that could not publish its external identity link.
 87    /// </summary>
 88    public Task RemoveAsync(User user, CancellationToken cancellationToken = default) =>
 489        userStore.DeleteAsync(new UserFilter { Id = user.Id }, cancellationToken);
 90
 91    /// <summary>
 92    /// Checks that the resolved user still exists in the source that supplied it.
 93    /// </summary>
 94    public async ValueTask<bool> ExistsAsync(User user, bool wasCreated, CancellationToken cancellationToken = default) 
 6495        wasCreated
 6496            ? await userStore.FindAsync(new UserFilter { Id = user.Id }, cancellationToken) is not null
 6497            : await userProvider.FindAsync(new UserFilter { Id = user.Id }, cancellationToken) is not null;
 98
 99    private static string NormalizeUserNamePrefix(string prefix)
 100    {
 171101        var normalized = new string((prefix ?? string.Empty).Trim().Where(character => char.IsAsciiLetterOrDigit(charact
 19102        return string.IsNullOrEmpty(normalized) ? "external" : normalized;
 103    }
 104
 105    private async ValueTask<IReadOnlyCollection<string>> ResolveRoleIdsAsync(IReadOnlyCollection<string>? roleIds, Cance
 106    {
 107        var requested = (roleIds ?? []).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.Ordinal).ToArr
 19108        if (requested.Length == 0)
 18109            return [];
 110        var found = (await roleProvider.FindByIdsAsync(requested, cancellationToken)).Select(x => x.Id).ToHashSet(String
 1111        if (!found.SetEquals(requested))
 0112            throw new InvalidOperationException("A configured default role no longer exists.");
 1113        return requested;
 19114    }
 115}