Port HTTP endpoint handlers for auth and faults
This commit is contained in:
parent
bd48f113cc
commit
87f836b0e3
|
|
@ -54,7 +54,7 @@ public class Inline<T> : Activity<T>
|
|||
public Inline(Func<ActivityExecutionContext, ValueTask<T>> activity, RegisterLocationReference? output = default)
|
||||
{
|
||||
_activity = activity;
|
||||
if (output != null) Result = new Output<T>(output);
|
||||
if (output != null) Result = new Output<T?>(output);
|
||||
}
|
||||
|
||||
public Inline(Func<ValueTask<T>> activity, RegisterLocationReference? output = default) : this(_ => activity(), output)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ public static class ActivityExtensions
|
|||
/// <summary>
|
||||
/// Creates an input from the activity's result.
|
||||
/// </summary>
|
||||
public static Input<T> CreateInput<T>(this Activity<T> activity) => activity.Result.CreateInput();
|
||||
public static Input<T?> CreateInput<T>(this Activity<T> activity) => activity.Result.CreateInput();
|
||||
|
||||
public static IEnumerable<Variable> GetVariables(this IActivity activity)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ public static class DictionaryExtensions
|
|||
return ConvertValue<T>(dictionary[key]);
|
||||
}
|
||||
|
||||
public static T GetOrAdd<T>(this IDictionary<string, object?> dictionary, string key, Func<T> valueFactory) where T : notnull
|
||||
public static T? GetOrAdd<T>(this IDictionary<string, object?> dictionary, string key, Func<T> valueFactory) where T : notnull
|
||||
{
|
||||
if (dictionary.TryGetValue<T>(key, out var value))
|
||||
return value;
|
||||
|
|
|
|||
|
|
@ -33,10 +33,26 @@ public abstract class Activity : IActivity
|
|||
|
||||
public abstract class ActivityWithResult : Activity
|
||||
{
|
||||
protected ActivityWithResult()
|
||||
{
|
||||
}
|
||||
|
||||
protected ActivityWithResult(string activityType) : base(activityType)
|
||||
{
|
||||
}
|
||||
|
||||
public Output? Result { get; set; }
|
||||
}
|
||||
|
||||
public abstract class Activity<T> : ActivityWithResult
|
||||
{
|
||||
public new Output<T>? Result { get; set; }
|
||||
protected Activity() : base()
|
||||
{
|
||||
}
|
||||
|
||||
protected Activity(string activityType) : base(activityType)
|
||||
{
|
||||
}
|
||||
|
||||
public new Output<T?>? Result { get; set; }
|
||||
}
|
||||
|
|
@ -6,7 +6,13 @@ public class ExpressionExecutionContext
|
|||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public ExpressionExecutionContext(IServiceProvider serviceProvider, Register register, Workflow workflow, IDictionary<string, object?> transientProperties, ExpressionExecutionContext? parentContext, CancellationToken cancellationToken)
|
||||
public ExpressionExecutionContext(
|
||||
IServiceProvider serviceProvider,
|
||||
Register register,
|
||||
Workflow workflow,
|
||||
IDictionary<string, object?> transientProperties,
|
||||
ExpressionExecutionContext? parentContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
Register = register;
|
||||
|
|
@ -30,7 +36,6 @@ public class ExpressionExecutionContext
|
|||
public object? Get(Output? output) => output != null ? GetLocation(output.LocationReference).Value : default;
|
||||
public T? GetVariable<T>(string name) => (T?)GetVariable(name);
|
||||
public T? GetVariable<T>() => (T?)GetVariable(typeof(T).Name);
|
||||
|
||||
public object? GetVariable(string name) => new Variable(name).Get(this);
|
||||
|
||||
public Variable SetVariable<T>(T? value) => SetVariable(typeof(T).Name, value);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,38 @@ public abstract class Trigger : Activity, ITrigger
|
|||
/// </summary>
|
||||
protected virtual IEnumerable<object> GetTriggerData(TriggerIndexingContext context) => new[]{ GetTriggerDatum(context) };
|
||||
|
||||
/// <summary>
|
||||
/// Override this method to return a trigger datum.
|
||||
/// </summary>
|
||||
protected virtual object GetTriggerDatum(TriggerIndexingContext context) => new();
|
||||
}
|
||||
|
||||
public abstract class Trigger<T> : Activity<T>, ITrigger
|
||||
{
|
||||
protected Trigger()
|
||||
{
|
||||
}
|
||||
|
||||
protected Trigger(string activityType) : base(activityType)
|
||||
{
|
||||
}
|
||||
|
||||
ValueTask<IEnumerable<object>> ITrigger.GetTriggerDataAsync(TriggerIndexingContext context) => GetTriggerDataAsync(context);
|
||||
|
||||
/// <summary>
|
||||
/// Override this method to return trigger data.
|
||||
/// </summary>
|
||||
protected virtual ValueTask<IEnumerable<object>> GetTriggerDataAsync(TriggerIndexingContext context)
|
||||
{
|
||||
var hashes = GetTriggerData(context);
|
||||
return ValueTask.FromResult(hashes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this method to return trigger data.
|
||||
/// </summary>
|
||||
protected virtual IEnumerable<object> GetTriggerData(TriggerIndexingContext context) => new[]{ GetTriggerDatum(context) };
|
||||
|
||||
/// <summary>
|
||||
/// Override this method to return a trigger datum.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ using Elsa.Modules.Http.Models;
|
|||
|
||||
namespace Elsa.Modules.Http;
|
||||
|
||||
[Activity("Elsa.Http.HttpEndpoint", "Waits for an inbound HTTP request that matches the specified path and methods", "HTTP")]
|
||||
public class HttpEndpoint : Trigger
|
||||
[Activity("Http", "Waits for an inbound HTTP request that matches the specified path and methods", category: "HTTP")]
|
||||
public class HttpEndpoint : Trigger<HttpRequestModel>
|
||||
{
|
||||
public const string InputKey = "HttpRequest";
|
||||
|
||||
|
||||
[Input] public Input<string> Path { get; set; } = default!;
|
||||
|
||||
[Input(
|
||||
|
|
@ -22,21 +22,32 @@ public class HttpEndpoint : Trigger
|
|||
)]
|
||||
public Input<ICollection<string>> SupportedMethods { get; set; } = new(new[] { HttpMethod.Get.Method });
|
||||
|
||||
[Output] public Output<HttpRequestModel>? Request { get; set; }
|
||||
[Input(
|
||||
Description = "Allow authenticated requests only",
|
||||
Category = "Security"
|
||||
)]
|
||||
public Input<bool> Authorize { get; set; } = new(false);
|
||||
|
||||
[Input(
|
||||
Description = "Provide a policy to evaluate. If the policy fails, the request is forbidden.",
|
||||
Category = "Security"
|
||||
)]
|
||||
public Input<string?> Policy { get; set; } = new(default(string?));
|
||||
|
||||
protected override IEnumerable<object> GetTriggerData(TriggerIndexingContext context) => GetBookmarkData(context.ExpressionExecutionContext);
|
||||
|
||||
protected override void Execute(ActivityExecutionContext context)
|
||||
{
|
||||
// If we did not receive external input, it means we are just now encountering this activity.
|
||||
if (!context.TryGetInput<HttpRequestModel>(InputKey, out var request ))
|
||||
if (!context.TryGetInput<HttpRequestModel>(InputKey, out var request))
|
||||
{
|
||||
// Create bookmarks for when we receive the expected HTTP request.
|
||||
context.CreateBookmarks(GetBookmarkData(context.ExpressionExecutionContext));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Provide the received HTTP request as output.
|
||||
context.Set(Request, request);
|
||||
context.Set(Result, request);
|
||||
}
|
||||
|
||||
private IEnumerable<object> GetBookmarkData(ExpressionExecutionContext context)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
using System.Threading.Tasks;
|
||||
using Elsa.Modules.Http.Models;
|
||||
|
||||
namespace Elsa.Modules.Http.Contracts
|
||||
{
|
||||
public interface IHttpEndpointAuthorizationHandler
|
||||
{
|
||||
ValueTask<bool> AuthorizeAsync(AuthorizeHttpEndpointContext context);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using System.Threading.Tasks;
|
||||
using Elsa.Modules.Http.Models;
|
||||
|
||||
namespace Elsa.Modules.Http.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Implement this to control what to return to the client in case an unhandled exception occurs while executing the workflow.
|
||||
/// </summary>
|
||||
public interface IHttpEndpointWorkflowFaultHandler
|
||||
{
|
||||
ValueTask HandleAsync(HttpEndpointFaultedWorkflowContext context);
|
||||
}
|
||||
|
|
@ -7,13 +7,15 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj" />
|
||||
<ProjectReference Include="..\..\runtime\Elsa.Runtime\Elsa.Runtime.csproj" />
|
||||
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj" />
|
||||
<ProjectReference Include="..\..\runtime\Elsa.Runtime\Elsa.Runtime.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
using System.Threading.Tasks;
|
||||
using Elsa.Modules.Http.Contracts;
|
||||
using Elsa.Modules.Http.Models;
|
||||
|
||||
namespace Elsa.Modules.Http.Handlers
|
||||
{
|
||||
public class AllowAnonymousHttpEndpointAuthorizationHandler : IHttpEndpointAuthorizationHandler
|
||||
{
|
||||
public ValueTask<bool> AuthorizeAsync(AuthorizeHttpEndpointContext context) => new(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
using System.Threading.Tasks;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Modules.Http.Contracts;
|
||||
using Elsa.Modules.Http.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Elsa.Modules.Http.Handlers
|
||||
{
|
||||
public class AuthenticationBasedHttpEndpointAuthorizationHandler : IHttpEndpointAuthorizationHandler
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
public AuthenticationBasedHttpEndpointAuthorizationHandler(IAuthorizationService authorizationService) => _authorizationService = authorizationService;
|
||||
|
||||
public async ValueTask<bool> AuthorizeAsync(AuthorizeHttpEndpointContext context)
|
||||
{
|
||||
var httpContext = context.HttpContext;
|
||||
var user = httpContext.User;
|
||||
var identity = user.Identity;
|
||||
|
||||
if (identity == null)
|
||||
return false;
|
||||
|
||||
if (identity.IsAuthenticated == false)
|
||||
return false;
|
||||
|
||||
var httpEndpoint = context.Activity;
|
||||
var expressionExecutionContext = context.ExpressionExecutionContext;
|
||||
var policyName = httpEndpoint.Policy.Get(expressionExecutionContext);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(policyName))
|
||||
return identity.IsAuthenticated;
|
||||
|
||||
var resource = new HttpWorkflowResource(expressionExecutionContext, httpEndpoint, context.WorkflowInstanceId);
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(user, resource, policyName);
|
||||
return authorizationResult.Succeeded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
using System.Net.Mime;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Elsa.Modules.Http.Contracts;
|
||||
using Elsa.Modules.Http.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.Modules.Http.Handlers;
|
||||
|
||||
public class DefaultHttpEndpointWorkflowFaultHandler : IHttpEndpointWorkflowFaultHandler
|
||||
{
|
||||
public virtual async ValueTask HandleAsync(HttpEndpointFaultedWorkflowContext context)
|
||||
{
|
||||
var httpContext = context.HttpContext;
|
||||
var workflowInstance = context.WorkflowInstance;
|
||||
|
||||
httpContext.Response.ContentType = MediaTypeNames.Application.Json;
|
||||
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
|
||||
var faultedResponse = JsonSerializer.Serialize(new
|
||||
{
|
||||
errorMessage = $"Workflow faulted at {workflowInstance.FaultedAt!} with error: {workflowInstance.Fault!.Message}",
|
||||
exception = workflowInstance.Fault?.Exception,
|
||||
workflow = new
|
||||
{
|
||||
name = workflowInstance.Name,
|
||||
version = workflowInstance.Version,
|
||||
instanceId = workflowInstance.Id
|
||||
}
|
||||
});
|
||||
|
||||
await httpContext.Response.WriteAsync(faultedResponse, context.CancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
using System.Threading;
|
||||
using Elsa.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.Modules.Http.Models
|
||||
{
|
||||
public record AuthorizeHttpEndpointContext(ExpressionExecutionContext ExpressionExecutionContext, HttpContext HttpContext, HttpEndpoint Activity, string WorkflowInstanceId);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using Elsa.Persistence.Entities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.Modules.Http.Models;
|
||||
|
||||
public record HttpEndpointFaultedWorkflowContext(HttpContext HttpContext, WorkflowInstance WorkflowInstance, Exception? Exception, CancellationToken CancellationToken);
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
using Elsa.Models;
|
||||
|
||||
namespace Elsa.Modules.Http.Models
|
||||
{
|
||||
public record HttpWorkflowResource(ExpressionExecutionContext ExpressionExecutionContext, HttpEndpoint Activity, string WorkflowInstance);
|
||||
}
|
||||
19
src/modules/Elsa.Modules.Http/Options/HttpActivityOptions.cs
Normal file
19
src/modules/Elsa.Modules.Http/Options/HttpActivityOptions.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using Elsa.Modules.Http.Contracts;
|
||||
using Elsa.Modules.Http.Handlers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Modules.Http.Options
|
||||
{
|
||||
public class HttpActivityOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The root path at which HTTP activities can be invoked.
|
||||
/// </summary>
|
||||
public PathString? BasePath { get; set; }
|
||||
|
||||
public Func<IServiceProvider, IHttpEndpointAuthorizationHandler> HttpEndpointAuthorizationHandlerFactory { get; set; } = ActivatorUtilities.GetServiceOrCreateInstance<AllowAnonymousHttpEndpointAuthorizationHandler>;
|
||||
public Func<IServiceProvider, IHttpEndpointWorkflowFaultHandler> HttpEndpointWorkflowFaultHandlerFactory { get; set; } = ActivatorUtilities.GetServiceOrCreateInstance<DefaultHttpEndpointWorkflowFaultHandler>;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using Elsa.Persistence.Models;
|
||||
using Elsa.State;
|
||||
|
||||
namespace Elsa.Persistence.Entities;
|
||||
|
|
@ -11,6 +12,7 @@ public class WorkflowInstance : Entity
|
|||
public WorkflowStatus WorkflowStatus { get; set; }
|
||||
public string CorrelationId { get; init; } = default!;
|
||||
public string? Name { get; set; }
|
||||
public WorkflowFault? Fault { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public DateTimeOffset? LastExecutedAt { get; set; }
|
||||
public DateTimeOffset? FinishedAt { get; set; }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
using System.Collections;
|
||||
|
||||
namespace Elsa.Persistence.Models
|
||||
{
|
||||
public class SimpleException
|
||||
{
|
||||
public SimpleException(Type type, string message, string? stackTrace, IDictionary data, SimpleException? innerException = default)
|
||||
{
|
||||
Type = type;
|
||||
Message = message;
|
||||
StackTrace = stackTrace;
|
||||
InnerException = innerException;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public Type Type { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string? StackTrace { get; set; }
|
||||
public SimpleException? InnerException { get; set; }
|
||||
public IDictionary Data { get; set; }
|
||||
|
||||
public static SimpleException? FromException(Exception? ex)
|
||||
{
|
||||
if (ex == null)
|
||||
return null;
|
||||
|
||||
var exceptionType = ex.GetType();
|
||||
var simpleException = new SimpleException(exceptionType, ex.Message, ex.StackTrace, ex.Data);
|
||||
|
||||
if (ex.InnerException != null)
|
||||
simpleException.InnerException = FromException(ex.InnerException);
|
||||
|
||||
return simpleException;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
namespace Elsa.Persistence.Models
|
||||
{
|
||||
public class SimpleExceptionProperty
|
||||
{
|
||||
public SimpleExceptionProperty(string name, object value)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public object Value { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
namespace Elsa.Persistence.Models
|
||||
{
|
||||
public record WorkflowFault(SimpleException? Exception, string Message, string? FaultedActivityId, object? ActivityInput, bool Resuming);
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||
namespace Elsa.Persistence.EntityFrameworkCore.Sqlite.Migrations
|
||||
{
|
||||
[DbContext(typeof(ElsaDbContext))]
|
||||
[Migration("20220308133708_Initial")]
|
||||
[Migration("20220309231832_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
|
|
@ -9,6 +9,7 @@ namespace Elsa.Persistence.EntityFrameworkCore.Configuration
|
|||
public void Configure(EntityTypeBuilder<WorkflowInstance> builder)
|
||||
{
|
||||
builder.Ignore(x => x.WorkflowState);
|
||||
builder.Ignore(x => x.Fault);
|
||||
builder.Property<string>("Data");
|
||||
builder.HasIndex(x => new { x.WorkflowStatus, x.DefinitionId, x.Version }).HasDatabaseName($"IX_{nameof(WorkflowInstance)}_{nameof(WorkflowInstance.WorkflowStatus)}_{nameof(WorkflowInstance.DefinitionId)}_{nameof(WorkflowInstance.Version)}");
|
||||
builder.HasIndex(x => new { x.WorkflowStatus, x.DefinitionId }).HasDatabaseName($"IX_{nameof(WorkflowInstance)}_{nameof(WorkflowInstance.WorkflowStatus)}_{nameof(WorkflowInstance.DefinitionId)}");
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System.Text.Json;
|
|||
using Elsa.Management.Serialization;
|
||||
using Elsa.Persistence.Entities;
|
||||
using Elsa.Persistence.EntityFrameworkCore.Contracts;
|
||||
using Elsa.Persistence.Models;
|
||||
using Elsa.State;
|
||||
|
||||
namespace Elsa.Persistence.EntityFrameworkCore.Handlers.Serialization;
|
||||
|
|
@ -17,7 +18,7 @@ public class WorkflowInstanceSerializer : IEntitySerializer<WorkflowInstance>
|
|||
|
||||
public void Serialize(ElsaDbContext dbContext, WorkflowInstance entity)
|
||||
{
|
||||
var data = new WorkflowInstanceState(entity.WorkflowState);
|
||||
var data = new WorkflowInstanceState(entity.WorkflowState, entity.Fault);
|
||||
var options = _workflowSerializerOptionsProvider.CreatePersistenceOptions();
|
||||
var json = JsonSerializer.Serialize(data, options);
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ public class WorkflowInstanceSerializer : IEntitySerializer<WorkflowInstance>
|
|||
|
||||
public void Deserialize(ElsaDbContext dbContext, WorkflowInstance entity)
|
||||
{
|
||||
var data = new WorkflowInstanceState(entity.WorkflowState);
|
||||
var data = new WorkflowInstanceState(entity.WorkflowState, entity.Fault);
|
||||
var json = (string?)dbContext.Entry(entity).Property("Data").CurrentValue;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(json))
|
||||
|
|
@ -36,6 +37,7 @@ public class WorkflowInstanceSerializer : IEntitySerializer<WorkflowInstance>
|
|||
}
|
||||
|
||||
entity.WorkflowState = data.WorkflowState;
|
||||
entity.Fault = data.Fault;
|
||||
}
|
||||
|
||||
// Can't use records when using System.Text.Json serialization and reference handling. Hence, using a class with default constructor.
|
||||
|
|
@ -45,11 +47,13 @@ public class WorkflowInstanceSerializer : IEntitySerializer<WorkflowInstance>
|
|||
{
|
||||
}
|
||||
|
||||
public WorkflowInstanceState(WorkflowState workflowState)
|
||||
public WorkflowInstanceState(WorkflowState workflowState, WorkflowFault? fault)
|
||||
{
|
||||
WorkflowState = workflowState;
|
||||
Fault = fault;
|
||||
}
|
||||
|
||||
public WorkflowState WorkflowState { get; init; } = default!;
|
||||
public WorkflowFault? Fault { get; set; }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue