Merge pull request #7573 from elsa-workflows/codex/workflow-secret-references-s1
[codex] Protect sensitive workflow input descriptors and state
This commit is contained in:
commit
0113f858dc
|
|
@ -43,6 +43,11 @@ public class InputDescriptor : PropertyDescriptor
|
|||
/// </summary>
|
||||
public bool? IsReadOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the input can contain secrets and should be treated as sensitive.
|
||||
/// </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.
|
||||
|
|
@ -53,4 +58,4 @@ public class InputDescriptor : PropertyDescriptor
|
|||
/// A dictionary of UI specifications to be used by the UI.
|
||||
/// </summary>
|
||||
public IDictionary<string, object>? UISpecifications { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,7 +164,10 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve
|
|||
null,
|
||||
propertyInfo,
|
||||
uiSpecification
|
||||
);
|
||||
)
|
||||
{
|
||||
IsSensitive = inputAttribute?.CanContainSecrets ?? false
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ActivityExecutionContext> ActivateAsync() => _fixture.ExecuteAsync();
|
||||
|
||||
private sealed class ActivityWithSensitiveInputs : CodeActivity
|
||||
{
|
||||
[Input(CanContainSecrets = true)]
|
||||
public Input<string?> SensitiveText { get; set; } = new("secret");
|
||||
|
||||
[Input]
|
||||
public Input<string?> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IPropertyDefaultValueResolver>();
|
||||
var propertyUIHandlerResolver = Substitute.For<IPropertyUIHandlerResolver>();
|
||||
|
||||
defaultValueResolver.GetDefaultValue(Arg.Any<PropertyInfo>()).Returns((object?)null);
|
||||
propertyUIHandlerResolver
|
||||
.GetUIPropertiesAsync(Arg.Any<PropertyInfo>(), Arg.Any<object?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(_ => new ValueTask<IDictionary<string, object>>(new Dictionary<string, object>()));
|
||||
|
||||
_describer = new ActivityDescriber(defaultValueResolver, propertyUIHandlerResolver);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DescribeActivityAsync_MapsCanContainSecretsToInputDescriptorSensitivity()
|
||||
{
|
||||
var descriptor = await _describer.DescribeActivityAsync(typeof(ActivityWithSensitiveInputs));
|
||||
|
||||
var sensitiveInput = descriptor.Inputs.Single(x => x.Name == nameof(ActivityWithSensitiveInputs.SensitiveText));
|
||||
var publicInput = descriptor.Inputs.Single(x => x.Name == nameof(ActivityWithSensitiveInputs.PublicText));
|
||||
|
||||
Assert.True(sensitiveInput.IsSensitive);
|
||||
Assert.False(publicInput.IsSensitive);
|
||||
}
|
||||
|
||||
private sealed class ActivityWithSensitiveInputs : CodeActivity
|
||||
{
|
||||
[Input(CanContainSecrets = true)]
|
||||
public Input<string?> SensitiveText { get; set; } = new("secret");
|
||||
|
||||
[Input]
|
||||
public Input<string?> PublicText { get; set; } = new("public");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue