Skip to content

Commit

Permalink
Add secret management functionalities
Browse files Browse the repository at this point in the history
Introduced services and interfaces for secret name generation, validation, and updating. Updated secret handling to include expiration metadata. Refactored methods in ISecretManager to streamline secret creation and update processes.
  • Loading branch information
sfmskywalker committed Sep 15, 2024
1 parent 161f4f6 commit f5bcaa3
Show file tree
Hide file tree
Showing 26 changed files with 228 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public RunJavaScript(string script, [CallerFilePath] string? source = default, [
/// <summary>
/// A list of possible outcomes. Use "setOutcome()" to set the outcome. Use "setOutcomes" to set multiple outcomes.
/// </summary>
[Input(Description = "A list of possible outcomes.", UIHint = InputUIHints.DynamicOutcomes)]
[Input(Description = "A list of possible outcomes.", UIHint = InputUIHints.DynamicOutcomes, UIHandler = typeof(DisableSyntaxSelection))]
public Input<ICollection<string>> PossibleOutcomes { get; set; } = default!;

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Elsa.JavaScript.TypeDefinitions.Abstractions;
using Elsa.JavaScript.TypeDefinitions.Models;
using Elsa.Workflows.Helpers;
using JetBrains.Annotations;

// ReSharper disable once CheckNamespace
namespace Elsa.JavaScript.Activities;

/// Produces <see cref="FunctionDefinition"/>s for common functions.
[UsedImplicitly]
internal class RunJavaScriptFunctionsDefinitionProvider() : FunctionDefinitionProvider
{
protected override IEnumerable<FunctionDefinition> GetFunctionDefinitions(TypeDefinitionContext context)
{
if (context.ActivityTypeName != ActivityTypeNameHelper.GenerateTypeName<RunJavaScript>())
yield break;

if(context.PropertyName != nameof(RunJavaScript.Script))
yield break;

yield return CreateFunctionDefinition(builder => builder
.Name("setOutcome")
.Parameter("name", "string")
.ReturnType("void"));

yield return CreateFunctionDefinition(builder => builder
.Name("setOutcomes")
.Parameter("names", "string[]")
.ReturnType("void"));
}
}
1 change: 1 addition & 0 deletions src/modules/Elsa.JavaScript/Features/JavaScriptFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public override void Apply()
.AddSingleton<ITypeAliasRegistry, TypeAliasRegistry>()
.AddFunctionDefinitionProvider<CommonFunctionsDefinitionProvider>()
.AddFunctionDefinitionProvider<ActivityOutputFunctionsDefinitionProvider>()
.AddFunctionDefinitionProvider<RunJavaScriptFunctionsDefinitionProvider>()
.AddTypeDefinitionProvider<CommonTypeDefinitionProvider>()
.AddTypeDefinitionProvider<VariableTypeDefinitionProvider>()
.AddTypeDefinitionProvider<WorkflowVariablesTypeDefinitionProvider>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Elsa.Secrets.Api.Endpoints.Secrets.Create;

[UsedImplicitly]
internal class Endpoint(ISecretManager manager, ISecretEncryptor secretEncryptor) : ElsaEndpoint<SecretInputModel, SecretModel>
internal class Endpoint(ISecretManager manager) : ElsaEndpoint<SecretInputModel, SecretModel>
{
public override void Configure()
{
Expand All @@ -15,8 +15,7 @@ public override void Configure()

public override async Task<SecretModel> ExecuteAsync(SecretInputModel request, CancellationToken cancellationToken)
{
var secret = await secretEncryptor.EncryptAsync(request, cancellationToken);
await manager.AddAsync(secret, cancellationToken);
var secret = await manager.CreateAsync(request, cancellationToken);
var secretModel = secret.ToModel();
return secretModel;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Elsa.Secrets.Api.Endpoints.Secrets.GenerateUniqueName;

/// Generates a unique name for a secret.
[UsedImplicitly]
public class Endpoint(ISecretManager manager) : ElsaEndpointWithoutRequest<GenerateUniqueNameResponse>
public class Endpoint(ISecretNameGenerator nameGenerator) : ElsaEndpointWithoutRequest<GenerateUniqueNameResponse>
{
/// <inheritdoc />
public override void Configure()
Expand All @@ -19,7 +19,7 @@ public override void Configure()
/// <inheritdoc />
public override async Task<GenerateUniqueNameResponse> ExecuteAsync(CancellationToken ct)
{
var newName = await manager.GenerateUniqueNameAsync(ct);
var newName = await nameGenerator.GenerateUniqueNameAsync(ct);
return new(newName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Elsa.Secrets.Api.Endpoints.Secrets.IsUniqueName;

/// Checks if a name is unique.
[UsedImplicitly]
public class Endpoint(ISecretManager agentManager) : ElsaEndpoint<IsUniqueNameRequest, IsUniqueNameResponse>
public class Endpoint(ISecretNameValidator nameValidator) : ElsaEndpoint<IsUniqueNameRequest, IsUniqueNameResponse>
{
/// <inheritdoc />
public override void Configure()
Expand All @@ -19,7 +19,7 @@ public override void Configure()
/// <inheritdoc />
public override async Task<IsUniqueNameResponse> ExecuteAsync(IsUniqueNameRequest req, CancellationToken ct)
{
var isUnique = await agentManager.IsNameUniqueAsync(req.Name, req.Id, ct);
var isUnique = await nameValidator.IsNameUniqueAsync(req.Name, req.Id, ct);
return new(isUnique);
}
}
19 changes: 3 additions & 16 deletions src/modules/Elsa.Secrets.Api/Endpoints/Secrets/Update/Endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Elsa.Secrets.Api.Endpoints.Secrets.Update;

/// Updates an agent.
[UsedImplicitly]
public class Endpoint(ISecretManager manager, ISecretEncryptor secretEncryptor) : ElsaEndpoint<SecretInputModel, SecretModel>
public class Endpoint(ISecretManager manager, ISecretNameValidator nameValidator) : ElsaEndpoint<SecretInputModel, SecretModel>
{
/// <inheritdoc />
public override void Configure()
Expand All @@ -28,29 +28,16 @@ public override async Task<SecretModel> ExecuteAsync(SecretInputModel req, Cance
return null!;
}

var isNameDuplicate = await IsNameDuplicateAsync(req.Name, id, ct);
var isNameDuplicate = !await nameValidator.IsNameUniqueAsync(req.Name, id, ct);

if (isNameDuplicate)
{
AddError("Another secret already exists with the specified name");
await SendErrorsAsync(cancellation: ct);
return entity.ToModel();
}

entity.IsLatest = false;
entity.Status = SecretStatus.Retired;
var newVersion = entity.Clone();
newVersion.IsLatest = true;
newVersion.Version = entity.Version + 1;

await secretEncryptor.EncryptAsync(newVersion, req, ct);
await manager.UpdateAsync(entity, ct);
await manager.UpdateAsync(newVersion, ct);
var newVersion = await manager.UpdateAsync(entity, req, ct);
return newVersion.ToModel();
}

private async Task<bool> IsNameDuplicateAsync(string name, string id, CancellationToken cancellationToken)
{
return !await manager.IsNameUniqueAsync(name, id, cancellationToken);
}
}
16 changes: 6 additions & 10 deletions src/modules/Elsa.Secrets.Management/Contracts/ISecretManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ namespace Elsa.Secrets.Management;

public interface ISecretManager
{
/// Adds a new entity to the store.
Task AddAsync(Secret entity, CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new entity from the specified input.
/// </summary>
Task<Secret> CreateAsync(SecretInputModel input, CancellationToken cancellationToken = default);

/// Updates the entity to the store.
Task UpdateAsync(Secret entity, CancellationToken cancellationToken = default);
/// Creates a new, updated version of the specified secret.
Task<Secret> UpdateAsync(Secret entity, SecretInputModel input, CancellationToken cancellationToken = default);

/// Gets the entity from the store.
Task<Secret?> GetAsync(string id, CancellationToken cancellationToken = default);
Expand All @@ -22,10 +24,4 @@ public interface ISecretManager

/// Deletes all entities from the store matching the specified filter.
Task<long> DeleteManyAsync(SecretFilter filter, CancellationToken cancellationToken = default);

/// Generates a unique name.
Task<string> GenerateUniqueNameAsync(CancellationToken cancellationToken = default);

/// Checks if a name is unique.
Task<bool> IsNameUniqueAsync(string name, string? notId, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Elsa.Secrets.Management;

public interface ISecretNameGenerator
{
Task<string> GenerateUniqueNameAsync(CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Elsa.Secrets.Management;

public interface ISecretNameValidator
{
Task<bool> IsNameUniqueAsync(string name, string? notId, CancellationToken cancellationToken = default);
}
2 changes: 2 additions & 0 deletions src/modules/Elsa.Secrets.Management/Contracts/ISecretStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public interface ISecretStore
/// Finds the entities from the store.
Task<Page<Secret>> FindManyAsync<TOrderBy>(SecretFilter filter, SecretOrder<TOrderBy> order, PageArgs pageArgs, CancellationToken cancellationToken = default);

Task<IEnumerable<Secret>> FindManyAsync(SecretFilter filter, CancellationToken cancellationToken = default);

/// Finds the entity from the store.
Task<Secret?> FindAsync<TOrderBy>(SecretFilter filter, SecretOrder<TOrderBy> order, CancellationToken cancellationToken = default);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Elsa.Secrets.Management;

public interface ISecretUpdater
{
Task<Secret> UpdateAsync(Secret secret, SecretInputModel input, CancellationToken cancellationToken = default);
}
4 changes: 3 additions & 1 deletion src/modules/Elsa.Secrets.Management/Entities/Secret.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public class Secret : ManagedEntity

/// The status of the secret.
public SecretStatus Status { get; set; }


public TimeSpan? ExpiresIn { get; set; }
public DateTimeOffset? ExpiresAt { get; set; }
public DateTimeOffset? LastAccessedAt { get; set; }

Expand All @@ -48,6 +49,7 @@ public Secret Clone()
Version = Version,
IsLatest = IsLatest,
Status = Status,
ExpiresIn = ExpiresIn,
ExpiresAt = ExpiresAt,
LastAccessedAt = LastAccessedAt,
CreatedAt = CreatedAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ public static SecretModel ToModel(this Secret secret)
Name = secret.Name,
Description = secret.Description,
EncryptedValue = secret.EncryptedValue,
ExpiresIn = secret.ExpiresIn,
ExpiresAt = secret.ExpiresAt,
Status = secret.Status,
Type = secret.Scope,
Scope = secret.Scope,
Version = secret.Version,
Owner = secret.Owner,
CreatedAt = secret.CreatedAt,
Expand All @@ -32,7 +33,7 @@ public static SecretInputModel ToInputModel(this Secret secret, string clearText
Name = secret.Name,
Description = secret.Description,
Value = clearTextValue,
ExpiresAt = secret.ExpiresAt,
ExpiresIn = secret.ExpiresIn,
Scope = secret.Scope
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public override void Apply()
.AddScoped<DataProtectionEncryptor>()
.AddScoped<StoreSecretProvider>()
.AddMemoryStore<Secret, MemorySecretStore>()
.AddScoped<ISecretNameGenerator, DefaultSecretNameGenerator>()
.AddScoped<ISecretNameValidator, DefaultSecretNameValidator>()
.AddScoped<ISecretUpdater, DefaultSecretUpdater>()
.AddScoped<ISecretManager, DefaultSecretManager>()
;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ public class DataProtectionEncryptor(IDataProtectionProvider dataProtectionProvi
/// <inheritdoc />
public Task<string> EncryptAsync(string value, CancellationToken cancellationToken = default)
{
if(string.IsNullOrWhiteSpace(value))
return Task.FromResult(value);

var protector = GetDataProtector();
return Task.FromResult(protector.Protect(value));
}

/// <inheritdoc />
public Task<string> DecryptAsync(string encryptedValue, CancellationToken cancellationToken = default)
{
if(string.IsNullOrWhiteSpace(encryptedValue))
return Task.FromResult(encryptedValue);

var protector = GetDataProtector();
var decryptedValue = protector.Unprotect(encryptedValue);
return Task.FromResult(decryptedValue);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,42 @@
using Elsa.Common.Contracts;
using Elsa.Workflows.Contracts;

namespace Elsa.Secrets.Management;

public class DefaultSecretEncryptor(IEncryptor encryptor, IDecryptor decryptor, IIdentityGenerator identityGenerator) : ISecretEncryptor
public class DefaultSecretEncryptor(IEncryptor encryptor, IDecryptor decryptor, IIdentityGenerator identityGenerator, ISystemClock systemClock) : ISecretEncryptor
{
public async Task<Secret> EncryptAsync(SecretInputModel input, CancellationToken cancellationToken = default)
{
var encryptedValue = string.IsNullOrWhiteSpace(input.Value) ? "" : await encryptor.EncryptAsync(input.Value, cancellationToken);

var secret = new Secret
{
Id = identityGenerator.GenerateId(),
SecretId = identityGenerator.GenerateId(),
Version = 1,
IsLatest = true,
Name = input.Name.Trim(),
Scope = input.Scope?.Trim(),
Description = input.Description.Trim(),
EncryptedValue = encryptedValue,
ExpiresAt = input.ExpiresAt,
Status = SecretStatus.Active
IsLatest = true
};


await EncryptAsync(secret, input, cancellationToken);
return secret;
}

public async Task EncryptAsync(Secret secret, SecretInputModel input, CancellationToken cancellationToken = default)
{
var encryptedValue = string.IsNullOrWhiteSpace(input.Value) ? "" : await encryptor.EncryptAsync(input.Value, cancellationToken);

secret.Name = input.Name.Trim();
secret.Scope = input.Scope?.Trim();
secret.Description = input.Description.Trim();
secret.EncryptedValue = encryptedValue;
secret.ExpiresAt = input.ExpiresAt;
secret.ExpiresIn = input.ExpiresIn;
secret.ExpiresAt = input.ExpiresIn != null ? systemClock.UtcNow + input.ExpiresIn.Value : null;
secret.Status = SecretStatus.Active;
}

public async Task<string> DecryptAsync(Secret secret, CancellationToken cancellationToken = default)
{
if(string.IsNullOrWhiteSpace(secret.EncryptedValue))
if (string.IsNullOrWhiteSpace(secret.EncryptedValue))
return "";

return await decryptor.DecryptAsync(secret.EncryptedValue, cancellationToken);
}
}
Loading

0 comments on commit f5bcaa3

Please sign in to comment.