diff --git a/Directory.Packages.props b/Directory.Packages.props
index 8be1c4833..3cbfdec57 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -95,7 +95,6 @@
-
diff --git a/Elsa.sln b/Elsa.sln
index 0a4b53ad0..b1d711eac 100644
--- a/Elsa.sln
+++ b/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}
diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
index f09ba8d52..d4e953462 100644
--- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
+++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
@@ -12,6 +12,7 @@
+
diff --git a/src/apps/Elsa.Server.Web/Filters/HttpRequestAuthenticationHeaderFilter.cs b/src/apps/Elsa.Server.Web/Filters/HttpRequestAuthenticationHeaderFilter.cs
new file mode 100644
index 000000000..b6ee83937
--- /dev/null
+++ b/src/apps/Elsa.Server.Web/Filters/HttpRequestAuthenticationHeaderFilter.cs
@@ -0,0 +1,30 @@
+using Elsa.Http;
+using Elsa.Workflows;
+using JetBrains.Annotations;
+
+namespace Elsa.Server.Web.Filters;
+
+///
+/// Mask the value of the input if it is a HttpRequest and the input name is "Authorization".
+///
+[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);
+ }
+}
\ No newline at end of file
diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs
index 0c2b5f6cb..6b0c7c169 100644
--- a/src/apps/Elsa.Server.Web/Program.cs
+++ b/src/apps/Elsa.Server.Web/Program.cs
@@ -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();
+
//services.Configure(options => options.CacheDuration = TimeSpan.FromDays(1));
services.AddHealthChecks();
services.AddControllers();
diff --git a/src/apps/Elsa.Server.Web/appsettings.json b/src/apps/Elsa.Server.Web/appsettings.json
index 5f929c4c0..79393dcd1 100644
--- a/src/apps/Elsa.Server.Web/appsettings.json
+++ b/src/apps/Elsa.Server.Web/appsettings.json
@@ -173,6 +173,11 @@
}
]
},
+ "Secrets": {
+ "Management": {
+ "SweepInterval": "04:00:00"
+ }
+ },
"Agents": {
"ApiKeys": [
{
diff --git a/src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs b/src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs
index 645e8b00f..4fdaa3fcc 100644
--- a/src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs
+++ b/src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs
@@ -15,18 +15,12 @@ namespace Elsa.Http;
/// Base class for activities that send HTTP requests.
///
[Output(IsSerializable = false)]
-public abstract class SendHttpRequestBase : Activity
+public abstract class SendHttpRequestBase(string? source = default, int? line = default) : Activity(source, line)
{
- ///
- protected SendHttpRequestBase(string? source = default, int? line = default) : base(source, line)
- {
- }
-
///
/// The URL to send the request to.
///
- [Input]
- public Input Url { get; set; } = default!;
+ [Input] public Input Url { get; set; } = default!;
///
/// The HTTP method to use when sending the request.
@@ -62,7 +56,10 @@ public abstract class SendHttpRequestBase : Activity
/// The Authorization header value to send with the request.
///
/// Bearer {some-access-token}
- [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 Authorization { get; set; } = default!;
///
diff --git a/src/modules/Elsa.JavaScript/Notifications/EvaluatedJavaScript.cs b/src/modules/Elsa.JavaScript/Notifications/EvaluatedJavaScript.cs
index 71f980e79..b05bea82a 100644
--- a/src/modules/Elsa.JavaScript/Notifications/EvaluatedJavaScript.cs
+++ b/src/modules/Elsa.JavaScript/Notifications/EvaluatedJavaScript.cs
@@ -7,4 +7,4 @@ namespace Elsa.JavaScript.Notifications;
///
/// This notification is published every time a JavaScript expression has been evaluated.
///
-public record EvaluatedJavaScript(Engine Engine, ExpressionExecutionContext Context, object? Result) : INotification;
\ No newline at end of file
+public record EvaluatedJavaScript(Engine Engine, ExpressionExecutionContext Context, string Expression, object? Result) : INotification;
\ No newline at end of file
diff --git a/src/modules/Elsa.JavaScript/Notifications/EvaluatingJavaScript.cs b/src/modules/Elsa.JavaScript/Notifications/EvaluatingJavaScript.cs
index 799f4cc31..98b6c85dd 100644
--- a/src/modules/Elsa.JavaScript/Notifications/EvaluatingJavaScript.cs
+++ b/src/modules/Elsa.JavaScript/Notifications/EvaluatingJavaScript.cs
@@ -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 with additional functions and variables.
///
-public record EvaluatingJavaScript(Engine Engine, ExpressionExecutionContext Context) : INotification;
\ No newline at end of file
+public record EvaluatingJavaScript(Engine Engine, ExpressionExecutionContext Context, string Expression) : INotification;
\ No newline at end of file
diff --git a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs
index 65d2a80e9..b9a9e65f7 100644
--- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs
+++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs
@@ -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);
}
diff --git a/src/modules/Elsa.JavaScript/TypeDefinitions/Abstractions/VariableDefinitionProvider.cs b/src/modules/Elsa.JavaScript/TypeDefinitions/Abstractions/VariableDefinitionProvider.cs
index 6a4bf63ee..6d4771f3b 100644
--- a/src/modules/Elsa.JavaScript/TypeDefinitions/Abstractions/VariableDefinitionProvider.cs
+++ b/src/modules/Elsa.JavaScript/TypeDefinitions/Abstractions/VariableDefinitionProvider.cs
@@ -23,6 +23,6 @@ public abstract class VariableDefinitionProvider : IVariableDefinitionProvider
{
var builder = new VariableDefinitionBuilder();
setup(builder);
- return builder.BuildVariableDefinition();
+ return builder.Build();
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa.JavaScript/TypeDefinitions/Builders/VariableDefinitionBuilder.cs b/src/modules/Elsa.JavaScript/TypeDefinitions/Builders/VariableDefinitionBuilder.cs
index bddfc8b74..fcec19810 100644
--- a/src/modules/Elsa.JavaScript/TypeDefinitions/Builders/VariableDefinitionBuilder.cs
+++ b/src/modules/Elsa.JavaScript/TypeDefinitions/Builders/VariableDefinitionBuilder.cs
@@ -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);
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Secrets.Api/Endpoints/Secrets/Update/Endpoint.cs b/src/modules/Elsa.Secrets.Api/Endpoints/Secrets/Update/Endpoint.cs
index 1997a60c8..2bc5566ba 100644
--- a/src/modules/Elsa.Secrets.Api/Endpoints/Secrets/Update/Endpoint.cs
+++ b/src/modules/Elsa.Secrets.Api/Endpoints/Secrets/Update/Endpoint.cs
@@ -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)
{
diff --git a/src/modules/Elsa.Secrets.Management/Contracts/IExpiredSecretsUpdater.cs b/src/modules/Elsa.Secrets.Management/Contracts/IExpiredSecretsUpdater.cs
new file mode 100644
index 000000000..112f67f1a
--- /dev/null
+++ b/src/modules/Elsa.Secrets.Management/Contracts/IExpiredSecretsUpdater.cs
@@ -0,0 +1,6 @@
+namespace Elsa.Secrets.Management;
+
+public interface IExpiredSecretsUpdater
+{
+ Task UpdateExpiredSecretsAsync(CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Secrets.Management/Contracts/ISecretManager.cs b/src/modules/Elsa.Secrets.Management/Contracts/ISecretManager.cs
index fac793b60..4b182ffd1 100644
--- a/src/modules/Elsa.Secrets.Management/Contracts/ISecretManager.cs
+++ b/src/modules/Elsa.Secrets.Management/Contracts/ISecretManager.cs
@@ -16,6 +16,9 @@ public interface ISecretManager
/// Finds the entity from the store.
Task FindAsync(SecretFilter filter, CancellationToken cancellationToken = default);
+ /// Finds all entities from the store matching the specified filter.
+ Task> FindManyAsync(SecretFilter filter, CancellationToken cancellationToken = default);
+
/// Gets all entities from the store.
Task> ListAsync(CancellationToken cancellationToken = default);
diff --git a/src/modules/Elsa.Secrets.Management/Features/SecretManagementFeature.cs b/src/modules/Elsa.Secrets.Management/Features/SecretManagementFeature.cs
index 13380010a..0cc61cf5a 100644
--- a/src/modules/Elsa.Secrets.Management/Features/SecretManagementFeature.cs
+++ b/src/modules/Elsa.Secrets.Management/Features/SecretManagementFeature.cs
@@ -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;
@@ -20,6 +21,12 @@ public class SecretManagementFeature(IModule module) : FeatureBase(module)
_secretStoreFactory = secretStoreFactory;
return this;
}
+
+ public SecretManagementFeature ConfigureOptions(Action configureOptions)
+ {
+ Services.Configure(configureOptions);
+ return this;
+ }
public override void Configure()
{
@@ -29,6 +36,11 @@ public class SecretManagementFeature(IModule module) : FeatureBase(module)
});
}
+ public override void ConfigureHostedServices()
+ {
+ ConfigureHostedService();
+ }
+
public override void Apply()
{
Services
@@ -43,6 +55,7 @@ public class SecretManagementFeature(IModule module) : FeatureBase(module)
.AddScoped()
.AddScoped()
.AddScoped()
+ .AddScoped()
;
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Secrets.Management/HostedService/ExpiredSecretsHostedService.cs b/src/modules/Elsa.Secrets.Management/HostedService/ExpiredSecretsHostedService.cs
new file mode 100644
index 000000000..93c8b3c8c
--- /dev/null
+++ b/src/modules/Elsa.Secrets.Management/HostedService/ExpiredSecretsHostedService.cs
@@ -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 options, IDistributedLockProvider distributedLockProvider, IServiceScopeFactory scopeFactory, ILogger 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();
+ await updater.UpdateExpiredSecretsAsync();
+
+ logger.LogInformation("Expired secrets have been swept.");
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Secrets.Management/Models/SecretFilter.cs b/src/modules/Elsa.Secrets.Management/Models/SecretFilter.cs
index e5829019a..6cbfe6451 100644
--- a/src/modules/Elsa.Secrets.Management/Models/SecretFilter.cs
+++ b/src/modules/Elsa.Secrets.Management/Models/SecretFilter.cs
@@ -6,24 +6,30 @@ public class SecretFilter
public ICollection? Ids { get; set; }
public string? SecretId { get; set; }
public ICollection? SecretIds { get; set; }
- public string? NotId { get; set; }
+ public string? NotSecretId { get; set; }
public string? Name { get; set; }
+ public ICollection? 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 Apply(IQueryable 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;
diff --git a/src/modules/Elsa.Secrets.Management/Options/SecretManagementOptions.cs b/src/modules/Elsa.Secrets.Management/Options/SecretManagementOptions.cs
new file mode 100644
index 000000000..08aec860b
--- /dev/null
+++ b/src/modules/Elsa.Secrets.Management/Options/SecretManagementOptions.cs
@@ -0,0 +1,9 @@
+namespace Elsa.Secrets.Management;
+
+public class SecretManagementOptions
+{
+ ///
+ /// The interval at which the background sweep should run for expired secrets.
+ ///
+ public TimeSpan SweepInterval { get; set; } = TimeSpan.FromHours(12);
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Secrets.Management/Options/StoreEncryptionKeyProviderOptions.cs b/src/modules/Elsa.Secrets.Management/Options/StoreEncryptionKeyProviderOptions.cs
deleted file mode 100644
index 9bb163dd1..000000000
--- a/src/modules/Elsa.Secrets.Management/Options/StoreEncryptionKeyProviderOptions.cs
+++ /dev/null
@@ -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);
-}
\ No newline at end of file
diff --git a/src/modules/Elsa.Secrets.Management/Services/DefaultExpiredSecretsUpdater.cs b/src/modules/Elsa.Secrets.Management/Services/DefaultExpiredSecretsUpdater.cs
new file mode 100644
index 000000000..a1771394f
--- /dev/null
+++ b/src/modules/Elsa.Secrets.Management/Services/DefaultExpiredSecretsUpdater.cs
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Secrets.Management/Services/DefaultSecretManager.cs b/src/modules/Elsa.Secrets.Management/Services/DefaultSecretManager.cs
index e34c58480..12eb29540 100644
--- a/src/modules/Elsa.Secrets.Management/Services/DefaultSecretManager.cs
+++ b/src/modules/Elsa.Secrets.Management/Services/DefaultSecretManager.cs
@@ -51,6 +51,11 @@ public class DefaultSecretManager(ISecretStore store, IEncryptor encryptor, ISec
return store.FindAsync(filter, cancellationToken);
}
+ public Task> FindManyAsync(SecretFilter filter, CancellationToken cancellationToken = default)
+ {
+ return store.FindManyAsync(filter, cancellationToken);
+ }
+
public Task> 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 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;
}
diff --git a/src/modules/Elsa.Secrets.Management/Services/DefaultSecretNameValidator.cs b/src/modules/Elsa.Secrets.Management/Services/DefaultSecretNameValidator.cs
index d0771e0de..5031f810a 100644
--- a/src/modules/Elsa.Secrets.Management/Services/DefaultSecretNameValidator.cs
+++ b/src/modules/Elsa.Secrets.Management/Services/DefaultSecretNameValidator.cs
@@ -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;
}
diff --git a/src/modules/Elsa.Secrets.Management/Services/DefaultSecretUpdater.cs b/src/modules/Elsa.Secrets.Management/Services/DefaultSecretUpdater.cs
index f29f41970..7f8eeab93 100644
--- a/src/modules/Elsa.Secrets.Management/Services/DefaultSecretUpdater.cs
+++ b/src/modules/Elsa.Secrets.Management/Services/DefaultSecretUpdater.cs
@@ -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 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;
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Secrets.Models/SecretStatus.cs b/src/modules/Elsa.Secrets.Models/SecretStatus.cs
index f6b8625eb..a6f3af816 100644
--- a/src/modules/Elsa.Secrets.Models/SecretStatus.cs
+++ b/src/modules/Elsa.Secrets.Models/SecretStatus.cs
@@ -2,7 +2,23 @@ namespace Elsa.Secrets;
public enum SecretStatus
{
+ ///
+ /// The secret is active.
+ ///
Active,
+
+ ///
+ /// The secret is retired due to an update which created a new version.
+ ///
Retired,
+
+ ///
+ /// The secret has expired.
+ ///
+ Expired,
+
+ ///
+ /// The secret has been revoked.
+ ///
Revoked
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Secrets.Scripting/Elsa.Secrets.Scripting.csproj b/src/modules/Elsa.Secrets.Scripting/Elsa.Secrets.Scripting.csproj
new file mode 100644
index 000000000..f0a73cba0
--- /dev/null
+++ b/src/modules/Elsa.Secrets.Scripting/Elsa.Secrets.Scripting.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Provides scripting integration with various languages like JavaScript, C#, Python and Liquid.
+ elsa module secrets scripting
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/Elsa.Secrets.Scripting/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Secrets.Scripting/Extensions/ModuleExtensions.cs
new file mode 100644
index 000000000..24209acf0
--- /dev/null
+++ b/src/modules/Elsa.Secrets.Scripting/Extensions/ModuleExtensions.cs
@@ -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? setup = null)
+ {
+ return module.Use(setup);
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Secrets.Scripting/Features/SecretsScriptingFeature.cs b/src/modules/Elsa.Secrets.Scripting/Features/SecretsScriptingFeature.cs
new file mode 100644
index 000000000..e62ca86b6
--- /dev/null
+++ b/src/modules/Elsa.Secrets.Scripting/Features/SecretsScriptingFeature.cs
@@ -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();
+ Services.AddScoped();
+ Services.AddTypeDefinitionProvider(sp => sp.GetRequiredService());
+ Services.AddVariableDefinitionProvider(sp => sp.GetRequiredService());
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Secrets.Scripting/FodyWeavers.xml b/src/modules/Elsa.Secrets.Scripting/FodyWeavers.xml
new file mode 100644
index 000000000..00e1d9a1c
--- /dev/null
+++ b/src/modules/Elsa.Secrets.Scripting/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/modules/Elsa.Secrets.Scripting/JavaScript/ConfigureEngineWithSecrets.cs b/src/modules/Elsa.Secrets.Scripting/JavaScript/ConfigureEngineWithSecrets.cs
new file mode 100644
index 000000000..230995080
--- /dev/null
+++ b/src/modules/Elsa.Secrets.Scripting/JavaScript/ConfigureEngineWithSecrets.cs
@@ -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
+{
+ ///
+ 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 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 GetSecretNamesFromExpression(string expression)
+ {
+ var secretNames = new List();
+
+#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
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Secrets.Scripting/JavaScript/SecretsTypeDefinitionProvider.cs b/src/modules/Elsa.Secrets.Scripting/JavaScript/SecretsTypeDefinitionProvider.cs
new file mode 100644
index 000000000..0f841e046
--- /dev/null
+++ b/src/modules/Elsa.Secrets.Scripting/JavaScript/SecretsTypeDefinitionProvider.cs
@@ -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> 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> GetVariableDefinitionsAsync(TypeDefinitionContext context)
+ {
+ var definitions = new List
+ {
+ new VariableDefinitionBuilder().Name("secrets").Type("SecretVariables").Build()
+ };
+
+
+ return new (definitions);
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Abstractions/ActivityStateFilterBase.cs b/src/modules/Elsa.Workflows.Core/Abstractions/ActivityStateFilterBase.cs
new file mode 100644
index 000000000..52b2aed60
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/Abstractions/ActivityStateFilterBase.cs
@@ -0,0 +1,26 @@
+using JetBrains.Annotations;
+
+namespace Elsa.Workflows;
+
+[UsedImplicitly]
+public abstract class ActivityStateFilterBase : IActivityStateFilter
+{
+ protected virtual Task 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 IActivityStateFilter.ExecuteAsync(ActivityStateFilterContext context)
+ {
+ return OnExecuteAsync(context);
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Attributes/InputAttribute.cs b/src/modules/Elsa.Workflows.Core/Attributes/InputAttribute.cs
index dfb1948a0..1db5c7314 100644
--- a/src/modules/Elsa.Workflows.Core/Attributes/InputAttribute.cs
+++ b/src/modules/Elsa.Workflows.Core/Attributes/InputAttribute.cs
@@ -84,6 +84,12 @@ public class InputAttribute : Attribute
/// A value indicating whether this input can be serialized as part of the workflow instance,
///
public bool IsSerializable { get; set; } = true;
+
+ ///
+ /// 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.
+ ///
+ public bool CanContainSecrets { get; set; }
///
/// A type that can be used to customize the UI for this property.
diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityStateFilterContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityStateFilterContext.cs
new file mode 100644
index 000000000..b191f8041
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityStateFilterContext.cs
@@ -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);
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionMiddleware.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionMiddleware.cs
index bb786f283..d67958272 100644
--- a/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionMiddleware.cs
+++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionMiddleware.cs
@@ -1,7 +1,13 @@
namespace Elsa.Workflows;
+///
+/// The interface for activity execution middleware components.
+///
public interface IActivityExecutionMiddleware
{
+ ///
+ /// The method that is called to execute the middleware.
+ ///
ValueTask InvokeAsync(ActivityExecutionContext context);
}
diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityStateFilter.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityStateFilter.cs
new file mode 100644
index 000000000..04e190fb2
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivityStateFilter.cs
@@ -0,0 +1,6 @@
+namespace Elsa.Workflows;
+
+public interface IActivityStateFilter
+{
+ Task ExecuteAsync(ActivityStateFilterContext context);
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityStateFilterManager.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityStateFilterManager.cs
new file mode 100644
index 000000000..15df4895e
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivityStateFilterManager.cs
@@ -0,0 +1,6 @@
+namespace Elsa.Workflows;
+
+public interface IActivityStateFilterManager
+{
+ Task RunFiltersAsync(ActivityStateFilterContext context);
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings
index b50a58f5d..a9f483ae3 100644
--- a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings
+++ b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings
@@ -12,4 +12,5 @@
True
True
True
+ True
True
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs
new file mode 100644
index 000000000..9955bb31d
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs
@@ -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
+{
+ ///
+ /// Evaluates each input property of the activity.
+ ///
+ 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();
+ }
+
+ ///
+ /// Evaluates the specified input property of the activity.
+ ///
+ public static async Task EvaluateInputPropertyAsync(this ActivityExecutionContext context, Expression>> propertyExpression)
+ {
+ var inputName = propertyExpression.GetProperty()!.Name;
+ var input = await EvaluateInputPropertyAsync(context, inputName);
+ return input.ConvertTo();
+ }
+
+ ///
+ /// Evaluates a specific input property of the activity.
+ ///
+ public static async Task
[PublicAPI]
-public static class ActivityExecutionContextExtensions
+public static partial class ActivityExecutionContextExtensions
{
///
/// Attempts to get a value from the input provided via . If a value was found, an attempt is made to convert it into the specified type T.
@@ -110,47 +108,6 @@ public static class ActivityExecutionContextExtensions
///
public static IDictionary GetVariableValues(this ActivityExecutionContext activityExecutionContext) => activityExecutionContext.ExpressionExecutionContext.ReadAndFlattenMemoryBlocks();
- ///
- /// Evaluates each input property of the activity.
- ///
- 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();
- }
-
- ///
- /// Evaluates the specified input property of the activity.
- ///
- public static async Task EvaluateInputPropertyAsync(this ActivityExecutionContext context, Expression>> propertyExpression)
- {
- var inputName = propertyExpression.GetProperty()!.Name;
- var input = await EvaluateInputPropertyAsync(context, inputName);
- return input.ConvertTo();
- }
-
- ///
- /// Evaluates a specific input property of the activity.
- ///
- public static async Task EvaluateInputPropertyAsync(this ActivityExecutionContext context, string inputName)
- {
- var activity = context.Activity;
- var activityRegistryLookup = context.GetRequiredService();
- 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);
- }
-
///
/// Returns a set of tuples containing the activity and its descriptor for all activities with outputs.
///
@@ -207,59 +164,6 @@ public static class ActivityExecutionContextExtensions
return node?.Activity;
}
- private static async Task 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();
- 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().SerializeToElementAsync(value);
- context.ActivityState[inputDescriptor.Name] = serializedValue;
- }
-
- return value;
- }
-
///
/// Returns the outcome name for the specified port property name.
///
@@ -280,22 +184,6 @@ public static class ActivityExecutionContextExtensions
return portProperty.GetCustomAttribute()?.Name ?? portProperty.Name;
}
- ///
- /// Evaluates the specified input and sets the result in the activity execution context's memory space.
- ///
- /// The being extended.
- /// The input to evaluate.
- /// The type of the input.
- /// The evaluated value.
- public static async Task EvaluateAsync(this ActivityExecutionContext context, Input input)
- {
- var evaluator = context.GetRequiredService();
- var memoryBlockReference = input.MemoryBlockReference();
- var value = await evaluator.EvaluateAsync(input, context.ExpressionExecutionContext);
- memoryBlockReference.Set(context, value);
- return value;
- }
-
///
/// Returns a flattened list of the current context's ancestors.
///
diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ModuleExtensions.cs
index 0796f6a5e..ccd559f5c 100644
--- a/src/modules/Elsa.Workflows.Core/Extensions/ModuleExtensions.cs
+++ b/src/modules/Elsa.Workflows.Core/Extensions/ModuleExtensions.cs
@@ -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(this IServiceCollection services) where T : class, IStorageDriver
- {
- return services.AddScoped();
- }
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ServiceCollectionExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 000000000..e09de6040
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/Extensions/ServiceCollectionExtensions.cs
@@ -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(this IServiceCollection services) where T : class, IStorageDriver
+ {
+ return services.AddScoped();
+ }
+
+ public static IServiceCollection AddActivityStateFilter(this IServiceCollection services) where T : class, IActivityStateFilter
+ {
+ return services.AddScoped();
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs
index 582dbf026..048c657e9 100644
--- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs
+++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs
@@ -129,6 +129,7 @@ public class WorkflowsFeature : FeatureBase
.AddScoped()
.AddScoped()
.AddScoped()
+ .AddScoped()
// Incident Strategies.
.AddTransient()
diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExecutionLogMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExecutionLogMiddleware.cs
index 2ae1746e7..d1e8790a3 100644
--- a/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExecutionLogMiddleware.cs
+++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExecutionLogMiddleware.cs
@@ -20,18 +20,8 @@ public static class ExecutionLogMiddlewareExtensions
/// An activity execution middleware component that extracts execution details as objects.
///
[UsedImplicitly]
-public class ExecutionLogMiddleware : IActivityExecutionMiddleware
+public class ExecutionLogMiddleware(ActivityMiddlewareDelegate next) : IActivityExecutionMiddleware
{
- private readonly ActivityMiddlewareDelegate _next;
-
- ///
- /// Constructor.
- ///
- public ExecutionLogMiddleware(ActivityMiddlewareDelegate next)
- {
- _next = next;
- }
-
///
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));
+ }
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityStateFilterResult.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityStateFilterResult.cs
new file mode 100644
index 000000000..f93236978
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/Models/ActivityStateFilterResult.cs
@@ -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 };
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs b/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs
index 11627ea13..7863424a9 100644
--- a/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs
+++ b/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs
@@ -89,6 +89,12 @@ public class InputDescriptor : PropertyDescriptor
///
public bool? IsReadOnly { get; set; }
+ ///
+ /// 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.
+ ///
+ public bool IsSensitive { get; set; }
+
///
/// 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.
diff --git a/src/modules/Elsa.Workflows.Core/Services/DefaultActivityStateFilterManager.cs b/src/modules/Elsa.Workflows.Core/Services/DefaultActivityStateFilterManager.cs
new file mode 100644
index 000000000..a6bdf90b3
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/Services/DefaultActivityStateFilterManager.cs
@@ -0,0 +1,17 @@
+namespace Elsa.Workflows;
+
+public class DefaultActivityStateFilterManager(IEnumerable filters) : IActivityStateFilterManager
+{
+ public async Task RunFiltersAsync(ActivityStateFilterContext context)
+ {
+ foreach (var filter in filters)
+ {
+ var result = await filter.ExecuteAsync(context);
+
+ if (result.IsFiltered)
+ return result.FilteredValue!;
+ }
+
+ return context.Value.ToString();
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs
index 972fc261a..f6ed24440 100644
--- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs
@@ -62,7 +62,7 @@ public class DefaultActivityExecutionMapper(IOptions options)
});
outputs = StorePropertyUsingPersistenceMode(outputs, activityPersistenceProperties!.GetValueOrDefault("outputs", () => new Dictionary())!, activityPersistencePropertyDefault);
- var activityState = StorePropertyUsingPersistenceMode(source.ActivityState, activityPersistenceProperties!.GetValueOrDefault("inputs", () => new Dictionary())!, activityPersistencePropertyDefault);
+ var inputs = StorePropertyUsingPersistenceMode(source.ActivityState, activityPersistenceProperties!.GetValueOrDefault("inputs", () => new Dictionary())!, activityPersistencePropertyDefault);
return new ActivityExecutionRecord
{
@@ -72,7 +72,7 @@ public class DefaultActivityExecutionMapper(IOptions 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 options)
return persistencePropertyDefault;
}
- private static Dictionary StorePropertyUsingPersistenceMode(IDictionary inputs,
- IDictionary persistenceModeConfiguration,
- LogPersistenceMode defaultLogPersistenceMode)
+ private static Dictionary StorePropertyUsingPersistenceMode(IDictionary state, IDictionary persistenceModeConfiguration, LogPersistenceMode defaultLogPersistenceMode)
{
var result = new Dictionary();
- 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;