Merge remote-tracking branch 'origin/release/3.8.0' into release/3.8.0

This commit is contained in:
Sipke Schoorstra 2026-06-18 18:27:36 +02:00
commit f74087ea8b
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
5 changed files with 97 additions and 10 deletions

View file

@ -123,10 +123,10 @@ See [Program.cs](../../src/apps/Elsa.Server.Web/Program.cs).
With default route prefix `elsa/api`, runtime admin endpoints include:
- `GET /elsa/api/admin/workflow-runtime/status`
- `POST /elsa/api/admin/workflow-runtime/pause`
- `POST /elsa/api/admin/workflow-runtime/resume`
- `POST /elsa/api/admin/workflow-runtime/force-drain`
- `GET /elsa/api/admin/workflow-runtime/status`: requires `read:workflow-runtime`; `ManageWorkflowRuntime` is also accepted for backward compatibility.
- `POST /elsa/api/admin/workflow-runtime/pause`: requires `ManageWorkflowRuntime`.
- `POST /elsa/api/admin/workflow-runtime/resume`: requires `ManageWorkflowRuntime`.
- `POST /elsa/api/admin/workflow-runtime/force-drain`: requires `ManageWorkflowRuntime`.
Structured log diagnostics endpoints include:

View file

@ -159,10 +159,10 @@ Ingress source adapters are currently registered by modules such as HTTP and Sch
The workflow API includes runtime admin endpoints:
- `GET /elsa/api/admin/workflow-runtime/status`
- `POST /elsa/api/admin/workflow-runtime/pause`
- `POST /elsa/api/admin/workflow-runtime/resume`
- `POST /elsa/api/admin/workflow-runtime/force-drain`
- `GET /elsa/api/admin/workflow-runtime/status`: requires `read:workflow-runtime`; `ManageWorkflowRuntime` is also accepted for backward compatibility.
- `POST /elsa/api/admin/workflow-runtime/pause`: requires `ManageWorkflowRuntime`.
- `POST /elsa/api/admin/workflow-runtime/resume`: requires `ManageWorkflowRuntime`.
- `POST /elsa/api/admin/workflow-runtime/force-drain`: requires `ManageWorkflowRuntime`.
Endpoint code lives under [Elsa.Workflows.Api/Endpoints/RuntimeAdmin](../../src/modules/Elsa.Workflows.Api/Endpoints/RuntimeAdmin). The service behind these endpoints is [WorkflowRuntimeAdminService](../../src/modules/Elsa.Workflows.Runtime/Services/WorkflowRuntimeAdminService.cs).

View file

@ -16,10 +16,15 @@ public static class PermissionNames
public const string ExecutePythonExpressions = "exec:python-expressions";
/// <summary>
/// Permission required to pause, resume, force-drain, or query the workflow runtime's graceful-shutdown status.
/// Permission required to pause, resume, or force-drain the workflow runtime.
/// </summary>
public const string ManageWorkflowRuntime = "ManageWorkflowRuntime";
/// <summary>
/// Permission required to query workflow runtime status.
/// </summary>
public const string ReadWorkflowRuntime = "read:workflow-runtime";
/// <summary>
/// Permission required to list or inspect bookmark queue dead-letter items.
/// </summary>

View file

@ -15,7 +15,7 @@ internal sealed class StatusEndpoint(IWorkflowRuntimeAdminService admin) : ElsaE
public override void Configure()
{
Get("/admin/workflow-runtime/status");
ConfigurePermissions(PermissionNames.ManageWorkflowRuntime);
ConfigurePermissions(PermissionNames.ReadWorkflowRuntime, PermissionNames.ManageWorkflowRuntime);
}
public override async Task HandleAsync(CancellationToken ct)

View file

@ -0,0 +1,82 @@
using System.Reflection;
using Elsa.Workflows.Runtime;
using FastEndpoints;
using NSubstitute;
using WorkflowsApiFeature = Elsa.Workflows.Api.Features.WorkflowsApiFeature;
namespace Elsa.Workflows.Api.UnitTests.Endpoints.RuntimeAdmin;
public class RuntimeAdminAuthorizationTests
{
[Fact]
public void StatusEndpoint_AllowsReadWorkflowRuntimePermission()
{
var permissions = GetConfiguredPermissions("Elsa.Workflows.Api.Endpoints.RuntimeAdmin.Status.StatusEndpoint");
Assert.Contains(PermissionNames.All, permissions);
Assert.Contains(PermissionNames.ReadWorkflowRuntime, permissions);
Assert.Contains(PermissionNames.ManageWorkflowRuntime, permissions);
}
[Theory]
[InlineData("Elsa.Workflows.Api.Endpoints.RuntimeAdmin.Pause.PauseEndpoint")]
[InlineData("Elsa.Workflows.Api.Endpoints.RuntimeAdmin.Resume.ResumeEndpoint")]
[InlineData("Elsa.Workflows.Api.Endpoints.RuntimeAdmin.ForceDrain.ForceDrainEndpoint")]
public void MutatingEndpoints_RequireManageWorkflowRuntimePermission(string endpointTypeName)
{
var permissions = GetConfiguredPermissions(endpointTypeName);
Assert.Contains(PermissionNames.ManageWorkflowRuntime, permissions);
Assert.DoesNotContain(PermissionNames.ReadWorkflowRuntime, permissions);
}
private static IReadOnlyCollection<string> GetConfiguredPermissions(string endpointTypeName)
{
var endpointType = typeof(WorkflowsApiFeature).Assembly.GetType(endpointTypeName, throwOnError: true)!;
var endpoint = Activator.CreateInstance(
endpointType,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
null,
[Substitute.For<IWorkflowRuntimeAdminService>()],
null)!;
var (requestDtoType, responseDtoType) = GetEndpointDtoTypes(endpointType);
var definition = new EndpointDefinition(endpointType, requestDtoType, responseDtoType);
endpointType
.GetProperty("Definition", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!
.SetValue(endpoint, definition);
endpointType.GetMethod("Configure")!.Invoke(endpoint, null);
var permissions = definition
.GetType()
.GetProperty("AllowedPermissions", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!
.GetValue(definition);
return Assert.IsAssignableFrom<IEnumerable<string>>(permissions).ToArray();
}
private static (Type RequestDtoType, Type ResponseDtoType) GetEndpointDtoTypes(Type endpointType)
{
var type = endpointType;
while (type.BaseType != null)
{
type = type.BaseType;
if (!type.IsGenericType)
continue;
var genericTypeDefinition = type.GetGenericTypeDefinition();
var genericArguments = type.GetGenericArguments();
if (genericTypeDefinition == typeof(Abstractions.ElsaEndpoint<,>))
return (genericArguments[0], genericArguments[1]);
if (genericTypeDefinition == typeof(Abstractions.ElsaEndpointWithoutRequest<>))
return (typeof(EmptyRequest), genericArguments[0]);
}
throw new InvalidOperationException($"Unsupported endpoint type '{endpointType.FullName}'.");
}
}