diff --git a/src/clients/Elsa.Api.Client/Resources/ActivityDescriptors/Models/InputDescriptor.cs b/src/clients/Elsa.Api.Client/Resources/ActivityDescriptors/Models/InputDescriptor.cs
index d19089503..e143a6af2 100644
--- a/src/clients/Elsa.Api.Client/Resources/ActivityDescriptors/Models/InputDescriptor.cs
+++ b/src/clients/Elsa.Api.Client/Resources/ActivityDescriptors/Models/InputDescriptor.cs
@@ -43,6 +43,11 @@ public class InputDescriptor : PropertyDescriptor
///
public bool? IsReadOnly { get; set; }
+ ///
+ /// True if the input can contain secrets and should be treated as sensitive.
+ ///
+ 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.
@@ -53,4 +58,4 @@ public class InputDescriptor : PropertyDescriptor
/// A dictionary of UI specifications to be used by the UI.
///
public IDictionary? UISpecifications { get; set; }
-}
\ 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
index 4bdbd64e6..bf07ae9ac 100644
--- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs
+++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs
@@ -143,6 +143,12 @@ public static partial class ActivityExecutionContextExtensions
private static Task StoreInputValueAsync(ActivityExecutionContext context, InputDescriptor inputDescriptor, object value)
{
+ if (inputDescriptor.IsSensitive)
+ {
+ context.ActivityState.Remove(inputDescriptor.Name);
+ return Task.CompletedTask;
+ }
+
// 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)
@@ -157,4 +163,4 @@ public static partial class ActivityExecutionContextExtensions
return Task.CompletedTask;
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs
index 8498bc4eb..3450b5c29 100644
--- a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs
+++ b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs
@@ -164,7 +164,10 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve
null,
propertyInfo,
uiSpecification
- );
+ )
+ {
+ IsSensitive = inputAttribute?.CanContainSecrets ?? false
+ };
}
///
diff --git a/src/modules/Elsa.Workflows.Management/Services/HostMethodActivityDescriber.cs b/src/modules/Elsa.Workflows.Management/Services/HostMethodActivityDescriber.cs
index f496e33a3..b57735d18 100644
--- a/src/modules/Elsa.Workflows.Management/Services/HostMethodActivityDescriber.cs
+++ b/src/modules/Elsa.Workflows.Management/Services/HostMethodActivityDescriber.cs
@@ -146,7 +146,8 @@ public class HostMethodActivityDescriber(IActivityDescriber activityDescriber) :
Order = inputAttribute?.Order ?? 0,
IsBrowsable = inputAttribute?.IsBrowsable ?? true,
AutoEvaluate = inputAttribute?.AutoEvaluate ?? true,
- IsSerializable = inputAttribute?.IsSerializable ?? true
+ IsSerializable = inputAttribute?.IsSerializable ?? true,
+ IsSensitive = inputAttribute?.CanContainSecrets ?? false
};
}
@@ -176,7 +177,8 @@ public class HostMethodActivityDescriber(IActivityDescriber activityDescriber) :
Order = inputAttribute?.Order ?? 0,
IsBrowsable = inputAttribute?.IsBrowsable ?? true,
AutoEvaluate = inputAttribute?.AutoEvaluate ?? true,
- IsSerializable = inputAttribute?.IsSerializable ?? true
+ IsSerializable = inputAttribute?.IsSerializable ?? true,
+ IsSensitive = inputAttribute?.CanContainSecrets ?? false
};
}
@@ -243,4 +245,4 @@ public class HostMethodActivityDescriber(IActivityDescriber activityDescriber) :
return true;
}
-}
\ No newline at end of file
+}
diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/Extensions/ActivityExecutionContextExtensions/InputEvaluationTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Extensions/ActivityExecutionContextExtensions/InputEvaluationTests.cs
new file mode 100644
index 000000000..d3ac82ec8
--- /dev/null
+++ b/test/unit/Elsa.Workflows.Core.UnitTests/Extensions/ActivityExecutionContextExtensions/InputEvaluationTests.cs
@@ -0,0 +1,57 @@
+using Elsa.Testing.Shared;
+using Elsa.Workflows.Attributes;
+using Elsa.Workflows.Models;
+using Xunit;
+
+namespace Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions;
+
+public class InputEvaluationTests
+{
+ private readonly ActivityWithSensitiveInputs _activity = new();
+ private readonly ActivityTestFixture _fixture;
+
+ public InputEvaluationTests()
+ {
+ _fixture = new(_activity);
+ }
+
+ [Fact]
+ public async Task EvaluateInputPropertiesAsync_WhenInputIsSensitive_RemovesItFromActivityState()
+ {
+ _fixture.ConfigureContext(context => context.ActivityState[nameof(ActivityWithSensitiveInputs.SensitiveText)] = "stale-secret");
+
+ var context = await ActivateAsync();
+
+ Assert.False(context.ActivityState.ContainsKey(nameof(ActivityWithSensitiveInputs.SensitiveText)));
+ Assert.Equal("secret", _activity.CapturedSensitiveText);
+ }
+
+ [Fact]
+ public async Task EvaluateInputPropertiesAsync_WhenInputIsNotSensitive_StoresItInActivityState()
+ {
+ var context = await ActivateAsync();
+
+ Assert.Equal("public", context.ActivityState[nameof(ActivityWithSensitiveInputs.PublicText)]);
+ Assert.Equal("public", _activity.CapturedPublicText);
+ }
+
+ private Task ActivateAsync() => _fixture.ExecuteAsync();
+
+ private sealed class ActivityWithSensitiveInputs : CodeActivity
+ {
+ [Input(CanContainSecrets = true)]
+ public Input SensitiveText { get; set; } = new("secret");
+
+ [Input]
+ public Input PublicText { get; set; } = new("public");
+
+ public string? CapturedSensitiveText { get; private set; }
+ public string? CapturedPublicText { get; private set; }
+
+ protected override void Execute(ActivityExecutionContext context)
+ {
+ CapturedSensitiveText = context.Get(SensitiveText);
+ CapturedPublicText = context.Get(PublicText);
+ }
+ }
+}
diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/Services/ActivityDescriberTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Services/ActivityDescriberTests.cs
new file mode 100644
index 000000000..328598c27
--- /dev/null
+++ b/test/unit/Elsa.Workflows.Core.UnitTests/Services/ActivityDescriberTests.cs
@@ -0,0 +1,46 @@
+using System.Reflection;
+using Elsa.Workflows.Attributes;
+using Elsa.Workflows.Models;
+using NSubstitute;
+using Xunit;
+
+namespace Elsa.Workflows.Core.UnitTests.Services;
+
+public class ActivityDescriberTests
+{
+ private readonly IActivityDescriber _describer;
+
+ public ActivityDescriberTests()
+ {
+ var defaultValueResolver = Substitute.For();
+ var propertyUIHandlerResolver = Substitute.For();
+
+ defaultValueResolver.GetDefaultValue(Arg.Any()).Returns((object?)null);
+ propertyUIHandlerResolver
+ .GetUIPropertiesAsync(Arg.Any(), Arg.Any