From 87ee19f5b3f116b419468855819065e2b6d87135 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 1 Jun 2026 23:47:05 +0200 Subject: [PATCH] Implement secret update functionality, add related tests, and update project references. --- Elsa.sln | 14 ++--- .../Elsa.ModularServer.Web.csproj | 1 + .../Elsa.ModularServer.Web/appsettings.json | 1 + .../Elsa.Secrets/Contracts/ISecretManager.cs | 1 + .../Endpoints/Secrets/Update/Endpoint.cs | 27 +++++++++ .../Elsa.Secrets/Models/SecretApiModels.cs | 6 ++ .../Services/DefaultSecretManager.cs | 12 ++++ .../SecretManagerTests.cs | 60 +++++++++++++++++++ 8 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 src/modules/Elsa.Secrets/Endpoints/Secrets/Update/Endpoint.cs diff --git a/Elsa.sln b/Elsa.sln index 95efa28df..496846670 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -1652,17 +1652,17 @@ Global {09B4B78B-FE02-44E2-8667-E182AF921C54} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} {7D905CEC-B30B-4C99-B5F7-3052D33EC8E9} = {18453B51-25EB-4317-A4B3-B10518252E92} {81CFD2E0-2E5E-4810-ADB8-A08301199166} = {18453B51-25EB-4317-A4B3-B10518252E92} - {8F4AD54E-8586-4D8C-82E6-69218DD4280F} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {60F844CC-6DA6-45EB-BA5E-B480B4E77624} = {18453B51-25EB-4317-A4B3-B10518252E92} {B6A62D42-D9AA-47BE-BCBC-FCB866D346F0} = {1B8D5897-902E-4632-8698-E89CAF3DDF54} {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {80F7ED38-15BA-4C82-88C8-12135B33F7EF} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} - {0C6F614C-A698-426B-8D11-0132C2FFF400} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} - {4DEE0574-C5B3-4765-9C48-89D384A17780} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} - {151C46EF-FE3D-400B-8CBE-F86886B3EB96} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} - {E4B1546A-3F35-4B5C-815C-07E58B00163F} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} - {E18931B2-82EE-4398-B38A-7B8B449957CD} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} - {729FD075-2863-48AB-A3C0-A4F7A9277263} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} + {0C6F614C-A698-426B-8D11-0132C2FFF400} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} + {E18931B2-82EE-4398-B38A-7B8B449957CD} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} + {729FD075-2863-48AB-A3C0-A4F7A9277263} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} + {E4B1546A-3F35-4B5C-815C-07E58B00163F} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} + {4DEE0574-C5B3-4765-9C48-89D384A17780} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} + {151C46EF-FE3D-400B-8CBE-F86886B3EB96} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} + {8F4AD54E-8586-4D8C-82E6-69218DD4280F} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj b/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj index 725e606b5..736a2a1d0 100644 --- a/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj +++ b/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj @@ -19,6 +19,7 @@ + diff --git a/src/apps/Elsa.ModularServer.Web/appsettings.json b/src/apps/Elsa.ModularServer.Web/appsettings.json index e14366c4f..ed66320c9 100644 --- a/src/apps/Elsa.ModularServer.Web/appsettings.json +++ b/src/apps/Elsa.ModularServer.Web/appsettings.json @@ -58,6 +58,7 @@ "Secrets": { "EncryptionKey": "Q0hBTkdFX01FX1RPX0FfU0VDVVJFX1JBTkRPTV9LRVk=" }, + "SecretsJavaScript": {}, "SqliteAlterationsPersistence": { "ConnectionString": "Data Source=elsa_workflows.db;Cache=Shared" }, diff --git a/src/modules/Elsa.Secrets/Contracts/ISecretManager.cs b/src/modules/Elsa.Secrets/Contracts/ISecretManager.cs index 384d4515a..10f3400f4 100644 --- a/src/modules/Elsa.Secrets/Contracts/ISecretManager.cs +++ b/src/modules/Elsa.Secrets/Contracts/ISecretManager.cs @@ -7,6 +7,7 @@ public interface ISecretManager Task> ListAsync(ListSecretsRequest request, CancellationToken cancellationToken = default); Task ListPageAsync(ListSecretsRequest request, CancellationToken cancellationToken = default); Task CountAsync(ListSecretsRequest request, CancellationToken cancellationToken = default); + Task UpdateAsync(string name, UpdateSecretRequest request, CancellationToken cancellationToken = default); Task RotateAsync(string name, RotateSecretRequest request, CancellationToken cancellationToken = default); Task RevokeAsync(string name, CancellationToken cancellationToken = default); Task DeleteAsync(string name, CancellationToken cancellationToken = default); diff --git a/src/modules/Elsa.Secrets/Endpoints/Secrets/Update/Endpoint.cs b/src/modules/Elsa.Secrets/Endpoints/Secrets/Update/Endpoint.cs new file mode 100644 index 000000000..fd26ae0c0 --- /dev/null +++ b/src/modules/Elsa.Secrets/Endpoints/Secrets/Update/Endpoint.cs @@ -0,0 +1,27 @@ +using Elsa.Abstractions; +using Elsa.Secrets.Permissions; +using Elsa.Secrets.Services; + +namespace Elsa.Secrets.Endpoints.Secrets.Update; + +internal class Endpoint(ISecretManager manager) : ElsaEndpoint +{ + public override void Configure() + { + Post("/secrets/{name}"); + ConfigurePermissions(SecretsPermissions.Write); + } + + public override async Task HandleAsync(UpdateSecretRequest request, CancellationToken cancellationToken) + { + try + { + var secret = await manager.UpdateAsync(Route("name")!, request, cancellationToken); + await Send.OkAsync(secret.ToModel(), cancellationToken); + } + catch (KeyNotFoundException) + { + await Send.NotFoundAsync(cancellationToken); + } + } +} diff --git a/src/modules/Elsa.Secrets/Models/SecretApiModels.cs b/src/modules/Elsa.Secrets/Models/SecretApiModels.cs index 342f4a58a..bc492797a 100644 --- a/src/modules/Elsa.Secrets/Models/SecretApiModels.cs +++ b/src/modules/Elsa.Secrets/Models/SecretApiModels.cs @@ -32,6 +32,12 @@ public class CreateSecretRequest public IDictionary Metadata { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); } +public class UpdateSecretRequest +{ + public string? DisplayName { get; set; } + public string? Description { get; set; } +} + public class RotateSecretRequest { public string? Value { get; set; } diff --git a/src/modules/Elsa.Secrets/Services/DefaultSecretManager.cs b/src/modules/Elsa.Secrets/Services/DefaultSecretManager.cs index 8a9463f3b..b6afb4f97 100644 --- a/src/modules/Elsa.Secrets/Services/DefaultSecretManager.cs +++ b/src/modules/Elsa.Secrets/Services/DefaultSecretManager.cs @@ -47,6 +47,18 @@ public class DefaultSecretManager(ISecretNameValidator nameValidator, ISecretSto return ApplyFilters(secrets, request).LongCount(); } + public async Task UpdateAsync(string name, UpdateSecretRequest request, CancellationToken cancellationToken = default) + { + var secret = await GetExistingAsync(name, cancellationToken); + + secret.DisplayName = string.IsNullOrWhiteSpace(request.DisplayName) ? secret.Name : request.DisplayName.Trim(); + secret.Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(); + secret.UpdatedAt = DateTimeOffset.UtcNow; + + await repository.SaveAsync(secret, cancellationToken); + return secret; + } + public async Task RotateAsync(string name, RotateSecretRequest request, CancellationToken cancellationToken = default) { var secret = await GetExistingAsync(name, cancellationToken); diff --git a/test/unit/Elsa.Secrets.UnitTests/SecretManagerTests.cs b/test/unit/Elsa.Secrets.UnitTests/SecretManagerTests.cs index 9aa78ee45..19949b273 100644 --- a/test/unit/Elsa.Secrets.UnitTests/SecretManagerTests.cs +++ b/test/unit/Elsa.Secrets.UnitTests/SecretManagerTests.cs @@ -173,6 +173,66 @@ public class SecretManagerTests Assert.Equal(3, count); } + [Fact] + public async Task UpdateAsync_UpdatesMetadataWithoutChangingIdentityOrVersion() + { + await _fixture.Manager.CreateAsync(new CreateSecretRequest + { + Name = "smtp:password", + DisplayName = "SMTP password", + Description = "Old description", + Scope = "production", + Value = "one" + }); + + var updated = await _fixture.Manager.UpdateAsync("smtp:password", new UpdateSecretRequest + { + DisplayName = " SMTP credential ", + Description = " Rotated manually " + }); + + Assert.Equal("smtp:password", updated.Name); + Assert.Equal("SMTP credential", updated.DisplayName); + Assert.Equal("Rotated manually", updated.Description); + Assert.Equal("production", updated.Scope); + Assert.Equal(SecretTypeNames.Text, updated.TypeName); + Assert.Equal(SecretStoreNames.Encrypted, updated.StoreName); + Assert.Single(updated.Versions); + Assert.NotNull(updated.UpdatedAt); + Assert.Equal("one", await _fixture.Resolver.ResolveAsync("smtp:password")); + } + + [Fact] + public async Task UpdateAsync_UsesTechnicalNameAsFallbackDisplayName_AndClearsBlankDescription() + { + await _fixture.Manager.CreateAsync(new CreateSecretRequest + { + Name = "smtp:password", + DisplayName = "SMTP password", + Description = "Old description", + Value = "one" + }); + + var updated = await _fixture.Manager.UpdateAsync("smtp:password", new UpdateSecretRequest + { + DisplayName = " ", + Description = " " + }); + + Assert.Equal("smtp:password", updated.DisplayName); + Assert.Null(updated.Description); + } + + [Fact] + public async Task UpdateAsync_Throws_WhenSecretDoesNotExist() + { + await Assert.ThrowsAsync(() => _fixture.Manager.UpdateAsync("missing:secret", new UpdateSecretRequest + { + DisplayName = "Missing", + Description = "Missing" + })); + } + [Fact] public async Task ListPageAsync_AppliesFiltersBeforePaging() {