Implemented workflow authorization (#3773)

* Returned 404 when path is not found and 500 when there are multiple workflows with the same path

* Return failure response in case of workflow fault

* Started workflow authorization implementation

* Implemented workflow authorization
This commit is contained in:
MariusVuscanNx 2023-03-08 10:49:43 +02:00 committed by GitHub
parent 03c4c2b051
commit c30586cd9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 339 additions and 97 deletions

View file

@ -142,10 +142,11 @@ public class HttpEndpoint : Trigger<HttpRequest>
// Generate bookmark data for path and selected methods.
var path = context.Get(Path);
var methods = context.Get(SupportedMethods);
return methods!.Select(x => new HttpEndpointBookmarkPayload(path!, x.ToLowerInvariant())
{
Policy = Guid.NewGuid().ToString()
}).Cast<object>().ToArray();
var authorize = context.Get(Authorize);
var policy = context.Get(Policy);
return methods!.Select(x =>
new HttpEndpointBookmarkPayload(path!, x.ToLowerInvariant(), authorize, policy))
.Cast<object>().ToArray();
}
private async Task HandleRequestAsync(ActivityExecutionContext context, HttpContext httpContext)

View file

@ -42,7 +42,7 @@ public class HttpFeature : FeatureBase
/// A delegate that is invoked when authorizing an inbound HTTP request.
/// </summary>
public Func<IServiceProvider, IHttpEndpointAuthorizationHandler> HttpEndpointAuthorizationHandler { get; set; } = ActivatorUtilities.GetServiceOrCreateInstance<AllowAnonymousHttpEndpointAuthorizationHandler>;
/// <summary>
/// A delegate that is invoked when an HTTP workflow faults.
/// </summary>
@ -70,7 +70,7 @@ public class HttpFeature : FeatureBase
typeof(HttpResponse),
typeof(HttpRequestHeaders)
}, "HTTP");
management.AddActivitiesFrom<HttpFeature>();
});
}
@ -113,9 +113,9 @@ public class HttpFeature : FeatureBase
// Add Http endpoint handlers.
.AddSingleton(HttpEndpointWorkflowFaultHandler)
.AddSingleton(HttpEndpointAuthorizationHandler)
// Add mediator handlers.
.AddNotificationHandlersFrom<HttpFeature>()
;
.AddNotificationHandlersFrom<HttpFeature>();
}
}

View file

@ -1,4 +1,3 @@
using Elsa.Extensions;
using Elsa.Http.Contracts;
using Elsa.Http.Models;
using Microsoft.AspNetCore.Authorization;
@ -18,19 +17,16 @@ public class AuthenticationBasedHttpEndpointAuthorizationHandler : IHttpEndpoint
if (identity == null)
return false;
if (identity.IsAuthenticated == false)
return false;
var httpEndpoint = context.Activity;
var expressionExecutionContext = context.ExpressionExecutionContext;
var policyName = httpEndpoint.Policy.TryGet(expressionExecutionContext);
if (string.IsNullOrWhiteSpace(policyName))
if (string.IsNullOrWhiteSpace(context.Policy))
return identity.IsAuthenticated;
var resource = new HttpWorkflowResource(expressionExecutionContext, httpEndpoint, context.WorkflowInstanceId);
var authorizationResult = await _authorizationService.AuthorizeAsync(user, resource, policyName);
var authorizationResult = await _authorizationService.AuthorizeAsync(user,
new { workflowInstanceId = context.WorkflowInstanceId }, context.Policy!);
return authorizationResult.Succeeded;
}
}

View file

