< Summary

Information
Class: Elsa.Identity.Services.RoleDeletionCoordinator
Assembly: Elsa.Identity
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Identity/Services/RoleDeletionCoordinator.cs
Line coverage
84%
Covered lines: 105
Uncovered lines: 20
Coverable lines: 125
Total lines: 195
Line coverage: 84%
Branch coverage
71%
Covered branches: 46
Total branches: 64
Branch coverage: 71.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
InspectAsync()83.33%6687.5%
DeleteAsync()50%8663.63%
RemediateAndDeleteAsync()64.7%653470.21%
InspectContributorsAsync()66.66%6687.5%
CreateImpact(...)100%44100%
CalculateDependencyVersion(...)100%11100%
GetRequiredConfirmations(...)100%1010100%
GetCurrentImpactAsync()100%11100%
HasPermission(...)50%22100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Identity/Services/RoleDeletionCoordinator.cs

#LineLine coverage
 1using System.Security.Claims;
 2using System.Security.Cryptography;
 3using System.Text;
 4using Elsa.Identity.Contracts;
 5using Elsa.Identity.Models;
 6
 7namespace Elsa.Identity.Services;
 8
 9/// <inheritdoc />
 810public sealed class RoleDeletionCoordinator(
 811    IRoleStore roleStore,
 812    IRoleAuthorizationService roleAuthorizationService,
 813    IEnumerable<IRoleDeletionDependencyContributor> contributors) : IRoleDeletionCoordinator
 14{
 1315    private readonly IReadOnlyDictionary<string, IRoleDeletionDependencyContributor> _contributors = contributors.ToDict
 16
 17    /// <inheritdoc />
 18    public async ValueTask<RoleDeletionInspectionResult> InspectAsync(string roleId, ClaimsPrincipal actor, Cancellation
 19    {
 920        var role = await roleStore.FindAsync(new() { Id = roleId }, cancellationToken);
 921        if (role is null)
 022            return new RoleDeletionInspectionResult.NotFound();
 923        if (!HasPermission(actor, "delete:role") || !roleAuthorizationService.CanMutateRole(actor, role))
 124            return new RoleDeletionInspectionResult.Forbidden();
 25
 826        var snapshots = await InspectContributorsAsync(roleId, cancellationToken);
 827        return new RoleDeletionInspectionResult.Success(CreateImpact(roleId, snapshots));
 928    }
 29
 30    /// <inheritdoc />
 31    public async ValueTask<RoleDeletionOperationResult> DeleteAsync(string roleId, ClaimsPrincipal actor, CancellationTo
 32    {
 133        var inspection = await InspectAsync(roleId, actor, cancellationToken);
 134        if (inspection is RoleDeletionInspectionResult.NotFound)
 035            return new RoleDeletionOperationResult.NotFound();
 136        if (inspection is RoleDeletionInspectionResult.Forbidden)
 037            return new RoleDeletionOperationResult.Forbidden();
 38
 139        var impact = ((RoleDeletionInspectionResult.Success)inspection).Impact;
 140        if (!impact.CanDelete)
 141            return new RoleDeletionOperationResult.Blocked(impact);
 42
 043        await roleStore.DeleteAsync(new() { Id = roleId }, cancellationToken);
 044        return new RoleDeletionOperationResult.Deleted([]);
 145    }
 46
 47    /// <inheritdoc />
 48    public async ValueTask<RoleDeletionOperationResult> RemediateAndDeleteAsync(RoleDeletionRemediationCommand command, 
 49    {
 350        var inspection = await InspectAsync(command.RoleId, command.Actor, cancellationToken);
 351        if (inspection is RoleDeletionInspectionResult.NotFound)
 052            return new RoleDeletionOperationResult.NotFound();
 353        if (inspection is RoleDeletionInspectionResult.Forbidden)
 054            return new RoleDeletionOperationResult.Forbidden();
 55
 356        var impact = ((RoleDeletionInspectionResult.Success)inspection).Impact;
 357        if (!string.Equals(impact.DependencyVersion, command.ExpectedDependencyVersion, StringComparison.Ordinal))
 058            return new RoleDeletionOperationResult.PreconditionFailed(impact);
 759        if (impact.Dependencies.Any(x => x.Ownership == RoleDeletionDependencyOwnership.Configuration))
 060            return new RoleDeletionOperationResult.Blocked(impact);
 361        if (impact.CanDelete)
 62        {
 063            await roleStore.DeleteAsync(new() { Id = command.RoleId }, cancellationToken);
 064            return new RoleDeletionOperationResult.Deleted([]);
 65        }
 66
 367        var warnings = GetRequiredConfirmations(impact, command);
 368        if (warnings.Count != 0)
 169            return new RoleDeletionOperationResult.ConfirmationRequired(impact, warnings);
 70
 271        var snapshots = await InspectContributorsAsync(command.RoleId, cancellationToken);
 272        var currentImpact = CreateImpact(command.RoleId, snapshots);
 273        if (!string.Equals(currentImpact.DependencyVersion, command.ExpectedDependencyVersion, StringComparison.Ordinal)
 074            return new RoleDeletionOperationResult.PreconditionFailed(currentImpact);
 75
 276        var requests = snapshots
 477            .Where(x => x.Dependencies.Any(dependency => dependency.Ownership == RoleDeletionDependencyOwnership.Databas
 278            .Select(snapshot => new RoleReferenceRemovalRequest(
 279                command.RoleId,
 280                command.Actor,
 281                snapshot.Version,
 582                snapshot.Dependencies.Where(x => x.Ownership == RoleDeletionDependencyOwnership.Database).ToArray()))
 283            .ToArray();
 84
 885        foreach (var request in requests)
 86        {
 287            var validation = await _contributors[request.Dependencies.First().Source].ValidateRemovalAsync(request, canc
 288            if (validation is RoleReferenceRemovalValidationResult.Forbidden)
 089                return new RoleDeletionOperationResult.Forbidden();
 290            if (validation is RoleReferenceRemovalValidationResult.Conflict)
 091                return new RoleDeletionOperationResult.PreconditionFailed(await GetCurrentImpactAsync(command.RoleId, ca
 92        }
 93
 294        var changedOwnerIds = new List<string>();
 795        foreach (var request in requests)
 96        {
 297            var removal = await _contributors[request.Dependencies.First().Source].RemoveEditableReferencesAsync(request
 98            switch (removal)
 99            {
 100                case RoleReferenceRemovalResult.Success success:
 1101                    changedOwnerIds.AddRange(success.ChangedOwnerIds);
 1102                    break;
 103                case RoleReferenceRemovalResult.Conflict conflict:
 0104                    changedOwnerIds.AddRange(conflict.ChangedOwnerIds);
 0105                    return new RoleDeletionOperationResult.Incomplete(await GetCurrentImpactAsync(command.RoleId, cancel
 106                case RoleReferenceRemovalResult.Failed failed:
 1107                    changedOwnerIds.AddRange(failed.ChangedOwnerIds);
 1108                    return new RoleDeletionOperationResult.Incomplete(await GetCurrentImpactAsync(command.RoleId, cancel
 109            }
 110        }
 111
 1112        var finalInspection = await InspectAsync(command.RoleId, command.Actor, cancellationToken);
 1113        if (finalInspection is RoleDeletionInspectionResult.NotFound)
 0114            return new RoleDeletionOperationResult.NotFound();
 1115        if (finalInspection is RoleDeletionInspectionResult.Forbidden)
 0116            return new RoleDeletionOperationResult.Forbidden();
 117
 1118        var finalImpact = ((RoleDeletionInspectionResult.Success)finalInspection).Impact;
 1119        if (!finalImpact.CanDelete)
 0120            return new RoleDeletionOperationResult.Incomplete(finalImpact, changedOwnerIds.Distinct(StringComparer.Ordin
 121
 1122        await roleStore.DeleteAsync(new() { Id = command.RoleId }, cancellationToken);
 1123        return new RoleDeletionOperationResult.Deleted(changedOwnerIds.Distinct(StringComparer.Ordinal).ToArray());
 3124    }
 125
 126    private async ValueTask<IReadOnlyCollection<RoleDeletionDependencySnapshot>> InspectContributorsAsync(string roleId,
 127    {
 11128        var snapshots = new List<RoleDeletionDependencySnapshot>(_contributors.Count);
 66129        foreach (var contributor in _contributors.OrderBy(x => x.Key, StringComparer.Ordinal).Select(x => x.Value))
 130        {
 11131            var snapshot = await contributor.InspectAsync(roleId, cancellationToken);
 11132            if (!string.Equals(snapshot.Source, contributor.Source, StringComparison.Ordinal) ||
 24133                snapshot.Dependencies.Any(x => !string.Equals(x.Source, contributor.Source, StringComparison.Ordinal)))
 0134                throw new InvalidOperationException($"Role-deletion contributor '{contributor.Source}' returned a mismat
 11135            snapshots.Add(snapshot);
 11136        }
 137
 11138        return snapshots;
 11139    }
 140
 141    private static RoleDeletionImpact CreateImpact(string roleId, IReadOnlyCollection<RoleDeletionDependencySnapshot> sn
 142    {
 11143        var dependencies = snapshots
 11144            .SelectMany(x => x.Dependencies)
 6145            .OrderBy(x => x.Source, StringComparer.Ordinal)
 6146            .ThenBy(x => x.Ownership)
 6147            .ThenBy(x => x.OwnerId, StringComparer.Ordinal)
 6148            .ThenBy(x => x.PolicyBranch, StringComparer.Ordinal)
 11149            .ToArray();
 150        // The current coordinator has no unit of work spanning contributor stores and IRoleStore.
 151        // Contributor-local atomicity alone cannot make the complete remove-then-delete command atomic.
 32152        var hasEditableDependencies = snapshots.Any(x => x.Dependencies.Any(dependency => dependency.Ownership == RoleDe
 11153        var executionMode = hasEditableDependencies ? RoleDeletionExecutionMode.BestEffort : RoleDeletionExecutionMode.A
 11154        return new RoleDeletionImpact(
 11155            roleId,
 11156            CalculateDependencyVersion(snapshots),
 11157            executionMode,
 11158            dependencies.Length == 0,
 13159            dependencies.Length != 0 && dependencies.All(x => x.Ownership == RoleDeletionDependencyOwnership.Database),
 11160            dependencies);
 161    }
 162
 163    private static string CalculateDependencyVersion(IEnumerable<RoleDeletionDependencySnapshot> snapshots)
 164    {
 11165        var payload = string.Join(
 11166            "\n",
 11167            snapshots
 11168                .OrderBy(x => x.Source, StringComparer.Ordinal)
 22169                .SelectMany(snapshot => new[] { $"{snapshot.Source}|{snapshot.Version}|{snapshot.SupportsAtomicRemoval}"
 22170                    .Concat(snapshot.Dependencies
 13171                        .OrderBy(x => x.Ownership)
 13172                        .ThenBy(x => x.OwnerId, StringComparer.Ordinal)
 13173                        .ThenBy(x => x.PolicyBranch, StringComparer.Ordinal)
 35174                        .Select(x => $"{x.Source}|{x.OwnerId}|{x.OwnerKey}|{x.PolicyBranch}|{x.Ownership}|{x.Configurati
 11175        return $"role-dependencies-{Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(payload))).ToLowerInvaria
 176    }
 177
 178    private static IReadOnlyCollection<string> GetRequiredConfirmations(RoleDeletionImpact impact, RoleDeletionRemediati
 179    {
 3180        var warnings = new List<string>();
 3181        if (!command.ConfirmRemoveFromEditablePolicies)
 1182            warnings.Add("confirm_remove_from_editable_jit_policies");
 7183        if (impact.Dependencies.Any(x => x.RemovesLastDefaultRole) && !command.ConfirmEmptyDefaultRoles)
 1184            warnings.Add("removes_last_default_role");
 3185        if (impact.ExecutionMode == RoleDeletionExecutionMode.BestEffort && !command.ConfirmBestEffort)
 1186            warnings.Add("confirm_best_effort");
 3187        return warnings;
 188    }
 189
 190    private async ValueTask<RoleDeletionImpact> GetCurrentImpactAsync(string roleId, CancellationToken cancellationToken
 1191        CreateImpact(roleId, await InspectContributorsAsync(roleId, cancellationToken));
 192
 193    private static bool HasPermission(ClaimsPrincipal actor, string permission) =>
 17194        actor.FindAll(PermissionNames.ClaimType).Any(x => x.Value == PermissionNames.All || string.Equals(x.Value, permi
 195}