From acecd9c9312e8e343f2eba631ae81ad872a8a518 Mon Sep 17 00:00:00 2001
From: Chris Turner <23338096+christopherjturner@users.noreply.github.com>
Date: Wed, 26 Jun 2024 11:33:16 +0100
Subject: [PATCH] 400511 secert key event handler
---
.../Defra.Cdp.Backend.Api.Tests.csproj | 3 +
.../Resources/payload-get-all-secrets.json | 20 +++++
.../Secrets/SecretEventHandlerTest.cs | 71 +++++++++++++++
.../Config/SecretManagerListenerOptions.cs | 8 ++
.../Endpoints/DeploymentsEndpointV2.cs | 14 +--
.../Endpoints/TenantSecretsEndpoint.cs | 21 +++++
Defra.Cdp.Backend.Api/Models/DeploymentV2.cs | 5 ++
.../Models/RequestedDeployment.cs | 7 ++
Defra.Cdp.Backend.Api/Models/TenantSecrets.cs | 23 +++++
Defra.Cdp.Backend.Api/Program.cs | 14 +++
.../Services/Secrets/SecretEventHandler.cs | 89 +++++++++++++++++++
.../Services/Secrets/SecretEventListener.cs | 50 +++++++++++
.../Services/Secrets/SecretsService.cs | 65 ++++++++++++++
.../Services/Secrets/events/Message.cs | 28 ++++++
Defra.Cdp.Backend.Api/appsettings.json | 4 +
15 files changed, 416 insertions(+), 6 deletions(-)
create mode 100644 Defra.Cdp.Backend.Api.Tests/Resources/payload-get-all-secrets.json
create mode 100644 Defra.Cdp.Backend.Api.Tests/Services/Secrets/SecretEventHandlerTest.cs
create mode 100644 Defra.Cdp.Backend.Api/Config/SecretManagerListenerOptions.cs
create mode 100644 Defra.Cdp.Backend.Api/Endpoints/TenantSecretsEndpoint.cs
create mode 100644 Defra.Cdp.Backend.Api/Models/TenantSecrets.cs
create mode 100644 Defra.Cdp.Backend.Api/Services/Secrets/SecretEventHandler.cs
create mode 100644 Defra.Cdp.Backend.Api/Services/Secrets/SecretEventListener.cs
create mode 100644 Defra.Cdp.Backend.Api/Services/Secrets/SecretsService.cs
create mode 100644 Defra.Cdp.Backend.Api/Services/Secrets/events/Message.cs
diff --git a/Defra.Cdp.Backend.Api.Tests/Defra.Cdp.Backend.Api.Tests.csproj b/Defra.Cdp.Backend.Api.Tests/Defra.Cdp.Backend.Api.Tests.csproj
index 6922c19..5b50245 100644
--- a/Defra.Cdp.Backend.Api.Tests/Defra.Cdp.Backend.Api.Tests.csproj
+++ b/Defra.Cdp.Backend.Api.Tests/Defra.Cdp.Backend.Api.Tests.csproj
@@ -32,6 +32,9 @@
PreserveNewest
+
+ PreserveNewest
+
diff --git a/Defra.Cdp.Backend.Api.Tests/Resources/payload-get-all-secrets.json b/Defra.Cdp.Backend.Api.Tests/Resources/payload-get-all-secrets.json
new file mode 100644
index 0000000..3d0cff8
--- /dev/null
+++ b/Defra.Cdp.Backend.Api.Tests/Resources/payload-get-all-secrets.json
@@ -0,0 +1,20 @@
+{
+ "source": "cdp-secret-manager-lambda",
+ "statusCode": 200,
+ "action": "get_all_secret_keys",
+ "body": {
+ "get_all_secret_keys": true,
+ "keys": {
+ "cdp/services/test-service-one": [
+ "placeholder",
+ "EXAMPLE_ONE"
+ ],
+ "cdp/services/test-service-two": [
+ "placeholder",
+ "EXAMPLE_TWO"
+ ]
+ },
+ "exception": "",
+ "environment": "dev"
+ }
+}
diff --git a/Defra.Cdp.Backend.Api.Tests/Services/Secrets/SecretEventHandlerTest.cs b/Defra.Cdp.Backend.Api.Tests/Services/Secrets/SecretEventHandlerTest.cs
new file mode 100644
index 0000000..57d61fe
--- /dev/null
+++ b/Defra.Cdp.Backend.Api.Tests/Services/Secrets/SecretEventHandlerTest.cs
@@ -0,0 +1,71 @@
+using System.Text.Json;
+using Defra.Cdp.Backend.Api.Models;
+using Defra.Cdp.Backend.Api.Services.Secrets;
+using Defra.Cdp.Backend.Api.Services.Secrets.events;
+using NSubstitute;
+
+namespace Defra.Cdp.Backend.Api.Tests.Services.Secrets;
+
+public class SecretEventHandlerTest
+{
+
+ [Fact]
+ public async void WillProcessGetAllSecretsPayload()
+ {
+ var service = Substitute.For();
+ var eventHandler = new SecretEventHandler(service, ConsoleLogger.CreateLogger());
+
+ var mockPayload = SecretEventHandler.TryParseMessageHeader(await File.ReadAllTextAsync("Resources/payload-get-all-secrets.json"));
+
+ Assert.NotNull(mockPayload);
+ service
+ .UpdateSecrets(Arg.Any>(), Arg.Any())
+ .Returns(Task.CompletedTask);
+ await eventHandler.Handle(mockPayload, new CancellationToken());
+
+ await service.ReceivedWithAnyArgs().UpdateSecrets(Arg.Any>(), Arg.Any());
+
+ }
+
+ [Fact]
+ public void TryParseMessageHeaderWithValidPayload()
+ {
+ var body = "{\"source\": \"cdp-secret-manager-lambda\", \"statusCode\": 200, \"action\": \"get_all_secret_keys\", \"body\": {}}";
+ var res = SecretEventHandler.TryParseMessageHeader(body);
+ Assert.NotNull(res);
+ }
+
+ [Fact]
+ public void TryParseMessageHeaderInvalid()
+ {
+ var otherLambda = "{\"source\": \"cdp-some-other-lambda\", \"statusCode\": 200, \"action\": \"get_all_secret_keys\", \"body\": {}}";
+ var res = SecretEventHandler.TryParseMessageHeader(otherLambda);
+ Assert.Null(res);
+
+ var otherMessage = "{\"foo\": \"bar\"}";
+ res = SecretEventHandler.TryParseMessageHeader(otherMessage);
+ Assert.Null(res);
+
+ var invalidJson = "foo";
+ res = SecretEventHandler.TryParseMessageHeader(invalidJson);
+ Assert.Null(res);
+ }
+
+ [Fact]
+ public void CanExtractBody()
+ {
+ var body = "{\"source\": \"cdp-secret-manager-lambda\", \"statusCode\": 200, \"action\": \"get_all_secret_keys\", \"body\": " +
+ "{ \"get_all_secret_keys\": true, " +
+ "\"exception\": \"\", " +
+ "\"environment\": \"dev\", " +
+ "\"keys\": {\"cdp/service/foo\": [\"FOO\"]}" +
+ "}}";
+ var res = SecretEventHandler.TryParseMessageHeader(body);
+ Assert.NotNull(res);
+
+ var parsedBody = res.Body.Deserialize();
+ Assert.Equal("dev", parsedBody?.Environment);
+ Assert.Equal("", parsedBody?.Exception);
+ Assert.Equal(1, parsedBody?.Keys.Count);
+ }
+}
\ No newline at end of file
diff --git a/Defra.Cdp.Backend.Api/Config/SecretManagerListenerOptions.cs b/Defra.Cdp.Backend.Api/Config/SecretManagerListenerOptions.cs
new file mode 100644
index 0000000..3c545c6
--- /dev/null
+++ b/Defra.Cdp.Backend.Api/Config/SecretManagerListenerOptions.cs
@@ -0,0 +1,8 @@
+namespace Defra.Cdp.Backend.Api.Config;
+
+public class SecretEventListenerOptions
+{
+ public const string Prefix = "SecretManagerEvents";
+ public string QueueUrl { get; set; } = null!;
+ public bool Enabled { get; set; }
+}
\ No newline at end of file
diff --git a/Defra.Cdp.Backend.Api/Endpoints/DeploymentsEndpointV2.cs b/Defra.Cdp.Backend.Api/Endpoints/DeploymentsEndpointV2.cs
index 26861f9..d3a14b0 100644
--- a/Defra.Cdp.Backend.Api/Endpoints/DeploymentsEndpointV2.cs
+++ b/Defra.Cdp.Backend.Api/Endpoints/DeploymentsEndpointV2.cs
@@ -1,5 +1,6 @@
using Defra.Cdp.Backend.Api.Models;
using Defra.Cdp.Backend.Api.Services.Deployments;
+using Defra.Cdp.Backend.Api.Services.Secrets;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
@@ -77,7 +78,7 @@ public static IEndpointRouteBuilder MapDeploymentsEndpointV2(this IEndpointRoute
var deployment = await deploymentsService.FindDeployment(deploymentId, cancellationToken);
return deployment == null
- ? Results.NotFound(new { Message = $"{deploymentId} was not found" })
+ ? Results.NotFound(new ApiError($"{deploymentId} was not found"))
: Results.Ok(deployment);
}
@@ -99,17 +100,18 @@ public static IEndpointRouteBuilder MapDeploymentsEndpointV2(this IEndpointRoute
private static async Task RegisterDeployment(
IDeploymentsServiceV2 deploymentsServiceV2,
+ ISecretsService secretsService,
IValidator validator,
- RequestedDeployment rd,
+ RequestedDeployment requestedDeployment,
ILoggerFactory loggerFactory,
CancellationToken cancellationToken)
{
- var validatedResult = await validator.ValidateAsync(rd, cancellationToken);
+ var validatedResult = await validator.ValidateAsync(requestedDeployment, cancellationToken);
if (!validatedResult.IsValid) return Results.ValidationProblem(validatedResult.ToDictionary());
-
+
var logger = loggerFactory.CreateLogger("RegisterDeployment");
- logger.LogInformation("Registering deployment {RdDeploymentId}", rd.DeploymentId);
- await deploymentsServiceV2.RegisterDeployment(rd, cancellationToken);
+ logger.LogInformation("Registering deployment {DeploymentId}", requestedDeployment.DeploymentId);
+ await deploymentsServiceV2.RegisterDeployment(requestedDeployment, cancellationToken);
return Results.Ok();
}
diff --git a/Defra.Cdp.Backend.Api/Endpoints/TenantSecretsEndpoint.cs b/Defra.Cdp.Backend.Api/Endpoints/TenantSecretsEndpoint.cs
new file mode 100644
index 0000000..8703a42
--- /dev/null
+++ b/Defra.Cdp.Backend.Api/Endpoints/TenantSecretsEndpoint.cs
@@ -0,0 +1,21 @@
+using Defra.Cdp.Backend.Api.Models;
+using Defra.Cdp.Backend.Api.Services.Secrets;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Defra.Cdp.Backend.Api.Endpoints;
+
+public static class TenantSecretsEndpoint
+{
+ public static IEndpointRouteBuilder MapTenantSecretsEndpoint(this IEndpointRouteBuilder app)
+ {
+ app.MapGet("secrets/{environment}/{service}", FindTenantSecrets);
+ return app;
+ }
+
+ static async Task FindTenantSecrets([FromServices] ISecretsService secretsService, string environment,
+ string service, CancellationToken cancellationToken)
+ {
+ var secrets = await secretsService.FindSecrets(environment, service, cancellationToken);
+ return secrets == null ? Results.NotFound(new ApiError("secrets not found")) : Results.Ok(secrets);
+ }
+}
\ No newline at end of file
diff --git a/Defra.Cdp.Backend.Api/Models/DeploymentV2.cs b/Defra.Cdp.Backend.Api/Models/DeploymentV2.cs
index 80be00a..e305346 100644
--- a/Defra.Cdp.Backend.Api/Models/DeploymentV2.cs
+++ b/Defra.Cdp.Backend.Api/Models/DeploymentV2.cs
@@ -31,6 +31,9 @@ public class DeploymentV2
public string Status { get; set; }
public bool Unstable { get; set; } = false;
+ public string? ConfigVersion { get; init; } = default!;
+ public List Secrets { get; init; } = new();
+
public static DeploymentV2 FromRequest(RequestedDeployment req)
{
return new DeploymentV2
@@ -46,6 +49,8 @@ public static DeploymentV2 FromRequest(RequestedDeployment req)
Created = DateTime.Now,
Updated = DateTime.Now,
Status = req.InstanceCount > 0 ? Requested : Undeployed,
+ ConfigVersion = req.ConfigVersion,
+ Secrets = req.Secrets
};
}
diff --git a/Defra.Cdp.Backend.Api/Models/RequestedDeployment.cs b/Defra.Cdp.Backend.Api/Models/RequestedDeployment.cs
index dbad4b5..85b9c9b 100644
--- a/Defra.Cdp.Backend.Api/Models/RequestedDeployment.cs
+++ b/Defra.Cdp.Backend.Api/Models/RequestedDeployment.cs
@@ -31,4 +31,11 @@ public sealed class RequestedDeployment
[property: JsonPropertyName("deploymentId")]
public string DeploymentId { get; init; } = default!;
+
+ [property: JsonPropertyName("configVersion")]
+ public string? ConfigVersion { get; init; } = default!;
+
+ [property: JsonPropertyName("secrets")]
+ public List Secrets { get; init; } = new();
+
}
\ No newline at end of file
diff --git a/Defra.Cdp.Backend.Api/Models/TenantSecrets.cs b/Defra.Cdp.Backend.Api/Models/TenantSecrets.cs
new file mode 100644
index 0000000..4f3ad2e
--- /dev/null
+++ b/Defra.Cdp.Backend.Api/Models/TenantSecrets.cs
@@ -0,0 +1,23 @@
+using System.Text.Json.Serialization;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+using MongoDB.Bson.Serialization.IdGenerators;
+
+namespace Defra.Cdp.Backend.Api.Models;
+
+public class TenantSecrets
+{
+ [BsonId(IdGenerator = typeof(ObjectIdGenerator))]
+ [BsonIgnoreIfDefault]
+ [JsonIgnore(Condition = JsonIgnoreCondition.Always)]
+ public ObjectId? Id { get; init; } = default;
+
+ [property: JsonPropertyName("service")]
+ public string Service { get; init; } = default!;
+
+ [property: JsonPropertyName("environment")]
+ public string Environment { get; init; } = default!;
+
+ [property: JsonPropertyName("secrets")]
+ public List Secrets { get; init; } = new();
+}
\ No newline at end of file
diff --git a/Defra.Cdp.Backend.Api/Program.cs b/Defra.Cdp.Backend.Api/Program.cs
index fa8e356..d619b2f 100644
--- a/Defra.Cdp.Backend.Api/Program.cs
+++ b/Defra.Cdp.Backend.Api/Program.cs
@@ -9,6 +9,7 @@
using Defra.Cdp.Backend.Api.Services.Deployments;
using Defra.Cdp.Backend.Api.Services.Github;
using Defra.Cdp.Backend.Api.Services.Github.ScheduledTasks;
+using Defra.Cdp.Backend.Api.Services.Secrets;
using Defra.Cdp.Backend.Api.Services.TenantArtifacts;
using Defra.Cdp.Backend.Api.Services.TestSuites;
using Defra.Cdp.Backend.Api.Utils;
@@ -71,6 +72,7 @@
// Setup the services
builder.Services.Configure(builder.Configuration.GetSection(EcsEventListenerOptions.Prefix));
builder.Services.Configure(builder.Configuration.GetSection(EcrEventListenerOptions.Prefix));
+builder.Services.Configure(builder.Configuration.GetSection(SecretEventListenerOptions.Prefix));
builder.Services.Configure(builder.Configuration.GetSection(DockerServiceOptions.Prefix));
builder.Services.Configure(builder.Configuration.GetSection(DeployablesClientOptions.Prefix));
builder.Services.AddScoped, RequestedDeploymentValidator>();
@@ -131,8 +133,12 @@
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
builder.Services.AddSingleton();
// Validators
@@ -172,6 +178,7 @@
app.MapLibrariesEndpoint();
app.MapRepositoriesEndpoint();
app.MapTestSuiteEndpoint();
+app.MapTenantSecretsEndpoint();
app.MapAdminEndpoint();
app.MapHealthChecks("/health");
@@ -188,6 +195,13 @@
Task.Run(() =>
ecrSqsEventListener?.ReadAsync(app.Lifetime
.ApplicationStopping)); // do not await this, we want it to run in the background
+
+var secretEventListener = app.Services.GetService();
+logger.Information("Starting Secret Event listener - reading secret update events from SQS");
+Task.Run(() =>
+ secretEventListener?.ReadAsync(app.Lifetime
+ .ApplicationStopping)); // do not await this, we want it to run in the background
+
#pragma warning restore CS4014
app.Run();
\ No newline at end of file
diff --git a/Defra.Cdp.Backend.Api/Services/Secrets/SecretEventHandler.cs b/Defra.Cdp.Backend.Api/Services/Secrets/SecretEventHandler.cs
new file mode 100644
index 0000000..a189d5c
--- /dev/null
+++ b/Defra.Cdp.Backend.Api/Services/Secrets/SecretEventHandler.cs
@@ -0,0 +1,89 @@
+using System.Text.Json;
+using Defra.Cdp.Backend.Api.Models;
+using Defra.Cdp.Backend.Api.Services.Secrets.events;
+
+namespace Defra.Cdp.Backend.Api.Services.Secrets;
+
+public interface ISecretEventHandler
+{
+ Task Handle(MessageHeader header, CancellationToken cancellationToken);
+}
+
+/**
+ * Handles specific payloads sent by the secret manager lambda.
+ * All messages have the same outer body detailing the source & action.
+ */
+public class SecretEventHandler : ISecretEventHandler
+{
+
+ private readonly ISecretsService _secretsService;
+ private readonly ILogger _logger;
+
+ public SecretEventHandler(ISecretsService secretsService, ILogger logger)
+ {
+ _logger = logger;
+ _secretsService = secretsService;
+ }
+
+ public async Task Handle(MessageHeader header, CancellationToken cancellationToken)
+ {
+ switch (header.Action)
+ {
+ case "get_all_secret_keys":
+ await HandleGetAllSecrets(header, cancellationToken);
+ break;
+ default:
+ _logger.LogDebug("Ignoring action: {Action} not handled", header.Action);
+ return;
+ }
+ }
+
+ /**
+ * Handler for get_all_secret_keys action. Contains a dict of all the services in an environment that have
+ * secret values set along with a list of the key/environment variable the secret is bound to,
+ * but NOT the actual secret itself.
+ */
+ public async Task HandleGetAllSecrets(MessageHeader header, CancellationToken cancellationToken)
+ {
+ var body = header.Body?.Deserialize();
+ if (body == null)
+ {
+ _logger.LogInformation("Failed to parse body of 'get_all_secret_keys' message");
+ return;
+ }
+
+ if (body.Exception != "")
+ {
+ _logger.LogError("get_all_secret_keys message contained exception {}", body.Exception);
+ return;
+ }
+
+ _logger.LogInformation("Updating secrets in {Environment}", body.Environment);
+ var secrets = new List();
+
+ foreach (var kv in body.Keys)
+ {
+ var service = kv.Key.Replace("cdp/services/", "");
+ secrets.Add(new TenantSecrets
+ {
+ Service = service, Environment = body.Environment, Secrets = kv.Value
+ });
+ }
+
+ await _secretsService.UpdateSecrets(secrets, cancellationToken);
+ _logger.LogInformation("Updated secrets for {Environment}", body.Environment);
+ }
+
+ public static MessageHeader? TryParseMessageHeader(string body)
+ {
+ try
+ {
+ var header = JsonSerializer.Deserialize(body);
+ return header?.Source != "cdp-secret-manager-lambda" ? null : header;
+ }
+ catch(Exception e)
+ {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Defra.Cdp.Backend.Api/Services/Secrets/SecretEventListener.cs b/Defra.Cdp.Backend.Api/Services/Secrets/SecretEventListener.cs
new file mode 100644
index 0000000..1a31eac
--- /dev/null
+++ b/Defra.Cdp.Backend.Api/Services/Secrets/SecretEventListener.cs
@@ -0,0 +1,50 @@
+using Amazon.SQS;
+using Amazon.SQS.Model;
+using Defra.Cdp.Backend.Api.Config;
+using Defra.Cdp.Backend.Api.Services.Aws;
+using Microsoft.Extensions.Options;
+
+namespace Defra.Cdp.Backend.Api.Services.Secrets;
+
+/**
+ * Listens for events sent by the cdp-secret-manager-lambda.
+ * Messages are sent cross-account to the portal and contain information about secret
+ * creation/deletion/changes.
+ */
+public class SecretEventListener : SqsListener
+{
+ private readonly ILogger _logger;
+ private readonly ISecretEventHandler _secretEventHandler;
+
+ public SecretEventListener(IAmazonSQS sqs,
+ IOptions config,
+ ISecretEventHandler secretEventHandler,
+ ILogger logger) : base(sqs, config.Value.QueueUrl, logger)
+ {
+ _secretEventHandler = secretEventHandler;
+ _logger = logger;
+ }
+
+ public override async Task HandleMessageAsync(Message message, CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Received message from {queue}: {MessageId}", QueueUrl, message.MessageId);
+
+ try
+ {
+ var secret = SecretEventHandler.TryParseMessageHeader(message.Body);
+ if (secret != null)
+ {
+ await _secretEventHandler.Handle(secret, cancellationToken);
+ }
+ else
+ {
+ _logger.LogInformation("Message from {queue}: {MessageId} was not readable", QueueUrl,
+ message.MessageId);
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogError("Failed to handle message: {id}, {err}", message.MessageId, e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Defra.Cdp.Backend.Api/Services/Secrets/SecretsService.cs b/Defra.Cdp.Backend.Api/Services/Secrets/SecretsService.cs
new file mode 100644
index 0000000..dde3b4f
--- /dev/null
+++ b/Defra.Cdp.Backend.Api/Services/Secrets/SecretsService.cs
@@ -0,0 +1,65 @@
+using Defra.Cdp.Backend.Api.Models;
+using Defra.Cdp.Backend.Api.Mongo;
+using MongoDB.Driver;
+
+namespace Defra.Cdp.Backend.Api.Services.Secrets;
+
+public interface ISecretsService
+{
+ public Task UpdateSecrets(TenantSecrets secret, CancellationToken cancellationToken);
+ public Task UpdateSecrets(List secrets, CancellationToken cancellationToken);
+ public Task FindSecrets( string environment, string service, CancellationToken cancellationToken);
+}
+
+public class SecretsService : MongoService, ISecretsService
+{
+
+ public SecretsService(IMongoDbClientFactory connectionFactory,ILoggerFactory loggerFactory) : base(connectionFactory, "tenantsecrets", loggerFactory)
+ {
+
+ }
+
+ protected override List> DefineIndexes(IndexKeysDefinitionBuilder builder)
+ {
+ var serviceAndEnv = new CreateIndexModel(builder.Combine(
+ builder.Ascending(s => s.Service),
+ builder.Ascending(s => s.Environment)
+ ));
+
+ return new List> { serviceAndEnv};
+ }
+
+ public async Task FindSecrets(string environment, string service, CancellationToken cancellationToken)
+ {
+ return await Collection.Find(t => t.Service == service && t.Environment == environment).FirstOrDefaultAsync(cancellationToken);
+ }
+
+ public async Task UpdateSecrets(TenantSecrets secret, CancellationToken cancellationToken)
+ {
+ await Collection.ReplaceOneAsync(
+ s => s.Service == secret.Service && s.Environment == secret.Environment, secret,
+ new ReplaceOptions { IsUpsert = true },
+ cancellationToken);
+ }
+
+ public async Task UpdateSecrets(List secrets, CancellationToken cancellationToken)
+ {
+ var updateSecretModels =
+ secrets.Select(secret =>
+ {
+ var filterBuilder = Builders.Filter;
+ var filter = filterBuilder
+ .And(
+ filterBuilder.Eq(s => s.Service, secret.Service),
+ filterBuilder.Eq(s => s.Environment, secret.Environment)
+ );
+ return new ReplaceOneModel(filter, secret) { IsUpsert = true };
+ }).ToList();
+
+ if (updateSecretModels.Any())
+ {
+ await Collection.BulkWriteAsync(updateSecretModels, new BulkWriteOptions(), cancellationToken);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/Defra.Cdp.Backend.Api/Services/Secrets/events/Message.cs b/Defra.Cdp.Backend.Api/Services/Secrets/events/Message.cs
new file mode 100644
index 0000000..832b497
--- /dev/null
+++ b/Defra.Cdp.Backend.Api/Services/Secrets/events/Message.cs
@@ -0,0 +1,28 @@
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+
+namespace Defra.Cdp.Backend.Api.Services.Secrets.events;
+
+/**
+ * Secret Manager Lambda sends various messages. We can parse the first few fields to work
+ * out how to parse the body.
+ * {"source": "cdp-secret-manager-lambda",
+ * "statusCode": 200,
+ * "action": "get_all_secret_keys",
+ * "body": { depends on 'action' type }
+ * }'
+ */
+public record MessageHeader
+{
+ [JsonPropertyName("statusCode")] public int? StatusCode { get; init; }
+ [JsonPropertyName("source")] public string? Source { get; init; }
+ [JsonPropertyName("action")] public string? Action { get; init; }
+ [JsonPropertyName("body")] public JsonObject? Body { get; init; }
+}
+
+public record BodyGetAllSecretKeys
+{
+ [JsonPropertyName("keys")] public Dictionary> Keys { get; init; } = new();
+ [JsonPropertyName("exception")] public string Exception { get; init; } = "";
+ [JsonPropertyName("environment")] public string Environment { get; init; } = "";
+}
diff --git a/Defra.Cdp.Backend.Api/appsettings.json b/Defra.Cdp.Backend.Api/appsettings.json
index edb9a8e..33d9086 100644
--- a/Defra.Cdp.Backend.Api/appsettings.json
+++ b/Defra.Cdp.Backend.Api/appsettings.json
@@ -34,6 +34,10 @@
"WaitTimeSeconds": 15,
"Enabled": true
},
+ "SecretManagerEvents": {
+ "QueueUrl": "http://localhost:4566/000000000000/secret_management_updates",
+ "Enabled": true
+ },
"EnvironmentMappings": {
"111111111": "prod",
"222222222": "perf-test",