@ -1,16 +1,17 @@
using System.Net.Mime;
using System.Text.Json;
using Elsa.Http.Contracts;
using Elsa.Http.Models;
using Elsa.Http.Options;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Helpers;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Runtime.Contracts;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using System.Net;
using Elsa.Http.Contracts;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Runtime.Contracts;
using System.Net.Mime;
using System.Text.Json;
namespace Elsa.Http.Middleware;
@ -22,11 +23,16 @@ public class WorkflowsMiddleware
{
private readonly RequestDelegate _next;
private readonly IWorkflowRuntime _workflowRuntime;
private readonly IHttpBookmarkProcessor _httpBookmarkProcessor;
private readonly IRouteMatcher _routeMatcher;
private readonly IRouteTable _routeTable;
private readonly IWorkflowInstanceStore _workflowInstanceStore;
private readonly IHttpBookmarkProcessor _httpBookmarkProcessor;
private readonly IHttpEndpointWorkflowFaultHandler _httpEndpointWorkflowFaultHandler;
private readonly IHttpEndpointAuthorizationHandler _httpEndpointAuthorizationHandler;
private readonly IBookmarkStore _bookmarkStore;
private readonly ITriggerStore _triggerStore;
private readonly IBookmarkHasher _hasher;
private readonly IBookmarkPayloadSerializer _serializer;
private readonly HttpActivityOptions _options;
private readonly string _activityTypeName = ActivityTypeNameHelper.GenerateTypeName<HttpEndpoint>();
@ -36,19 +42,29 @@ public class WorkflowsMiddleware
public WorkflowsMiddleware(
RequestDelegate next,
IWorkflowRuntime workflowRuntime,
IHttpBookmarkProcessor httpBookmarkProcessor,
IWorkflowInstanceStore workflowInstanceStore,
IHttpEndpointWorkflowFaultHandler httpEndpointWorkflowFaultHandler,
IOptions<HttpActivityOptions> options,
IHttpBookmarkProcessor httpBookmarkProcessor,
IHttpEndpointWorkflowFaultHandler httpEndpointWorkflowFaultHandler,
IHttpEndpointAuthorizationHandler httpEndpointAuthorizationHandler,
IBookmarkStore bookmarkStore,
ITriggerStore triggerStore,
IBookmarkHasher hasher,
IBookmarkPayloadSerializer serializer,
IRouteMatcher routeMatcher,
IRouteTable routeTable)
{
_next = next;
_workflowRuntime = workflowRuntime;
_httpBookmarkProcessor = httpBookmarkProcessor;
_workflowInstanceStore = workflowInstanceStore;
_httpEndpointWorkflowFaultHandler = httpEndpointWorkflowFaultHandler;
_options = options.Value;
_httpBookmarkProcessor = httpBookmarkProcessor;
_httpEndpointWorkflowFaultHandler = httpEndpointWorkflowFaultHandler;
_httpEndpointAuthorizationHandler = httpEndpointAuthorizationHandler;
_bookmarkStore = bookmarkStore;
_triggerStore = triggerStore;
_hasher = hasher;
_serializer = serializer;
_routeMatcher = routeMatcher;
_routeTable = routeTable;
}
@ -91,23 +107,29 @@ public class WorkflowsMiddleware
var triggerOptions = new TriggerWorkflowsRuntimeOptions(correlationId, input);
var cancellationToken = httpContext.RequestAborted;
// Trigger the workflow.
var triggerResult = await _workflowRuntime.TriggerWorkflowsAsync(_activityTypeName, bookmarkPayload, triggerOptions, cancellationToken);
var workflowsQuery = new WorkflowsQuery(_activityTypeName, bookmarkPayload, triggerOptions);
var pendingWorkflows = await _workflowRuntime.FindWorkflowsAsync(workflowsQuery, cancellationToken);
if (await HandleNoWorkflowsFoundAsync(httpContext, triggerResult.TriggeredWorkflows, basePath))
if (await HandleNoWorkflowsFoundAsync(httpContext, pendingWorkflows, basePath))
return;
if (await HandleMultipleWorkflowsFoundAsync(httpContext, triggerResult.TriggeredWorkflows, cancellationToken))
if (await HandleMultipleWorkflowsFoundAsync(httpContext, pendingWorkflows, cancellationToken))
return;
if (await HandleWorkflowFaultAsync(httpContext, triggerResult, cancellationToken))
if (await HandleWorkflowFaultAsync(httpContext, pendingWorkflows.Single(), cancellationToken))
return;
if (await AuthorizeAsync(httpContext, pendingWorkflows.Single(), bookmarkPayload, cancellationToken))
return;
var executionResult = await _workflowRuntime.ExecutePendingWorkflowAsync(pendingWorkflows.Single(), input, cancellationToken);
// Process the trigger result by resuming each HTTP bookmark, if any.
await _httpBookmarkProcessor.ProcessBookmarks(triggerResult.TriggeredWorkflows, correlationId, input, cancellationToken);
await _httpBookmarkProcessor.ProcessBookmarks(new List<WorkflowExecutionResult> { executionResult }, correlationId, input, cancellationToken);
}
private string? GetMatchingRoute(string? path) {
private string? GetMatchingRoute(string? path)
{
var matchingRouteQuery =
from route in _routeTable
@ -142,9 +164,9 @@ public class WorkflowsMiddleware
private string GetPath(HttpContext httpContext) => httpContext.Request.Path.Value.ToLowerInvariant();
private async Task<bool> HandleNoWorkflowsFoundAsync(HttpContext httpContext, ICollection<WorkflowExecutionResult> triggeredWorkflows, PathString? basePath)
private async Task<bool> HandleNoWorkflowsFoundAsync(HttpContext httpContext, IEnumerable<CollectedWorkflow> pendingWorkflows, PathString? basePath)
{
if (triggeredWorkflows.Any())
if (pendingWorkflows.Any())
return false;
// If a base path was configured, we are sure the requester tried to execute a workflow that doesn't exist.
@ -161,9 +183,9 @@ public class WorkflowsMiddleware
return true;
}
private async Task<bool> HandleMultipleWorkflowsFoundAsync(HttpContext httpContext, ICollection<WorkflowExecutionResult> triggeredWorkflows, CancellationToken cancellationToken)
private async Task<bool> HandleMultipleWorkflowsFoundAsync(HttpContext httpContext, IEnumerable<CollectedWorkflow> pendingWorkflows, CancellationToken cancellationToken)
{
if (triggeredWorkflows.Count <= 1)
if (pendingWorkflows.ToList().Count <= 1)
return false;
httpContext.Response.ContentType = "application/json";
@ -172,16 +194,16 @@ public class WorkflowsMiddleware
var responseContent = JsonSerializer.Serialize(new
{
errorMessage = "The call is ambiguous and matches multiple workflows.",
workflows = triggeredWorkflows
workflows = pendingWorkflows
});
await httpContext.Response.WriteAsync(responseContent, cancellationToken);
return true;
}
private async Task<bool> HandleWorkflowFaultAsync(HttpContext httpContext, TriggerWorkflowsResult triggerResult, CancellationToken cancellationToken)
private async Task<bool> HandleWorkflowFaultAsync(HttpContext httpContext, CollectedWorkflow pendingWorkflow, CancellationToken cancellationToken)
{
var instanceFilter = new WorkflowInstanceFilter { Id = triggerResult.TriggeredWorkflows.Single().InstanceId };
var instanceFilter = new WorkflowInstanceFilter { Id = pendingWorkflow.WorkflowInstanceId };
var workflowInstance = await _workflowInstanceStore.FindAsync(instanceFilter, cancellationToken);
if (workflowInstance is not null
@ -194,4 +216,41 @@ public class WorkflowsMiddleware
return false;
}
private async Task<bool> AuthorizeAsync(
HttpContext httpContext,
CollectedWorkflow pendingWorkflow,
HttpEndpointBookmarkPayload bookmarkPayload,
CancellationToken cancellationToken = default)
{
var hash = _hasher.Hash(_activityTypeName, bookmarkPayload);
var payload = default(HttpEndpointBookmarkPayload);
if (pendingWorkflow is CollectedStartableWorkflow)
{
var triggerFilter = new TriggerFilter() { Hash = hash };
var triggers = (await _triggerStore.FindManyAsync(triggerFilter, cancellationToken))
.Select(x => _serializer.Deserialize<HttpEndpointBookmarkPayload>(x.Data!)).ToList();
payload = triggers.Single();
}
else
{
var bookmarkFilter = new BookmarkFilter() { Hash = hash };
var bookmarks = (await _bookmarkStore.FindManyAsync(bookmarkFilter, cancellationToken))
.Select(x => _serializer.Deserialize<HttpEndpointBookmarkPayload>(x.Data!)).ToList();
payload = bookmarks.Single();
}
if (!(payload.Authorize ?? false))
return false;
var authorized = await _httpEndpointAuthorizationHandler.AuthorizeAsync(new AuthorizeHttpEndpointContext(httpContext, pendingWorkflow.WorkflowInstanceId, payload.Policy));
if (!authorized)
{
httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
return !authorized;
}
}

View file

@ -1,6 +1,5 @@
using Elsa.Expressions.Models;
using Microsoft.AspNetCore.Http;
namespace Elsa.Http.Models;
public record AuthorizeHttpEndpointContext(ExpressionExecutionContext ExpressionExecutionContext, HttpContext HttpContext, HttpEndpoint Activity, string WorkflowInstanceId);
public record AuthorizeHttpEndpointContext(HttpContext HttpContext, string WorkflowInstanceId, string? Policy = default);

View file

@ -1,5 +1,5 @@
using System.Text.Json.Serialization;
using Elsa.Workflows.Core.Attributes;
using System.Text.Json.Serialization;
namespace Elsa.Http.Models;
@ -12,11 +12,14 @@ public record HttpEndpointBookmarkPayload
public HttpEndpointBookmarkPayload()
{
}
public HttpEndpointBookmarkPayload(string path, string method)
public HttpEndpointBookmarkPayload(string path, string method,
bool? authorize = default, string? policy = default)
{
Path = path;
Method = method;
Authorize = authorize;
Policy = policy;
}
public string Path
@ -32,5 +35,8 @@ public record HttpEndpointBookmarkPayload
}
[ExcludeFromHash]
public string Policy { get; set; }
public string? Policy { get; set; }
[ExcludeFromHash]
public bool? Authorize { get; set; }
}

View file

@ -1,5 +0,0 @@
using Elsa.Expressions.Models;
namespace Elsa.Http.Models;
public record HttpWorkflowResource(ExpressionExecutionContext ExpressionExecutionContext, HttpEndpoint Activity, string WorkflowInstance);

View file

@ -1,4 +1,3 @@
using System.Text.Json;
using Elsa.Common.Models;
using Elsa.Extensions;
using Elsa.ProtoActor.Extensions;
@ -6,10 +5,10 @@ using Elsa.ProtoActor.Protos;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Serialization;
using Elsa.Workflows.Core.Services;
using Elsa.Workflows.Core.State;
using Elsa.Workflows.Runtime.Contracts;
using Proto.Cluster;
using System.Text.Json;
namespace Elsa.ProtoActor.Services;
@ -23,6 +22,7 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime
private readonly ITriggerStore _triggerStore;
private readonly IIdentityGenerator _identityGenerator;
private readonly IBookmarkHasher _hasher;
private readonly IWorkflowInstanceFactory _workflowInstanceFactory;
/// <summary>
/// Constructor.
@ -32,13 +32,15 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime
SerializerOptionsProvider serializerOptionsProvider,
ITriggerStore triggerStore,
IIdentityGenerator identityGenerator,
IBookmarkHasher hasher)
IBookmarkHasher hasher,
IWorkflowInstanceFactory workflowInstanceFactory)
{
_cluster = cluster;
_serializerOptionsProvider = serializerOptionsProvider;
_triggerStore = triggerStore;
_identityGenerator = identityGenerator;
_hasher = hasher;
_workflowInstanceFactory = workflowInstanceFactory;
}
/// <inheritdoc />
@ -162,6 +164,38 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime
return new TriggerWorkflowsResult(results);
}
/// <inheritdoc />
public async Task<WorkflowExecutionResult> ExecutePendingWorkflowAsync(CollectedWorkflow collectedWorkflow, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default)
{
if (collectedWorkflow is CollectedStartableWorkflow collectedStartableWorkflow)
{
var startOptions = new StartWorkflowRuntimeOptions(collectedStartableWorkflow.CorrelationId, input, VersionOptions.Published,
collectedStartableWorkflow.ActivityId, collectedStartableWorkflow.WorkflowInstanceId);
var startResult = await StartWorkflowAsync(collectedStartableWorkflow.DefinitionId!, startOptions, cancellationToken);
return new WorkflowExecutionResult(startResult.InstanceId, startResult.Bookmarks);
}
else
{
var collectedResumableWorkflow = (collectedWorkflow as CollectedResumableWorkflow)!;
var runtimeOptions = new ResumeWorkflowRuntimeOptions(collectedResumableWorkflow.CorrelationId, Input: input);
var resumeResult = await ResumeWorkflowAsync(
collectedWorkflow.WorkflowInstanceId,
runtimeOptions with { BookmarkId = collectedResumableWorkflow.BookmarkId },
cancellationToken);
return new WorkflowExecutionResult(collectedResumableWorkflow.WorkflowInstanceId, resumeResult.Bookmarks);
}
}
/// <inheritdoc />
public async Task<IEnumerable<CollectedWorkflow>> FindWorkflowsAsync(WorkflowsQuery workflowsQuery, CancellationToken cancellationToken = default)
{
var startableWorkflows = await CollectStartableWorkflowsAsync(workflowsQuery, cancellationToken);
var resumableWorkflows = await CollectResumableWorkflowsAsync(workflowsQuery, cancellationToken);
var results = startableWorkflows.Concat(resumableWorkflows).ToList();
return results;
}
/// <inheritdoc />
public async Task<WorkflowState?> ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default)
{
@ -277,4 +311,46 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime
x.ActivityInstanceId,
x.AutoBurn,
x.CallbackMethodName.NullIfEmpty()));
private async Task<IEnumerable<CollectedWorkflow>> CollectStartableWorkflowsAsync(WorkflowsQuery workflowsQuery, CancellationToken cancellationToken)
{
var hash = _hasher.Hash(workflowsQuery.ActivityTypeName, workflowsQuery.BookmarkPayload);
var filter = new TriggerFilter { Hash = hash };
var triggers = await _triggerStore.FindManyAsync(filter, cancellationToken);
var results = new List<CollectedWorkflow>();
foreach (var trigger in triggers)
{
var definitionId = trigger.WorkflowDefinitionId;
var startOptions = new StartWorkflowRuntimeOptions(workflowsQuery.Options.CorrelationId, workflowsQuery.Options.Input, VersionOptions.Published, trigger.ActivityId);
var canStartResult = await CanStartWorkflowAsync(definitionId, startOptions, cancellationToken);
var workflowInstance = await _workflowInstanceFactory.CreateAsync(definitionId, workflowsQuery.Options.CorrelationId, cancellationToken);
if (canStartResult.CanStart)
{
results.Add(new CollectedStartableWorkflow(workflowInstance.Id, workflowInstance, workflowsQuery.Options.CorrelationId, trigger.ActivityId, definitionId));
}
}
return results;
}
private async Task<IEnumerable<CollectedWorkflow>> CollectResumableWorkflowsAsync(WorkflowsQuery workflowsQuery, CancellationToken cancellationToken)
{
var hash = _hasher.Hash(workflowsQuery.ActivityTypeName, workflowsQuery.BookmarkPayload);
var client = _cluster.GetNamedBookmarkGrain(hash);
var request = new ResolveBookmarksRequest
{
ActivityTypeName = workflowsQuery.ActivityTypeName,
CorrelationId = workflowsQuery.Options.CorrelationId.EmptyIfNull()
};
var bookmarksResponse = await client.Resolve(request, cancellationToken);
var bookmarks = bookmarksResponse!.Bookmarks;
var collectedWorkflows = bookmarks.Select(b => new CollectedResumableWorkflow(b.WorkflowInstanceId, default, workflowsQuery.Options.CorrelationId, b.BookmarkId)).ToList();
return collectedWorkflows;
}
}

