< Summary

Information
Class: Elsa.UserTasks.Services.EmptyUserTaskParticipantDirectory
Assembly: Elsa.UserTasks
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.UserTasks/Services/DefaultUserTaskAccessPolicy.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 3
Coverable lines: 3
Total lines: 150
Line coverage: 0%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
SearchAsync(...)100%210%
ResolveDisplayNamesAsync(...)100%210%
EnumerateGroupMembersAsync(...)100%210%

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>
 14    private static readonly UserTaskAccessOperation[] GuestOperations =
 15    [
 16        UserTaskAccessOperation.ReadSummary,
 17        UserTaskAccessOperation.ReadProtected,
 18        UserTaskAccessOperation.Complete
 19    ];
 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.
 24        if (actor.IsGuest || !actor.HasPermission(UserTasksResourcePermissions.UserTasks, CoreVerbs.View))
 25            return Task.FromResult<UserTaskQueryScope?>(null);
 26
 27        var isManager = IsManager(actor);
 28        if (!isManager && kind is UserTaskQueryScopeKind.All or UserTaskQueryScopeKind.NeedsAttention)
 29            return Task.FromResult<UserTaskQueryScope?>(null);
 30
 31        return Task.FromResult<UserTaskQueryScope?>(new UserTaskQueryScope(
 32            actor.Subject.TenantId,
 33            actor.Subject,
 34            actor.Groups,
 35            isManager,
 36            kind,
 37            ExcludeBlocking: !isManager));
 38    }
 39
 40    public Task<bool> AuthorizeAsync(UserTask task, UserTaskActor actor, UserTaskAccessOperation operation, Cancellation
 41    {
 42        if (!string.Equals(task.TenantId, actor.Subject.TenantId, StringComparison.Ordinal))
 43            return Task.FromResult(false);
 44        if (!actor.HasPermission(RequiredPermission(operation)))
 45            return Task.FromResult(false);
 46
 47        if (actor.IsGuest)
 48            return Task.FromResult(AuthorizeGuest(task, actor, operation));
 49
 50        var isManager = IsManager(actor);
 51        if (task.HealthSeverity == UserTaskHealthSeverity.Blocking && !isManager)
 52            return Task.FromResult(false);
 53
 54        var isAssignee = task.Assignee?.Matches(actor.Subject) == true;
 55        var isCandidate = IsCandidate(task, actor);
 56        var isCompleter = task.CompletedBy?.Matches(actor.Subject) == true;
 57        var acted = task.Events.Any(x => x.Actor?.Matches(actor.Subject) == true);
 58
 59        var result = operation switch
 60        {
 61            UserTaskAccessOperation.ReadSummary => isManager || isAssignee || isCandidate || isCompleter || acted,
 62            UserTaskAccessOperation.ReadProtected => isManager || (task.IsTerminal ? isCompleter : isAssignee),
 63            UserTaskAccessOperation.Claim => !task.IsTerminal && task.Status is (UserTaskStatus.Available or UserTaskSta
 64            UserTaskAccessOperation.Release => !task.IsTerminal && isAssignee,
 65            UserTaskAccessOperation.Assign => isManager,
 66            UserTaskAccessOperation.UpdateScheduling => isManager,
 67            // The completer may replay the same terminal operation for idempotency; new terminal
 68            // commands are rejected by the manager after operation lookup.
 69            UserTaskAccessOperation.Complete => task.IsTerminal ? isCompleter : isAssignee,
 70            UserTaskAccessOperation.Cancel => isManager,
 71            UserTaskAccessOperation.Manage => isManager,
 72            UserTaskAccessOperation.IssueInvitation => isManager,
 73            UserTaskAccessOperation.RetryResolution => isManager,
 74            _ => false
 75        };
 76
 77        return Task.FromResult(result);
 78    }
 79
 80    private static bool AuthorizeGuest(UserTask task, UserTaskActor actor, UserTaskAccessOperation operation)
 81    {
 82        if (!string.Equals(actor.GuestTaskId, task.Id, StringComparison.Ordinal))
 83            return false;
 84        if (!GuestOperations.Contains(operation))
 85            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.
 88        if (task.Assignee?.Matches(actor.Subject) != true)
 89            return false;
 90        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) =>
 98        !actor.IsGuest && actor.IsManager && actor.HasPermission(UserTasksResourcePermissions.UserTasks, UserTaskVerbs.S
 99
 100    private static bool IsCandidate(UserTask task, UserTaskActor actor)
 101    {
 102        if (task.ExcludedUsers.Any(x => x.Matches(actor.Subject)))
 103            return false;
 104
 105        if (task.MembershipResolutionMode == UserTaskMembershipResolutionMode.Snapshot)
 106            return task.SnapshotMembers.Any(x => x.Matches(actor.Subject));
 107
 108        return task.CandidateUsers.Any(x => x.Matches(actor.Subject))
 109               || task.CandidateGroups.Any(candidate => actor.Groups.Any(candidate.Matches));
 110    }
 111
 112    private static bool IsExcluded(UserTask task, UserTaskActor actor) =>
 113        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) =>
 120        new(UserTasksResourcePermissions.UserTasks, RequiredVerb(operation));
 121
 122    private static string RequiredVerb(UserTaskAccessOperation operation) => operation switch
 123    {
 124        UserTaskAccessOperation.ReadSummary or UserTaskAccessOperation.ReadProtected => CoreVerbs.View,
 125        UserTaskAccessOperation.Claim or UserTaskAccessOperation.Release => UserTaskVerbs.Claim,
 126        UserTaskAccessOperation.Complete => UserTaskVerbs.Complete,
 127        UserTaskAccessOperation.Assign => UserTaskVerbs.Assign,
 128        UserTaskAccessOperation.UpdateScheduling => CoreVerbs.Update,
 129        UserTaskAccessOperation.Cancel => UserTaskVerbs.Cancel,
 130        UserTaskAccessOperation.Manage or UserTaskAccessOperation.RetryResolution => UserTaskVerbs.Supervise,
 131        UserTaskAccessOperation.IssueInvitation => UserTaskVerbs.Invite,
 132        _ => CoreVerbs.View
 133    };
 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
 0143        Task.FromResult(new ParticipantSearchResult([], null, 0));
 144
 145    public Task<IReadOnlyCollection<ParticipantReference>> ResolveDisplayNamesAsync(IReadOnlyCollection<ParticipantRefer
 0146        Task.FromResult(participants);
 147
 148    public Task<IReadOnlyCollection<ParticipantReference>> EnumerateGroupMembersAsync(ParticipantReference group, Cancel
 0149        Task.FromResult<IReadOnlyCollection<ParticipantReference>>([]);
 150}