Implement Activity State Filtering and JavaScript Integration (#5993)
* Add secret scripting integration for JavaScript Introduced a new `Elsa.Secrets.Scripting` module that provides secret management capabilities within JavaScript workflows. This includes configuring the Jint engine to use workflow variables, adding new type and variable definition providers, and integrating with existing secret management features. * Refactor secret name extraction to a separate method Moved the logic for extracting secret names from the main method to a dedicated private method `GetSecretNamesFromExpression`. This improves code readability and maintains the single responsibility principle by delegating secret name extraction to its own method. * Add input evaluation, sensitive input handling, and middleware refactor Introduced methods for evaluating activity input properties and handling inputs marked as sensitive. Refactored `ExecutionLogMiddleware` constructor for consistency. Enhanced `SendHttpRequestBase` to mark authorization inputs as potentially containing secrets. Removed obsolete entries and adjusted persistence logic for clarity. * Refactor IActivityStateProtector interface Remove unused using directives and unnecessary comments. Simplify the definition of the `ProtectedActivityStateContext` record. * Add activity state filtering mechanism Introduce an abstract filter base class, context, and result models to enable filtering of activity state. Implement a default filter manager to run these filters and apply a specific filter for obfuscating HTTP request headers. Update necessary dependencies and extension methods to integrate the new filtering functionality. * Add expired secrets management Implemented services to manage expired secrets by periodically checking and updating their status. Introduced a new hosted service to perform the sweep and configurable options for the sweep interval. Updated related classes and configurations accordingly. * Update SweepInterval in appsettings.json Changed the Secrets Management SweepInterval from 30 seconds to 4 hours. This adjustment aims to reduce the frequency of sweep operations and improve overall system performance. * Update comment to reflect configuring engine with secrets The comment was changed to better describe the handler's function, specifying that it configures the Jint engine with secrets instead of workflow variables. This clarifies the purpose and usage of the handler in the context of the code. * Remove unused inputDescriptors variable This commit removes the inputDescriptors variable, which was declared but never used in DefaultActivityExecutionMapper.cs. This helps in cleaning up the code and potentially reducing memory usage. Ensuring that all declared variables are utilized can improve code readability and maintainability.
This commit is contained in:
parent
12f947cf9b
commit
bbedd61138
|
|
@ -95,7 +95,6 @@
|
|||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Linq.Dynamic.Core" Version="1.4.3" />
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageVersion Include="Testcontainers" Version="3.9.0" />
|
||||
<PackageVersion Include="Testcontainers.RabbitMq" Version="3.9.0" />
|
||||
<PackageVersion Include="Testcontainers.Redis" Version="3.9.0" />
|
||||
|
|
|
|||
7
Elsa.sln
7
Elsa.sln
|
|
@ -466,6 +466,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Agents.Persistence.Ent
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Secrets.Models", "src\modules\Elsa.Secrets.Models\Elsa.Secrets.Models.csproj", "{29D12ADC-55E9-40D0-9E4C-F0EBB6E098EC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Secrets.Scripting", "src\modules\Elsa.Secrets.Scripting\Elsa.Secrets.Scripting.csproj", "{6C606FEB-9A1F-4816-ABE4-22AFA8CEE771}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -1058,6 +1060,10 @@ Global
|
|||
{29D12ADC-55E9-40D0-9E4C-F0EBB6E098EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{29D12ADC-55E9-40D0-9E4C-F0EBB6E098EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{29D12ADC-55E9-40D0-9E4C-F0EBB6E098EC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6C606FEB-9A1F-4816-ABE4-22AFA8CEE771}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6C606FEB-9A1F-4816-ABE4-22AFA8CEE771}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6C606FEB-9A1F-4816-ABE4-22AFA8CEE771}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6C606FEB-9A1F-4816-ABE4-22AFA8CEE771}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -1243,6 +1249,7 @@ Global
|
|||
{A9140976-DFF9-432A-B1DC-E188A07C49E5} = {50470834-4CD8-479A-8B58-0A1869BA5D37}
|
||||
{B3046301-6F00-4885-8B01-080BD489055C} = {50470834-4CD8-479A-8B58-0A1869BA5D37}
|
||||
{29D12ADC-55E9-40D0-9E4C-F0EBB6E098EC} = {8CEEC194-820A-4C8D-AB9E-E51E6D3E9CC1}
|
||||
{6C606FEB-9A1F-4816-ABE4-22AFA8CEE771} = {8CEEC194-820A-4C8D-AB9E-E51E6D3E9CC1}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
<ProjectReference Include="..\..\modules\Elsa.OpenTelemetry\Elsa.OpenTelemetry.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Secrets.Api\Elsa.Secrets.Api.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite\Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Secrets.Scripting\Elsa.Secrets.Scripting.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa\Elsa.csproj"/>
|
||||
<ProjectReference Include="..\..\common\Elsa.DropIns\Elsa.DropIns.csproj"/>
|
||||
<ProjectReference Include="..\..\modules\Elsa.Alterations.MassTransit\Elsa.Alterations.MassTransit.csproj"/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
using Elsa.Http;
|
||||
using Elsa.Workflows;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Server.Web.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// Mask the value of the input if it is a HttpRequest and the input name is "Authorization".
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class HttpRequestAuthenticationHeaderFilter : ActivityStateFilterBase
|
||||
{
|
||||
protected override ActivityStateFilterResult OnExecute(ActivityStateFilterContext context)
|
||||
{
|
||||
var activityExecutionContext = context.ActivityExecutionContext;
|
||||
var activity = activityExecutionContext.Activity;
|
||||
var inputDescriptor = context.InputDescriptor;
|
||||
|
||||
if (activity is not SendHttpRequestBase || inputDescriptor.Name is not nameof(SendHttpRequestBase.Authorization))
|
||||
return ActivityStateFilterResult.Pass();
|
||||
|
||||
var contextValue = context.Value.GetString();
|
||||
|
||||
if (contextValue == null)
|
||||
return ActivityStateFilterResult.Pass();
|
||||
|
||||
var maskedValue = new string('*', contextValue.Length);
|
||||
return Filtered(maskedValue);
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ using Elsa.OpenTelemetry.Middleware;
|
|||
using Elsa.Secrets.Extensions;
|
||||
using Elsa.Secrets.Persistence;
|
||||
using Elsa.Server.Web;
|
||||
using Elsa.Server.Web.Filters;
|
||||
using Elsa.Tenants.Extensions;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Api;
|
||||
|
|
@ -436,8 +437,13 @@ services
|
|||
{
|
||||
elsa
|
||||
.UseSecrets()
|
||||
.UseSecretsManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
|
||||
.UseSecretsManagement(management =>
|
||||
{
|
||||
management.ConfigureOptions(options => configuration.GetSection("Secrets:Management").Bind(options));
|
||||
management.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString));
|
||||
})
|
||||
.UseSecretsApi()
|
||||
.UseSecretsScripting()
|
||||
;
|
||||
}
|
||||
|
||||
|
|
@ -463,6 +469,9 @@ services
|
|||
ConfigureForTest?.Invoke(elsa);
|
||||
});
|
||||
|
||||
// Obfuscate HTTP request headers.
|
||||
services.AddActivityStateFilter<HttpRequestAuthenticationHeaderFilter>();
|
||||
|
||||
//services.Configure<CachingOptions>(options => options.CacheDuration = TimeSpan.FromDays(1));
|
||||
services.AddHealthChecks();
|
||||
services.AddControllers();
|
||||
|
|
|
|||
|
|
@ -173,6 +173,11 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"Secrets": {
|
||||
"Management": {
|
||||
"SweepInterval": "04:00:00"
|
||||
}
|
||||
},
|
||||
"Agents": {
|
||||
"ApiKeys": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,18 +15,12 @@ namespace Elsa.Http;
|
|||
/// Base class for activities that send HTTP requests.
|
||||
/// </summary>
|
||||
[Output(IsSerializable = false)]
|
||||
public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
|
||||
public abstract class SendHttpRequestBase(string? source = default, int? line = default) : Activity<HttpResponseMessage>(source, line)
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected SendHttpRequestBase(string? source = default, int? line = default) : base(source, line)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The URL to send the request to.
|
||||
/// </summary>
|
||||
[Input]
|
||||
public Input<Uri?> Url { get; set; } = default!;
|
||||
[Input] public Input<Uri?> Url { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP method to use when sending the request.
|
||||
|
|
@ -62,7 +56,10 @@ public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
|
|||
/// The Authorization header value to send with the request.
|
||||
/// </summary>
|
||||
/// <example>Bearer {some-access-token}</example>
|
||||
[Input(Description = "The Authorization header value to send with the request. For example: Bearer {some-access-token}", Category = "Security")]
|
||||
[Input(
|
||||
Description = "The Authorization header value to send with the request. For example: Bearer {some-access-token}",
|
||||
Category = "Security",
|
||||
CanContainSecrets = true)]
|
||||
public Input<string?> Authorization { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@ namespace Elsa.JavaScript.Notifications;
|
|||
/// <summary>
|
||||
/// This notification is published every time a JavaScript expression has been evaluated.
|
||||
/// </summary>
|
||||
public record EvaluatedJavaScript(Engine Engine, ExpressionExecutionContext Context, object? Result) : INotification;
|
||||
public record EvaluatedJavaScript(Engine Engine, ExpressionExecutionContext Context, string Expression, object? Result) : INotification;
|
||||
|
|
@ -8,4 +8,4 @@ namespace Elsa.JavaScript.Notifications;
|
|||
/// This notification is published every time a JavaScript expression is about to be evaluated.
|
||||
/// It gives subscribers a chance to configure the <see cref="Engine"/> with additional functions and variables.
|
||||
/// </summary>
|
||||
public record EvaluatingJavaScript(Engine Engine, ExpressionExecutionContext Context) : INotification;
|
||||
public record EvaluatingJavaScript(Engine Engine, ExpressionExecutionContext Context, string Expression) : INotification;
|
||||
|
|
@ -37,9 +37,9 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification
|
|||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var engine = await GetConfiguredEngine(configureEngine, context, options, cancellationToken);
|
||||
await mediator.SendAsync(new EvaluatingJavaScript(engine, context), cancellationToken);
|
||||
await mediator.SendAsync(new EvaluatingJavaScript(engine, context, expression), cancellationToken);
|
||||
var result = ExecuteExpressionAndGetResult(engine, expression);
|
||||
await mediator.SendAsync(new EvaluatedJavaScript(engine, context, result), cancellationToken);
|
||||
await mediator.SendAsync(new EvaluatedJavaScript(engine, context, expression, result), cancellationToken);
|
||||
|
||||
return result.ConvertTo(returnType);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@ public abstract class VariableDefinitionProvider : IVariableDefinitionProvider
|
|||
{
|
||||
var builder = new VariableDefinitionBuilder();
|
||||
setup(builder);
|
||||
return builder.BuildVariableDefinition();
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
|
|
@ -18,5 +18,5 @@ public class VariableDefinitionBuilder
|
|||
return this;
|
||||
}
|
||||
|
||||
public VariableDefinition BuildVariableDefinition() => new(_variableDefinition.Name, _variableDefinition.Type);
|
||||
public VariableDefinition Build() => new(_variableDefinition.Name, _variableDefinition.Type);
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ public class Endpoint(ISecretManager manager, ISecretNameValidator nameValidator
|
|||
return null!;
|
||||
}
|
||||
|
||||
var isNameDuplicate = !await nameValidator.IsNameUniqueAsync(req.Name, id, ct);
|
||||
var isNameDuplicate = !await nameValidator.IsNameUniqueAsync(req.Name, entity.SecretId, ct);
|
||||
|
||||
if (isNameDuplicate)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace Elsa.Secrets.Management;
|
||||
|
||||
public interface IExpiredSecretsUpdater
|
||||
{
|
||||
Task UpdateExpiredSecretsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -16,6 +16,9 @@ public interface ISecretManager
|
|||
/// Finds the entity from the store.
|
||||
Task<Secret?> FindAsync(SecretFilter filter, CancellationToken cancellationToken = default);
|
||||
|
||||
/// Finds all entities from the store matching the specified filter.
|
||||
Task<IEnumerable<Secret>> FindManyAsync(SecretFilter filter, CancellationToken cancellationToken = default);
|
||||
|
||||
/// Gets all entities from the store.
|
||||
Task<IEnumerable<Secret>> ListAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using Elsa.Features.Attributes;
|
|||
using Elsa.Features.Services;
|
||||
using Elsa.Secrets.Extensions;
|
||||
using Elsa.Secrets.Features;
|
||||
using Elsa.Secrets.Management.HostedService;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Secrets.Management.Features;
|
||||
|
|
@ -21,6 +22,12 @@ public class SecretManagementFeature(IModule module) : FeatureBase(module)
|
|||
return this;
|
||||
}
|
||||
|
||||
public SecretManagementFeature ConfigureOptions(Action<SecretManagementOptions> configureOptions)
|
||||
{
|
||||
Services.Configure(configureOptions);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Module.UseSecrets(secrets =>
|
||||
|
|
@ -29,6 +36,11 @@ public class SecretManagementFeature(IModule module) : FeatureBase(module)
|
|||
});
|
||||
}
|
||||
|
||||
public override void ConfigureHostedServices()
|
||||
{
|
||||
ConfigureHostedService<ExpiredSecretsHostedService>();
|
||||
}
|
||||
|
||||
public override void Apply()
|
||||
{
|
||||
Services
|
||||
|
|
@ -43,6 +55,7 @@ public class SecretManagementFeature(IModule module) : FeatureBase(module)
|
|||
.AddScoped<ISecretNameValidator, DefaultSecretNameValidator>()
|
||||
.AddScoped<ISecretUpdater, DefaultSecretUpdater>()
|
||||
.AddScoped<ISecretManager, DefaultSecretManager>()
|
||||
.AddScoped<IExpiredSecretsUpdater, DefaultExpiredSecretsUpdater>()
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
using JetBrains.Annotations;
|
||||
using Medallion.Threading;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Secrets.Management.HostedService;
|
||||
|
||||
[UsedImplicitly]
|
||||
public class ExpiredSecretsHostedService(IOptions<SecretManagementOptions> options, IDistributedLockProvider distributedLockProvider, IServiceScopeFactory scopeFactory, ILogger<ExpiredSecretsHostedService> logger) : BackgroundService
|
||||
{
|
||||
private Timer _timer = default!;
|
||||
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Get the configured sweep interval from the options, and use it to periodically sweep expired secrets.
|
||||
var sweepInterval = options.Value.SweepInterval;
|
||||
|
||||
// Set up a timer that will sweep expired secrets at the configured interval.
|
||||
_timer = new Timer(SweepExpiredSecrets, null, sweepInterval, sweepInterval);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_timer.Change(Timeout.Infinite, 0);
|
||||
_timer.Dispose();
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async void SweepExpiredSecrets(object? state)
|
||||
{
|
||||
// Acquire a distributed lock to ensure that only one instance of the hosted service is running at any given time.
|
||||
await using var distributedLock = await distributedLockProvider.TryAcquireLockAsync("expired-secrets-sweep");
|
||||
|
||||
// If the lock could not be acquired, return a completed task.
|
||||
if (distributedLock == null)
|
||||
{
|
||||
logger.LogInformation("Another instance of the expired secrets hosted service is already running. Exiting...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sweep expired secrets here.
|
||||
logger.LogInformation("Sweeping expired secrets...");
|
||||
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var updater = scope.ServiceProvider.GetRequiredService<IExpiredSecretsUpdater>();
|
||||
await updater.UpdateExpiredSecretsAsync();
|
||||
|
||||
logger.LogInformation("Expired secrets have been swept.");
|
||||
}
|
||||
}
|
||||
|
|
@ -6,24 +6,30 @@ public class SecretFilter
|
|||
public ICollection<string>? Ids { get; set; }
|
||||
public string? SecretId { get; set; }
|
||||
public ICollection<string>? SecretIds { get; set; }
|
||||
public string? NotId { get; set; }
|
||||
public string? NotSecretId { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public ICollection<string>? Names { get; set; }
|
||||
public int? Version { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public SecretStatus? Status { get; set; }
|
||||
public bool IsLatest { get; set; }
|
||||
public string? SearchTerm { get; set; }
|
||||
public DateTimeOffset? ExpiresAtLessThan { get; set; }
|
||||
|
||||
public IQueryable<Secret> Apply(IQueryable<Secret> queryable)
|
||||
{
|
||||
if (Id != null) queryable = queryable.Where(x => x.Id == Id);
|
||||
if (Ids != null) queryable = queryable.Where(x => Ids.Contains(x.Id));
|
||||
if (NotId != null) queryable = queryable.Where(x => x.Id != NotId);
|
||||
if (SecretId != null) queryable = queryable.Where(x => x.SecretId == SecretId);
|
||||
if (SecretIds != null) queryable = queryable.Where(x => SecretIds.Contains(x.SecretId));
|
||||
if (NotSecretId != null) queryable = queryable.Where(x => x.SecretId != NotSecretId);
|
||||
if (Name != null) queryable = queryable.Where(x => x.Name == Name);
|
||||
if (Names != null) queryable = queryable.Where(x => Names.Contains(x.Name));
|
||||
if (Version != null) queryable = queryable.Where(x => x.Version == Version);
|
||||
if (Type != null) queryable = queryable.Where(x => x.Scope == Type);
|
||||
if(Status != null) queryable = queryable.Where(x => x.Status == Status);
|
||||
if (IsLatest) queryable = queryable.Where(x => x.IsLatest);
|
||||
if (ExpiresAtLessThan != null) queryable = queryable.Where(x => x.ExpiresAt < ExpiresAtLessThan);
|
||||
if (!string.IsNullOrWhiteSpace(SearchTerm)) queryable = queryable.Where(x => x.Name.Contains(SearchTerm) || x.Description.Contains(SearchTerm) || x.Id.Contains(SearchTerm));
|
||||
|
||||
return queryable;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Elsa.Secrets.Management;
|
||||
|
||||
public class SecretManagementOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The interval at which the background sweep should run for expired secrets.
|
||||
/// </summary>
|
||||
public TimeSpan SweepInterval { get; set; } = TimeSpan.FromHours(12);
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
using System.Security.Cryptography;
|
||||
|
||||
namespace Elsa.Secrets.Management;
|
||||
|
||||
public class EncryptionKeyProviderOptions
|
||||
{
|
||||
public string Key { get; set; } = default!;
|
||||
public string IV { get; set; } = default!;
|
||||
public string Algorithm { get; set; } = nameof(Aes);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using Elsa.Common.Contracts;
|
||||
|
||||
namespace Elsa.Secrets.Management;
|
||||
|
||||
public class DefaultExpiredSecretsUpdater(ISecretStore store, ISystemClock systemClock) : IExpiredSecretsUpdater
|
||||
{
|
||||
public async Task UpdateExpiredSecretsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = systemClock.UtcNow;
|
||||
|
||||
var filter = new SecretFilter
|
||||
{
|
||||
Status = SecretStatus.Active,
|
||||
ExpiresAtLessThan = now
|
||||
};
|
||||
|
||||
var secrets = (await store.FindManyAsync(filter, cancellationToken)).ToList();
|
||||
|
||||
foreach (var secret in secrets)
|
||||
{
|
||||
secret.Status = SecretStatus.Expired;
|
||||
secret.UpdatedAt = now;
|
||||
await store.UpdateAsync(secret, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -51,6 +51,11 @@ public class DefaultSecretManager(ISecretStore store, IEncryptor encryptor, ISec
|
|||
return store.FindAsync(filter, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<Secret>> FindManyAsync(SecretFilter filter, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return store.FindManyAsync(filter, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<Secret>> ListAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return store.ListAsync(cancellationToken);
|
||||
|
|
@ -58,13 +63,24 @@ public class DefaultSecretManager(ISecretStore store, IEncryptor encryptor, ISec
|
|||
|
||||
public async Task DeleteAsync(Secret entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await store.DeleteAsync(entity, cancellationToken);
|
||||
var filter = new SecretFilter
|
||||
{
|
||||
SecretId = entity.SecretId
|
||||
};
|
||||
await store.DeleteManyAsync(filter, cancellationToken);
|
||||
await notificationSender.SendAsync(new SecretDeleted(entity), cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<long> DeleteManyAsync(SecretFilter filter, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var count = await store.DeleteManyAsync(filter, cancellationToken);
|
||||
var secretVersions = await store.FindManyAsync(filter, cancellationToken);
|
||||
var secretIds = secretVersions.Select(x => x.Id).Distinct().ToList();
|
||||
var allSecretsAndVersionsFilter = new SecretFilter
|
||||
{
|
||||
SecretIds = secretIds
|
||||
};
|
||||
|
||||
var count = await store.DeleteManyAsync(allSecretsAndVersionsFilter, cancellationToken);
|
||||
await notificationSender.SendAsync(new SecretsDeletedInBulk(), cancellationToken);
|
||||
return count;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ public class DefaultSecretNameValidator(ISecretStore store) : ISecretNameValidat
|
|||
var filter = new SecretFilter
|
||||
{
|
||||
Name = name,
|
||||
NotId = notId
|
||||
NotSecretId = notId
|
||||
};
|
||||
return await store.FindAsync(filter, cancellationToken) == null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
using Elsa.Common.Contracts;
|
||||
using Elsa.Workflows.Contracts;
|
||||
|
||||
namespace Elsa.Secrets.Management;
|
||||
|
||||
public class DefaultSecretUpdater(ISecretStore store, IEncryptor encryptor, ISystemClock systemClock) : ISecretUpdater
|
||||
public class DefaultSecretUpdater(ISecretStore store, IEncryptor encryptor, IIdentityGenerator identityGenerator, ISystemClock systemClock) : ISecretUpdater
|
||||
{
|
||||
public async Task<Secret> UpdateAsync(Secret secret, SecretInputModel input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
|
@ -15,17 +16,21 @@ public class DefaultSecretUpdater(ISecretStore store, IEncryptor encryptor, ISys
|
|||
|
||||
// There should always be at most one latest version of the secret, but we'll use FindManyAsync to be safe as a defensive programming measure.
|
||||
var currentLatestVersions = (await store.FindManyAsync(filter, cancellationToken)).OrderBy(x => x.Version).ToList();
|
||||
var currentLatestVersion = currentLatestVersions.Last();
|
||||
var currentLatestVersion = currentLatestVersions.LastOrDefault() ?? secret;
|
||||
|
||||
foreach (var version in currentLatestVersions)
|
||||
{
|
||||
version.IsLatest = false;
|
||||
version.Status = SecretStatus.Retired;
|
||||
|
||||
// Only retire the version if it's active.
|
||||
if(version.Status == SecretStatus.Active)
|
||||
version.Status = SecretStatus.Retired;
|
||||
await store.UpdateAsync(version, cancellationToken);
|
||||
}
|
||||
|
||||
var newVersion = secret.Clone();
|
||||
var encryptedValue = await encryptor.EncryptAsync(input.Value, cancellationToken);
|
||||
newVersion.Id = identityGenerator.GenerateId();
|
||||
newVersion.IsLatest = true;
|
||||
newVersion.Version = currentLatestVersion.Version + 1;
|
||||
newVersion.Name = input.Name.Trim();
|
||||
|
|
@ -36,7 +41,7 @@ public class DefaultSecretUpdater(ISecretStore store, IEncryptor encryptor, ISys
|
|||
newVersion.ExpiresAt = input.ExpiresIn != null ? systemClock.UtcNow + input.ExpiresIn.Value : null;
|
||||
newVersion.Status = SecretStatus.Active;
|
||||
|
||||
await store.UpdateAsync(newVersion, cancellationToken);
|
||||
await store.AddAsync(newVersion, cancellationToken);
|
||||
return newVersion;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,23 @@ namespace Elsa.Secrets;
|
|||
|
||||
public enum SecretStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The secret is active.
|
||||
/// </summary>
|
||||
Active,
|
||||
|
||||
/// <summary>
|
||||
/// The secret is retired due to an update which created a new version.
|
||||
/// </summary>
|
||||
Retired,
|
||||
|
||||
/// <summary>
|
||||
/// The secret has expired.
|
||||
/// </summary>
|
||||
Expired,
|
||||
|
||||
/// <summary>
|
||||
/// The secret has been revoked.
|
||||
/// </summary>
|
||||
Revoked
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Provides scripting integration with various languages like JavaScript, C#, Python and Liquid.</Description>
|
||||
<PackageTags>elsa module secrets scripting</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Elsa.JavaScript\Elsa.JavaScript.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Secrets.Management\Elsa.Secrets.Management.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using Elsa.Features.Services;
|
||||
using Elsa.Secrets.Management.Features;
|
||||
using Elsa.Secrets.Scripting.Features;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
public static IModule UseSecretsScripting(this IModule module, Action<SecretsScriptingFeature>? setup = null)
|
||||
{
|
||||
return module.Use(setup);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.JavaScript.Features;
|
||||
using Elsa.JavaScript.Extensions;
|
||||
using Elsa.Secrets.Management.Features;
|
||||
using Elsa.Secrets.Scripting.JavaScript;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Secrets.Scripting.Features;
|
||||
|
||||
[DependencyOf(typeof(SecretManagementFeature))]
|
||||
[DependencyOf(typeof(JavaScriptFeature))]
|
||||
public class SecretsScriptingFeature(IModule module) : FeatureBase(module)
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Services.AddHandlersFrom<SecretsScriptingFeature>();
|
||||
Services.AddScoped<SecretsTypeDefinitionProvider>();
|
||||
Services.AddTypeDefinitionProvider<SecretsTypeDefinitionProvider>(sp => sp.GetRequiredService<SecretsTypeDefinitionProvider>());
|
||||
Services.AddVariableDefinitionProvider<SecretsTypeDefinitionProvider>(sp => sp.GetRequiredService<SecretsTypeDefinitionProvider>());
|
||||
}
|
||||
}
|
||||
3
src/modules/Elsa.Secrets.Scripting/FodyWeavers.xml
Normal file
3
src/modules/Elsa.Secrets.Scripting/FodyWeavers.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
using System.Dynamic;
|
||||
using System.Text.RegularExpressions;
|
||||
using Elsa.JavaScript.Notifications;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Secrets.Management;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Secrets.Scripting.JavaScript;
|
||||
|
||||
/// A handler that configures the Jint engine with secrets.
|
||||
[UsedImplicitly]
|
||||
public partial class ConfigureEngineWithSecrets(ISecretManager secretManager, IDecryptor decryptor) : INotificationHandler<EvaluatingJavaScript>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken)
|
||||
{
|
||||
await CopySecretsIntoEngineAsync(notification, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task CopySecretsIntoEngineAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var engine = notification.Engine;
|
||||
var expression = notification.Expression;
|
||||
var secretNames = GetSecretNamesFromExpression(expression);
|
||||
|
||||
if (secretNames.Count == 0)
|
||||
return;
|
||||
|
||||
var filter = new SecretFilter
|
||||
{
|
||||
Names = secretNames,
|
||||
Status = SecretStatus.Active
|
||||
};
|
||||
var secrets = await secretManager.FindManyAsync(filter, cancellationToken);
|
||||
IDictionary<string, object?> secretsContainer = new ExpandoObject();
|
||||
|
||||
foreach (var secret in secrets)
|
||||
{
|
||||
var secretValue = await decryptor.DecryptAsync(secret.EncryptedValue, cancellationToken);
|
||||
secretsContainer[secret.Name] = secretValue;
|
||||
}
|
||||
|
||||
engine.SetValue("secrets", secretsContainer);
|
||||
}
|
||||
|
||||
private ICollection<string> GetSecretNamesFromExpression(string expression)
|
||||
{
|
||||
var secretNames = new List<string>();
|
||||
|
||||
#if NET6_0
|
||||
const string pattern = @"(?<=secrets\.)\w+";
|
||||
var matches = Regex.Matches(expression, pattern);
|
||||
#elif NET7_0_OR_GREATER
|
||||
var matches = SecretsRegex().Matches(expression);
|
||||
#endif
|
||||
|
||||
foreach (Match match in matches)
|
||||
secretNames.Add(match.Value);
|
||||
|
||||
return secretNames;
|
||||
}
|
||||
|
||||
#if NET7_0_OR_GREATER
|
||||
[GeneratedRegex(@"(?<=secrets\.)\w+")]
|
||||
private static partial Regex SecretsRegex();
|
||||
#endif
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
using Elsa.JavaScript.TypeDefinitions.Builders;
|
||||
using Elsa.JavaScript.TypeDefinitions.Contracts;
|
||||
using Elsa.JavaScript.TypeDefinitions.Models;
|
||||
using Elsa.Secrets.Management;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Secrets.Scripting.JavaScript;
|
||||
|
||||
[UsedImplicitly]
|
||||
internal class SecretsTypeDefinitionProvider(ISecretManager secretManager) : ITypeDefinitionProvider, IVariableDefinitionProvider
|
||||
{
|
||||
public async ValueTask<IEnumerable<TypeDefinition>> GetTypeDefinitionsAsync(TypeDefinitionContext context)
|
||||
{
|
||||
var cancellationToken = context.CancellationToken;
|
||||
var filter = new SecretFilter
|
||||
{
|
||||
Status = SecretStatus.Active
|
||||
};
|
||||
var secrets = await secretManager.FindManyAsync(filter, cancellationToken);
|
||||
|
||||
var secretsContainerClass = new TypeDefinition
|
||||
{
|
||||
Name = "SecretVariables",
|
||||
DeclarationKeyword = "class"
|
||||
};
|
||||
|
||||
foreach (var secret in secrets)
|
||||
{
|
||||
secretsContainerClass.Properties.Add(new PropertyDefinition
|
||||
{
|
||||
Name = secret.Name,
|
||||
Type = "string"
|
||||
});
|
||||
}
|
||||
|
||||
return [secretsContainerClass];
|
||||
}
|
||||
|
||||
public ValueTask<IEnumerable<VariableDefinition>> GetVariableDefinitionsAsync(TypeDefinitionContext context)
|
||||
{
|
||||
var definitions = new List<VariableDefinition>
|
||||
{
|
||||
new VariableDefinitionBuilder().Name("secrets").Type("SecretVariables").Build()
|
||||
};
|
||||
|
||||
|
||||
return new (definitions);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
[UsedImplicitly]
|
||||
public abstract class ActivityStateFilterBase : IActivityStateFilter
|
||||
{
|
||||
protected virtual Task<ActivityStateFilterResult> OnExecuteAsync(ActivityStateFilterContext context)
|
||||
{
|
||||
var result = OnExecute(context);
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
protected virtual ActivityStateFilterResult OnExecute(ActivityStateFilterContext context)
|
||||
{
|
||||
return Pass();
|
||||
}
|
||||
|
||||
protected ActivityStateFilterResult Pass() => ActivityStateFilterResult.Pass();
|
||||
protected ActivityStateFilterResult Filtered(string filteredValue) => ActivityStateFilterResult.Filtered(filteredValue);
|
||||
|
||||
Task<ActivityStateFilterResult> IActivityStateFilter.ExecuteAsync(ActivityStateFilterContext context)
|
||||
{
|
||||
return OnExecuteAsync(context);
|
||||
}
|
||||
}
|
||||
|
|
@ -85,6 +85,12 @@ public class InputAttribute : Attribute
|
|||
/// </summary>
|
||||
public bool IsSerializable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this input can contain secrets.
|
||||
/// When set to true, the input will be treated as a secret and will be encrypted, masked or otherwise protected, depending on the configured policy.
|
||||
/// </summary>
|
||||
public bool CanContainSecrets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IPropertyUIHandler"/> type that can be used to customize the UI for this property.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Workflows.Models;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
public record ActivityStateFilterContext(ActivityExecutionContext ActivityExecutionContext, InputDescriptor InputDescriptor, JsonElement Value, CancellationToken CancellationToken);
|
||||
|
|
@ -1,7 +1,13 @@
|
|||
namespace Elsa.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// The interface for activity execution middleware components.
|
||||
/// </summary>
|
||||
public interface IActivityExecutionMiddleware
|
||||
{
|
||||
/// <summary>
|
||||
/// The method that is called to execute the middleware.
|
||||
/// </summary>
|
||||
ValueTask InvokeAsync(ActivityExecutionContext context);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace Elsa.Workflows;
|
||||
|
||||
public interface IActivityStateFilter
|
||||
{
|
||||
Task<ActivityStateFilterResult> ExecuteAsync(ActivityStateFilterContext context);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace Elsa.Workflows;
|
||||
|
||||
public interface IActivityStateFilterManager
|
||||
{
|
||||
Task<string> RunFiltersAsync(ActivityStateFilterContext context);
|
||||
}
|
||||
|
|
@ -12,4 +12,5 @@
|
|||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=contracts/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=enums/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
using System.Linq.Expressions;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Helpers;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Contracts;
|
||||
using Elsa.Workflows.Models;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
public static partial class ActivityExecutionContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates each input property of the activity.
|
||||
/// </summary>
|
||||
public static async Task EvaluateInputPropertiesAsync(this ActivityExecutionContext context)
|
||||
{
|
||||
var activityDescriptor = context.ActivityDescriptor;
|
||||
var inputDescriptors = activityDescriptor.Inputs.Where(x => x.AutoEvaluate).ToList();
|
||||
|
||||
// Evaluate inputs.
|
||||
foreach (var inputDescriptor in inputDescriptors)
|
||||
await EvaluateInputPropertyAsync(context, activityDescriptor, inputDescriptor);
|
||||
|
||||
context.SetHasEvaluatedProperties();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the specified input property of the activity.
|
||||
/// </summary>
|
||||
public static async Task<T?> EvaluateInputPropertyAsync<TActivity, T>(this ActivityExecutionContext context, Expression<Func<TActivity, Input<T>>> propertyExpression)
|
||||
{
|
||||
var inputName = propertyExpression.GetProperty()!.Name;
|
||||
var input = await EvaluateInputPropertyAsync(context, inputName);
|
||||
return input.ConvertTo<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates a specific input property of the activity.
|
||||
/// </summary>
|
||||
public static async Task<object?> EvaluateInputPropertyAsync(this ActivityExecutionContext context, string inputName)
|
||||
{
|
||||
var activity = context.Activity;
|
||||
var activityRegistryLookup = context.GetRequiredService<IActivityRegistryLookupService>();
|
||||
var activityDescriptor = await activityRegistryLookup.FindAsync(activity.Type) ?? throw new Exception("Activity descriptor not found");
|
||||
var inputDescriptor = activityDescriptor.GetWrappedInputPropertyDescriptor(activity, inputName);
|
||||
|
||||
if (inputDescriptor == null)
|
||||
throw new Exception($"No input with name {inputName} could be found");
|
||||
|
||||
return await EvaluateInputPropertyAsync(context, activityDescriptor, inputDescriptor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the specified input and sets the result in the activity execution context's memory space.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="ActivityExecutionContext"/> being extended.</param>
|
||||
/// <param name="input">The input to evaluate.</param>
|
||||
/// <typeparam name="T">The type of the input.</typeparam>
|
||||
/// <returns>The evaluated value.</returns>
|
||||
public static async Task<T?> EvaluateAsync<T>(this ActivityExecutionContext context, Input<T> input)
|
||||
{
|
||||
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
|
||||
var memoryBlockReference = input.MemoryBlockReference();
|
||||
var value = await evaluator.EvaluateAsync(input, context.ExpressionExecutionContext);
|
||||
memoryBlockReference.Set(context, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static async Task<object?> EvaluateInputPropertyAsync(this ActivityExecutionContext context, ActivityDescriptor activityDescriptor, InputDescriptor inputDescriptor)
|
||||
{
|
||||
var activity = context.Activity;
|
||||
var defaultValue = inputDescriptor.DefaultValue;
|
||||
var value = defaultValue;
|
||||
var input = inputDescriptor.ValueGetter(activity);
|
||||
var identityGenerator = context.GetRequiredService<IIdentityGenerator>();
|
||||
|
||||
if (inputDescriptor.IsWrapped)
|
||||
{
|
||||
var wrappedInput = (Input?)input;
|
||||
|
||||
if (defaultValue != null && wrappedInput == null)
|
||||
{
|
||||
var typedInput = typeof(Input<>).MakeGenericType(inputDescriptor.Type);
|
||||
var valueExpression = new Literal(defaultValue)
|
||||
{
|
||||
Id = identityGenerator.GenerateId(),
|
||||
};
|
||||
wrappedInput = (Input)Activator.CreateInstance(typedInput, valueExpression)!;
|
||||
inputDescriptor.ValueSetter(activity, wrappedInput);
|
||||
}
|
||||
else
|
||||
{
|
||||
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
|
||||
var expressionExecutionContext = context.ExpressionExecutionContext;
|
||||
value = wrappedInput?.Expression != null ? await evaluator.EvaluateAsync(wrappedInput, expressionExecutionContext) : defaultValue;
|
||||
}
|
||||
|
||||
var memoryReference = wrappedInput?.MemoryBlockReference();
|
||||
|
||||
// When input is created from an activity provider, there may be no memory block reference.
|
||||
if (memoryReference?.Id != null!)
|
||||
{
|
||||
// Declare the input memory block on the current context.
|
||||
context.ExpressionExecutionContext.Set(memoryReference, value!);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
value = input;
|
||||
}
|
||||
|
||||
await StoreInputValueAsync(context, inputDescriptor, value);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static async Task StoreInputValueAsync(ActivityExecutionContext context, InputDescriptor inputDescriptor, object? value)
|
||||
{
|
||||
// Store the serialized input value in the activity state.
|
||||
// Serializing the value ensures we store a copy of the value and not a reference to the input, which may change over time.
|
||||
if (inputDescriptor.IsSerializable != false)
|
||||
{
|
||||
var serializedValue = await context.GetRequiredService<ISafeSerializer>().SerializeToElementAsync(value);
|
||||
var manager = context.GetRequiredService<IActivityStateFilterManager>();
|
||||
var filterContext = new ActivityStateFilterContext(context, inputDescriptor, serializedValue, context.CancellationToken);
|
||||
var filterResult = await manager.RunFiltersAsync(filterContext);
|
||||
context.ActivityState[inputDescriptor.Name] = filterResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Elsa.Expressions.Contracts;
|
||||
|
|
@ -23,7 +21,7 @@ namespace Elsa.Extensions;
|
|||
/// Provides extension methods for <see cref="ActivityExecutionContext"/>.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public static class ActivityExecutionContextExtensions
|
||||
public static partial class ActivityExecutionContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to get a value from the input provided via <see cref="WorkflowExecutionContext"/>. If a value was found, an attempt is made to convert it into the specified type <code>T</code>.
|
||||
|
|
@ -110,47 +108,6 @@ public static class ActivityExecutionContextExtensions
|
|||
/// </summary>
|
||||
public static IDictionary<string, object> GetVariableValues(this ActivityExecutionContext activityExecutionContext) => activityExecutionContext.ExpressionExecutionContext.ReadAndFlattenMemoryBlocks();
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates each input property of the activity.
|
||||
/// </summary>
|
||||
public static async Task EvaluateInputPropertiesAsync(this ActivityExecutionContext context)
|
||||
{
|
||||
var activityDescriptor = context.ActivityDescriptor;
|
||||
var inputDescriptors = activityDescriptor.Inputs.Where(x => x.AutoEvaluate).ToList();
|
||||
|
||||
// Evaluate inputs.
|
||||
foreach (var inputDescriptor in inputDescriptors)
|
||||
await EvaluateInputPropertyAsync(context, activityDescriptor, inputDescriptor);
|
||||
|
||||
context.SetHasEvaluatedProperties();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the specified input property of the activity.
|
||||
/// </summary>
|
||||
public static async Task<T?> EvaluateInputPropertyAsync<TActivity, T>(this ActivityExecutionContext context, Expression<Func<TActivity, Input<T>>> propertyExpression)
|
||||
{
|
||||
var inputName = propertyExpression.GetProperty()!.Name;
|
||||
var input = await EvaluateInputPropertyAsync(context, inputName);
|
||||
return input.ConvertTo<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates a specific input property of the activity.
|
||||
/// </summary>
|
||||
public static async Task<object?> EvaluateInputPropertyAsync(this ActivityExecutionContext context, string inputName)
|
||||
{
|
||||
var activity = context.Activity;
|
||||
var activityRegistryLookup = context.GetRequiredService<IActivityRegistryLookupService>();
|
||||
var activityDescriptor = await activityRegistryLookup.FindAsync(activity.Type) ?? throw new Exception("Activity descriptor not found");
|
||||
var inputDescriptor = activityDescriptor.GetWrappedInputPropertyDescriptor(activity, inputName);
|
||||
|
||||
if (inputDescriptor == null)
|
||||
throw new Exception($"No input with name {inputName} could be found");
|
||||
|
||||
return await EvaluateInputPropertyAsync(context, activityDescriptor, inputDescriptor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a set of tuples containing the activity and its descriptor for all activities with outputs.
|
||||
/// </summary>
|
||||
|
|
@ -207,59 +164,6 @@ public static class ActivityExecutionContextExtensions
|
|||
return node?.Activity;
|
||||
}
|
||||
|
||||
private static async Task<object?> EvaluateInputPropertyAsync(this ActivityExecutionContext context, ActivityDescriptor activityDescriptor, InputDescriptor inputDescriptor)
|
||||
{
|
||||
var activity = context.Activity;
|
||||
var defaultValue = inputDescriptor.DefaultValue;
|
||||
var value = defaultValue;
|
||||
var input = inputDescriptor.ValueGetter(activity);
|
||||
|
||||
if (inputDescriptor.IsWrapped)
|
||||
{
|
||||
var wrappedInput = (Input?)input;
|
||||
|
||||
if (defaultValue != null && wrappedInput == null)
|
||||
{
|
||||
var typedInput = typeof(Input<>).MakeGenericType(inputDescriptor.Type);
|
||||
var valueExpression = new Literal(defaultValue)
|
||||
{
|
||||
Id = Guid.NewGuid().ToString()
|
||||
};
|
||||
wrappedInput = (Input)Activator.CreateInstance(typedInput, valueExpression)!;
|
||||
inputDescriptor.ValueSetter(activity, wrappedInput);
|
||||
}
|
||||
else
|
||||
{
|
||||
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
|
||||
var expressionExecutionContext = context.ExpressionExecutionContext;
|
||||
value = wrappedInput?.Expression != null ? await evaluator.EvaluateAsync(wrappedInput, expressionExecutionContext) : defaultValue;
|
||||
}
|
||||
|
||||
var memoryReference = wrappedInput?.MemoryBlockReference();
|
||||
|
||||
// When input is created from an activity provider, there may be no memory block reference.
|
||||
if (memoryReference?.Id != null!)
|
||||
{
|
||||
// Declare the input memory block on the current context.
|
||||
context.ExpressionExecutionContext.Set(memoryReference, value!);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
value = input;
|
||||
}
|
||||
|
||||
// Store the serialized input value in the activity state.
|
||||
// Serializing the value ensures we store a copy of the value and not a reference to the input, which may change over time.
|
||||
if (inputDescriptor.IsSerializable != false)
|
||||
{
|
||||
var serializedValue = await context.GetRequiredService<ISafeSerializer>().SerializeToElementAsync(value);
|
||||
context.ActivityState[inputDescriptor.Name] = serializedValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the outcome name for the specified port property name.
|
||||
/// </summary>
|
||||
|
|
@ -280,22 +184,6 @@ public static class ActivityExecutionContextExtensions
|
|||
return portProperty.GetCustomAttribute<PortAttribute>()?.Name ?? portProperty.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the specified input and sets the result in the activity execution context's memory space.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="ActivityExecutionContext"/> being extended.</param>
|
||||
/// <param name="input">The input to evaluate.</param>
|
||||
/// <typeparam name="T">The type of the input.</typeparam>
|
||||
/// <returns>The evaluated value.</returns>
|
||||
public static async Task<T?> EvaluateAsync<T>(this ActivityExecutionContext context, Input<T> input)
|
||||
{
|
||||
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
|
||||
var memoryBlockReference = input.MemoryBlockReference();
|
||||
var value = await evaluator.EvaluateAsync(input, context.ExpressionExecutionContext);
|
||||
memoryBlockReference.Set(context, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a flattened list of the current context's ancestors.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
using Elsa.Features.Services;
|
||||
using Elsa.Workflows.Contracts;
|
||||
using Elsa.Workflows.Features;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
|
@ -13,9 +11,4 @@ public static class ModuleExtensions
|
|||
configuration.Configure(configure);
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddStorageDriver<T>(this IServiceCollection services) where T : class, IStorageDriver
|
||||
{
|
||||
return services.AddScoped<IStorageDriver, T>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Contracts;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddStorageDriver<T>(this IServiceCollection services) where T : class, IStorageDriver
|
||||
{
|
||||
return services.AddScoped<IStorageDriver, T>();
|
||||
}
|
||||
|
||||
public static IServiceCollection AddActivityStateFilter<T>(this IServiceCollection services) where T : class, IActivityStateFilter
|
||||
{
|
||||
return services.AddScoped<IActivityStateFilter, T>();
|
||||
}
|
||||
}
|
||||
|
|
@ -129,6 +129,7 @@ public class WorkflowsFeature : FeatureBase
|
|||
.AddScoped<IWorkflowBuilderFactory, WorkflowBuilderFactory>()
|
||||
.AddScoped<IVariablePersistenceManager, VariablePersistenceManager>()
|
||||
.AddScoped<IIncidentStrategyResolver, DefaultIncidentStrategyResolver>()
|
||||
.AddScoped<IActivityStateFilterManager, DefaultActivityStateFilterManager>()
|
||||
|
||||
// Incident Strategies.
|
||||
.AddTransient<IIncidentStrategy, FaultStrategy>()
|
||||
|
|
|
|||
|
|
@ -20,18 +20,8 @@ public static class ExecutionLogMiddlewareExtensions
|
|||
/// An activity execution middleware component that extracts execution details as <see cref="WorkflowExecutionLogEntry"/> objects.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class ExecutionLogMiddleware : IActivityExecutionMiddleware
|
||||
public class ExecutionLogMiddleware(ActivityMiddlewareDelegate next) : IActivityExecutionMiddleware
|
||||
{
|
||||
private readonly ActivityMiddlewareDelegate _next;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public ExecutionLogMiddleware(ActivityMiddlewareDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InvokeAsync(ActivityExecutionContext context)
|
||||
{
|
||||
|
|
@ -39,7 +29,7 @@ public class ExecutionLogMiddleware : IActivityExecutionMiddleware
|
|||
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
await next(context);
|
||||
|
||||
if (context.Status == ActivityStatus.Running)
|
||||
{
|
||||
|
|
@ -65,6 +55,8 @@ public class ExecutionLogMiddleware : IActivityExecutionMiddleware
|
|||
}
|
||||
}
|
||||
|
||||
private static bool IsActivityBookmarked(ActivityExecutionContext context) =>
|
||||
context.WorkflowExecutionContext.Bookmarks.Any(b => b.ActivityNodeId.Equals(context.ActivityNode.NodeId));
|
||||
private static bool IsActivityBookmarked(ActivityExecutionContext context)
|
||||
{
|
||||
return context.WorkflowExecutionContext.Bookmarks.Any(b => b.ActivityNodeId.Equals(context.ActivityNode.NodeId));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
[UsedImplicitly]
|
||||
public class ActivityStateFilterResult
|
||||
{
|
||||
public string? FilteredValue { get; set; }
|
||||
public bool IsFiltered { get; set; }
|
||||
|
||||
public static ActivityStateFilterResult Filtered(string filteredValue) => new() { IsFiltered = true, FilteredValue = filteredValue };
|
||||
public static ActivityStateFilterResult Pass() => new() { IsFiltered = false };
|
||||
}
|
||||
|
|
@ -89,6 +89,12 @@ public class InputDescriptor : PropertyDescriptor
|
|||
/// </summary>
|
||||
public bool? IsReadOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this input can contain secrets.
|
||||
/// When set to true, the input will be treated as a secret and will be encrypted, masked or otherwise protected, depending on the configured policy.
|
||||
/// </summary>
|
||||
public bool IsSensitive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The storage driver type to use for persistence.
|
||||
/// If no driver is specified, the referenced memory block will remain in memory for as long as the expression execution context exists.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
namespace Elsa.Workflows;
|
||||
|
||||
public class DefaultActivityStateFilterManager(IEnumerable<IActivityStateFilter> filters) : IActivityStateFilterManager
|
||||
{
|
||||
public async Task<string> RunFiltersAsync(ActivityStateFilterContext context)
|
||||
{
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
var result = await filter.ExecuteAsync(context);
|
||||
|
||||
if (result.IsFiltered)
|
||||
return result.FilteredValue!;
|
||||
}
|
||||
|
||||
return context.Value.ToString();
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ public class DefaultActivityExecutionMapper(IOptions<ManagementOptions> options)
|
|||
});
|
||||
|
||||
outputs = StorePropertyUsingPersistenceMode(outputs, activityPersistenceProperties!.GetValueOrDefault("outputs", () => new Dictionary<string, object>())!, activityPersistencePropertyDefault);
|
||||
var activityState = StorePropertyUsingPersistenceMode(source.ActivityState, activityPersistenceProperties!.GetValueOrDefault("inputs", () => new Dictionary<string, object>())!, activityPersistencePropertyDefault);
|
||||
var inputs = StorePropertyUsingPersistenceMode(source.ActivityState, activityPersistenceProperties!.GetValueOrDefault("inputs", () => new Dictionary<string, object>())!, activityPersistencePropertyDefault);
|
||||
|
||||
return new ActivityExecutionRecord
|
||||
{
|
||||
|
|
@ -72,7 +72,7 @@ public class DefaultActivityExecutionMapper(IOptions<ManagementOptions> options)
|
|||
WorkflowInstanceId = source.WorkflowExecutionContext.Id,
|
||||
ActivityType = source.Activity.Type,
|
||||
ActivityName = source.Activity.Name,
|
||||
ActivityState = activityState,
|
||||
ActivityState = inputs,
|
||||
Outputs = outputs,
|
||||
Properties = source.Properties,
|
||||
Payload = payload,
|
||||
|
|
@ -95,18 +95,16 @@ public class DefaultActivityExecutionMapper(IOptions<ManagementOptions> options)
|
|||
return persistencePropertyDefault;
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> StorePropertyUsingPersistenceMode(IDictionary<string, object?> inputs,
|
||||
IDictionary<string, object> persistenceModeConfiguration,
|
||||
LogPersistenceMode defaultLogPersistenceMode)
|
||||
private static Dictionary<string, object?> StorePropertyUsingPersistenceMode(IDictionary<string, object?> state, IDictionary<string, object> persistenceModeConfiguration, LogPersistenceMode defaultLogPersistenceMode)
|
||||
{
|
||||
var result = new Dictionary<string, object?>();
|
||||
|
||||
foreach (var input in inputs)
|
||||
foreach (var value in state)
|
||||
{
|
||||
var persistence = persistenceModeConfiguration.GetValueOrDefault(input.Key.Camelize(), () => defaultLogPersistenceMode);
|
||||
var persistence = persistenceModeConfiguration.GetValueOrDefault(value.Key.Camelize(), () => defaultLogPersistenceMode);
|
||||
if (persistence.Equals(LogPersistenceMode.Include)
|
||||
|| (persistence.Equals(LogPersistenceMode.Inherit) && defaultLogPersistenceMode is LogPersistenceMode.Include or LogPersistenceMode.Inherit))
|
||||
result.Add(input.Key, input.Value);
|
||||
result.Add(value.Key, value.Value);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
Loading…
Reference in a new issue