View file

@ -1,10 +1,10 @@
using System.Collections.ObjectModel;
using Elsa.Expressions.Helpers;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Services;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.ObjectModel;
namespace Elsa.Workflows.Core.Models;
@ -66,57 +66,57 @@ public class WorkflowExecutionContext
/// The <see cref="Workflow"/> associated with the execution context.
/// </summary>
public Workflow Workflow { get; }
/// <summary>
/// A graph of the workflow structure.
/// </summary>
public ActivityNode Graph { get; }
/// <summary>
/// The current status of the workflow.
/// </summary>
public WorkflowStatus Status => GetMainStatus(SubStatus);
/// <summary>
/// The current sub status of the workflow.
/// </summary>
public WorkflowSubStatus SubStatus { get; internal set; }
/// <summary>
/// The root <see cref="MemoryRegister"/> associated with the execution context.
/// </summary>
public MemoryRegister MemoryRegister { get; }
/// <summary>
/// A unique ID of the execution context.
/// </summary>
public string Id { get; set; }
/// <summary>
/// An application-specific identifier associated with the execution context.
/// </summary>
public string? CorrelationId { get; set; }
/// <summary>
/// A flattened list of <see cref="ActivityNode"/>s from the <see cref="Graph"/>.
/// </summary>
public IReadOnlyCollection<ActivityNode> Nodes => new ReadOnlyCollection<ActivityNode>(_nodes);
/// <summary>
/// A map between activity IDs and <see cref="ActivityNode"/>s in the workflow graph.
/// </summary>
public IDictionary<string, ActivityNode> NodeIdLookup { get; }
/// <summary>
/// A map between <see cref="IActivity"/>s and <see cref="ActivityNode"/>s in the workflow graph.
/// </summary>
public IDictionary<IActivity, ActivityNode> NodeActivityLookup { get; }
/// <summary>
/// The <see cref="IActivityScheduler"/> for the execution context.
/// </summary>
public IActivityScheduler Scheduler { get; }
/// <summary>
/// A collection of collected bookmarks during workflow execution.
/// </summary>
@ -152,22 +152,22 @@ public class WorkflowExecutionContext
/// The current <see cref="ExecuteActivityDelegate"/> delegate to invoke when executing the next activity.
/// </summary>
public ExecuteActivityDelegate? ExecuteDelegate { get; set; }
/// <summary>
/// Provides context about the bookmark that was used to resume workflow execution, if any.
/// </summary>
public ResumedBookmarkContext? ResumedBookmarkContext { get; set; }
/// <summary>
/// The ID of the activity associated with the trigger that caused this workflow execution, if any.
/// </summary>
public string? TriggerActivityId { get; set; }
/// <summary>
/// A <see cref="CancellationToken"/> that can be used to cancel asynchronous operations.
/// </summary>
public CancellationToken CancellationToken { get; }
/// <summary>
/// A list of <see cref="ActivityCompletionCallbackEntry"/> callbacks that are invoked when the associated child activity completes.
/// </summary>
@ -191,32 +191,32 @@ public class WorkflowExecutionContext
/// Resolves the specified service type from the service provider.
/// </summary>
public T GetRequiredService<T>() where T : notnull => _serviceProvider.GetRequiredService<T>();
/// <summary>
/// Resolves the specified service type from the service provider.
/// </summary>
public object GetRequiredService(Type serviceType) => _serviceProvider.GetRequiredService(serviceType);
/// <summary>
/// Resolves the specified service type from the service provider, or creates a new instance if the service type was not found in the service container.
/// </summary>
public T GetOrCreateService<T>() where T : notnull => ActivatorUtilities.GetServiceOrCreateInstance<T>(_serviceProvider);
/// <summary>
/// Resolves the specified service type from the service provider, or creates a new instance if the service type was not found in the service container.
/// </summary>
public object GetOrCreateService(Type serviceType) => ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, serviceType);
/// <summary>
/// Resolves the specified service type from the service provider.
/// </summary>
public T? GetService<T>() where T : notnull => _serviceProvider.GetService<T>();
/// <summary>
/// Resolves the specified service type from the service provider.
/// </summary>
public object? GetService(Type serviceType) => _serviceProvider.GetService(serviceType);
/// <summary>
/// Resolves multiple implementations of the specified service type from the service provider.
/// </summary>
@ -257,22 +257,29 @@ public class WorkflowExecutionContext
/// Returns the <see cref="ActivityNode"/> with the specified activity ID from the workflow graph.
/// </summary>
public ActivityNode FindNodeById(string nodeId) => NodeIdLookup[nodeId];
/// <summary>
/// Returns the <see cref="ActivityNode"/> containing the specified activity from the workflow graph.
/// </summary>
public ActivityNode FindNodeByActivity(IActivity activity) => NodeActivityLookup[activity];
/// <summary>
/// Returns the <see cref="IActivity"/> with the specified ID from the workflow graph.
/// </summary>
public IActivity FindActivityByNodeId(string nodeId) => FindNodeById(nodeId).Activity;
/// <summary>
///
/// </summary>
/// <param name="activityId"></param>
/// <returns></returns>
public IActivity FindActivityByActivityId(string activityId) => FindNodeById(NodeIdLookup.Single(n => n.Key.Contains(activityId)).Value.NodeId).Activity;
/// <summary>
/// Returns a custom property with the specified key from the <see cref="Properties"/> dictionary.
/// </summary>
public T? GetProperty<T>(string key) => Properties.TryGetValue(key, out var value) ? value.ConvertTo<T>() : default;
/// <summary>
/// Sets a custom property with the specified key on the <see cref="Properties"/> dictionary.
/// </summary>
@ -317,7 +324,7 @@ public class WorkflowExecutionContext
expressionExecutionContext.TransientProperties[ExpressionExecutionContextExtensions.ActivityExecutionContextKey] = activityExecutionContext;
return activityExecutionContext;
}
/// <summary>
/// Removes the specified <see cref="ActivityExecutionContext"/>.
/// </summary>
@ -330,15 +337,15 @@ public class WorkflowExecutionContext
// Remove the context.
_activityExecutionContexts.Remove(context);
// Remove all associated completion callbacks.
context.ClearCompletionCallbacks();
// Remove all associated variables.
var variablePersistenceManager = context.GetRequiredService<IVariablePersistenceManager>();
var variables = variablePersistenceManager.GetVariables(context);
await variablePersistenceManager.DeleteVariablesAsync(this, variables);
// Remove all associated bookmarks.
Bookmarks.RemoveWhere(x => x.ActivityInstanceId == context.Id);
}

