Http endpoint security (#1389)
This commit is contained in:
parent
f192870ce8
commit
a3631d6d26
7
Elsa.sln
7
Elsa.sln
|
|
@ -345,6 +345,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElsaDashboard.Samples.Blazo
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElsaDashboard.Samples.BlazorServer", "src\samples\dashboard\blazor\ElsaDashboard.Samples.BlazorServer\ElsaDashboard.Samples.BlazorServer.csproj", "{9A4B1C48-16FB-4A65-AE98-5CDE7BCB506C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.HttpEndpointSecurity", "src\samples\aspnet\Elsa.Samples.HttpEndpointSecurity\Elsa.Samples.HttpEndpointSecurity.csproj", "{82B115DA-E3D0-49D7-AD08-DE9387656756}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -782,6 +784,10 @@ Global
|
|||
{9A4B1C48-16FB-4A65-AE98-5CDE7BCB506C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9A4B1C48-16FB-4A65-AE98-5CDE7BCB506C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9A4B1C48-16FB-4A65-AE98-5CDE7BCB506C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{82B115DA-E3D0-49D7-AD08-DE9387656756}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{82B115DA-E3D0-49D7-AD08-DE9387656756}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{82B115DA-E3D0-49D7-AD08-DE9387656756}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{82B115DA-E3D0-49D7-AD08-DE9387656756}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -929,6 +935,7 @@ Global
|
|||
{BC4B4F3F-6E2C-4736-AAB8-6DF78BFA3ACB} = {FC9F520F-BA51-4AD2-BFEE-EF787798E734}
|
||||
{DCB3C4DD-2B7D-44E9-A366-F36F4ABFF488} = {D86B94DC-A53C-4A67-A820-828DD359C49B}
|
||||
{9A4B1C48-16FB-4A65-AE98-5CDE7BCB506C} = {D86B94DC-A53C-4A67-A820-828DD359C49B}
|
||||
{82B115DA-E3D0-49D7-AD08-DE9387656756} = {22E75696-6FE9-436A-9097-EE21C603F818}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {8B0975FD-7050-48B0-88C5-48C33378E158}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,20 @@ namespace Elsa.Activities.Http
|
|||
[ActivityInput(Category = PropertyCategories.Advanced)]
|
||||
public Type? TargetType { get; set; }
|
||||
|
||||
[ActivityInput(
|
||||
Hint = "Check to allow authenticated requests only",
|
||||
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid },
|
||||
Category = "Security"
|
||||
)]
|
||||
public bool Authorize { get; set; }
|
||||
|
||||
[ActivityInput(
|
||||
Hint = "Provide a policy to evaluate. If the policy fails, the request is forbidden.",
|
||||
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid },
|
||||
Category = "Security"
|
||||
)]
|
||||
public string? Policy { get; set; }
|
||||
|
||||
[ActivityOutput(Hint = "The received HTTP request.")]
|
||||
public HttpRequestModel? Output { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -45,5 +45,17 @@ namespace Elsa.Activities.Http
|
|||
public static ISetupActivity<HttpEndpoint> WithTargetType(this ISetupActivity<HttpEndpoint> activity, Func<Type?> value) => activity.Set(x => x.TargetType, value).WithReadContent();
|
||||
public static ISetupActivity<HttpEndpoint> WithTargetType(this ISetupActivity<HttpEndpoint> activity, Type? value) => activity.Set(x => x.TargetType, value).WithReadContent();
|
||||
public static ISetupActivity<HttpEndpoint> WithTargetType<T>(this ISetupActivity<HttpEndpoint> activity) => activity.Set(x => x.TargetType, typeof(T)).WithReadContent();
|
||||
|
||||
public static ISetupActivity<HttpEndpoint> WithAuthorize(this ISetupActivity<HttpEndpoint> activity, Func<ActivityExecutionContext, ValueTask<bool>> value) => activity.Set(x => x.Authorize, value);
|
||||
public static ISetupActivity<HttpEndpoint> WithAuthorize(this ISetupActivity<HttpEndpoint> activity, Func<ValueTask<bool>> value) => activity.Set(x => x.Authorize, value);
|
||||
public static ISetupActivity<HttpEndpoint> WithAuthorize(this ISetupActivity<HttpEndpoint> activity, Func<ActivityExecutionContext, bool> value) => activity.Set(x => x.Authorize, value);
|
||||
public static ISetupActivity<HttpEndpoint> WithAuthorize(this ISetupActivity<HttpEndpoint> activity, Func<bool> value) => activity.Set(x => x.Authorize, value);
|
||||
public static ISetupActivity<HttpEndpoint> WithAuthorize(this ISetupActivity<HttpEndpoint> activity, bool value = true) => activity.Set(x => x.Authorize, value);
|
||||
|
||||
public static ISetupActivity<HttpEndpoint> WithPolicy(this ISetupActivity<HttpEndpoint> activity, Func<ActivityExecutionContext, ValueTask<string?>> value) => activity.Set(x => x.Policy, value);
|
||||
public static ISetupActivity<HttpEndpoint> WithPolicy(this ISetupActivity<HttpEndpoint> activity, Func<ValueTask<string?>> value) => activity.Set(x => x.Policy, value);
|
||||
public static ISetupActivity<HttpEndpoint> WithPolicy(this ISetupActivity<HttpEndpoint> activity, Func<ActivityExecutionContext, string?> value) => activity.Set(x => x.Policy, value);
|
||||
public static ISetupActivity<HttpEndpoint> WithPolicy(this ISetupActivity<HttpEndpoint> activity, Func<string?> value) => activity.Set(x => x.Policy, value);
|
||||
public static ISetupActivity<HttpEndpoint> WithPolicy(this ISetupActivity<HttpEndpoint> activity, string? value) => activity.Set(x => x.Policy, value);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="5.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Threading.Tasks;
|
||||
using Elsa.Activities.Signaling.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Open.Linq.AsyncExtensions;
|
||||
|
||||
|
|
@ -8,6 +9,7 @@ namespace Elsa.Activities.Http.Endpoints.Signals
|
|||
[ApiController]
|
||||
[Route("signals/dispatch/{token}")]
|
||||
[Produces("application/json")]
|
||||
[Authorize()]
|
||||
public class DispatchEndpoint : ControllerBase
|
||||
{
|
||||
private readonly ISignaler _signaler;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ using Elsa.Scripting.Liquid.Extensions;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
|
|
@ -33,6 +34,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
|
||||
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddHttpClient(nameof(SendHttpRequest));
|
||||
services.AddAuthorizationCore();
|
||||
|
||||
services
|
||||
.AddSingleton<IHttpRequestBodyParser, DefaultHttpRequestBodyParser>()
|
||||
|
|
@ -43,6 +45,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
.AddSingleton<IHttpResponseContentReader, FileResponseContentReader>()
|
||||
.AddSingleton<IActionContextAccessor, ActionContextAccessor>()
|
||||
.AddSingleton<IAbsoluteUrlProvider, DefaultAbsoluteUrlProvider>()
|
||||
.AddSingleton(sp => sp.GetRequiredService<IOptions<HttpActivityOptions>>().Value.HttpEndpointAuthorizationHandlerFactory(sp))
|
||||
.AddBookmarkProvider<HttpEndpointBookmarkProvider>()
|
||||
.AddHttpContextAccessor()
|
||||
.AddNotificationHandlers(typeof(ConfigureJavaScriptEngine))
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ using Elsa.Models;
|
|||
using Elsa.Persistence;
|
||||
using Elsa.Services;
|
||||
using Elsa.Services.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
|
|
@ -36,30 +37,19 @@ namespace Elsa.Activities.Http.Middleware
|
|||
IWorkflowInstanceStore workflowInstanceStore,
|
||||
IWorkflowRegistry workflowRegistry,
|
||||
IWorkflowBlueprintReflector workflowBlueprintReflector,
|
||||
IHttpEndpointAuthorizationHandler authorizationHandler,
|
||||
IEnumerable<IHttpRequestBodyParser> contentParsers)
|
||||
{
|
||||
var basePath = options.Value.BasePath;
|
||||
var path = GetPath(basePath, httpContext);
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
await _next(httpContext);
|
||||
return;
|
||||
}
|
||||
|
||||
var request = httpContext.Request;
|
||||
|
||||
string path;
|
||||
|
||||
// If a base path was configured, try to match against that first.
|
||||
if (basePath != null)
|
||||
{
|
||||
// If no match, continue with the next middleware in the pipeline.
|
||||
if (!request.Path.StartsWithSegments(basePath.Value, out _, out var remainingPath))
|
||||
{
|
||||
await _next(httpContext);
|
||||
return;
|
||||
}
|
||||
|
||||
path = remainingPath.Value.ToLowerInvariant();
|
||||
}
|
||||
else
|
||||
{
|
||||
path = httpContext.Request.Path.Value.ToLowerInvariant();
|
||||
}
|
||||
|
||||
var cancellationToken = CancellationToken.None; // Prevent half-way request abortion (which also happens when WriteHttpResponse writes to the response).
|
||||
var method = httpContext.Request.Method!.ToLowerInvariant();
|
||||
|
||||
|
|
@ -70,38 +60,15 @@ namespace Elsa.Activities.Http.Middleware
|
|||
var collectWorkflowsContext = new WorkflowsQuery(activityType, bookmark, correlationId, default, default, TenantId);
|
||||
var pendingWorkflows = await workflowLaunchpad.FindWorkflowsAsync(collectWorkflowsContext, cancellationToken).ToList();
|
||||
|
||||
if (!pendingWorkflows.Any())
|
||||
{
|
||||
// If a base path was configured, we are sure the requester tried to execute a workflow that doesn't exist.
|
||||
// Therefore, sending a 404 response seems appropriate instead of continuing with any subsequent middlewares.
|
||||
if (basePath != null)
|
||||
{
|
||||
httpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
// If no base path was configured on the other hand, the request could be targeting anything else and should be handled by subsequent middlewares.
|
||||
await _next(httpContext);
|
||||
if (await HandleNoWorkflowsFoundAsync(httpContext, pendingWorkflows, basePath))
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingWorkflows.Count > 1)
|
||||
{
|
||||
httpContext.Response.ContentType = "application/json";
|
||||
httpContext.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||
|
||||
var responseContent = JsonConvert.SerializeObject(new
|
||||
{
|
||||
errorMessage = "The call is ambiguous and matches multiple workflows.",
|
||||
workflows = pendingWorkflows
|
||||
});
|
||||
|
||||
await httpContext.Response.WriteAsync(responseContent, cancellationToken);
|
||||
if (await HandleMultipleWorkflowsFoundAsync(httpContext, pendingWorkflows, cancellationToken))
|
||||
return;
|
||||
}
|
||||
|
||||
var pendingWorkflow = pendingWorkflows.Single();
|
||||
var pendingWorkflowInstance = await workflowInstanceStore.FindByIdAsync(pendingWorkflow.WorkflowInstanceId, cancellationToken);
|
||||
|
||||
if (pendingWorkflowInstance is null)
|
||||
{
|
||||
await _next(httpContext);
|
||||
|
|
@ -116,6 +83,19 @@ namespace Elsa.Activities.Http.Middleware
|
|||
}
|
||||
|
||||
var workflowBlueprintWrapper = await workflowBlueprintReflector.ReflectAsync(httpContext.RequestServices, workflowBlueprint, cancellationToken);
|
||||
var orderedContentParsers = contentParsers.OrderByDescending(x => x.Priority).ToList();
|
||||
var simpleContentType = request.ContentType?.Split(';').First();
|
||||
var contentParser = orderedContentParsers.FirstOrDefault(x => x.SupportedContentTypes.Contains(simpleContentType, StringComparer.OrdinalIgnoreCase)) ?? orderedContentParsers.LastOrDefault() ?? new DefaultHttpRequestBodyParser();
|
||||
var activityWrapper = workflowBlueprintWrapper.GetUnfilteredActivity<HttpEndpoint>(pendingWorkflow.ActivityId!)!;
|
||||
|
||||
if (!await AuthorizeAsync(httpContext, activityWrapper, workflowBlueprint, pendingWorkflow, authorizationHandler, cancellationToken))
|
||||
{
|
||||
httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||
return;
|
||||
}
|
||||
|
||||
var readContent = await activityWrapper.EvaluatePropertyValueAsync(x => x.ReadContent, cancellationToken);
|
||||
|
||||
var inputModel = new HttpRequestModel(
|
||||
request.Path.ToString(),
|
||||
request.Method,
|
||||
|
|
@ -123,13 +103,6 @@ namespace Elsa.Activities.Http.Middleware
|
|||
request.Headers.ToDictionary(x => x.Key, x => x.Value.ToString())
|
||||
);
|
||||
|
||||
var orderedContentParsers = contentParsers.OrderByDescending(x => x.Priority).ToList();
|
||||
var simpleContentType = request.ContentType?.Split(';').First();
|
||||
var contentParser = orderedContentParsers.FirstOrDefault(x => x.SupportedContentTypes.Contains(simpleContentType, StringComparer.OrdinalIgnoreCase)) ?? orderedContentParsers.LastOrDefault() ?? new DefaultHttpRequestBodyParser();
|
||||
|
||||
var activityWrapper = workflowBlueprintWrapper.GetUnfilteredActivity<HttpEndpoint>(pendingWorkflow.ActivityId!);
|
||||
var readContent = await activityWrapper!.EvaluatePropertyValueAsync(x => x.ReadContent, cancellationToken);
|
||||
|
||||
if (readContent)
|
||||
{
|
||||
var targetType = await activityWrapper.EvaluatePropertyValueAsync(x => x.TargetType, cancellationToken);
|
||||
|
|
@ -142,7 +115,7 @@ namespace Elsa.Activities.Http.Middleware
|
|||
await workflowLaunchpad.DispatchPendingWorkflowAsync(pendingWorkflow, new WorkflowInput(inputModel), cancellationToken);
|
||||
|
||||
httpContext.Response.ContentType = "application/json";
|
||||
httpContext.Response.StatusCode = (int) HttpStatusCode.Accepted;
|
||||
httpContext.Response.StatusCode = (int)HttpStatusCode.Accepted;
|
||||
await httpContext.Response.WriteAsync(JsonConvert.SerializeObject(pendingWorkflows), cancellationToken);
|
||||
}
|
||||
else
|
||||
|
|
@ -155,7 +128,7 @@ namespace Elsa.Activities.Http.Middleware
|
|||
&& !httpContext.Response.HasStarted)
|
||||
{
|
||||
httpContext.Response.ContentType = "application/json";
|
||||
httpContext.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||
httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
|
||||
var faultedResponse = JsonConvert.SerializeObject(new
|
||||
{
|
||||
|
|
@ -172,5 +145,62 @@ namespace Elsa.Activities.Http.Middleware
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> AuthorizeAsync(
|
||||
HttpContext httpContext,
|
||||
IActivityBlueprintWrapper<HttpEndpoint> httpEndpoint,
|
||||
IWorkflowBlueprint workflowBlueprint,
|
||||
CollectedWorkflow pendingWorkflow,
|
||||
IHttpEndpointAuthorizationHandler authorizationHandler,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var authorize = await httpEndpoint.EvaluatePropertyValueAsync(x => x.Authorize, cancellationToken);
|
||||
|
||||
if (!authorize)
|
||||
return true;
|
||||
|
||||
return await authorizationHandler.AuthorizeAsync(new AuthorizeHttpEndpointContext(httpContext, httpEndpoint, workflowBlueprint, pendingWorkflow.WorkflowInstanceId, cancellationToken));
|
||||
}
|
||||
|
||||
private async Task<bool> HandleNoWorkflowsFoundAsync(HttpContext httpContext, IList<CollectedWorkflow> pendingWorkflows, PathString? basePath)
|
||||
{
|
||||
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.
|
||||
// Therefore, sending a 404 response seems appropriate instead of continuing with any subsequent middlewares.
|
||||
if (basePath != null)
|
||||
{
|
||||
httpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If no base path was configured on the other hand, the request could be targeting anything else and should be handled by subsequent middlewares.
|
||||
await _next(httpContext);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> HandleMultipleWorkflowsFoundAsync(HttpContext httpContext, IList<CollectedWorkflow> pendingWorkflows, CancellationToken cancellationToken)
|
||||
{
|
||||
if (pendingWorkflows.Count <= 1)
|
||||
return false;
|
||||
|
||||
httpContext.Response.ContentType = "application/json";
|
||||
httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
|
||||
var responseContent = JsonConvert.SerializeObject(new
|
||||
{
|
||||
errorMessage = "The call is ambiguous and matches multiple workflows.",
|
||||
workflows = pendingWorkflows
|
||||
});
|
||||
|
||||
await httpContext.Response.WriteAsync(responseContent, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
private string? GetPath(PathString? basePath, HttpContext httpContext) => basePath != null
|
||||
? httpContext.Request.Path.StartsWithSegments(basePath.Value, out _, out var remainingPath) ? remainingPath.Value.ToLowerInvariant() : null
|
||||
: httpContext.Request.Path.Value.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
using System.Threading;
|
||||
using Elsa.Services.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.Activities.Http.Models
|
||||
{
|
||||
public record AuthorizeHttpEndpointContext(HttpContext HttpContext, IActivityBlueprintWrapper<HttpEndpoint> HttpEndpointActivity, IWorkflowBlueprint WorkflowBlueprint, string WorkflowInstanceId, CancellationToken CancellationToken);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
using Elsa.Services.Models;
|
||||
|
||||
namespace Elsa.Activities.Http.Models
|
||||
{
|
||||
public record HttpWorkflowResource(IWorkflowBlueprint WorkflowBlueprint, IActivityBlueprint ActivityBlueprint, string WorkflowInstance);
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
using System;
|
||||
using Elsa.Activities.Http.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Activities.Http.Options
|
||||
{
|
||||
|
|
@ -9,10 +11,12 @@ namespace Elsa.Activities.Http.Options
|
|||
/// The base URL of the server. This should be set to the same value at which the Elsa Server is publicly available. It will be used when generating absolute URLs need to be generated by activities such as SendEmail.
|
||||
/// </summary>
|
||||
public Uri BaseUrl { get; set; } = default!;
|
||||
|
||||
|
||||
/// <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<AuthenticationBasedHttpEndpointAuthorizationHandler>;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
using System.Threading.Tasks;
|
||||
using Elsa.Activities.Http.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Elsa.Activities.Http.Services
|
||||
{
|
||||
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;
|
||||
|
||||
if (!user.Identity.IsAuthenticated)
|
||||
return false;
|
||||
|
||||
var cancellationToken = context.CancellationToken;
|
||||
var httpEndpoint = context.HttpEndpointActivity;
|
||||
var policyName = await httpEndpoint.EvaluatePropertyValueAsync(x => x.Policy, cancellationToken);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(policyName))
|
||||
return user.Identity.IsAuthenticated;
|
||||
|
||||
var resource = new HttpWorkflowResource(context.WorkflowBlueprint, httpEndpoint.ActivityBlueprint, context.WorkflowInstanceId);
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(user, resource, policyName);
|
||||
return authorizationResult.Succeeded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using System.Threading.Tasks;
|
||||
using Elsa.Activities.Http.Models;
|
||||
|
||||
namespace Elsa.Activities.Http.Services
|
||||
{
|
||||
public interface IHttpEndpointAuthorizationHandler
|
||||
{
|
||||
ValueTask<bool> AuthorizeAsync(AuthorizeHttpEndpointContext context);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,231 +1,231 @@
|
|||
<!DOCTYPE html>
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0"/>
|
||||
<title>Elsa Dashboard</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/build/assets/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/build/assets/images/favicon-16x16.png">
|
||||
<link rel="stylesheet" href="/build/assets/fonts/inter/inter.css">
|
||||
<link href="/build/elsa-workflows-studio.css" rel="stylesheet">
|
||||
<script src="/build/assets/js/monaco-editor/min/vs/loader.js"></script>
|
||||
<script type="module" src="/build/elsa-workflows-studio.esm.js"></script>
|
||||
<script nomodule src="/build/elsa-workflows-studio.js"></script>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0"/>
|
||||
<title>Elsa Dashboard</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/build/assets/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/build/assets/images/favicon-16x16.png">
|
||||
<link rel="stylesheet" href="/build/assets/fonts/inter/inter.css">
|
||||
<link href="/build/elsa-workflows-studio.css" rel="stylesheet">
|
||||
<script src="/build/assets/js/monaco-editor/min/vs/loader.js"></script>
|
||||
<script type="module" src="/build/elsa-workflows-studio.esm.js"></script>
|
||||
<script nomodule src="/build/elsa-workflows-studio.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<elsa-studio-root server-url="https://localhost:11000" monaco-lib-path="build/assets/js/monaco-editor/min" culture="en-US">
|
||||
<!-- The root dashboard component -->
|
||||
<elsa-studio-dashboard></elsa-studio-dashboard>
|
||||
<!-- The root dashboard component -->
|
||||
<elsa-studio-dashboard></elsa-studio-dashboard>
|
||||
|
||||
<!-- Instead of using the full dashboard component, you can display lower-level components instead-->
|
||||
<!--<elsa-workflow-instance-list-screen></elsa-workflow-instance-list-screen>-->
|
||||
<!--<elsa-workflow-definition-editor-screen></elsa-workflow-definition-editor-screen>-->
|
||||
<!-- Instead of using the full dashboard component, you can display lower-level components instead-->
|
||||
<!--<elsa-workflow-instance-list-screen></elsa-workflow-instance-list-screen>-->
|
||||
<!--<elsa-workflow-definition-editor-screen></elsa-workflow-definition-editor-screen>-->
|
||||
</elsa-studio-root>
|
||||
|
||||
<!-- Keep this to avoid Stencil from stripping classes added by dagre-d3 <!-->
|
||||
<div class="node add label-container hidden"></div>
|
||||
|
||||
<script type="module">
|
||||
// Integration demos.
|
||||
// Integration demos.
|
||||
|
||||
// Import publicly exposed services and models.
|
||||
import {confirmDialogService, EventTypes} from "/build/index.esm.js";
|
||||
// Import publicly exposed services and models.
|
||||
import {confirmDialogService, EventTypes} from "/build/index.esm.js";
|
||||
|
||||
// Custom plugin that changes the icon of the ReadLine activity.
|
||||
function CustomReadLinePlugin(elsaStudio) {
|
||||
// Replace icon used for 'ReadLine' activity.
|
||||
elsaStudio.activityIconProvider.register(
|
||||
'ReadLine',
|
||||
`<span class="elsa-rounded-lg elsa-inline-flex elsa-p-3 elsa-bg-blue-50 elsa-text-blue-700 elsa-ring-4 elsa-ring-white">
|
||||
<svg class="elsa-h-6 elsa-w-6" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" />
|
||||
</svg>
|
||||
</span>`);
|
||||
}
|
||||
// Custom plugin that changes the icon of the ReadLine activity.
|
||||
function CustomReadLinePlugin(elsaStudio) {
|
||||
// Replace icon used for 'ReadLine' activity.
|
||||
elsaStudio.activityIconProvider.register(
|
||||
'ReadLine',
|
||||
`<span class="elsa-rounded-lg elsa-inline-flex elsa-p-3 elsa-bg-blue-50 elsa-text-blue-700 elsa-ring-4 elsa-ring-white">
|
||||
<svg class="elsa-h-6 elsa-w-6" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" />
|
||||
</svg>
|
||||
</span>`);
|
||||
}
|
||||
|
||||
// Custom plugin that adds a menu item to the bulk actions list on the workflow instance list view.
|
||||
function CustomBulkActionsPlugin(elsaStudio) {
|
||||
const eventBus = elsaStudio.eventBus;
|
||||
// Custom plugin that adds a menu item to the bulk actions list on the workflow instance list view.
|
||||
function CustomBulkActionsPlugin(elsaStudio) {
|
||||
const eventBus = elsaStudio.eventBus;
|
||||
|
||||
eventBus.on(EventTypes.WorkflowInstanceBulkActionsLoading, async e => {
|
||||
e.bulkActions.push({
|
||||
name: 'Reset', text: 'Reset', handler: async () => {
|
||||
if (!await confirmDialogService.show("Reset Workflows", "Are you sure you want to do this??"))
|
||||
return;
|
||||
eventBus.on(EventTypes.WorkflowInstanceBulkActionsLoading, async e => {
|
||||
e.bulkActions.push({
|
||||
name: 'Reset', text: 'Reset', handler: async () => {
|
||||
if (!await confirmDialogService.show("Reset Workflows", "Are you sure you want to do this??"))
|
||||
return;
|
||||
|
||||
alert('Resetting workflows!');
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Custom plugin that intercepts outgoing HTTP requests and their responses.
|
||||
function CustomHttpMiddlewarePlugin(elsaStudio) {
|
||||
const eventBus = elsaStudio.eventBus;
|
||||
|
||||
eventBus.on('http-client-created', e => {
|
||||
// Register a sample middleware.
|
||||
e.service.register({
|
||||
onRequest(request) {
|
||||
console.log('onRequest');
|
||||
return request;
|
||||
},
|
||||
|
||||
onResponse(response) {
|
||||
console.log('onResponse');
|
||||
return response;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
eventBus.on('workflow-imported', e => {
|
||||
console.log('workflow-imported');
|
||||
});
|
||||
}
|
||||
|
||||
// Simple plugin that listens for the 'workflow-model-changed' event.
|
||||
function WorkflowModelChangedListenerPlugin(elsaStudio) {
|
||||
const {eventBus} = elsaStudio;
|
||||
|
||||
eventBus.on('workflow-model-changed', e => {
|
||||
console.log(`Workflow model changed. New model: ${e}`)
|
||||
});
|
||||
}
|
||||
|
||||
// Plugin that completely customizes the activity editor for the WriteLine activity.
|
||||
function CustomActivityEditorPlugin(elsaStudio) {
|
||||
const {eventBus, getOrCreateProperty, htmlToElement} = elsaStudio;
|
||||
|
||||
// When the activity editor is about to be rendered.
|
||||
eventBus.on('activity-editor-displaying', e => {
|
||||
const {activityDescriptor, activityModel} = e;
|
||||
|
||||
// We only handle WriteLine activities.
|
||||
if(activityDescriptor.type !== 'WriteLine')
|
||||
return;
|
||||
|
||||
let tabs = [...e.tabs];
|
||||
|
||||
// Remove the 'Properties' tab:
|
||||
tabs = tabs.filter(x => x.tabName !== 'Properties');
|
||||
|
||||
// Create a custom tab.
|
||||
const customTab = {
|
||||
tabName: 'Custom Tab',
|
||||
renderContent: () => this.renderWriteLineEditor(activityDescriptor, activityModel)
|
||||
}
|
||||
|
||||
// Add the custom tab.
|
||||
tabs = [customTab, ...tabs];
|
||||
|
||||
// Update the tabs array.
|
||||
e.tabs = tabs;
|
||||
});
|
||||
|
||||
this.renderWriteLineEditor = (activityDescriptor, activityModel) => {
|
||||
const propertyEditor = document.createElement('elsa-property-editor');
|
||||
const propertyDescriptor = activityDescriptor.inputProperties.find(x => x.name === 'Text');
|
||||
const propertyModel = getOrCreateProperty(activityModel, propertyDescriptor.name);
|
||||
const defaultSyntax = propertyDescriptor.defaultSyntax || 'Literal';
|
||||
const currentValue = propertyModel.expressions[defaultSyntax] || '';
|
||||
const editorHtml = `<textarea class="focus:elsa-ring-blue-500 focus:elsa-border-blue-500 elsa-block elsa-w-full elsa-min-w-0 elsa-rounded-md sm:elsa-text-sm elsa-border-gray-300" rows="5">${currentValue}</textarea>`;
|
||||
const editorElement = htmlToElement(editorHtml);
|
||||
|
||||
propertyEditor.append(editorElement);
|
||||
propertyEditor.propertyDescriptor = propertyDescriptor;
|
||||
propertyEditor.propertyModel = propertyModel;
|
||||
|
||||
editorElement.addEventListener('change', (e) => {
|
||||
const input = e.currentTarget;
|
||||
propertyModel.expressions[defaultSyntax] = input.value;
|
||||
});
|
||||
|
||||
return propertyEditor;
|
||||
alert('Resetting workflows!');
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Custom plugin that intercepts outgoing HTTP requests and their responses.
|
||||
function CustomHttpMiddlewarePlugin(elsaStudio) {
|
||||
const eventBus = elsaStudio.eventBus;
|
||||
|
||||
eventBus.on('http-client-created', e => {
|
||||
// Register a sample middleware.
|
||||
e.service.register({
|
||||
onRequest(request) {
|
||||
console.log('onRequest');
|
||||
return request;
|
||||
},
|
||||
|
||||
onResponse(response) {
|
||||
console.log('onResponse');
|
||||
return response;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
eventBus.on('workflow-imported', e => {
|
||||
console.log('workflow-imported');
|
||||
});
|
||||
}
|
||||
|
||||
// Simple plugin that listens for the 'workflow-model-changed' event.
|
||||
function WorkflowModelChangedListenerPlugin(elsaStudio) {
|
||||
const {eventBus} = elsaStudio;
|
||||
|
||||
eventBus.on('workflow-model-changed', e => {
|
||||
console.log(`Workflow model changed. New model: ${e}`)
|
||||
});
|
||||
}
|
||||
|
||||
// Plugin that completely customizes the activity editor for the WriteLine activity.
|
||||
function CustomActivityEditorPlugin(elsaStudio) {
|
||||
const {eventBus, getOrCreateProperty, htmlToElement} = elsaStudio;
|
||||
|
||||
// When the activity editor is about to be rendered.
|
||||
eventBus.on('activity-editor-displaying', e => {
|
||||
const {activityDescriptor, activityModel} = e;
|
||||
|
||||
// We only handle WriteLine activities.
|
||||
if (activityDescriptor.type !== 'WriteLine')
|
||||
return;
|
||||
|
||||
let tabs = [...e.tabs];
|
||||
|
||||
// Remove the 'Properties' tab:
|
||||
tabs = tabs.filter(x => x.tabName !== 'Properties');
|
||||
|
||||
// Create a custom tab.
|
||||
const customTab = {
|
||||
tabName: 'Custom Tab',
|
||||
renderContent: () => this.renderWriteLineEditor(activityDescriptor, activityModel)
|
||||
}
|
||||
|
||||
// Add the custom tab.
|
||||
tabs = [customTab, ...tabs];
|
||||
|
||||
// Update the tabs array.
|
||||
e.tabs = tabs;
|
||||
});
|
||||
|
||||
this.renderWriteLineEditor = (activityDescriptor, activityModel) => {
|
||||
const propertyEditor = document.createElement('elsa-property-editor');
|
||||
const propertyDescriptor = activityDescriptor.inputProperties.find(x => x.name === 'Text');
|
||||
const propertyModel = getOrCreateProperty(activityModel, propertyDescriptor.name);
|
||||
const defaultSyntax = propertyDescriptor.defaultSyntax || 'Literal';
|
||||
const currentValue = propertyModel.expressions[defaultSyntax] || '';
|
||||
const editorHtml = `<textarea class="focus:elsa-ring-blue-500 focus:elsa-border-blue-500 elsa-block elsa-w-full elsa-min-w-0 elsa-rounded-md sm:elsa-text-sm elsa-border-gray-300" rows="5">${currentValue}</textarea>`;
|
||||
const editorElement = htmlToElement(editorHtml);
|
||||
|
||||
propertyEditor.append(editorElement);
|
||||
propertyEditor.propertyDescriptor = propertyDescriptor;
|
||||
propertyEditor.propertyModel = propertyModel;
|
||||
|
||||
editorElement.addEventListener('change', (e) => {
|
||||
const input = e.currentTarget;
|
||||
propertyModel.expressions[defaultSyntax] = input.value;
|
||||
});
|
||||
|
||||
return propertyEditor;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom activity property type input control plugin:
|
||||
function CustomPropertyFieldPlugin(elsaStudio) {
|
||||
const {propertyDisplayManager} = elsaStudio;
|
||||
// Custom activity property type input control plugin:
|
||||
function CustomPropertyFieldPlugin(elsaStudio) {
|
||||
const {propertyDisplayManager} = elsaStudio;
|
||||
|
||||
// Register custom driver.
|
||||
propertyDisplayManager.addDriver('my-custom-property-type', () => new CustomPropertyFieldDriver(elsaStudio));
|
||||
}
|
||||
// Register custom driver.
|
||||
propertyDisplayManager.addDriver('my-custom-property-type', () => new CustomPropertyFieldDriver(elsaStudio));
|
||||
}
|
||||
|
||||
// Custom activity property type input control driver:
|
||||
function CustomPropertyFieldDriver(elsaStudio) {
|
||||
// Custom activity property type input control driver:
|
||||
function CustomPropertyFieldDriver(elsaStudio) {
|
||||
|
||||
// Get convenience methods.
|
||||
const {getOrCreateProperty, htmlToElement} = elsaStudio;
|
||||
// Get convenience methods.
|
||||
const {getOrCreateProperty, htmlToElement} = elsaStudio;
|
||||
|
||||
this.display = (activity, propertyDescriptor) => {
|
||||
this.display = (activity, propertyDescriptor) => {
|
||||
|
||||
// Get the property model.
|
||||
const propertyModel = getOrCreateProperty(activity, propertyDescriptor.name);
|
||||
// Get the property model.
|
||||
const propertyModel = getOrCreateProperty(activity, propertyDescriptor.name);
|
||||
|
||||
// Get the configured default syntax name.
|
||||
const defaultSyntax = propertyDescriptor.defaultSyntax || 'Literal';
|
||||
// Get the configured default syntax name.
|
||||
const defaultSyntax = propertyDescriptor.defaultSyntax || 'Literal';
|
||||
|
||||
// Get the current property value for the default syntax.
|
||||
const currentValue = propertyModel.expressions[defaultSyntax] || '';
|
||||
// Get the current property value for the default syntax.
|
||||
const currentValue = propertyModel.expressions[defaultSyntax] || '';
|
||||
|
||||
// Create a property editor element (for displaying label, hint and syntax toggle).
|
||||
// This will wrap our custom control.
|
||||
const propertyEditor = document.createElement('elsa-property-editor');
|
||||
// Create a property editor element (for displaying label, hint and syntax toggle).
|
||||
// This will wrap our custom control.
|
||||
const propertyEditor = document.createElement('elsa-property-editor');
|
||||
|
||||
// Our custom input element control. Can be anything you want.
|
||||
// Using HTML string to easily construct an actual element object.
|
||||
// Better yet would be to implement a component with Stencil, Angular or React if you;re using any of these frameworks.
|
||||
const inputHtml =
|
||||
`<input type="text"
|
||||
// Our custom input element control. Can be anything you want.
|
||||
// Using HTML string to easily construct an actual element object.
|
||||
// Better yet would be to implement a component with Stencil, Angular or React if you;re using any of these frameworks.
|
||||
const inputHtml =
|
||||
`<input type="text"
|
||||
class="disabled:elsa-opacity-50 disabled:elsa-cursor-not-allowed focus:elsa-ring-blue-500 focus:elsa-border-blue-500 elsa-block elsa-w-full elsa-min-w-0 elsa-rounded-md sm:elsa-text-sm elsa-border-gray-300"
|
||||
value="${currentValue}"
|
||||
/>`;
|
||||
|
||||
// Create an actual input element from the HTML string.
|
||||
const inputElement = htmlToElement(inputHtml);
|
||||
// Create an actual input element from the HTML string.
|
||||
const inputElement = htmlToElement(inputHtml);
|
||||
|
||||
// Add the custom input control element to the property editor as a child.
|
||||
propertyEditor.append(inputElement);
|
||||
// Add the custom input control element to the property editor as a child.
|
||||
propertyEditor.append(inputElement);
|
||||
|
||||
// Initialize the property editor.
|
||||
propertyEditor.propertyDescriptor = propertyDescriptor;
|
||||
propertyEditor.propertyModel = propertyModel;
|
||||
// Initialize the property editor.
|
||||
propertyEditor.propertyDescriptor = propertyDescriptor;
|
||||
propertyEditor.propertyModel = propertyModel;
|
||||
|
||||
// Setup change handler for custom control that updates the property model.
|
||||
inputElement.addEventListener('change', (e) => {
|
||||
const input = e.currentTarget;
|
||||
propertyModel.expressions[defaultSyntax] = input.value;
|
||||
});
|
||||
// Setup change handler for custom control that updates the property model.
|
||||
inputElement.addEventListener('change', (e) => {
|
||||
const input = e.currentTarget;
|
||||
propertyModel.expressions[defaultSyntax] = input.value;
|
||||
});
|
||||
|
||||
// return the created custom control.
|
||||
return propertyEditor;
|
||||
};
|
||||
}
|
||||
// return the created custom control.
|
||||
return propertyEditor;
|
||||
};
|
||||
}
|
||||
|
||||
// Get a handle to the elsa-studio-root element.
|
||||
const elsaStudioRoot = document.querySelector('elsa-studio-root');
|
||||
// Get a handle to the elsa-studio-root element.
|
||||
const elsaStudioRoot = document.querySelector('elsa-studio-root');
|
||||
|
||||
// Configure Elsa.
|
||||
// (async () => {
|
||||
// // Wait until the component is available.
|
||||
// await customElements.whenDefined('elsa-studio-root');
|
||||
//
|
||||
// // Install plugins.
|
||||
// await elsaStudioRoot.addPlugins([CustomReadLinePlugin, CustomBulkActionsPlugin, CustomHttpMiddlewarePlugin, CustomPropertyFieldPlugin]);
|
||||
// })();
|
||||
// Configure Elsa.
|
||||
// (async () => {
|
||||
// // Wait until the component is available.
|
||||
// await customElements.whenDefined('elsa-studio-root');
|
||||
//
|
||||
// // Install plugins.
|
||||
// await elsaStudioRoot.addPlugins([CustomReadLinePlugin, CustomBulkActionsPlugin, CustomHttpMiddlewarePlugin, CustomPropertyFieldPlugin]);
|
||||
// })();
|
||||
|
||||
// Alternatively, configure Elsa during the 'initializing' event.
|
||||
elsaStudioRoot.addEventListener('initializing', e => {
|
||||
const elsa = e.detail;
|
||||
elsa.pluginManager.registerPlugins([CustomReadLinePlugin, CustomBulkActionsPlugin, CustomHttpMiddlewarePlugin, CustomPropertyFieldPlugin, WorkflowModelChangedListenerPlugin, CustomActivityEditorPlugin]);
|
||||
});
|
||||
// Alternatively, configure Elsa during the 'initializing' event.
|
||||
elsaStudioRoot.addEventListener('initializing', e => {
|
||||
const elsa = e.detail;
|
||||
elsa.pluginManager.registerPlugins([CustomReadLinePlugin, CustomBulkActionsPlugin, CustomHttpMiddlewarePlugin, CustomPropertyFieldPlugin, WorkflowModelChangedListenerPlugin, CustomActivityEditorPlugin]);
|
||||
});
|
||||
|
||||
// Some components publish DOM events that we can handle directly:
|
||||
elsaStudioRoot.addEventListener('workflow-changed', e => {
|
||||
console.log('Workflow model changed! New model: ${e.detail}');
|
||||
})
|
||||
// Some components publish DOM events that we can handle directly:
|
||||
elsaStudioRoot.addEventListener('workflow-changed', e => {
|
||||
console.log('Workflow model changed! New model: ${e.detail}');
|
||||
})
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.9" />
|
||||
<PackageReference Include="System.Text.Encodings.Web" Version="5.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Http\Elsa.Activities.Http.csproj" />
|
||||
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using Elsa.Samples.HttpEndpointSecurity.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Elsa.Samples.HttpEndpointSecurity.Endpoints.Tokens
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/tokens")]
|
||||
public class Create : Controller
|
||||
{
|
||||
private readonly ITokenService _tokenService;
|
||||
public Create(ITokenService tokenService) => _tokenService = tokenService;
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult Handle(CreateTokenRequestModel model)
|
||||
{
|
||||
var token = _tokenService.CreateToken(model.UserName, model.HasMagic);
|
||||
return Ok(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
namespace Elsa.Samples.HttpEndpointSecurity.Endpoints.Tokens
|
||||
{
|
||||
public record CreateTokenRequestModel(string UserName, bool HasMagic);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Elsa.Samples.HttpEndpointSecurity.Options
|
||||
{
|
||||
public class JwtOptions
|
||||
{
|
||||
public string SecretKey { get; set; }
|
||||
public string Issuer { get; set; }
|
||||
public string Audience { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Elsa.Samples.HttpEndpointSecurity
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args) => CreateHostBuilder(args).Build().Run();
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:18601",
|
||||
"sslPort": 44326
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"Elsa.Samples.HttpEndpointSecurity": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": false,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
namespace Elsa.Samples.HttpEndpointSecurity.Services
|
||||
{
|
||||
public interface ITokenService
|
||||
{
|
||||
string CreateToken(string userName, bool hasMagic);
|
||||
bool ValidateToken(string token);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Elsa.Samples.HttpEndpointSecurity.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Elsa.Samples.HttpEndpointSecurity.Services
|
||||
{
|
||||
public class TokenService : ITokenService
|
||||
{
|
||||
private readonly JwtOptions _options;
|
||||
|
||||
public TokenService(IOptions<JwtOptions> options)
|
||||
{
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public string CreateToken(string userName, bool hasMagic)
|
||||
{
|
||||
var claims = new List<Claim>()
|
||||
{
|
||||
new(JwtRegisteredClaimNames.Sub, userName),
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
};
|
||||
|
||||
if (hasMagic)
|
||||
claims.Add(new Claim("has-magic", "true"));
|
||||
|
||||
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.SecretKey));
|
||||
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
||||
var tokenDescriptor = new JwtSecurityToken(_options.Issuer, _options.Audience, claims, expires: DateTime.Now.AddYears(1), signingCredentials: credentials);
|
||||
return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
|
||||
}
|
||||
|
||||
public bool ValidateToken(string token)
|
||||
{
|
||||
var mySecret = Encoding.UTF8.GetBytes(_options.SecretKey);
|
||||
var mySecurityKey = new SymmetricSecurityKey(mySecret);
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
try
|
||||
{
|
||||
tokenHandler.ValidateToken(token, new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidIssuer = _options.Issuer,
|
||||
ValidAudience = _options.Audience,
|
||||
IssuerSigningKey = mySecurityKey,
|
||||
}, out SecurityToken validatedToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Text;
|
||||
using Elsa.Samples.HttpEndpointSecurity.Options;
|
||||
using Elsa.Samples.HttpEndpointSecurity.Services;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Elsa.Samples.HttpEndpointSecurity
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; set; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// Controllers.
|
||||
services.AddControllers();
|
||||
|
||||
// Authentication & Authorization.
|
||||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
|
||||
|
||||
services
|
||||
.AddAuthentication(auth =>
|
||||
{
|
||||
auth.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = Configuration["Jwt:Issuer"],
|
||||
ValidAudience = Configuration["Jwt:Audience"],
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SecretKey"])),
|
||||
NameClaimType = JwtRegisteredClaimNames.Sub,
|
||||
};
|
||||
});
|
||||
|
||||
// Add a custom policy.
|
||||
services
|
||||
.AddAuthorization(auth => auth
|
||||
.AddPolicy("HasMagic", policy => policy
|
||||
.RequireClaim("has-magic", "true")));
|
||||
|
||||
services.Configure<JwtOptions>(options => Configuration.GetSection("Jwt").Bind(options));
|
||||
|
||||
// Elsa.
|
||||
services
|
||||
.AddElsa(elsa => elsa
|
||||
.AddHttpActivities(http => Configuration.GetSection("Elsa:Server").Bind(http))
|
||||
.AddWorkflowsFrom<Startup>()
|
||||
);
|
||||
|
||||
// Application Services.
|
||||
services.AddSingleton<ITokenService, TokenService>();
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
app
|
||||
.UseRouting()
|
||||
.UseAuthentication()
|
||||
.UseAuthorization()
|
||||
.UseHttpActivities()
|
||||
.UseEndpoints(endpoints => endpoints.MapControllers());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
using System.Net;
|
||||
using Elsa.Activities.Http;
|
||||
using Elsa.Builders;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.Samples.HttpEndpointSecurity.Workflows
|
||||
{
|
||||
public class SecureHelloWorkflow : IWorkflow
|
||||
{
|
||||
public void Build(IWorkflowBuilder builder)
|
||||
{
|
||||
builder
|
||||
.HttpEndpoint(setup => setup
|
||||
.WithPath("/safe-hello")
|
||||
.WithMethod("GET")
|
||||
.WithAuthorize()
|
||||
.WithPolicy("HasMagic"))
|
||||
.WriteHttpResponse(setup => setup.WithStatusCode(HttpStatusCode.OK)
|
||||
.WithContent(context =>
|
||||
{
|
||||
var httpContext = context.GetService<IHttpContextAccessor>().HttpContext!;
|
||||
var user = httpContext.User;
|
||||
return $"Hello {user.Identity!.Name}!";
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"Jwt": {
|
||||
"SecretKey": "This is where you should specify your secret key. A secret key is used to sign and verify Jwt tokens.",
|
||||
"Issuer": "your-identity-server",
|
||||
"Audience": "your-api-server"
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Elsa": {
|
||||
"Server": {
|
||||
"BaseUrl": "https://localhost:5001",
|
||||
"BasePath": "/workflows"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
POST https://localhost:5001/api/tokens
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"userName": "Jason",
|
||||
"hasMagic": false
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
POST https://localhost:5001/api/tokens
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"userName": "Janet",
|
||||
"hasMagic": true
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# Jason does not have magic.
|
||||
GET https://localhost:5001/workflows/safe-hello
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJKYXNvbiIsImp0aSI6IjU3ZDUzOGE4LWQwNmYtNDg2Zi05MzAzLTMyODIxMmZlOWI4MCIsImV4cCI6MTY2MDM5NDg3NSwiaXNzIjoieW91ci1pZGVudGl0eS1zZXJ2ZXIiLCJhdWQiOiJ5b3VyLWFwaS1zZXJ2ZXIifQ.-6mjl-AMXLVSKrM7ofySNbGdf7YgvUmWILloAXB56q8
|
||||
|
||||
###
|
||||
|
||||
# Janet does have magic.
|
||||
GET https://localhost:5001/workflows/safe-hello
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJKYW5ldCIsImp0aSI6IjNhMmNlZDg5LWQzYWMtNGQ5NC04YzQzLTU2OWZhZmE1YjYwMCIsImhhcy1tYWdpYyI6InRydWUiLCJleHAiOjE2NjAzOTUyNTksImlzcyI6InlvdXItaWRlbnRpdHktc2VydmVyIiwiYXVkIjoieW91ci1hcGktc2VydmVyIn0.bdBXGdqmiWr5mLcgAvjiKVNxcBgr5w63TJ0OLwZ3kf4
|
||||
|
||||
###
|
||||
Loading…
Reference in a new issue