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",