View file

@ -2,6 +2,7 @@ using Elsa.Common.Models;
using Elsa.Workflows.Core.Helpers;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.State;
using Elsa.Workflows.Management.Entities;
namespace Elsa.Workflows.Runtime.Contracts;
@ -14,7 +15,7 @@ public interface IWorkflowRuntime
/// Returns a value whether or not the specified workflow definition can create a new instance.
/// </summary>
Task<CanStartWorkflowResult> CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeOptions options, CancellationToken cancellationToken);
/// <summary>
/// Creates a new workflow instance of the specified definition ID and executes it.
/// </summary>
@ -32,7 +33,7 @@ public interface IWorkflowRuntime
object bookmarkPayload,
TriggerWorkflowsRuntimeOptions options,
CancellationToken cancellationToken = default);
/// <summary>
/// Resumes an existing workflow instance.
/// </summary>
@ -40,27 +41,44 @@ public interface IWorkflowRuntime
/// <param name="options"></param>
/// <param name="cancellationToken"></param>
Task<ResumeWorkflowResult> ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Resumes all workflows that are bookmarked on the specified activity type.
/// </summary>
Task<ICollection<WorkflowExecutionResult>> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsRuntimeOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Starts all workflows and resumes existing workflow instances based on the specified activity type and bookmark payload.
/// </summary>
Task<TriggerWorkflowsResult> TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsRuntimeOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Executes a pending workflow.
/// </summary>
/// <param name="collectedWorkflow"></param>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<WorkflowExecutionResult> ExecutePendingWorkflowAsync(CollectedWorkflow collectedWorkflow, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
/// <summary>
/// Finds all the workflows that can be started or resumed based on a query model.
/// </summary>
/// <param name="workflowsQuery"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IEnumerable<CollectedWorkflow>> FindWorkflowsAsync(WorkflowsQuery workflowsQuery, CancellationToken cancellationToken = default);
/// <summary>
/// Exports the <see cref="WorkflowState"/> of the specified workflow instance.
/// </summary>
Task<WorkflowState?> ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default);
/// <summary>
/// Imports the specified <see cref="WorkflowState"/>.
/// </summary>
Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default);
/// <summary>
/// Adds and removes bookmarks based on the provided bookmarks diff.
/// </summary>
@ -78,8 +96,14 @@ public record CanStartWorkflowResult(string? InstanceId, bool CanStart);
public record ResumeWorkflowResult(ICollection<Bookmark> Bookmarks);
public record TriggerWorkflowsRuntimeOptions(string? CorrelationId = default, IDictionary<string, object>? Input = default);
public record TriggerWorkflowsResult(ICollection<WorkflowExecutionResult> TriggeredWorkflows);
public record WorkflowExecutionResult(string InstanceId, ICollection<Bookmark> Bookmarks);
public record WorkflowExecutionResult(string InstanceId, ICollection<Bookmark> Bookmarks, string? ActivityId = null);
public record UpdateBookmarksContext(string InstanceId, Diff<Bookmark> Diff, string? CorrelationId);
public record WorkflowsQuery(string ActivityTypeName, object BookmarkPayload, TriggerWorkflowsRuntimeOptions Options);
public record CollectedWorkflow(string WorkflowInstanceId, WorkflowInstance? WorkflowInstance, string? CorrelationId);
public record CollectedStartableWorkflow(string WorkflowInstanceId, WorkflowInstance? WorkflowInstance, string? CorrelationId, string? ActivityId, string? DefinitionId)
: CollectedWorkflow(WorkflowInstanceId, WorkflowInstance, CorrelationId);
public record CollectedResumableWorkflow(string WorkflowInstanceId, WorkflowInstance? WorkflowInstance, string? CorrelationId, string? BookmarkId)
: CollectedWorkflow(WorkflowInstanceId, WorkflowInstance, CorrelationId);
/// <summary>
/// Contains arguments to use for counting the number of workflow instances.
@ -95,7 +119,7 @@ public class CountRunningWorkflowsArgs
/// The workflow definition version to include in the query.
/// </summary>
public int? Version { get; set; }
/// <summary>
/// The correlation ID to include in the query.
/// </summary>

