diff --git a/doc/migrations/authorization-model.md b/doc/migrations/authorization-model.md index ff7a5829a..304d2a293 100644 --- a/doc/migrations/authorization-model.md +++ b/doc/migrations/authorization-model.md @@ -39,7 +39,11 @@ Today they are literal claim values, not patterns: `read:*` authorizes only the - Where host code is **disabled**, nothing changes. - Where host code is **enabled**, any author who may write workflow definitions may use C# and Python, and the editor offers those expression types to every such author. -Deployments that enabled host code while trusting only *some* authors lose that granularity until [#7975](https://github.com/elsa-workflows/elsa-core/issues/7975) lands. If that matters to you, disable host code until then. +Deployments that enabled host code while trusting only *some* authors lose that granularity, and this is now the +intended posture rather than a gap awaiting a fix: [#7975](https://github.com/elsa-workflows/elsa-core/issues/7975) +was closed as won't-do. Authoring a workflow is a trusted act, and a per-author gate would not have changed what a +script can do once it runs. If some of your authors are not trusted with host code, give them a host with the +switch off; the switch is per language, so C# and Python can be decided separately. ## Revocation @@ -146,8 +150,8 @@ If you have duplicate names across tenants today, they were impossible to create | `read:output-converters` | `workflows/descriptors/output-converters:view` | | `read:workflow-activation-strategies` | `workflows/descriptors/activation-strategies:view` | | `read:javascript-type-definitions` | `workflows/scripting/javascript:view` | -| `exec:csharp-expressions` | *removed* — see #7975 | -| `exec:python-expressions` | *removed* — see #7975 | +| `exec:csharp-expressions` | *removed* — the host switch is the control; see #7975 | +| `exec:python-expressions` | *removed* — the host switch is the control; see #7975 | | `read:user` | `identity/users:view` | | `create:user` | `identity/users:create` | | `update:user` | `identity/users:update` | diff --git a/doc/wiki/expressions-and-scripting.md b/doc/wiki/expressions-and-scripting.md index f49072967..32060eb45 100644 --- a/doc/wiki/expressions-and-scripting.md +++ b/doc/wiki/expressions-and-scripting.md @@ -61,13 +61,13 @@ elsa.UseCSharp(options => }); ``` -Roslyn C# scripting is privileged host-code execution, not a sandbox. Hosts must explicitly set `CSharpOptions.AllowHostCodeExecution` to `true` before C# expressions or `RunCSharp` can be authored or executed. API callers that author, publish, dispatch, or directly execute workflows containing C# must have the `exec:csharp-expressions` permission. +Roslyn C# scripting is privileged host-code execution, not a sandbox. Hosts must explicitly set `CSharpOptions.AllowHostCodeExecution` to `true` before C# expressions or `RunCSharp` can be authored or executed. That switch is the whole control: there is no per-caller permission, because a workflow runs under the server's authority rather than the caller's, so gating the caller never constrained what a script could do. Any author who may write workflow definitions may use C# where the switch is on. If only some of your authors are trusted with host code, give the others a host with the switch off — see [#7975](https://github.com/elsa-workflows/elsa-core/issues/7975). ## Python [PythonFeature](../../src/modules/Elsa.Expressions.Python/Features/PythonFeature.cs) registers pythonnet-based evaluation and configures `PythonGlobalInterpreterManager` as a hosted service. Python.NET execution is privileged host-code execution, not a sandbox. Python code can access host process capabilities through pythonnet and must only be enabled for trusted workflow authors. -Hosts must explicitly set `PythonOptions.AllowHostCodeExecution` to `true` before Python expressions or `RunPython` can be authored or executed. API callers that author, publish, dispatch, or directly execute workflows containing Python must have the `exec:python-expressions` permission. Hosts must also configure the Python DLL path or set `PYTHONNET_PYDLL`. +Hosts must explicitly set `PythonOptions.AllowHostCodeExecution` to `true` before Python expressions or `RunPython` can be authored or executed. As with C#, that switch is the whole control and there is no per-caller permission; the switches are independent, so Python can be enabled while C# stays off. Hosts must also configure the Python DLL path or set `PYTHONNET_PYDLL`. The reference server binds `Scripting:Python` configuration in [Program.cs](../../src/apps/Elsa.Server.Web/Program.cs). diff --git a/src/common/Elsa.Api.Common/PermissionNames.cs b/src/common/Elsa.Api.Common/PermissionNames.cs index 951010aa9..09021a161 100644 --- a/src/common/Elsa.Api.Common/PermissionNames.cs +++ b/src/common/Elsa.Api.Common/PermissionNames.cs @@ -5,15 +5,6 @@ public static class PermissionNames public const string All = "*"; public const string ClaimType = "permissions"; - /// - /// Permission required to author or execute C# workflow expressions. - /// - public const string ExecuteCSharpExpressions = "exec:csharp-expressions"; - - /// - /// Permission required to author or execute Python.NET workflow expressions. - /// - public const string ExecutePythonExpressions = "exec:python-expressions"; /// /// Permission required to pause, resume, or force-drain the workflow runtime. diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/Scripting/ExpressionDescriptors/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/Scripting/ExpressionDescriptors/List/Endpoint.cs index 07a7eadbd..ffd032d00 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/Scripting/ExpressionDescriptors/List/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/Scripting/ExpressionDescriptors/List/Endpoint.cs @@ -14,11 +14,18 @@ namespace Elsa.Workflows.Api.Endpoints.Scripting.ExpressionDescriptors.List; [UsedImplicitly] internal class List(IExpressionDescriptorRegistry expressionDescriptorRegistry) : ElsaEndpointWithoutRequest> { - private static readonly IReadOnlyDictionary PrivilegedExpressionPermissions = new Dictionary - { - ["CSharp"] = PermissionNames.ExecuteCSharpExpressions, - ["Python"] = PermissionNames.ExecutePythonExpressions - }.ToFrozenDictionary(StringComparer.Ordinal); + /// + /// The expression types the host can switch off, which are omitted entirely rather than listed as + /// non-browsable so a disabled language does not appear in the editor at all. Every other type is listed + /// and carries IsBrowsable for the client to act on. + /// + /// + /// This was a map from expression type to a per-author permission. The permission was only ever read to + /// test membership -- the value went unused even before it was retired -- and the decision has always + /// been the host switch, surfaced as IsBrowsable. A set says that without implying a check that + /// does not happen. Per-author script trust was considered and declined in #7975: the host switch is the control. + /// + private static readonly FrozenSet HostCodeExpressionTypes = new[] { "CSharp", "Python" }.ToFrozenSet(StringComparer.Ordinal); /// public override void Configure() @@ -36,13 +43,8 @@ internal class List(IExpressionDescriptorRegistry expressionDescriptorRegistry) return Send.OkAsync(response, cancellationToken); } - private bool CanListDescriptor(ExpressionDescriptor descriptor) - { - if (!PrivilegedExpressionPermissions.TryGetValue(descriptor.Type, out var permission)) - return true; - - return descriptor.IsBrowsable; - } + private static bool CanListDescriptor(ExpressionDescriptor descriptor) => + !HostCodeExpressionTypes.Contains(descriptor.Type) || descriptor.IsBrowsable; private static IEnumerable Map(List descriptors) => descriptors.Select(Map); diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDispatch/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDispatch/Endpoint.cs index 22d324158..71b90b1c2 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDispatch/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDispatch/Endpoint.cs @@ -36,10 +36,10 @@ internal class Endpoint( return; } - var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, User, cancellationToken); + var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, cancellationToken); if (!scriptAuthorizationResult.Succeeded) { - await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken); + await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, message => AddError(message), Send.ErrorsAsync, cancellationToken); return; } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs index 9549f38df..1864ca4a0 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs @@ -80,10 +80,10 @@ internal class BulkPublish( foreach (var (_, definition) in publishableDefinitions) { var workflowGraph = await workflowDefinitionService.MaterializeWorkflowAsync(definition, cancellationToken); - var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, User, cancellationToken); + var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, cancellationToken); if (!scriptAuthorizationResult.Succeeded) { - await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken); + await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, message => AddError(message), Send.ErrorsAsync, cancellationToken); return null!; } } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Dispatch/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Dispatch/Endpoint.cs index 6c1dea319..affe0169d 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Dispatch/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Dispatch/Endpoint.cs @@ -34,10 +34,10 @@ internal class Endpoint( return; } - var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, User, cancellationToken); + var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, cancellationToken); if (!scriptAuthorizationResult.Succeeded) { - await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken); + await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, message => AddError(message), Send.ErrorsAsync, cancellationToken); return; } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/WorkflowExecutionHelper.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/WorkflowExecutionHelper.cs index fc6d1da8f..84b3f0f68 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/WorkflowExecutionHelper.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/WorkflowExecutionHelper.cs @@ -31,7 +31,7 @@ internal static class WorkflowExecutionHelper return; } - var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, httpContext.User, cancellationToken); + var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, cancellationToken); if (!scriptAuthorizationResult.Succeeded) { await SendScriptAuthorizationFailureAsync(httpContext, scriptAuthorizationResult, cancellationToken); @@ -99,12 +99,8 @@ internal static class WorkflowExecutionHelper private static async Task SendScriptAuthorizationFailureAsync(HttpContext httpContext, WorkflowDefinitionScriptAuthorizationResult result, CancellationToken cancellationToken) { - if (result.FailureReason == WorkflowDefinitionScriptAuthorizationFailureReason.MissingPermission) - { - await httpContext.Response.SendForbiddenAsync(cancellation: cancellationToken); - return; - } - + // Only one failure is possible: the host has the language switched off. That is a property of the + // deployment rather than of the caller, so it is a 400 explaining what is disabled, never a 403. httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; await httpContext.Response.WriteAsync(result.Message ?? "Workflow script authorization failed.", cancellationToken); } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs index 87d251372..ff8c12486 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs @@ -49,10 +49,10 @@ internal class Import : ElsaEndpoint var definitionId = model.DefinitionId; var isNew = string.IsNullOrWhiteSpace(definitionId); - var scriptAuthorizationResult = await _scriptAuthorizationService.AuthorizeAsync(model, User, cancellationToken); + var scriptAuthorizationResult = await _scriptAuthorizationService.AuthorizeAsync(model, cancellationToken); if (!scriptAuthorizationResult.Succeeded) { - await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken); + await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, message => AddError(message), Send.ErrorsAsync, cancellationToken); return; } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs index c56b5ae7f..fb395a44f 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs @@ -81,10 +81,10 @@ internal class ImportFiles : ElsaEndpoint { foreach (var model in models) { - var scriptAuthorizationResult = await _scriptAuthorizationService.AuthorizeAsync(model, User, cancellationToken); + var scriptAuthorizationResult = await _scriptAuthorizationService.AuthorizeAsync(model, cancellationToken); if (!scriptAuthorizationResult.Succeeded) { - await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken); + await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, message => AddError(message), Send.ErrorsAsync, cancellationToken); return false; } } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs index e20344f9b..4aed98ee5 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs @@ -67,10 +67,10 @@ internal class Post( return; } - var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(model, User, cancellationToken); + var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(model, cancellationToken); if (!scriptAuthorizationResult.Succeeded) { - await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken); + await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, message => AddError(message), Send.ErrorsAsync, cancellationToken); return; } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs index a8b2eae79..344b84b35 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs @@ -52,10 +52,10 @@ internal class Publish( } var workflowGraph = await workflowDefinitionService.MaterializeWorkflowAsync(definition, cancellationToken); - var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, User, cancellationToken); + var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, cancellationToken); if (!scriptAuthorizationResult.Succeeded) { - await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken); + await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, message => AddError(message), Send.ErrorsAsync, cancellationToken); return; } diff --git a/src/modules/Elsa.Workflows.Api/Security/WorkflowDefinitionScriptAuthorizationFailure.cs b/src/modules/Elsa.Workflows.Api/Security/WorkflowDefinitionScriptAuthorizationFailure.cs index 445ff6050..80bc75952 100644 --- a/src/modules/Elsa.Workflows.Api/Security/WorkflowDefinitionScriptAuthorizationFailure.cs +++ b/src/modules/Elsa.Workflows.Api/Security/WorkflowDefinitionScriptAuthorizationFailure.cs @@ -4,17 +4,12 @@ internal static class WorkflowDefinitionScriptAuthorizationFailure { public static async Task SendAsync( WorkflowDefinitionScriptAuthorizationResult result, - Func sendForbiddenAsync, Action addError, Func sendErrorsAsync, CancellationToken cancellationToken) { - if (result.FailureReason == WorkflowDefinitionScriptAuthorizationFailureReason.MissingPermission) - { - await sendForbiddenAsync(cancellationToken); - return; - } - + // Only one failure is possible: the host has the language switched off. That is a property of the + // deployment rather than of the caller, so it is a 400 explaining what is disabled, never a 403. addError(result.Message ?? "Workflow script authorization failed."); await sendErrorsAsync(400, cancellationToken); } diff --git a/src/modules/Elsa.Workflows.Api/Security/WorkflowDefinitionScriptAuthorizationService.cs b/src/modules/Elsa.Workflows.Api/Security/WorkflowDefinitionScriptAuthorizationService.cs index 355443786..eb0819cf6 100644 --- a/src/modules/Elsa.Workflows.Api/Security/WorkflowDefinitionScriptAuthorizationService.cs +++ b/src/modules/Elsa.Workflows.Api/Security/WorkflowDefinitionScriptAuthorizationService.cs @@ -1,4 +1,3 @@ -using System.Security.Claims; using Elsa.Expressions.Contracts; using Elsa.Extensions; using Elsa.Workflows.Activities; @@ -22,20 +21,20 @@ internal class WorkflowDefinitionScriptAuthorizationService( "Python.NET workflow expression execution is disabled by the host. Set PythonOptions.AllowHostCodeExecution to true only for trusted workflow authors; Python.NET is not a sandbox.") ]; - public async Task AuthorizeAsync(WorkflowDefinitionModel model, ClaimsPrincipal user, CancellationToken cancellationToken = default) + public async Task AuthorizeAsync(WorkflowDefinitionModel model, CancellationToken cancellationToken = default) { if (model.Root == null) return WorkflowDefinitionScriptAuthorizationResult.Allowed(); - return await AuthorizeAsync(model.Root, user, cancellationToken); + return await AuthorizeAsync(model.Root, cancellationToken); } - public async Task AuthorizeAsync(IActivity root, ClaimsPrincipal user, CancellationToken cancellationToken = default) + public async Task AuthorizeAsync(IActivity root, CancellationToken cancellationToken = default) { var scriptUsages = await GetUsedScriptPoliciesAsync(root, cancellationToken); var failure = scriptUsages - .Select(policy => AuthorizeScriptUsage(policy, user)) + .Select(AuthorizeScriptUsage) .FirstOrDefault(result => result is { Succeeded: false }); if (failure.FailureReason.HasValue) @@ -44,21 +43,23 @@ internal class WorkflowDefinitionScriptAuthorizationService( return WorkflowDefinitionScriptAuthorizationResult.Allowed(); } - public async Task AuthorizeAsync(Workflow workflow, ClaimsPrincipal user, CancellationToken cancellationToken = default) + public async Task AuthorizeAsync(Workflow workflow, CancellationToken cancellationToken = default) { - return await AuthorizeAsync((IActivity)workflow, user, cancellationToken); + return await AuthorizeAsync((IActivity)workflow, cancellationToken); } - private WorkflowDefinitionScriptAuthorizationResult AuthorizeScriptUsage(ScriptPolicy policy, ClaimsPrincipal user) + private WorkflowDefinitionScriptAuthorizationResult AuthorizeScriptUsage(ScriptPolicy policy) { // Language-specific options live in optional modules. Workflows.Api observes the descriptor state projected by those module providers. if (expressionDescriptorRegistry.Find(policy.ExpressionType)?.IsBrowsable != true) return WorkflowDefinitionScriptAuthorizationResult.HostDisabled(policy.HostDisabledMessage); - // The host switch is the single control. The former per-author permission conflated an incoherent - // execution-side gate -- a workflow runs under the server's authority, not the caller's, so the - // check never constrained what a script could do -- with a meaningful authoring-side one. Per-author - // script trust is redesigned separately; see #7975. + // The host switch is the only control, so there is nothing left to decide once it is on. The former + // per-author permission conflated an incoherent execution-side gate -- a workflow runs under the + // server's authority, not the caller's, so the check never constrained what a script could do -- + // with a meaningful authoring-side one. Neither the caller nor a failure reason for a denied caller + // is modelled here any more, because nothing produces one. Per-author script trust was considered and + // declined in #7975, so this is the settled shape rather than a stop on the way to one. return WorkflowDefinitionScriptAuthorizationResult.Allowed(); } @@ -87,12 +88,9 @@ internal readonly record struct WorkflowDefinitionScriptAuthorizationResult(bool public static WorkflowDefinitionScriptAuthorizationResult Allowed() => new(true, null, null); public static WorkflowDefinitionScriptAuthorizationResult HostDisabled(string message) => new(false, WorkflowDefinitionScriptAuthorizationFailureReason.HostDisabled, message); - - public static WorkflowDefinitionScriptAuthorizationResult MissingPermission() => new(false, WorkflowDefinitionScriptAuthorizationFailureReason.MissingPermission, null); } internal enum WorkflowDefinitionScriptAuthorizationFailureReason { - HostDisabled, - MissingPermission + HostDisabled } diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Security/WorkflowDefinitionScriptAuthorizationServiceTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Security/WorkflowDefinitionScriptAuthorizationServiceTests.cs index ce997e76e..f06b84a70 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Security/WorkflowDefinitionScriptAuthorizationServiceTests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Security/WorkflowDefinitionScriptAuthorizationServiceTests.cs @@ -1,4 +1,3 @@ -using System.Security.Claims; using Elsa.Expressions.Contracts; using Elsa.Expressions.Models; using Elsa.Workflows.Activities; @@ -14,9 +13,6 @@ namespace Elsa.Workflows.IntegrationTests.Security; public class WorkflowDefinitionScriptAuthorizationServiceTests { - private static readonly ClaimsPrincipal UserWithCSharpPermission = CreateUser(PermissionNames.ExecuteCSharpExpressions); - private static readonly ClaimsPrincipal UserWithPythonPermission = CreateUser(PermissionNames.ExecutePythonExpressions); - private static readonly ClaimsPrincipal UserWithoutScriptPermission = CreateUser("workflows/definitions:write"); [Fact] public async Task AuthorizeAsync_BlocksCSharpExpression_WhenHostHasNotOptedIn() @@ -24,38 +20,28 @@ public class WorkflowDefinitionScriptAuthorizationServiceTests var service = CreateService(hostAllowsCSharp: false, hostAllowsPython: true); var model = CreateModelWithCSharpExpression(); - var result = await service.AuthorizeAsync(model, UserWithCSharpPermission); + var result = await service.AuthorizeAsync(model); Assert.Equal(WorkflowDefinitionScriptAuthorizationFailureReason.HostDisabled, result.FailureReason); Assert.Contains("CSharpOptions.AllowHostCodeExecution", result.Message); } [Fact] - public async Task AuthorizeAsync_AllowsCSharpExpression_WhenHostOptedIn_RegardlessOfUserPermissions() + public async Task AuthorizeAsync_AllowsCSharpExpression_WhenHostOptedIn() { var service = CreateService(hostAllowsCSharp: true, hostAllowsPython: true); var model = CreateModelWithCSharpExpression(); - var result = await service.AuthorizeAsync(model, UserWithoutScriptPermission); + var result = await service.AuthorizeAsync(model); - // The per-author permission was removed: a workflow runs under the server's authority, not the - // caller's, so gating execution on the caller never constrained what a script could do. The host - // switch is the only control now. Per-author script trust is redesigned in #7975. + // The host switch is the only control. The per-author permission was removed because a workflow runs + // under the server's authority, not the caller's, so gating on the caller never constrained what a + // script could do. The service no longer takes a principal at all, and #7975 closed won't-do, so + // this is the settled behaviour rather than an interim state. Assert.True(result.Succeeded); Assert.Null(result.FailureReason); } - [Fact] - public async Task AuthorizeAsync_AllowsCSharpExpression_WhenHostAndUserAllowIt() - { - var service = CreateService(hostAllowsCSharp: true, hostAllowsPython: true); - var model = CreateModelWithCSharpExpression(); - - var result = await service.AuthorizeAsync(model, UserWithCSharpPermission); - - Assert.True(result.Succeeded); - } - [Fact] public async Task AuthorizeAsync_AllowsWorkflowWithoutScriptUsage() { @@ -65,7 +51,7 @@ public class WorkflowDefinitionScriptAuthorizationServiceTests Root = new WriteLine("hello") }; - var result = await service.AuthorizeAsync(model, UserWithoutScriptPermission); + var result = await service.AuthorizeAsync(model); Assert.True(result.Succeeded); } @@ -82,11 +68,12 @@ public class WorkflowDefinitionScriptAuthorizationServiceTests } }; - var result = await service.AuthorizeAsync(model, UserWithoutScriptPermission); + var result = await service.AuthorizeAsync(model); - // The per-author permission was removed: a workflow runs under the server's authority, not the - // caller's, so gating execution on the caller never constrained what a script could do. The host - // switch is the only control now. Per-author script trust is redesigned in #7975. + // The host switch is the only control. The per-author permission was removed because a workflow runs + // under the server's authority, not the caller's, so gating on the caller never constrained what a + // script could do. The service no longer takes a principal at all, and #7975 closed won't-do, so + // this is the settled behaviour rather than an interim state. Assert.True(result.Succeeded); Assert.Null(result.FailureReason); } @@ -97,38 +84,28 @@ public class WorkflowDefinitionScriptAuthorizationServiceTests var service = CreateService(hostAllowsCSharp: true, hostAllowsPython: false); var model = CreateModelWithPythonExpression(); - var result = await service.AuthorizeAsync(model, UserWithPythonPermission); + var result = await service.AuthorizeAsync(model); Assert.Equal(WorkflowDefinitionScriptAuthorizationFailureReason.HostDisabled, result.FailureReason); Assert.Contains("PythonOptions.AllowHostCodeExecution", result.Message); } [Fact] - public async Task AuthorizeAsync_AllowsPythonExpression_WhenHostOptedIn_RegardlessOfUserPermissions() + public async Task AuthorizeAsync_AllowsPythonExpression_WhenHostOptedIn() { var service = CreateService(hostAllowsCSharp: true, hostAllowsPython: true); var model = CreateModelWithPythonExpression(); - var result = await service.AuthorizeAsync(model, UserWithoutScriptPermission); + var result = await service.AuthorizeAsync(model); - // The per-author permission was removed: a workflow runs under the server's authority, not the - // caller's, so gating execution on the caller never constrained what a script could do. The host - // switch is the only control now. Per-author script trust is redesigned in #7975. + // The host switch is the only control. The per-author permission was removed because a workflow runs + // under the server's authority, not the caller's, so gating on the caller never constrained what a + // script could do. The service no longer takes a principal at all, and #7975 closed won't-do, so + // this is the settled behaviour rather than an interim state. Assert.True(result.Succeeded); Assert.Null(result.FailureReason); } - [Fact] - public async Task AuthorizeAsync_AllowsPythonExpression_WhenHostAndUserAllowIt() - { - var service = CreateService(hostAllowsCSharp: true, hostAllowsPython: true); - var model = CreateModelWithPythonExpression(); - - var result = await service.AuthorizeAsync(model, UserWithPythonPermission); - - Assert.True(result.Succeeded); - } - [Fact] public async Task AuthorizeAsync_TreatsRunPythonActivityAsPythonUsage() { @@ -141,11 +118,12 @@ public class WorkflowDefinitionScriptAuthorizationServiceTests } }; - var result = await service.AuthorizeAsync(model, UserWithoutScriptPermission); + var result = await service.AuthorizeAsync(model); - // The per-author permission was removed: a workflow runs under the server's authority, not the - // caller's, so gating execution on the caller never constrained what a script could do. The host - // switch is the only control now. Per-author script trust is redesigned in #7975. + // The host switch is the only control. The per-author permission was removed because a workflow runs + // under the server's authority, not the caller's, so gating on the caller never constrained what a + // script could do. The service no longer takes a principal at all, and #7975 closed won't-do, so + // this is the settled behaviour rather than an interim state. Assert.True(result.Succeeded); Assert.Null(result.FailureReason); } @@ -206,9 +184,4 @@ public class WorkflowDefinitionScriptAuthorizationServiceTests return new(visitor, registry); } - private static ClaimsPrincipal CreateUser(params string[] permissions) - { - var identity = new ClaimsIdentity(permissions.Select(x => new Claim("permissions", x)), "Test"); - return new(identity); - } }