diff --git a/doc/wiki/build-run-operate.md b/doc/wiki/build-run-operate.md
index 99aabe337..90ba5214e 100644
--- a/doc/wiki/build-run-operate.md
+++ b/doc/wiki/build-run-operate.md
@@ -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:
diff --git a/doc/wiki/workflow-runtime.md b/doc/wiki/workflow-runtime.md
index 33e7f4a6c..d3d539fc3 100644
--- a/doc/wiki/workflow-runtime.md
+++ b/doc/wiki/workflow-runtime.md
@@ -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).
diff --git a/src/common/Elsa.Api.Common/PermissionNames.cs b/src/common/Elsa.Api.Common/PermissionNames.cs
index c56266d90..951010aa9 100644
--- a/src/common/Elsa.Api.Common/PermissionNames.cs
+++ b/src/common/Elsa.Api.Common/PermissionNames.cs
@@ -16,10 +16,15 @@ public static class PermissionNames
public const string ExecutePythonExpressions = "exec:python-expressions";
///
- /// 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.
///
public const string ManageWorkflowRuntime = "ManageWorkflowRuntime";
+ ///
+ /// Permission required to query workflow runtime status.
+ ///
+ public const string ReadWorkflowRuntime = "read:workflow-runtime";
+
///
/// Permission required to list or inspect bookmark queue dead-letter items.
///
diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/RuntimeAdmin/Status/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/RuntimeAdmin/Status/Endpoint.cs
index 4cd740836..5667447f7 100644
--- a/src/modules/Elsa.Workflows.Api/Endpoints/RuntimeAdmin/Status/Endpoint.cs
+++ b/src/modules/Elsa.Workflows.Api/Endpoints/RuntimeAdmin/Status/Endpoint.cs
@@ -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)
diff --git a/test/unit/Elsa.Workflows.Api.UnitTests/Endpoints/RuntimeAdmin/RuntimeAdminAuthorizationTests.cs b/test/unit/Elsa.Workflows.Api.UnitTests/Endpoints/RuntimeAdmin/RuntimeAdminAuthorizationTests.cs
new file mode 100644
index 000000000..aceb4b08c
--- /dev/null
+++ b/test/unit/Elsa.Workflows.Api.UnitTests/Endpoints/RuntimeAdmin/RuntimeAdminAuthorizationTests.cs
@@ -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 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()],
+ 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>(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}'.");
+ }
+}