View file

@ -20,6 +20,7 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
private readonly IBookmarkStore _bookmarkStore;
private readonly IBookmarkHasher _hasher;
private readonly IDistributedLockProvider _distributedLockProvider;
private readonly IWorkflowInstanceFactory _workflowInstanceFactory;
/// <summary>
/// Constructor.
@ -31,7 +32,8 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
ITriggerStore triggerStore,
IBookmarkStore bookmarkStore,
IBookmarkHasher hasher,
IDistributedLockProvider distributedLockProvider)
IDistributedLockProvider distributedLockProvider,
IWorkflowInstanceFactory workflowInstanceFactory)
{
_workflowHostFactory = workflowHostFactory;
_workflowDefinitionService = workflowDefinitionService;
@ -40,6 +42,7 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
_bookmarkStore = bookmarkStore;
_hasher = hasher;
_distributedLockProvider = distributedLockProvider;
_workflowInstanceFactory = workflowInstanceFactory;
}
/// <inheritdoc />
@ -96,7 +99,7 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
continue;
var startResult = await StartWorkflowAsync(definitionId, startOptions, cancellationToken);
results.Add(new WorkflowExecutionResult(startResult.InstanceId, startResult.Bookmarks));
results.Add(new WorkflowExecutionResult(startResult.InstanceId, startResult.Bookmarks, trigger.ActivityId));
}
}
@ -157,6 +160,36 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
return new TriggerWorkflowsResult(results);
}
public async Task<WorkflowExecutionResult> ExecutePendingWorkflowAsync(CollectedWorkflow collectedWorkflow, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default)
{
if (collectedWorkflow is CollectedStartableWorkflow collectedStartableWorkflow)
{
var startOptions = new StartWorkflowRuntimeOptions(collectedStartableWorkflow.CorrelationId, input, VersionOptions.Published,
collectedStartableWorkflow.ActivityId, collectedStartableWorkflow.WorkflowInstanceId);
var startResult = await StartWorkflowAsync(collectedStartableWorkflow.DefinitionId!, startOptions, cancellationToken);
return new WorkflowExecutionResult(startResult.InstanceId, startResult.Bookmarks, collectedStartableWorkflow.ActivityId);
}
else
{
var collectedResumableWorkflow = (collectedWorkflow as CollectedResumableWorkflow)!;
var runtimeOptions = new ResumeWorkflowRuntimeOptions(collectedResumableWorkflow.CorrelationId, Input: input);
var resumeResult = await ResumeWorkflowAsync(
collectedWorkflow.WorkflowInstanceId,
runtimeOptions with { BookmarkId = collectedResumableWorkflow.BookmarkId },
cancellationToken);
return new WorkflowExecutionResult(collectedResumableWorkflow.WorkflowInstanceId, resumeResult.Bookmarks);
}
}
public async Task<IEnumerable<CollectedWorkflow>> FindWorkflowsAsync(WorkflowsQuery workflowsQuery, CancellationToken cancellationToken = default)
{
var startableWorkflows = await CollectStartableWorkflowsAsync(workflowsQuery, cancellationToken);
var resumableWorkflows = await CollectResumableWorkflowsAsync(workflowsQuery, cancellationToken);
var results = startableWorkflows.Concat(resumableWorkflows).ToList();
return results;
}
/// <inheritdoc />
public async Task<WorkflowState?> ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => await _workflowStateStore.LoadAsync(workflowInstanceId, cancellationToken);
@ -192,8 +225,11 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
foreach (var bookmark in bookmarks)
{
var workflowInstanceId = bookmark.WorkflowInstanceId;
var resumeOptions = new ResumeWorkflowRuntimeOptions(runtimeOptions.CorrelationId, bookmark.BookmarkId, Input: runtimeOptions.Input);
var resumeResult = await ResumeWorkflowAsync(workflowInstanceId, resumeOptions, cancellationToken);
var resumeResult = await ResumeWorkflowAsync(
workflowInstanceId,
runtimeOptions with { BookmarkId = bookmark.BookmarkId },
cancellationToken);
resumedWorkflows.Add(new WorkflowExecutionResult(workflowInstanceId, resumeResult.Bookmarks));
}
@ -221,4 +257,47 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
await _bookmarkStore.DeleteAsync(filter, cancellationToken);
}
}
private async Task<IEnumerable<CollectedWorkflow>> CollectStartableWorkflowsAsync(
WorkflowsQuery workflowsQuery,
CancellationToken cancellationToken = default)
{
var results = new List<CollectedWorkflow>();
var hash = _hasher.Hash(workflowsQuery.ActivityTypeName, workflowsQuery.BookmarkPayload);
// Start new workflows. Notice that this happens in a process-synchronized fashion to avoid multiple instances from being created.
var sharedResource = $"{nameof(DefaultWorkflowRuntime)}__StartTriggeredWorkflows__{hash}";
await using (await _distributedLockProvider.AcquireLockAsync(sharedResource, TimeSpan.FromMinutes(10), cancellationToken))
{
var filter = new TriggerFilter { Hash = hash };
var triggers = await _triggerStore.FindManyAsync(filter, cancellationToken);
foreach (var trigger in triggers)
{
var definitionId = trigger.WorkflowDefinitionId;
var startOptions = new StartWorkflowRuntimeOptions(workflowsQuery.Options.CorrelationId, workflowsQuery.Options.Input, VersionOptions.Published, trigger.ActivityId);
var canStartResult = await CanStartWorkflowAsync(definitionId, startOptions, cancellationToken);
var workflowInstance = await _workflowInstanceFactory.CreateAsync(definitionId, workflowsQuery.Options.CorrelationId, cancellationToken);
if (canStartResult.CanStart)
{
results.Add(new CollectedStartableWorkflow(workflowInstance.Id, workflowInstance, workflowsQuery.Options.CorrelationId, trigger.ActivityId, definitionId));
}
}
}
return results;
}
private async Task<IEnumerable<CollectedWorkflow>> CollectResumableWorkflowsAsync(WorkflowsQuery workflowsQuery, CancellationToken cancellationToken = default)
{
var hash = _hasher.Hash(workflowsQuery.ActivityTypeName, workflowsQuery.BookmarkPayload);
var correlationId = workflowsQuery.Options.CorrelationId;
var filter = new BookmarkFilter { Hash = hash, CorrelationId = correlationId };
var bookmarks = await _bookmarkStore.FindManyAsync(filter, cancellationToken);
var collectedWorkflows = bookmarks.Select(b => new CollectedResumableWorkflow(b.WorkflowInstanceId, default, correlationId, b.BookmarkId)).ToList();
return collectedWorkflows;
}
}