Implement secret update functionality, add related tests, and update project references.
This commit is contained in:
parent
aa55274bcf
commit
87ee19f5b3
14
Elsa.sln
14
Elsa.sln
|
|
@ -1652,17 +1652,17 @@ Global
|
||||||
{09B4B78B-FE02-44E2-8667-E182AF921C54} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
|
{09B4B78B-FE02-44E2-8667-E182AF921C54} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
|
||||||
{7D905CEC-B30B-4C99-B5F7-3052D33EC8E9} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
{7D905CEC-B30B-4C99-B5F7-3052D33EC8E9} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||||
{81CFD2E0-2E5E-4810-ADB8-A08301199166} = {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}
|
{60F844CC-6DA6-45EB-BA5E-B480B4E77624} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
|
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
|
||||||
{D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
{D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
||||||
{80F7ED38-15BA-4C82-88C8-12135B33F7EF} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
|
{80F7ED38-15BA-4C82-88C8-12135B33F7EF} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
|
||||||
{0C6F614C-A698-426B-8D11-0132C2FFF400} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
{0C6F614C-A698-426B-8D11-0132C2FFF400} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
|
||||||
{4DEE0574-C5B3-4765-9C48-89D384A17780} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
{E18931B2-82EE-4398-B38A-7B8B449957CD} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
|
||||||
{151C46EF-FE3D-400B-8CBE-F86886B3EB96} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
{729FD075-2863-48AB-A3C0-A4F7A9277263} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
|
||||||
{E4B1546A-3F35-4B5C-815C-07E58B00163F} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
{E4B1546A-3F35-4B5C-815C-07E58B00163F} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
|
||||||
{E18931B2-82EE-4398-B38A-7B8B449957CD} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
{4DEE0574-C5B3-4765-9C48-89D384A17780} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
|
||||||
{729FD075-2863-48AB-A3C0-A4F7A9277263} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
{151C46EF-FE3D-400B-8CBE-F86886B3EB96} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
|
||||||
|
{8F4AD54E-8586-4D8C-82E6-69218DD4280F} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
<ProjectReference Include="..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />
|
<ProjectReference Include="..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />
|
||||||
<ProjectReference Include="..\..\modules\Elsa.Persistence.EFCore.Sqlite\Elsa.Persistence.EFCore.Sqlite.csproj" />
|
<ProjectReference Include="..\..\modules\Elsa.Persistence.EFCore.Sqlite\Elsa.Persistence.EFCore.Sqlite.csproj" />
|
||||||
<ProjectReference Include="..\..\modules\Elsa.Resilience\Elsa.Resilience.csproj" />
|
<ProjectReference Include="..\..\modules\Elsa.Resilience\Elsa.Resilience.csproj" />
|
||||||
|
<ProjectReference Include="..\..\modules\Elsa.Secrets.JavaScript\Elsa.Secrets.JavaScript.csproj" />
|
||||||
<ProjectReference Include="..\..\modules\Elsa.Secrets\Elsa.Secrets.csproj" />
|
<ProjectReference Include="..\..\modules\Elsa.Secrets\Elsa.Secrets.csproj" />
|
||||||
<ProjectReference Include="..\..\modules\Elsa.Shells.Api\Elsa.Shells.Api.csproj" />
|
<ProjectReference Include="..\..\modules\Elsa.Shells.Api\Elsa.Shells.Api.csproj" />
|
||||||
<ProjectReference Include="..\..\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
|
<ProjectReference Include="..\..\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@
|
||||||
"Secrets": {
|
"Secrets": {
|
||||||
"EncryptionKey": "Q0hBTkdFX01FX1RPX0FfU0VDVVJFX1JBTkRPTV9LRVk="
|
"EncryptionKey": "Q0hBTkdFX01FX1RPX0FfU0VDVVJFX1JBTkRPTV9LRVk="
|
||||||
},
|
},
|
||||||
|
"SecretsJavaScript": {},
|
||||||
"SqliteAlterationsPersistence": {
|
"SqliteAlterationsPersistence": {
|
||||||
"ConnectionString": "Data Source=elsa_workflows.db;Cache=Shared"
|
"ConnectionString": "Data Source=elsa_workflows.db;Cache=Shared"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ public interface ISecretManager
|
||||||
Task<IReadOnlyCollection<Secret>> ListAsync(ListSecretsRequest request, CancellationToken cancellationToken = default);
|
Task<IReadOnlyCollection<Secret>> ListAsync(ListSecretsRequest request, CancellationToken cancellationToken = default);
|
||||||
Task<ListSecretsResult> ListPageAsync(ListSecretsRequest request, CancellationToken cancellationToken = default);
|
Task<ListSecretsResult> ListPageAsync(ListSecretsRequest request, CancellationToken cancellationToken = default);
|
||||||
Task<long> CountAsync(ListSecretsRequest request, CancellationToken cancellationToken = default);
|
Task<long> CountAsync(ListSecretsRequest request, CancellationToken cancellationToken = default);
|
||||||
|
Task<Secret> UpdateAsync(string name, UpdateSecretRequest request, CancellationToken cancellationToken = default);
|
||||||
Task<Secret> RotateAsync(string name, RotateSecretRequest request, CancellationToken cancellationToken = default);
|
Task<Secret> RotateAsync(string name, RotateSecretRequest request, CancellationToken cancellationToken = default);
|
||||||
Task<Secret?> RevokeAsync(string name, CancellationToken cancellationToken = default);
|
Task<Secret?> RevokeAsync(string name, CancellationToken cancellationToken = default);
|
||||||
Task<bool> DeleteAsync(string name, CancellationToken cancellationToken = default);
|
Task<bool> DeleteAsync(string name, CancellationToken cancellationToken = default);
|
||||||
|
|
|
||||||
|
|
@ -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<UpdateSecretRequest, SecretModel>
|
||||||
|
{
|
||||||
|
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<string>("name")!, request, cancellationToken);
|
||||||
|
await Send.OkAsync(secret.ToModel(), cancellationToken);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException)
|
||||||
|
{
|
||||||
|
await Send.NotFoundAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,6 +32,12 @@ public class CreateSecretRequest
|
||||||
public IDictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
public IDictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class UpdateSecretRequest
|
||||||
|
{
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class RotateSecretRequest
|
public class RotateSecretRequest
|
||||||
{
|
{
|
||||||
public string? Value { get; set; }
|
public string? Value { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,18 @@ public class DefaultSecretManager(ISecretNameValidator nameValidator, ISecretSto
|
||||||
return ApplyFilters(secrets, request).LongCount();
|
return ApplyFilters(secrets, request).LongCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Secret> 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<Secret> RotateAsync(string name, RotateSecretRequest request, CancellationToken cancellationToken = default)
|
public async Task<Secret> RotateAsync(string name, RotateSecretRequest request, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var secret = await GetExistingAsync(name, cancellationToken);
|
var secret = await GetExistingAsync(name, cancellationToken);
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,66 @@ public class SecretManagerTests
|
||||||
Assert.Equal(3, count);
|
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<KeyNotFoundException>(() => _fixture.Manager.UpdateAsync("missing:secret", new UpdateSecretRequest
|
||||||
|
{
|
||||||
|
DisplayName = "Missing",
|
||||||
|
Description = "Missing"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ListPageAsync_AppliesFiltersBeforePaging()
|
public async Task ListPageAsync_AppliesFiltersBeforePaging()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue