< Summary

Information
Class: Elsa.UserTasks.Services.DefaultUserTaskAccessPolicy
Assembly: Elsa.UserTasks
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.UserTasks/Services/DefaultUserTaskAccessPolicy.cs
Line coverage
85%
Covered lines: 66
Uncovered lines: 11
Coverable lines: 77
Total lines: 150
Line coverage: 85.7%
Branch coverage
79%
Covered branches: 68
Total branches: 86
Branch coverage: 79%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
CreateScopeAsync(...)100%1010100%
AuthorizeAsync(...)77.5%524080.64%
AuthorizeGuest(...)70%121071.42%
IsManager(...)100%44100%
IsCandidate(...)66.66%7666.66%
IsExcluded(...)25%44100%
RequiredPermission(...)100%11100%
RequiredVerb(...)91.66%121291.66%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.UserTasks/Services/DefaultUserTaskAccessPolicy.cs

#LineLine coverage
 1using Elsa.Authorization;
 2using Elsa.UserTasks.Contracts;
 3using Elsa.UserTasks.Models;
 4using Elsa.UserTasks.Permissions;
 5
 6namespace Elsa.UserTasks.Services;
 7
 8public sealed class DefaultUserTaskAccessPolicy : IUserTaskAccessPolicy
 9{
 10    /// <summary>
 11    /// Guests authenticate through a task-scoped invitation session. They may read and complete exactly one
 12    /// task and can never claim, release, reassign, schedule, invite, cancel, or manage.
 13    /// </summary>
 114    private static readonly UserTaskAccessOperation[] GuestOperations =
 115    [
 116        UserTaskAccessOperation.ReadSummary,
 117        UserTaskAccessOperation.ReadProtected,
 118        UserTaskAccessOperation.Complete
 119    ];
 20
 21    public Task<UserTaskQueryScope?> CreateScopeAsync(UserTaskActor actor, UserTaskQueryScopeKind kind, CancellationToke
 22    {
 23        // A guest session is issued for one task and carries no list capability at all.
 2524        if (actor.IsGuest || !actor.HasPermission(UserTasksResourcePermissions.UserTasks, CoreVerbs.View))
 1125            return Task.FromResult<UserTaskQueryScope?>(null);
 26
 1427        var isManager = IsManager(actor);
 1428        if (!isManager && kind is UserTaskQueryScopeKind.All or UserTaskQueryScopeKind.NeedsAttention)
 329            return Task.FromResult<UserTaskQueryScope?>(null);
 30
 1131        return Task.FromResult<UserTaskQueryScope?>(new UserTaskQueryScope(
 1132            actor.Subject.TenantId,
 1133            actor.Subject,
 1134            actor.Groups,
 1135            isManager,
 1136            kind,
 1137            ExcludeBlocking: !isManager));
 38    }
 39
 40    public Task<bool> AuthorizeAsync(UserTask task, UserTaskActor actor, UserTaskAccessOperation operation, Cancellation
 41    {
 18742        if (!string.Equals(task.TenantId, actor.Subject.TenantId, StringComparison.Ordinal))
 043            return Task.FromResult(false);
 18744        if (!actor.HasPermission(RequiredPermission(operation)))
 5845            return Task.FromResult(false);
 46
 12947        if (actor.IsGuest)
 1248            return Task.FromResult(AuthorizeGuest(task, actor, operation));
 49
 11750        var isManager = IsManager(actor);
 11751        if (task.HealthSeverity == UserTaskHealthSeverity.Blocking && !isManager)
 052            return Task.FromResult(false);
 53
 11754        var isAssignee = task.Assignee?.Matches(actor.Subject) == true;
 11755        var isCandidate = IsCandidate(task, actor);
 11756        var isCompleter = task.CompletedBy?.Matches(actor.Subject) == true;
 25757        var acted = task.Events.Any(x => x.Actor?.Matches(actor.Subject) == true);
 58
 11759        var result = operation switch
 11760        {
 561            UserTaskAccessOperation.ReadSummary => isManager || isAssignee || isCandidate || isCompleter || acted,
 1062            UserTaskAccessOperation.ReadProtected => isManager || (task.IsTerminal ? isCompleter : isAssignee),
 1763            UserTaskAccessOperation.Claim => !task.IsTerminal && task.Status is (UserTaskStatus.Available or UserTaskSta
 764            UserTaskAccessOperation.Release => !task.IsTerminal && isAssignee,
 165            UserTaskAccessOperation.Assign => isManager,
 066            UserTaskAccessOperation.UpdateScheduling => isManager,
 11767            // The completer may replay the same terminal operation for idempotency; new terminal
 11768            // commands are rejected by the manager after operation lookup.
 1669            UserTaskAccessOperation.Complete => task.IsTerminal ? isCompleter : isAssignee,
 070            UserTaskAccessOperation.Cancel => isManager,
 171            UserTaskAccessOperation.Manage => isManager,
 6072            UserTaskAccessOperation.IssueInvitation => isManager,
 073            UserTaskAccessOperation.RetryResolution => isManager,
 074            _ => false
 11775        };
 76
 11777        return Task.FromResult(result);
 78    }
 79
 80    private static bool AuthorizeGuest(UserTask task, UserTaskActor actor, UserTaskAccessOperation operation)
 81    {
 1282        if (!string.Equals(actor.GuestTaskId, task.Id, StringComparison.Ordinal))
 283            return false;
 1084        if (!GuestOperations.Contains(operation))
 085            return false;
 86        // The invitation claimed the task for the guest participant; losing that assignment (through a
 87        // manager reassignment or reissue) revokes the guest's access on the next request.
 1088        if (task.Assignee?.Matches(actor.Subject) != true)
 089            return false;
 1090        return operation != UserTaskAccessOperation.Complete || !task.IsTerminal;
 91    }
 92
 93    /// <summary>
 94    /// A tenant-scoped manager must hold <c>user-tasks:supervise</c> (or a grant covering it). The actor flag
 95    /// on its own is host-supplied metadata and is never sufficient.
 96    /// </summary>
 97    private static bool IsManager(UserTaskActor actor) =>
 13198        !actor.IsGuest && actor.IsManager && actor.HasPermission(UserTasksResourcePermissions.UserTasks, UserTaskVerbs.S
 99
 100    private static bool IsCandidate(UserTask task, UserTaskActor actor)
 101    {
 124102        if (task.ExcludedUsers.Any(x => x.Matches(actor.Subject)))
 0103            return false;
 104
 117105        if (task.MembershipResolutionMode == UserTaskMembershipResolutionMode.Snapshot)
 0106            return task.SnapshotMembers.Any(x => x.Matches(actor.Subject));
 107
 234108        return task.CandidateUsers.Any(x => x.Matches(actor.Subject))
 117109               || task.CandidateGroups.Any(candidate => actor.Groups.Any(candidate.Matches));
 110    }
 111
 112    private static bool IsExcluded(UserTask task, UserTaskActor actor) =>
 16113        task.ExcludedUsers.Any(x => x.Matches(actor.Subject)) && !(IsManager(actor) && task.AllowManagerExclusionOverrid
 114
 115    /// <summary>
 116    /// The permission an operation needs, which is the same one the endpoint offering it declares. Every
 117    /// operation acts on the task itself, so only the verb varies.
 118    /// </summary>
 119    private static Permission RequiredPermission(UserTaskAccessOperation operation) =>
 187120        new(UserTasksResourcePermissions.UserTasks, RequiredVerb(operation));
 121
 187122    private static string RequiredVerb(UserTaskAccessOperation operation) => operation switch
 187123    {
 22124        UserTaskAccessOperation.ReadSummary or UserTaskAccessOperation.ReadProtected => CoreVerbs.View,
 31125        UserTaskAccessOperation.Claim or UserTaskAccessOperation.Release => UserTaskVerbs.Claim,
 21126        UserTaskAccessOperation.Complete => UserTaskVerbs.Complete,
 11127        UserTaskAccessOperation.Assign => UserTaskVerbs.Assign,
 10128        UserTaskAccessOperation.UpdateScheduling => CoreVerbs.Update,
 10129        UserTaskAccessOperation.Cancel => UserTaskVerbs.Cancel,
 12130        UserTaskAccessOperation.Manage or UserTaskAccessOperation.RetryResolution => UserTaskVerbs.Supervise,
 70131        UserTaskAccessOperation.IssueInvitation => UserTaskVerbs.Invite,
 0132        _ => CoreVerbs.View
 187133    };
 134}
 135
 136/// <summary>
 137/// A safe default for hosts that do not expose a participant directory. It keeps lookup optional and
 138/// avoids coupling User Tasks to Elsa.Identity.
 139/// </summary>
 140public sealed class EmptyUserTaskParticipantDirectory : IUserTaskParticipantDirectory
 141{
 142    public Task<ParticipantSearchResult> SearchAsync(UserTaskParticipantQuery query, CancellationToken cancellationToken
 143        Task.FromResult(new ParticipantSearchResult([], null, 0));
 144
 145    public Task<IReadOnlyCollection<ParticipantReference>> ResolveDisplayNamesAsync(IReadOnlyCollection<ParticipantRefer
 146        Task.FromResult(participants);
 147
 148    public Task<IReadOnlyCollection<ParticipantReference>> EnumerateGroupMembersAsync(ParticipantReference group, Cancel
 149        Task.FromResult<IReadOnlyCollection<ParticipantReference>>([]);
 150}