Skip to content

Commit

Permalink
Add secret management functionalities
Browse files Browse the repository at this point in the history
Introduced secret management services with CRUD operations, notifications, and bulk actions. Added unique name generation and validation for secrets, and implemented corresponding API endpoints.
  • Loading branch information
sfmskywalker committed Sep 11, 2024
1 parent 34f59be commit ed4e8a8
Show file tree
Hide file tree
Showing 36 changed files with 632 additions and 4 deletions.
7 changes: 7 additions & 0 deletions Elsa.sln
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Agents.Persistence.Ent
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Agents.Persistence.EntityFrameworkCore.MySql", "src\modules\Elsa.Agents.Persistence.EntityFrameworkCore.MySql\Elsa.Agents.Persistence.EntityFrameworkCore.MySql.csproj", "{B3046301-6F00-4885-8B01-080BD489055C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Secrets.Models", "src\modules\Elsa.Secrets.Models\Elsa.Secrets.Models.csproj", "{29D12ADC-55E9-40D0-9E4C-F0EBB6E098EC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1052,6 +1054,10 @@ Global
{B3046301-6F00-4885-8B01-080BD489055C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B3046301-6F00-4885-8B01-080BD489055C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B3046301-6F00-4885-8B01-080BD489055C}.Release|Any CPU.Build.0 = Release|Any CPU
{29D12ADC-55E9-40D0-9E4C-F0EBB6E098EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{29D12ADC-55E9-40D0-9E4C-F0EBB6E098EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{29D12ADC-55E9-40D0-9E4C-F0EBB6E098EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{29D12ADC-55E9-40D0-9E4C-F0EBB6E098EC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1236,6 +1242,7 @@ Global
{44A03CF6-4EB6-434C-90D1-FF8142FE394E} = {8CEEC194-820A-4C8D-AB9E-E51E6D3E9CC1}
{A9140976-DFF9-432A-B1DC-E188A07C49E5} = {50470834-4CD8-479A-8B58-0A1869BA5D37}
{B3046301-6F00-4885-8B01-080BD489055C} = {50470834-4CD8-479A-8B58-0A1869BA5D37}
{29D12ADC-55E9-40D0-9E4C-F0EBB6E098EC} = {8CEEC194-820A-4C8D-AB9E-E51E6D3E9CC1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class Endpoint(IAgentManager agentManager) : ElsaEndpoint<IsUniqueNameReq
public override void Configure()
{
Post("/ai/queries/agents/is-unique-name");
ConfigurePermissions("ai/agents:write");
ConfigurePermissions("ai/agents:read");
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public override async Task<AgentModel> ExecuteAsync(AgentInputModel req, Cancell

if (isNameDuplicate)
{
AddError("Another service already exists with the specified name");
AddError("Another agent already exists with the specified name");
await SendErrorsAsync(cancellation: ct);
return entity.ToModel();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Elsa.Abstractions;
using Elsa.Secrets.BulkActions;
using Elsa.Secrets.Management;
using JetBrains.Annotations;

namespace Elsa.Secrets.Api.Endpoints.Secrets.BulkDelete;

/// Deletes an agent.
[UsedImplicitly]
public class Endpoint(ISecretManager manager) : ElsaEndpoint<BulkDeleteRequest, BulkDeleteResponse>
{
/// <inheritdoc />
public override void Configure()
{
Post("/bulk-actions/secrets/delete");
ConfigurePermissions("secrets:delete");
}

/// <inheritdoc />
public override async Task<BulkDeleteResponse> ExecuteAsync(BulkDeleteRequest req, CancellationToken ct)
{
var ids = req.Ids;
var filter = new SecretFilter
{
Ids = ids
};
var count = await manager.DeleteManyAsync(filter, ct);
return new BulkDeleteResponse(count);
}
}
41 changes: 41 additions & 0 deletions src/modules/Elsa.Secrets.Api/Endpoints/Secrets/Create/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Elsa.Abstractions;
using Elsa.Secrets.Management;
using Elsa.Workflows.Contracts;
using JetBrains.Annotations;

namespace Elsa.Secrets.Api.Endpoints.Secrets.Create;

[UsedImplicitly]
internal class Endpoint(ISecretManager manager, IIdentityGenerator identityGenerator) : ElsaEndpoint<SecretInputModel, SecretModel>
{
public override void Configure()
{
Post("/secrets");
ConfigurePermissions("write:secrets");
}

public override async Task<SecretModel> ExecuteAsync(SecretInputModel request, CancellationToken cancellationToken)
{
var newEntity = new Secret
{
Id = identityGenerator.GenerateId(),
SecretId = identityGenerator.GenerateId(),
Version = 1,
Name = request.Name.Trim(),
Type = request.Type?.Trim(),
Description = request.Description.Trim(),
EncryptedValue = request.EncryptedValue,
IV = request.IV,
EncryptionKeyId = request.EncryptionKeyId,
Algorithm = request.Algorithm,
ExpiresAt = request.ExpiresAt,
RotationPolicy = request.RotationPolicy,
Status = SecretStatus.Active
};

await manager.AddAsync(newEntity, cancellationToken);

var secretModel = newEntity.ToModel();
return secretModel;
}
}
31 changes: 31 additions & 0 deletions src/modules/Elsa.Secrets.Api/Endpoints/Secrets/Delete/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Elsa.Abstractions;
using Elsa.Secrets.Management;
using JetBrains.Annotations;

namespace Elsa.Secrets.Api.Endpoints.Secrets.Delete;

/// Deletes a secret.
[UsedImplicitly]
public class Endpoint(ISecretManager manager) : ElsaEndpoint<Request>
{
/// <inheritdoc />
public override void Configure()
{
Delete("/secrets/{id}");
ConfigurePermissions("secrets:delete");
}

/// <inheritdoc />
public override async Task HandleAsync(Request req, CancellationToken ct)
{
var entity = await manager.GetAsync(req.Id, ct);

if(entity == null)
{
await SendNotFoundAsync(ct);
return;
}

await manager.DeleteAsync(entity, ct);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;

namespace Elsa.Secrets.Api.Endpoints.Secrets.Delete;

public class Request
{
[Required] public string Id { get; set; } = default!;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Elsa.Abstractions;
using Elsa.Secrets.Management;
using Elsa.Secrets.UniqueName;
using JetBrains.Annotations;

namespace Elsa.Secrets.Api.Endpoints.Secrets.GenerateUniqueName;

/// Generates a unique name for a secret.
[UsedImplicitly]
public class Endpoint(ISecretManager manager) : ElsaEndpointWithoutRequest<GenerateUniqueNameResponse>
{
/// <inheritdoc />
public override void Configure()
{
Post("/actions/secrets/generate-unique-name");
ConfigurePermissions("secrets:write");
}

/// <inheritdoc />
public override async Task<GenerateUniqueNameResponse> ExecuteAsync(CancellationToken ct)
{
var newName = await manager.GenerateUniqueNameAsync(ct);
return new(newName);
}
}
31 changes: 31 additions & 0 deletions src/modules/Elsa.Secrets.Api/Endpoints/Secrets/Get/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Elsa.Abstractions;
using Elsa.Secrets.Management;
using JetBrains.Annotations;

namespace Elsa.Secrets.Api.Endpoints.Secrets.Get;

/// Gets a secret.
[UsedImplicitly]
public class Endpoint(ISecretManager agentManager) : ElsaEndpoint<Request, SecretModel>
{
/// <inheritdoc />
public override void Configure()
{
Get("/secrets/{id}");
ConfigurePermissions("secrets:read");
}

/// <inheritdoc />
public override async Task<SecretModel> ExecuteAsync(Request req, CancellationToken ct)
{
var entity = await agentManager.GetAsync(req.Id, ct);

if(entity == null)
{
await SendNotFoundAsync(ct);
return null!;
}

return entity.ToModel();
}
}
8 changes: 8 additions & 0 deletions src/modules/Elsa.Secrets.Api/Endpoints/Secrets/Get/Request.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;

namespace Elsa.Secrets.Api.Endpoints.Secrets.Get;

public class Request
{
[Required] public string Id { get; set; } = default!;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Elsa.Abstractions;
using Elsa.Secrets.Management;
using Elsa.Secrets.UniqueName;
using JetBrains.Annotations;

namespace Elsa.Secrets.Api.Endpoints.Secrets.IsUniqueName;

/// Checks if a name is unique.
[UsedImplicitly]
public class Endpoint(ISecretManager agentManager) : ElsaEndpoint<IsUniqueNameRequest, IsUniqueNameResponse>
{
/// <inheritdoc />
public override void Configure()
{
Post("/queries/secrets/is-unique-name");
ConfigurePermissions("secrets:write");
}

/// <inheritdoc />
public override async Task<IsUniqueNameResponse> ExecuteAsync(IsUniqueNameRequest req, CancellationToken ct)
{
var isUnique = await agentManager.IsNameUniqueAsync(req.Name, req.Id, ct);
return new(isUnique);
}
}
59 changes: 59 additions & 0 deletions src/modules/Elsa.Secrets.Api/Endpoints/Secrets/Update/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Elsa.Abstractions;
using Elsa.Common.Contracts;
using Elsa.Secrets.Management;
using JetBrains.Annotations;

namespace Elsa.Secrets.Api.Endpoints.Secrets.Update;

/// Updates an agent.
[UsedImplicitly]
public class Endpoint(ISecretManager agentManager, ISystemClock systemClock) : ElsaEndpoint<SecretInputModel, SecretModel>
{
/// <inheritdoc />
public override void Configure()
{
Post("/secrets/{id}");
ConfigurePermissions("secrets:write");
}

/// <inheritdoc />
public override async Task<SecretModel> ExecuteAsync(SecretInputModel req, CancellationToken ct)
{
var id = Route<string>("id")!;
var entity = await agentManager.GetAsync(id, ct);

if (entity == null)
{
await SendNotFoundAsync(ct);
return null!;
}

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

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

entity.Name = req.Name.Trim();
entity.Description = req.Description.Trim();
entity.Algorithm = req.Algorithm;
entity.Type = req.Type;
entity.EncryptedValue = req.EncryptedValue;
entity.IV = req.IV;
entity.ExpiresAt = req.ExpiresAt;
entity.RotationPolicy = req.RotationPolicy;
entity.EncryptionKeyId = req.EncryptionKeyId;
entity.UpdatedAt = systemClock.UtcNow;

await agentManager.UpdateAsync(entity, ct);
return entity.ToModel();
}

private async Task<bool> IsNameDuplicateAsync(string name, string id, CancellationToken cancellationToken)
{
return !await agentManager.IsNameUniqueAsync(name, id, cancellationToken);
}
}
1 change: 1 addition & 0 deletions src/modules/Elsa.Secrets.Core/Elsa.Secrets.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj" />
<ProjectReference Include="..\Elsa.Secrets.Models\Elsa.Secrets.Models.csproj" />
</ItemGroup>

</Project>
31 changes: 31 additions & 0 deletions src/modules/Elsa.Secrets.Management/Contracts/ISecretManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Elsa.Secrets.Management;

public interface ISecretManager
{
/// Adds a new entity to the store.
Task AddAsync(Secret entity, CancellationToken cancellationToken = default);

/// Updates the entity to the store.
Task UpdateAsync(Secret entity, CancellationToken cancellationToken = default);

/// Gets the entity from the store.
Task<Secret?> GetAsync(string id, CancellationToken cancellationToken = default);

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

/// Gets all entities from the store.
Task<IEnumerable<Secret>> ListAsync(CancellationToken cancellationToken = default);

/// Deletes the entity from the store.
Task DeleteAsync(Secret entity, CancellationToken cancellationToken = default);

/// 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);
}
24 changes: 24 additions & 0 deletions src/modules/Elsa.Secrets.Management/Contracts/ISecretStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,30 @@ namespace Elsa.Secrets.Management;

public interface ISecretStore
{
/// Finds the entities from the store.
Task<Page<Secret>> FindManyAsync<TOrderBy>(SecretFilter filter, SecretOrder<TOrderBy> order, PageArgs pageArgs, CancellationToken cancellationToken = default);

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

/// Adds a new entity to the store.
Task AddAsync(Secret entity, CancellationToken cancellationToken = default);

/// Updates the entity to the store.
Task UpdateAsync(Secret entity, CancellationToken cancellationToken = default);

/// Gets the entity from the store.
Task<Secret?> GetAsync(string id, CancellationToken cancellationToken = default);

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

/// Gets all entities from the store.
Task<IEnumerable<Secret>> ListAsync(CancellationToken cancellationToken = default);

/// Deletes the entity from the store.
Task DeleteAsync(Secret entity, CancellationToken cancellationToken = default);

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

0 comments on commit ed4e8a8

Please sign in to comment.