| | | 1 | | using Elsa.Expressions.Contracts; |
| | | 2 | | using Elsa.Extensions; |
| | | 3 | | using Elsa.Workflows.Activities; |
| | | 4 | | using Elsa.Workflows.Management.Models; |
| | | 5 | | |
| | | 6 | | namespace Elsa.Workflows.Api.Security; |
| | | 7 | | |
| | | 8 | | internal class WorkflowDefinitionScriptAuthorizationService( |
| | | 9 | | IActivityVisitor activityVisitor, |
| | | 10 | | IExpressionDescriptorRegistry expressionDescriptorRegistry) |
| | | 11 | | { |
| | | 12 | | private static readonly ScriptPolicy[] ScriptPolicies = |
| | | 13 | | [ |
| | | 14 | | new( |
| | | 15 | | "CSharp", |
| | | 16 | | WorkflowScriptActivityTypeNames.RunCSharp, |
| | | 17 | | "C# workflow expression execution is disabled by the host. Set CSharpOptions.AllowHostCodeExecution to true |
| | | 18 | | new( |
| | | 19 | | "Python", |
| | | 20 | | WorkflowScriptActivityTypeNames.RunPython, |
| | | 21 | | "Python.NET workflow expression execution is disabled by the host. Set PythonOptions.AllowHostCodeExecution |
| | | 22 | | ]; |
| | | 23 | | |
| | | 24 | | public async Task<WorkflowDefinitionScriptAuthorizationResult> AuthorizeAsync(WorkflowDefinitionModel model, Cancell |
| | | 25 | | { |
| | | 26 | | if (model.Root == null) |
| | | 27 | | return WorkflowDefinitionScriptAuthorizationResult.Allowed(); |
| | | 28 | | |
| | | 29 | | return await AuthorizeAsync(model.Root, cancellationToken); |
| | | 30 | | } |
| | | 31 | | |
| | | 32 | | public async Task<WorkflowDefinitionScriptAuthorizationResult> AuthorizeAsync(IActivity root, CancellationToken canc |
| | | 33 | | { |
| | | 34 | | var scriptUsages = await GetUsedScriptPoliciesAsync(root, cancellationToken); |
| | | 35 | | |
| | | 36 | | var failure = scriptUsages |
| | | 37 | | .Select(AuthorizeScriptUsage) |
| | | 38 | | .FirstOrDefault(result => result is { Succeeded: false }); |
| | | 39 | | |
| | | 40 | | if (failure.FailureReason.HasValue) |
| | | 41 | | return failure; |
| | | 42 | | |
| | | 43 | | return WorkflowDefinitionScriptAuthorizationResult.Allowed(); |
| | | 44 | | } |
| | | 45 | | |
| | | 46 | | public async Task<WorkflowDefinitionScriptAuthorizationResult> AuthorizeAsync(Workflow workflow, CancellationToken c |
| | | 47 | | { |
| | | 48 | | return await AuthorizeAsync((IActivity)workflow, cancellationToken); |
| | | 49 | | } |
| | | 50 | | |
| | | 51 | | private WorkflowDefinitionScriptAuthorizationResult AuthorizeScriptUsage(ScriptPolicy policy) |
| | | 52 | | { |
| | | 53 | | // Language-specific options live in optional modules. Workflows.Api observes the descriptor state projected by |
| | | 54 | | if (expressionDescriptorRegistry.Find(policy.ExpressionType)?.IsBrowsable != true) |
| | | 55 | | return WorkflowDefinitionScriptAuthorizationResult.HostDisabled(policy.HostDisabledMessage); |
| | | 56 | | |
| | | 57 | | // The host switch is the only control, so there is nothing left to decide once it is on. The former |
| | | 58 | | // per-author permission conflated an incoherent execution-side gate -- a workflow runs under the |
| | | 59 | | // server's authority, not the caller's, so the check never constrained what a script could do -- |
| | | 60 | | // with a meaningful authoring-side one. Neither the caller nor a failure reason for a denied caller |
| | | 61 | | // is modelled here any more, because nothing produces one. Per-author script trust was considered and |
| | | 62 | | // declined in #7975, so this is the settled shape rather than a stop on the way to one. |
| | | 63 | | return WorkflowDefinitionScriptAuthorizationResult.Allowed(); |
| | | 64 | | } |
| | | 65 | | |
| | | 66 | | private async Task<IEnumerable<ScriptPolicy>> GetUsedScriptPoliciesAsync(IActivity root, CancellationToken cancellat |
| | | 67 | | { |
| | | 68 | | var graph = await activityVisitor.VisitAsync(root, cancellationToken); |
| | | 69 | | var nodes = new[] { graph }.Concat(graph.Descendants()).ToList(); |
| | | 70 | | var policies = ScriptPolicies |
| | | 71 | | .Where(policy => nodes.Any(x => IsRunActivity(x.Activity, policy) || HasExpression(x.Activity, policy))) |
| | | 72 | | .ToList(); |
| | | 73 | | |
| | | 74 | | return policies; |
| | | 75 | | } |
| | | 76 | | |
| | | 77 | | private static bool IsRunActivity(IActivity activity, ScriptPolicy policy) => |
| | | 78 | | string.Equals(activity.Type, policy.RunActivityType, StringComparison.Ordinal); |
| | | 79 | | |
| | | 80 | | private static bool HasExpression(IActivity activity, ScriptPolicy policy) => |
| | | 81 | | activity.GetInputs().Any(x => string.Equals(x.Expression?.Type, policy.ExpressionType, StringComparison.Ordinal) |
| | | 82 | | |
| | | 83 | | private sealed record ScriptPolicy(string ExpressionType, string RunActivityType, string HostDisabledMessage); |
| | | 84 | | } |
| | | 85 | | |
| | 27 | 86 | | internal readonly record struct WorkflowDefinitionScriptAuthorizationResult(bool Succeeded, WorkflowDefinitionScriptAuth |
| | | 87 | | { |
| | 16 | 88 | | public static WorkflowDefinitionScriptAuthorizationResult Allowed() => new(true, null, null); |
| | | 89 | | |
| | 0 | 90 | | public static WorkflowDefinitionScriptAuthorizationResult HostDisabled(string message) => new(false, WorkflowDefinit |
| | | 91 | | } |
| | | 92 | | |
| | | 93 | | internal enum WorkflowDefinitionScriptAuthorizationFailureReason |
| | | 94 | | { |
| | | 95 | | HostDisabled |
| | | 96 | | } |