< Summary

Information
Class: Elsa.Identity.HostedServices.StoredPermissionValidator
Assembly: Elsa.Identity
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Identity/HostedServices/StoredPermissionValidator.cs
Line coverage
85%
Covered lines: 30
Uncovered lines: 5
Coverable lines: 35
Total lines: 96
Line coverage: 85.7%
Branch coverage
86%
Covered branches: 19
Total branches: 22
Branch coverage: 86.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
StartAsync()100%6676.19%
Unresolvable()100%11100%
StopAsync(...)100%11100%
Resolves(...)81.25%1616100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Identity/HostedServices/StoredPermissionValidator.cs

#LineLine coverage
 1using Elsa.Authorization;
 2using Elsa.Identity.Contracts;
 3using Elsa.Permissions;
 4using JetBrains.Annotations;
 5using Microsoft.Extensions.DependencyInjection;
 6using Microsoft.Extensions.Hosting;
 7using Microsoft.Extensions.Logging;
 8
 9namespace Elsa.Identity.HostedServices;
 10
 11/// <summary>
 12/// Reports stored role permissions that no longer resolve, so an upgrade fails loudly rather than
 13/// silently narrowing roles.
 14/// </summary>
 15/// <remarks>
 16/// The authorization model deliberately breaks legacy permission strings rather than carrying a permanent
 17/// alias layer, which would keep two vocabularies valid forever. This makes the consequence visible: every
 18/// unresolvable permission is logged against the role that holds it. The whole-vocabulary grant survives
 19/// unchanged, so an administrator cannot be locked out while the rest is re-authored.
 20/// </remarks>
 21[UsedImplicitly]
 1422public class StoredPermissionValidator(IServiceScopeFactory scopeFactory, ILogger<StoredPermissionValidator> logger) : I
 23{
 24    /// <inheritdoc />
 25    public async Task StartAsync(CancellationToken cancellationToken)
 26    {
 1427        using var scope = scopeFactory.CreateScope();
 1428        var roleProvider = scope.ServiceProvider.GetRequiredService<IRoleProvider>();
 1429        var registry = scope.ServiceProvider.GetRequiredService<IPermissionDescriptorRegistry>();
 30
 31        IReadOnlyCollection<string> Unresolvable(IEnumerable<string> permissions) =>
 2832            permissions.Where(x => !Resolves(registry, x)).ToArray();
 33
 34        try
 35        {
 1436            var roles = await roleProvider.FindManyAsync(new(), cancellationToken);
 1437            var affected = 0;
 38
 5639            foreach (var role in roles)
 40            {
 1441                var unresolvable = Unresolvable(role.Permissions);
 42
 1443                if (unresolvable.Count == 0)
 44                    continue;
 45
 646                affected++;
 647                logger.LogWarning(
 648                    "Role '{RoleName}' ({RoleId}) holds {Count} permission(s) that no longer resolve and will not author
 649                    role.Name, role.Id, unresolvable.Count, string.Join(", ", unresolvable));
 50            }
 51
 1452            if (affected > 0)
 653                logger.LogWarning("{Count} role(s) hold unresolvable permissions. Re-author them against the permission 
 1454        }
 055        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 56        {
 057            throw;
 58        }
 059        catch (Exception ex) when (ex is not OutOfMemoryException and not StackOverflowException)
 60        {
 61            // Reporting must never prevent the host from starting: an unreachable or half-migrated store
 62            // is exactly when an operator most needs the host up to fix it.
 063            logger.LogWarning(ex, "Could not validate stored role permissions.");
 064        }
 1465    }
 66
 67    /// <inheritdoc />
 668    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
 69
 70    private static bool Resolves(IPermissionDescriptorRegistry registry, string value)
 71    {
 1472        if (!Permission.TryParse(value, out var permission) || !permission.IsValidPattern)
 273            return false;
 74
 1275        if (permission.IsResourceWildcard)
 476            return true;
 77
 78        // A subtree grant reaching nothing is far more likely a typo ('workflow/*') than a grant for a
 79        // module yet to be installed, so it is reported rather than assumed forward-reaching.
 880        if (permission.IsSubtree)
 81        {
 582            var reached = registry.Reach(permission.Resource);
 83
 84            // A concrete verb is only resolved when something under the subtree actually supports it:
 85            // 'workflows/*:frobnicate' reaches plenty and authorizes nothing, which is the same inert
 86            // grant an unreachable subtree is, and deserves the same warning.
 587            return permission.IsVerbWildcard
 588                ? reached.Count > 0
 889                : reached.Any(x => registry.Find(x)?.Supports(permission.Verb) == true);
 90        }
 91
 392        var descriptor = registry.Find(permission.Resource);
 93
 394        return descriptor is not null && (permission.IsVerbWildcard || descriptor.Supports(permission.Verb));
 95    }
 96}