[codex] Harden C# expression host-code execution (#7519)
* Harden C# expression host-code execution * Address script authorization review feedback * Harden script authorization failure responses * Address code quality review feedback * Use explicit failure filter in script authorization * Centralize script activity type names * Address script authorization review feedback
This commit is contained in:
parent
d23e61e9be
commit
2fa1a9ef8e
|
|
@ -55,11 +55,14 @@ Additional JavaScript libraries are in [Elsa.Expressions.JavaScript.Libraries](.
|
|||
```csharp
|
||||
elsa.UseCSharp(options =>
|
||||
{
|
||||
options.AllowHostCodeExecution = true;
|
||||
options.DisableWrappers = disableVariableWrappers;
|
||||
options.AppendScript("string Greet(string name) => $\"Hello {name}!\";");
|
||||
});
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ services
|
|||
.UseScheduling()
|
||||
.UseCSharp(options =>
|
||||
{
|
||||
configuration.GetSection("Scripting:CSharp").Bind(options);
|
||||
options.DisableWrappers = disableVariableWrappers;
|
||||
options.AppendScript("string Greet(string name) => $\"Hello {name}!\";");
|
||||
options.AppendScript("string SayHelloWorld() => Greet(\"World\");");
|
||||
|
|
|
|||
|
|
@ -84,6 +84,9 @@
|
|||
]
|
||||
},
|
||||
"Scripting": {
|
||||
"CSharp": {
|
||||
"AllowHostCodeExecution": false
|
||||
},
|
||||
"Python": {
|
||||
"AllowHostCodeExecution": true,
|
||||
"PythonDllPath": "",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ public static class PermissionNames
|
|||
public const string All = "*";
|
||||
public const string ClaimType = "permissions";
|
||||
|
||||
/// <summary>
|
||||
/// Permission required to author or execute C# workflow expressions.
|
||||
/// </summary>
|
||||
public const string ExecuteCSharpExpressions = "exec:csharp-expressions";
|
||||
|
||||
/// <summary>
|
||||
/// Permission required to author or execute Python.NET workflow expressions.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public class TestApplicationBuilder
|
|||
_configureElsa += elsa => elsa
|
||||
.AddActivitiesFrom<WriteLine>()
|
||||
.UseScheduling()
|
||||
.UseCSharp()
|
||||
.UseCSharp(options => options.AllowHostCodeExecution = true)
|
||||
.UseJavaScript()
|
||||
.UseLiquid()
|
||||
.UseWorkflowManagement()
|
||||
|
|
@ -119,4 +119,4 @@ public class TestApplicationBuilder
|
|||
_configureElsa += elsa => elsa.UseFluentStorageProvider(storage => storage.BlobStorage = sp => StorageFactory.Blobs.DirectoryFiles(Path.Combine(workflowsDirectory)));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Elsa.Expressions.CSharp.Activities;
|
|||
/// <summary>
|
||||
/// Executes C# code.
|
||||
/// </summary>
|
||||
[Activity("Elsa", "Scripting", "Executes C# code", DisplayName = "Run C#")]
|
||||
[Activity(WorkflowScriptActivityTypeNames.Namespace, WorkflowScriptActivityTypeNames.RunCSharpType, 1, "Executes C# code", "Scripting", DisplayName = "Run C#")]
|
||||
public class RunCSharp : CodeActivity<object?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -72,4 +72,4 @@ public class RunCSharp : CodeActivity<object?>
|
|||
// Complete the activity with the outcome.
|
||||
await context.CompleteActivityWithOutcomesAsync(outcomes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
using Elsa.Expressions.CSharp.Options;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Models;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Expressions.CSharp.ActivityDescriptorModifiers;
|
||||
|
||||
internal class CSharpActivityDescriptorModifier(IOptions<CSharpOptions> options) : IActivityDescriptorModifier
|
||||
{
|
||||
public void Modify(ActivityDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.TypeName != WorkflowScriptActivityTypeNames.RunCSharp)
|
||||
return;
|
||||
|
||||
descriptor.IsBrowsable = options.Value.AllowHostCodeExecution;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using Elsa.Caching.Features;
|
||||
using Elsa.Common.Features;
|
||||
using Elsa.Expressions.CSharp.ActivityDescriptorModifiers;
|
||||
using Elsa.Expressions.CSharp.Activities;
|
||||
using Elsa.Expressions.CSharp.Contracts;
|
||||
using Elsa.Expressions.CSharp.Options;
|
||||
|
|
@ -42,6 +43,7 @@ public class CSharpFeature : FeatureBase
|
|||
Services
|
||||
.AddExpressionDescriptorProvider<CSharpExpressionDescriptorProvider>()
|
||||
.AddScoped<ICSharpEvaluator, CSharpEvaluator>()
|
||||
.AddSingleton<IActivityDescriptorModifier, CSharpActivityDescriptorModifier>()
|
||||
;
|
||||
|
||||
// Handlers.
|
||||
|
|
@ -53,4 +55,4 @@ public class CSharpFeature : FeatureBase
|
|||
// UI property handlers.
|
||||
Services.AddScoped<IPropertyUIHandler, RunCSharpOptionsProvider>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ namespace Elsa.Expressions.CSharp.Options;
|
|||
/// </summary>
|
||||
public class CSharpOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets whether workflow-authored Roslyn C# host-code execution is allowed.
|
||||
/// Roslyn scripting is not a sandbox and can access host process capabilities.
|
||||
/// </summary>
|
||||
public bool AllowHostCodeExecution { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of callbacks that is invoked when a C# expression is evaluated. Use this to configure the <see cref="ScriptOptions"/>.
|
||||
/// </summary>
|
||||
|
|
@ -101,4 +107,4 @@ public class CSharpOptions
|
|||
ConfigureScriptCallbacks.Add((s, c) => s.ContinueWith(script(c)));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
using Elsa.Expressions.CSharp.Expressions;
|
||||
using Elsa.Expressions.CSharp.Options;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Expressions.CSharp.Providers;
|
||||
|
||||
internal class CSharpExpressionDescriptorProvider : IExpressionDescriptorProvider
|
||||
internal class CSharpExpressionDescriptorProvider(IOptions<CSharpOptions> options) : IExpressionDescriptorProvider
|
||||
{
|
||||
private const string TypeName = "CSharp";
|
||||
|
||||
|
|
@ -16,8 +18,9 @@ internal class CSharpExpressionDescriptorProvider : IExpressionDescriptorProvide
|
|||
{
|
||||
Type = TypeName,
|
||||
DisplayName = "C#",
|
||||
IsBrowsable = options.Value.AllowHostCodeExecution,
|
||||
Properties = new { MonacoLanguage = "csharp" }.ToDictionary(),
|
||||
HandlerFactory = ActivatorUtilities.GetServiceOrCreateInstance<CSharpExpressionHandler>
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ public class CSharpEvaluator(INotificationSender notificationSender, IOptions<CS
|
|||
Func<Script<object>, Script<object>>? configureScript = default,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_csharpOptions.AllowHostCodeExecution)
|
||||
throw new InvalidOperationException("C# workflow expression execution is disabled. Set CSharpOptions.AllowHostCodeExecution to true only for trusted workflow authors; Roslyn scripting is not a sandbox.");
|
||||
|
||||
var scriptOptions = ScriptOptions.Default.WithOptimizationLevel(OptimizationLevel.Release);
|
||||
|
||||
if (configureScriptOptions != null)
|
||||
|
|
@ -85,4 +88,4 @@ public class CSharpEvaluator(INotificationSender notificationSender, IOptions<CS
|
|||
var hash = SHA256.HashData(segment.AsSpan());
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using CShells.Features;
|
||||
using Elsa.Expressions.CSharp.ActivityDescriptorModifiers;
|
||||
using Elsa.Expressions.CSharp.Activities;
|
||||
using Elsa.Expressions.CSharp.Contracts;
|
||||
using Elsa.Expressions.CSharp.Options;
|
||||
|
|
@ -33,7 +34,8 @@ public class CSharpFeature : IShellFeature
|
|||
// C# services.
|
||||
services
|
||||
.AddExpressionDescriptorProvider<CSharpExpressionDescriptorProvider>()
|
||||
.AddScoped<ICSharpEvaluator, CSharpEvaluator>();
|
||||
.AddScoped<ICSharpEvaluator, CSharpEvaluator>()
|
||||
.AddSingleton<IActivityDescriptorModifier, CSharpActivityDescriptorModifier>();
|
||||
|
||||
// Handlers.
|
||||
services.AddNotificationHandlersFrom<CSharpFeature>();
|
||||
|
|
@ -43,4 +45,3 @@ public class CSharpFeature : IShellFeature
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ namespace Elsa.Expressions.Python.Activities;
|
|||
/// <summary>
|
||||
/// Executes Python code.
|
||||
/// </summary>
|
||||
[Activity("Elsa", "Scripting", "Executes Python code", DisplayName = "Run Python")]
|
||||
[Activity(WorkflowScriptActivityTypeNames.Namespace, WorkflowScriptActivityTypeNames.RunPythonType, 1, "Executes Python code", "Scripting", DisplayName = "Run Python")]
|
||||
public class RunPython : CodeActivity<object?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -69,4 +69,4 @@ public class RunPython : CodeActivity<object?>
|
|||
// Complete the activity with the outcome.
|
||||
await context.CompleteActivityWithOutcomesAsync(outcomes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using Elsa.Expressions.Python.Options;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Helpers;
|
||||
using Elsa.Workflows.Models;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
|
@ -8,11 +7,9 @@ namespace Elsa.Expressions.Python.ActivityDescriptorModifiers;
|
|||
|
||||
internal class PythonActivityDescriptorModifier(IOptions<PythonOptions> options) : IActivityDescriptorModifier
|
||||
{
|
||||
private static readonly string RunPythonActivityType = ActivityTypeNameHelper.GenerateTypeName<Activities.RunPython>();
|
||||
|
||||
public void Modify(ActivityDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.TypeName != RunPythonActivityType)
|
||||
if (descriptor.TypeName != WorkflowScriptActivityTypeNames.RunPython)
|
||||
return;
|
||||
|
||||
descriptor.IsBrowsable = options.Value.AllowHostCodeExecution;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Frozen;
|
||||
using Elsa.Abstractions;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Models;
|
||||
|
|
@ -12,7 +13,11 @@ namespace Elsa.Workflows.Api.Endpoints.Scripting.ExpressionDescriptors.List;
|
|||
[UsedImplicitly]
|
||||
internal class List(IExpressionDescriptorRegistry expressionDescriptorRegistry) : ElsaEndpointWithoutRequest<ListResponse<ExpressionDescriptorModel>>
|
||||
{
|
||||
private const string PythonExpressionType = "Python";
|
||||
private static readonly IReadOnlyDictionary<string, string> PrivilegedExpressionPermissions = new Dictionary<string, string>
|
||||
{
|
||||
["CSharp"] = PermissionNames.ExecuteCSharpExpressions,
|
||||
["Python"] = PermissionNames.ExecutePythonExpressions
|
||||
}.ToFrozenDictionary(StringComparer.Ordinal);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
|
|
@ -32,9 +37,10 @@ internal class List(IExpressionDescriptorRegistry expressionDescriptorRegistry)
|
|||
|
||||
private bool CanListDescriptor(ExpressionDescriptor descriptor)
|
||||
{
|
||||
return descriptor.Type != PythonExpressionType ||
|
||||
(descriptor.IsBrowsable &&
|
||||
User.Claims.Any(x => x.Type == "permissions" && (x.Value == PermissionNames.All || x.Value == PermissionNames.ExecutePythonExpressions)));
|
||||
if (!PrivilegedExpressionPermissions.TryGetValue(descriptor.Type, out var permission))
|
||||
return true;
|
||||
|
||||
return descriptor.IsBrowsable && User.Claims.Any(x => x.Type == PermissionNames.ClaimType && (x.Value == PermissionNames.All || x.Value == permission));
|
||||
}
|
||||
|
||||
private static IEnumerable<ExpressionDescriptorModel> Map(List<ExpressionDescriptor> descriptors) => descriptors.Select(Map);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ internal class Endpoint(
|
|||
IWorkflowDefinitionService workflowDefinitionService,
|
||||
IWorkflowDispatcher workflowDispatcher,
|
||||
IIdentityGenerator identityGenerator,
|
||||
PythonWorkflowDefinitionAuthorizationService pythonAuthorizationService)
|
||||
WorkflowDefinitionScriptAuthorizationService scriptAuthorizationService)
|
||||
: ElsaEndpoint<Request, Response>
|
||||
{
|
||||
public override void Configure()
|
||||
|
|
@ -35,10 +35,10 @@ internal class Endpoint(
|
|||
return;
|
||||
}
|
||||
|
||||
var pythonAuthorizationResult = await pythonAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, User, cancellationToken);
|
||||
if (pythonAuthorizationResult != PythonWorkflowDefinitionAuthorizationResult.Allowed)
|
||||
var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, User, cancellationToken);
|
||||
if (!scriptAuthorizationResult.Succeeded)
|
||||
{
|
||||
await PythonWorkflowDefinitionAuthorizationFailure.SendAsync(pythonAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ internal class BulkPublish(
|
|||
IWorkflowDefinitionPublisher workflowDefinitionPublisher,
|
||||
IAuthorizationService authorizationService,
|
||||
IWorkflowDefinitionService workflowDefinitionService,
|
||||
PythonWorkflowDefinitionAuthorizationService pythonAuthorizationService)
|
||||
WorkflowDefinitionScriptAuthorizationService scriptAuthorizationService)
|
||||
: ElsaEndpoint<Request, Response>
|
||||
{
|
||||
public override void Configure()
|
||||
|
|
@ -77,10 +77,10 @@ internal class BulkPublish(
|
|||
foreach (var (_, definition) in publishableDefinitions)
|
||||
{
|
||||
var workflowGraph = await workflowDefinitionService.MaterializeWorkflowAsync(definition, cancellationToken);
|
||||
var pythonAuthorizationResult = await pythonAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, User, cancellationToken);
|
||||
if (pythonAuthorizationResult != PythonWorkflowDefinitionAuthorizationResult.Allowed)
|
||||
var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, User, cancellationToken);
|
||||
if (!scriptAuthorizationResult.Succeeded)
|
||||
{
|
||||
await PythonWorkflowDefinitionAuthorizationFailure.SendAsync(pythonAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ internal class Endpoint(
|
|||
IWorkflowDefinitionService workflowDefinitionService,
|
||||
IWorkflowDispatcher workflowDispatcher,
|
||||
IIdentityGenerator identityGenerator,
|
||||
PythonWorkflowDefinitionAuthorizationService pythonAuthorizationService) : ElsaEndpoint<Request, Response>
|
||||
WorkflowDefinitionScriptAuthorizationService scriptAuthorizationService) : ElsaEndpoint<Request, Response>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
|
|
@ -33,10 +33,10 @@ internal class Endpoint(
|
|||
return;
|
||||
}
|
||||
|
||||
var pythonAuthorizationResult = await pythonAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, User, cancellationToken);
|
||||
if (pythonAuthorizationResult != PythonWorkflowDefinitionAuthorizationResult.Allowed)
|
||||
var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, User, cancellationToken);
|
||||
if (!scriptAuthorizationResult.Succeeded)
|
||||
{
|
||||
await PythonWorkflowDefinitionAuthorizationFailure.SendAsync(pythonAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ internal class GetEndpoint(
|
|||
IWorkflowRuntime workflowRuntime,
|
||||
IWorkflowStarter workflowStarter,
|
||||
IApiSerializer apiSerializer,
|
||||
PythonWorkflowDefinitionAuthorizationService pythonAuthorizationService)
|
||||
WorkflowDefinitionScriptAuthorizationService scriptAuthorizationService)
|
||||
: ElsaEndpoint<GetRequest>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -35,7 +35,7 @@ internal class GetEndpoint(
|
|||
workflowRuntime,
|
||||
workflowStarter,
|
||||
apiSerializer,
|
||||
pythonAuthorizationService,
|
||||
scriptAuthorizationService,
|
||||
HttpContext,
|
||||
cancellationToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ internal class PostEndpoint(
|
|||
IWorkflowRuntime workflowRuntime,
|
||||
IWorkflowStarter workflowStarter,
|
||||
IApiSerializer apiSerializer,
|
||||
PythonWorkflowDefinitionAuthorizationService pythonAuthorizationService)
|
||||
WorkflowDefinitionScriptAuthorizationService scriptAuthorizationService)
|
||||
: ElsaEndpointWithoutRequest<Response>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -73,7 +73,7 @@ internal class PostEndpoint(
|
|||
workflowRuntime,
|
||||
workflowStarter,
|
||||
apiSerializer,
|
||||
pythonAuthorizationService,
|
||||
scriptAuthorizationService,
|
||||
HttpContext,
|
||||
cancellationToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ internal static class WorkflowExecutionHelper
|
|||
IWorkflowRuntime workflowRuntime,
|
||||
IWorkflowStarter workflowStarter,
|
||||
IApiSerializer apiSerializer,
|
||||
PythonWorkflowDefinitionAuthorizationService pythonAuthorizationService,
|
||||
WorkflowDefinitionScriptAuthorizationService scriptAuthorizationService,
|
||||
HttpContext httpContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
|
|
@ -31,10 +31,10 @@ internal static class WorkflowExecutionHelper
|
|||
return;
|
||||
}
|
||||
|
||||
var pythonAuthorizationResult = await pythonAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, httpContext.User, cancellationToken);
|
||||
if (pythonAuthorizationResult != PythonWorkflowDefinitionAuthorizationResult.Allowed)
|
||||
var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, httpContext.User, cancellationToken);
|
||||
if (!scriptAuthorizationResult.Succeeded)
|
||||
{
|
||||
await SendPythonAuthorizationFailureAsync(httpContext, pythonAuthorizationResult, cancellationToken);
|
||||
await SendScriptAuthorizationFailureAsync(httpContext, scriptAuthorizationResult, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -97,15 +97,15 @@ internal static class WorkflowExecutionHelper
|
|||
await httpContext.Response.WriteAsync(faultedResponse, cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task SendPythonAuthorizationFailureAsync(HttpContext httpContext, PythonWorkflowDefinitionAuthorizationResult result, CancellationToken cancellationToken)
|
||||
private static async Task SendScriptAuthorizationFailureAsync(HttpContext httpContext, WorkflowDefinitionScriptAuthorizationResult result, CancellationToken cancellationToken)
|
||||
{
|
||||
if (result == PythonWorkflowDefinitionAuthorizationResult.MissingPermission)
|
||||
if (result.FailureReason == WorkflowDefinitionScriptAuthorizationFailureReason.MissingPermission)
|
||||
{
|
||||
await httpContext.Response.SendForbiddenAsync(cancellation: cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
await httpContext.Response.WriteAsync(PythonWorkflowDefinitionAuthorizationService.HostDisabledMessage, cancellationToken);
|
||||
await httpContext.Response.WriteAsync(result.Message ?? "Workflow script authorization failed.", cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ internal class Import : ElsaEndpoint<WorkflowDefinitionModel>
|
|||
private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter;
|
||||
private readonly IWorkflowDefinitionLinker _linker;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly PythonWorkflowDefinitionAuthorizationService _pythonAuthorizationService;
|
||||
private readonly WorkflowDefinitionScriptAuthorizationService _scriptAuthorizationService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Import(
|
||||
|
|
@ -25,13 +25,13 @@ internal class Import : ElsaEndpoint<WorkflowDefinitionModel>
|
|||
IWorkflowDefinitionImporter workflowDefinitionImporter,
|
||||
IWorkflowDefinitionLinker linker,
|
||||
IAuthorizationService authorizationService,
|
||||
PythonWorkflowDefinitionAuthorizationService pythonAuthorizationService)
|
||||
WorkflowDefinitionScriptAuthorizationService scriptAuthorizationService)
|
||||
{
|
||||
_workflowDefinitionStore = workflowDefinitionStore;
|
||||
_workflowDefinitionImporter = workflowDefinitionImporter;
|
||||
_linker = linker;
|
||||
_authorizationService = authorizationService;
|
||||
_pythonAuthorizationService = pythonAuthorizationService;
|
||||
_scriptAuthorizationService = scriptAuthorizationService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -48,10 +48,10 @@ internal class Import : ElsaEndpoint<WorkflowDefinitionModel>
|
|||
var definitionId = model.DefinitionId;
|
||||
var isNew = string.IsNullOrWhiteSpace(definitionId);
|
||||
|
||||
var pythonAuthorizationResult = await _pythonAuthorizationService.AuthorizeAsync(model, User, cancellationToken);
|
||||
if (pythonAuthorizationResult != PythonWorkflowDefinitionAuthorizationResult.Allowed)
|
||||
var scriptAuthorizationResult = await _scriptAuthorizationService.AuthorizeAsync(model, User, cancellationToken);
|
||||
if (!scriptAuthorizationResult.Succeeded)
|
||||
{
|
||||
await PythonWorkflowDefinitionAuthorizationFailure.SendAsync(pythonAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ internal class ImportFiles : ElsaEndpoint<WorkflowDefinitionModel>
|
|||
private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter;
|
||||
private readonly IApiSerializer _apiSerializer;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly PythonWorkflowDefinitionAuthorizationService _pythonAuthorizationService;
|
||||
private readonly WorkflowDefinitionScriptAuthorizationService _scriptAuthorizationService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImportFiles(
|
||||
|
|
@ -26,13 +26,13 @@ internal class ImportFiles : ElsaEndpoint<WorkflowDefinitionModel>
|
|||
IWorkflowDefinitionImporter workflowDefinitionImporter,
|
||||
IApiSerializer apiSerializer,
|
||||
IAuthorizationService authorizationService,
|
||||
PythonWorkflowDefinitionAuthorizationService pythonAuthorizationService)
|
||||
WorkflowDefinitionScriptAuthorizationService scriptAuthorizationService)
|
||||
{
|
||||
_workflowDefinitionStore = workflowDefinitionStore;
|
||||
_workflowDefinitionImporter = workflowDefinitionImporter;
|
||||
_apiSerializer = apiSerializer;
|
||||
_authorizationService = authorizationService;
|
||||
_pythonAuthorizationService = pythonAuthorizationService;
|
||||
_scriptAuthorizationService = scriptAuthorizationService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -80,10 +80,10 @@ internal class ImportFiles : ElsaEndpoint<WorkflowDefinitionModel>
|
|||
{
|
||||
foreach (var model in models)
|
||||
{
|
||||
var pythonAuthorizationResult = await _pythonAuthorizationService.AuthorizeAsync(model, User, cancellationToken);
|
||||
if (pythonAuthorizationResult != PythonWorkflowDefinitionAuthorizationResult.Allowed)
|
||||
var scriptAuthorizationResult = await _scriptAuthorizationService.AuthorizeAsync(model, User, cancellationToken);
|
||||
if (!scriptAuthorizationResult.Succeeded)
|
||||
{
|
||||
await PythonWorkflowDefinitionAuthorizationFailure.SendAsync(pythonAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ internal class Post(
|
|||
IDistributedLockProvider distributedLockProvider,
|
||||
IWorkflowDefinitionLinker linker,
|
||||
IAuthorizationService authorizationService,
|
||||
PythonWorkflowDefinitionAuthorizationService pythonAuthorizationService)
|
||||
WorkflowDefinitionScriptAuthorizationService scriptAuthorizationService)
|
||||
: ElsaEndpoint<SaveWorkflowDefinitionRequest, LinkedWorkflowDefinitionModel>
|
||||
{
|
||||
public override void Configure()
|
||||
|
|
@ -66,10 +66,10 @@ internal class Post(
|
|||
return;
|
||||
}
|
||||
|
||||
var pythonAuthorizationResult = await pythonAuthorizationService.AuthorizeAsync(model, User, cancellationToken);
|
||||
if (pythonAuthorizationResult != PythonWorkflowDefinitionAuthorizationResult.Allowed)
|
||||
var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(model, User, cancellationToken);
|
||||
if (!scriptAuthorizationResult.Succeeded)
|
||||
{
|
||||
await PythonWorkflowDefinitionAuthorizationFailure.SendAsync(pythonAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ internal class Publish(
|
|||
IWorkflowDefinitionLinker linker,
|
||||
IAuthorizationService authorizationService,
|
||||
IWorkflowDefinitionService workflowDefinitionService,
|
||||
PythonWorkflowDefinitionAuthorizationService pythonAuthorizationService)
|
||||
WorkflowDefinitionScriptAuthorizationService scriptAuthorizationService)
|
||||
: ElsaEndpoint<Request, Response>
|
||||
{
|
||||
public override void Configure()
|
||||
|
|
@ -51,10 +51,10 @@ internal class Publish(
|
|||
}
|
||||
|
||||
var workflowGraph = await workflowDefinitionService.MaterializeWorkflowAsync(definition, cancellationToken);
|
||||
var pythonAuthorizationResult = await pythonAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, User, cancellationToken);
|
||||
if (pythonAuthorizationResult != PythonWorkflowDefinitionAuthorizationResult.Allowed)
|
||||
var scriptAuthorizationResult = await scriptAuthorizationService.AuthorizeAsync(workflowGraph.Workflow, User, cancellationToken);
|
||||
if (!scriptAuthorizationResult.Succeeded)
|
||||
{
|
||||
await PythonWorkflowDefinitionAuthorizationFailure.SendAsync(pythonAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
await WorkflowDefinitionScriptAuthorizationFailure.SendAsync(scriptAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ public class WorkflowsApiFeature(IModule module) : FeatureBase(module)
|
|||
Module.AddFastEndpointsFromModule();
|
||||
|
||||
Services.AddScoped<IWorkflowDefinitionLinker, StaticWorkflowDefinitionLinker>();
|
||||
Services.AddScoped<PythonWorkflowDefinitionAuthorizationService>();
|
||||
Services.AddScoped<WorkflowDefinitionScriptAuthorizationService>();
|
||||
Services.AddScoped<IAuthorizationHandler, NotReadOnlyRequirementHandler>();
|
||||
Services.Configure<AuthorizationOptions>(options =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
using System.Security.Claims;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
|
||||
namespace Elsa.Workflows.Api.Security;
|
||||
|
||||
internal class PythonWorkflowDefinitionAuthorizationService(
|
||||
IActivityVisitor activityVisitor,
|
||||
IExpressionDescriptorRegistry expressionDescriptorRegistry)
|
||||
{
|
||||
public const string HostDisabledMessage = "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.";
|
||||
private const string PythonExpressionType = "Python";
|
||||
// Keep in sync with ActivityTypeNameHelper.GenerateTypeName<Elsa.Expressions.Python.Activities.RunPython>().
|
||||
private const string RunPythonActivityType = "Elsa.RunPython";
|
||||
private const string PermissionsClaimType = "permissions";
|
||||
|
||||
public async Task<PythonWorkflowDefinitionAuthorizationResult> AuthorizeAsync(WorkflowDefinitionModel model, ClaimsPrincipal user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (model.Root == null || !await UsesPythonAsync(model.Root, cancellationToken))
|
||||
return PythonWorkflowDefinitionAuthorizationResult.Allowed;
|
||||
|
||||
return AuthorizePythonUsage(user);
|
||||
}
|
||||
|
||||
public async Task<PythonWorkflowDefinitionAuthorizationResult> AuthorizeAsync(Workflow workflow, ClaimsPrincipal user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!await UsesPythonAsync(workflow, cancellationToken))
|
||||
return PythonWorkflowDefinitionAuthorizationResult.Allowed;
|
||||
|
||||
return AuthorizePythonUsage(user);
|
||||
}
|
||||
|
||||
private PythonWorkflowDefinitionAuthorizationResult AuthorizePythonUsage(ClaimsPrincipal user)
|
||||
{
|
||||
// PythonOptions lives in the optional Python module. Workflows.Api observes the descriptor state projected by that module's provider.
|
||||
if (expressionDescriptorRegistry.Find(PythonExpressionType)?.IsBrowsable != true)
|
||||
return PythonWorkflowDefinitionAuthorizationResult.HostDisabled;
|
||||
|
||||
return HasPermission(user, PermissionNames.ExecutePythonExpressions)
|
||||
? PythonWorkflowDefinitionAuthorizationResult.Allowed
|
||||
: PythonWorkflowDefinitionAuthorizationResult.MissingPermission;
|
||||
}
|
||||
|
||||
private async Task<bool> UsesPythonAsync(IActivity root, CancellationToken cancellationToken)
|
||||
{
|
||||
var graph = await activityVisitor.VisitAsync(root, cancellationToken);
|
||||
var nodes = new[] { graph }.Concat(graph.Descendants());
|
||||
|
||||
return nodes.Any(x => IsRunPythonActivity(x.Activity) || HasPythonExpression(x.Activity));
|
||||
}
|
||||
|
||||
private static bool IsRunPythonActivity(IActivity activity)
|
||||
{
|
||||
return string.Equals(activity.Type, RunPythonActivityType, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool HasPythonExpression(IActivity activity)
|
||||
{
|
||||
return activity.GetInputs().Any(x => string.Equals(x.Expression?.Type, PythonExpressionType, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static bool HasPermission(ClaimsPrincipal user, string permission)
|
||||
{
|
||||
return user.Claims.Any(x =>
|
||||
x.Type == PermissionsClaimType &&
|
||||
(string.Equals(x.Value, PermissionNames.All, StringComparison.Ordinal) ||
|
||||
string.Equals(x.Value, permission, StringComparison.Ordinal)));
|
||||
}
|
||||
}
|
||||
|
||||
internal enum PythonWorkflowDefinitionAuthorizationResult
|
||||
{
|
||||
Allowed,
|
||||
HostDisabled,
|
||||
MissingPermission
|
||||
}
|
||||
|
|
@ -1,21 +1,21 @@
|
|||
namespace Elsa.Workflows.Api.Security;
|
||||
|
||||
internal static class PythonWorkflowDefinitionAuthorizationFailure
|
||||
internal static class WorkflowDefinitionScriptAuthorizationFailure
|
||||
{
|
||||
public static async Task SendAsync(
|
||||
PythonWorkflowDefinitionAuthorizationResult result,
|
||||
WorkflowDefinitionScriptAuthorizationResult result,
|
||||
Func<CancellationToken, Task> sendForbiddenAsync,
|
||||
Action<string> addError,
|
||||
Func<int, CancellationToken, Task> sendErrorsAsync,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (result == PythonWorkflowDefinitionAuthorizationResult.MissingPermission)
|
||||
if (result.FailureReason == WorkflowDefinitionScriptAuthorizationFailureReason.MissingPermission)
|
||||
{
|
||||
await sendForbiddenAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
addError(PythonWorkflowDefinitionAuthorizationService.HostDisabledMessage);
|
||||
addError(result.Message ?? "Workflow script authorization failed.");
|
||||
await sendErrorsAsync(400, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
using System.Security.Claims;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
|
||||
namespace Elsa.Workflows.Api.Security;
|
||||
|
||||
internal class WorkflowDefinitionScriptAuthorizationService(
|
||||
IActivityVisitor activityVisitor,
|
||||
IExpressionDescriptorRegistry expressionDescriptorRegistry)
|
||||
{
|
||||
private static readonly ScriptPolicy[] ScriptPolicies =
|
||||
[
|
||||
new(
|
||||
"CSharp",
|
||||
WorkflowScriptActivityTypeNames.RunCSharp,
|
||||
PermissionNames.ExecuteCSharpExpressions,
|
||||
"C# workflow expression execution is disabled by the host. Set CSharpOptions.AllowHostCodeExecution to true only for trusted workflow authors; Roslyn scripting is not a sandbox."),
|
||||
new(
|
||||
"Python",
|
||||
WorkflowScriptActivityTypeNames.RunPython,
|
||||
PermissionNames.ExecutePythonExpressions,
|
||||
"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<WorkflowDefinitionScriptAuthorizationResult> AuthorizeAsync(WorkflowDefinitionModel model, ClaimsPrincipal user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (model.Root == null)
|
||||
return WorkflowDefinitionScriptAuthorizationResult.Allowed();
|
||||
|
||||
return await AuthorizeAsync(model.Root, user, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<WorkflowDefinitionScriptAuthorizationResult> AuthorizeAsync(IActivity root, ClaimsPrincipal user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var scriptUsages = await GetUsedScriptPoliciesAsync(root, cancellationToken);
|
||||
|
||||
var failure = scriptUsages
|
||||
.Select(policy => AuthorizeScriptUsage(policy, user))
|
||||
.FirstOrDefault(result => result is { Succeeded: false });
|
||||
|
||||
if (failure.FailureReason.HasValue)
|
||||
return failure;
|
||||
|
||||
return WorkflowDefinitionScriptAuthorizationResult.Allowed();
|
||||
}
|
||||
|
||||
public async Task<WorkflowDefinitionScriptAuthorizationResult> AuthorizeAsync(Workflow workflow, ClaimsPrincipal user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await AuthorizeAsync((IActivity)workflow, user, cancellationToken);
|
||||
}
|
||||
|
||||
private WorkflowDefinitionScriptAuthorizationResult AuthorizeScriptUsage(ScriptPolicy policy, ClaimsPrincipal user)
|
||||
{
|
||||
// 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);
|
||||
|
||||
return HasPermission(user, policy.Permission)
|
||||
? WorkflowDefinitionScriptAuthorizationResult.Allowed()
|
||||
: WorkflowDefinitionScriptAuthorizationResult.MissingPermission();
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<ScriptPolicy>> GetUsedScriptPoliciesAsync(IActivity root, CancellationToken cancellationToken)
|
||||
{
|
||||
var graph = await activityVisitor.VisitAsync(root, cancellationToken);
|
||||
var nodes = new[] { graph }.Concat(graph.Descendants()).ToList();
|
||||
var policies = ScriptPolicies
|
||||
.Where(policy => nodes.Any(x => IsRunActivity(x.Activity, policy) || HasExpression(x.Activity, policy)))
|
||||
.ToList();
|
||||
|
||||
return policies;
|
||||
}
|
||||
|
||||
private static bool IsRunActivity(IActivity activity, ScriptPolicy policy) =>
|
||||
string.Equals(activity.Type, policy.RunActivityType, StringComparison.Ordinal);
|
||||
|
||||
private static bool HasExpression(IActivity activity, ScriptPolicy policy) =>
|
||||
activity.GetInputs().Any(x => string.Equals(x.Expression?.Type, policy.ExpressionType, StringComparison.Ordinal));
|
||||
|
||||
private static bool HasPermission(ClaimsPrincipal user, string permission)
|
||||
{
|
||||
return user.Claims.Any(x =>
|
||||
x.Type == PermissionNames.ClaimType &&
|
||||
(string.Equals(x.Value, PermissionNames.All, StringComparison.Ordinal) ||
|
||||
string.Equals(x.Value, permission, StringComparison.Ordinal)));
|
||||
}
|
||||
|
||||
private sealed record ScriptPolicy(string ExpressionType, string RunActivityType, string Permission, string HostDisabledMessage);
|
||||
}
|
||||
|
||||
internal readonly record struct WorkflowDefinitionScriptAuthorizationResult(bool Succeeded, WorkflowDefinitionScriptAuthorizationFailureReason? FailureReason, string? Message)
|
||||
{
|
||||
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
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ public class WorkflowsApiFeature : IFastEndpointsShellFeature
|
|||
{
|
||||
services.AddSerializationOptionsConfigurator<SerializationConfigurator>();
|
||||
services.AddScoped<IWorkflowDefinitionLinker, StaticWorkflowDefinitionLinker>();
|
||||
services.AddScoped<PythonWorkflowDefinitionAuthorizationService>();
|
||||
services.AddScoped<WorkflowDefinitionScriptAuthorizationService>();
|
||||
services.AddScoped<IAuthorizationHandler, NotReadOnlyRequirementHandler>();
|
||||
services.Configure<AuthorizationOptions>(options =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
namespace Elsa.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Activity type names for built-in workflow script activities.
|
||||
/// </summary>
|
||||
public static class WorkflowScriptActivityTypeNames
|
||||
{
|
||||
/// <summary>
|
||||
/// The Elsa activity namespace for built-in script activities.
|
||||
/// </summary>
|
||||
public const string Namespace = "Elsa";
|
||||
|
||||
/// <summary>
|
||||
/// The unqualified C# script activity type.
|
||||
/// </summary>
|
||||
public const string RunCSharpType = "RunCSharp";
|
||||
|
||||
/// <summary>
|
||||
/// The fully qualified C# script activity type name.
|
||||
/// </summary>
|
||||
public const string RunCSharp = $"{Namespace}.{RunCSharpType}";
|
||||
|
||||
/// <summary>
|
||||
/// The unqualified Python script activity type.
|
||||
/// </summary>
|
||||
public const string RunPythonType = "RunPython";
|
||||
|
||||
/// <summary>
|
||||
/// The fully qualified Python script activity type name.
|
||||
/// </summary>
|
||||
public const string RunPython = $"{Namespace}.{RunPythonType}";
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
using System.Security.Claims;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Api.Security;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.PortResolvers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Security;
|
||||
|
||||
public class PythonWorkflowDefinitionAuthorizationServiceTests
|
||||
{
|
||||
private static readonly ClaimsPrincipal UserWithPythonPermission = CreateUser(PermissionNames.ExecutePythonExpressions);
|
||||
private static readonly ClaimsPrincipal UserWithoutPythonPermission = CreateUser("write:workflow-definitions");
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizeAsync_BlocksPythonExpression_WhenHostHasNotOptedIn()
|
||||
{
|
||||
var service = CreateService(hostAllowsPython: false);
|
||||
var model = CreateModelWithPythonExpression();
|
||||
|
||||
var result = await service.AuthorizeAsync(model, UserWithPythonPermission);
|
||||
|
||||
Assert.Equal(PythonWorkflowDefinitionAuthorizationResult.HostDisabled, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizeAsync_BlocksPythonExpression_WhenUserLacksPermission()
|
||||
{
|
||||
var service = CreateService(hostAllowsPython: true);
|
||||
var model = CreateModelWithPythonExpression();
|
||||
|
||||
var result = await service.AuthorizeAsync(model, UserWithoutPythonPermission);
|
||||
|
||||
Assert.Equal(PythonWorkflowDefinitionAuthorizationResult.MissingPermission, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizeAsync_AllowsPythonExpression_WhenHostAndUserAllowIt()
|
||||
{
|
||||
var service = CreateService(hostAllowsPython: true);
|
||||
var model = CreateModelWithPythonExpression();
|
||||
|
||||
var result = await service.AuthorizeAsync(model, UserWithPythonPermission);
|
||||
|
||||
Assert.Equal(PythonWorkflowDefinitionAuthorizationResult.Allowed, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizeAsync_TreatsRunPythonActivityAsPythonUsage()
|
||||
{
|
||||
var service = CreateService(hostAllowsPython: true);
|
||||
var model = new WorkflowDefinitionModel
|
||||
{
|
||||
Root = new WriteLine("hello")
|
||||
{
|
||||
Type = "Elsa.RunPython"
|
||||
}
|
||||
};
|
||||
|
||||
var result = await service.AuthorizeAsync(model, UserWithoutPythonPermission);
|
||||
|
||||
Assert.Equal(PythonWorkflowDefinitionAuthorizationResult.MissingPermission, result);
|
||||
}
|
||||
|
||||
private static WorkflowDefinitionModel CreateModelWithPythonExpression()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Root = new WriteLine("placeholder")
|
||||
{
|
||||
Text = new Input<string>(new Expression("Python", "'hello'"))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static PythonWorkflowDefinitionAuthorizationService CreateService(bool hostAllowsPython)
|
||||
{
|
||||
var expressionDescriptor = new ExpressionDescriptor
|
||||
{
|
||||
Type = "Python",
|
||||
DisplayName = "Python",
|
||||
IsBrowsable = hostAllowsPython,
|
||||
HandlerFactory = _ => Substitute.For<IExpressionHandler>()
|
||||
};
|
||||
|
||||
var provider = Substitute.For<IExpressionDescriptorProvider>();
|
||||
provider.GetDescriptors().Returns([expressionDescriptor]);
|
||||
|
||||
var registry = new ExpressionDescriptorRegistry([provider]);
|
||||
var visitor = new ActivityVisitor(
|
||||
[
|
||||
new SwitchActivityResolver(),
|
||||
new PropertyBasedActivityResolver()
|
||||
],
|
||||
new ServiceCollection().BuildServiceProvider());
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
using System.Security.Claims;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Api.Security;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.PortResolvers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
|
||||
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("write:workflow-definitions");
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizeAsync_BlocksCSharpExpression_WhenHostHasNotOptedIn()
|
||||
{
|
||||
var service = CreateService(hostAllowsCSharp: false, hostAllowsPython: true);
|
||||
var model = CreateModelWithCSharpExpression();
|
||||
|
||||
var result = await service.AuthorizeAsync(model, UserWithCSharpPermission);
|
||||
|
||||
Assert.Equal(WorkflowDefinitionScriptAuthorizationFailureReason.HostDisabled, result.FailureReason);
|
||||
Assert.Contains("CSharpOptions.AllowHostCodeExecution", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizeAsync_BlocksCSharpExpression_WhenUserLacksPermission()
|
||||
{
|
||||
var service = CreateService(hostAllowsCSharp: true, hostAllowsPython: true);
|
||||
var model = CreateModelWithCSharpExpression();
|
||||
|
||||
var result = await service.AuthorizeAsync(model, UserWithoutScriptPermission);
|
||||
|
||||
Assert.Equal(WorkflowDefinitionScriptAuthorizationFailureReason.MissingPermission, 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()
|
||||
{
|
||||
var service = CreateService(hostAllowsCSharp: true, hostAllowsPython: true);
|
||||
var model = new WorkflowDefinitionModel
|
||||
{
|
||||
Root = new WriteLine("hello")
|
||||
};
|
||||
|
||||
var result = await service.AuthorizeAsync(model, UserWithoutScriptPermission);
|
||||
|
||||
Assert.True(result.Succeeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizeAsync_TreatsRunCSharpActivityAsCSharpUsage()
|
||||
{
|
||||
var service = CreateService(hostAllowsCSharp: true, hostAllowsPython: true);
|
||||
var model = new WorkflowDefinitionModel
|
||||
{
|
||||
Root = new WriteLine("hello")
|
||||
{
|
||||
Type = WorkflowScriptActivityTypeNames.RunCSharp
|
||||
}
|
||||
};
|
||||
|
||||
var result = await service.AuthorizeAsync(model, UserWithoutScriptPermission);
|
||||
|
||||
Assert.Equal(WorkflowDefinitionScriptAuthorizationFailureReason.MissingPermission, result.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizeAsync_BlocksPythonExpression_WhenHostHasNotOptedIn()
|
||||
{
|
||||
var service = CreateService(hostAllowsCSharp: true, hostAllowsPython: false);
|
||||
var model = CreateModelWithPythonExpression();
|
||||
|
||||
var result = await service.AuthorizeAsync(model, UserWithPythonPermission);
|
||||
|
||||
Assert.Equal(WorkflowDefinitionScriptAuthorizationFailureReason.HostDisabled, result.FailureReason);
|
||||
Assert.Contains("PythonOptions.AllowHostCodeExecution", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizeAsync_BlocksPythonExpression_WhenUserLacksPermission()
|
||||
{
|
||||
var service = CreateService(hostAllowsCSharp: true, hostAllowsPython: true);
|
||||
var model = CreateModelWithPythonExpression();
|
||||
|
||||
var result = await service.AuthorizeAsync(model, UserWithoutScriptPermission);
|
||||
|
||||
Assert.Equal(WorkflowDefinitionScriptAuthorizationFailureReason.MissingPermission, 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()
|
||||
{
|
||||
var service = CreateService(hostAllowsCSharp: true, hostAllowsPython: true);
|
||||
var model = new WorkflowDefinitionModel
|
||||
{
|
||||
Root = new WriteLine("hello")
|
||||
{
|
||||
Type = WorkflowScriptActivityTypeNames.RunPython
|
||||
}
|
||||
};
|
||||
|
||||
var result = await service.AuthorizeAsync(model, UserWithoutScriptPermission);
|
||||
|
||||
Assert.Equal(WorkflowDefinitionScriptAuthorizationFailureReason.MissingPermission, result.FailureReason);
|
||||
}
|
||||
|
||||
private static WorkflowDefinitionModel CreateModelWithCSharpExpression()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Root = new WriteLine("placeholder")
|
||||
{
|
||||
Text = new Input<string>(new Expression("CSharp", "\"hello\""))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static WorkflowDefinitionModel CreateModelWithPythonExpression()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Root = new WriteLine("placeholder")
|
||||
{
|
||||
Text = new Input<string>(new Expression("Python", "'hello'"))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static WorkflowDefinitionScriptAuthorizationService CreateService(bool hostAllowsCSharp, bool hostAllowsPython)
|
||||
{
|
||||
var expressionDescriptors = new[]
|
||||
{
|
||||
new ExpressionDescriptor
|
||||
{
|
||||
Type = "CSharp",
|
||||
DisplayName = "C#",
|
||||
IsBrowsable = hostAllowsCSharp,
|
||||
HandlerFactory = _ => Substitute.For<IExpressionHandler>()
|
||||
},
|
||||
new ExpressionDescriptor
|
||||
{
|
||||
Type = "Python",
|
||||
DisplayName = "Python",
|
||||
IsBrowsable = hostAllowsPython,
|
||||
HandlerFactory = _ => Substitute.For<IExpressionHandler>()
|
||||
}
|
||||
};
|
||||
|
||||
var provider = Substitute.For<IExpressionDescriptorProvider>();
|
||||
provider.GetDescriptors().Returns(expressionDescriptors);
|
||||
|
||||
var registry = new ExpressionDescriptorRegistry([provider]);
|
||||
var visitor = new ActivityVisitor(
|
||||
[
|
||||
new SwitchActivityResolver(),
|
||||
new PropertyBasedActivityResolver()
|
||||
],
|
||||
new ServiceCollection().BuildServiceProvider());
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.CSharp.Contracts;
|
||||
using Elsa.Expressions.CSharp.Options;
|
||||
using Elsa.Expressions.CSharp.Services;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Elsa.Expressions.UnitTests.CSharp;
|
||||
|
||||
public class CSharpHostCodeExecutionTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Evaluator_BlocksExecution_WhenHostHasNotOptedIn()
|
||||
{
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var evaluator = new CSharpEvaluator(
|
||||
Substitute.For<Elsa.Mediator.Contracts.INotificationSender>(),
|
||||
Microsoft.Extensions.Options.Options.Create(new CSharpOptions()),
|
||||
memoryCache);
|
||||
var context = await new ActivityTestFixture(new WriteLine("test")).BuildAsync();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
evaluator.EvaluateAsync("\"hello\"", typeof(string), context.ExpressionExecutionContext, new ExpressionEvaluatorOptions()));
|
||||
|
||||
Assert.Contains(nameof(CSharpOptions.AllowHostCodeExecution), exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void Descriptor_Browsability_FollowsHostOptIn(bool allowHostCodeExecution)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddOptions();
|
||||
services.AddMemoryCache();
|
||||
new Elsa.Expressions.CSharp.ShellFeatures.CSharpFeature
|
||||
{
|
||||
CSharpOptions = options => options.AllowHostCodeExecution = allowHostCodeExecution
|
||||
}.ConfigureServices(services);
|
||||
services.AddSingleton<IExpressionDescriptorRegistry, Elsa.Workflows.Management.Services.ExpressionDescriptorRegistry>();
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
var registry = serviceProvider.GetRequiredService<IExpressionDescriptorRegistry>();
|
||||
|
||||
var descriptor = registry.Find("CSharp");
|
||||
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal(allowHostCodeExecution, descriptor.IsBrowsable);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue