mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 13:03:12 +00:00
117 lines
4.7 KiB
C#
117 lines
4.7 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using Amazon.SecretsManager;
|
|
using Amazon.SecretsManager.Model;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using SeaHaven.Services.Configuration;
|
|
using SeaHaven.Services.Interfaces;
|
|
|
|
namespace Api.SeaHavenIndustries.Infrastructure
|
|
{
|
|
public sealed class AwsWorkOrderWebhookSecretProvider : IWorkOrderWebhookSecretProvider
|
|
{
|
|
private readonly IAmazonSecretsManager _client;
|
|
private readonly IOptionsMonitor<WorkOrderWebhookOptions> _options;
|
|
private readonly TimeProvider _timeProvider;
|
|
private readonly ILogger<AwsWorkOrderWebhookSecretProvider> _logger;
|
|
private readonly ConcurrentDictionary<string, CacheEntry> _cache =
|
|
new(StringComparer.Ordinal);
|
|
private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks =
|
|
new(StringComparer.Ordinal);
|
|
|
|
public AwsWorkOrderWebhookSecretProvider(
|
|
IAmazonSecretsManager client,
|
|
IOptionsMonitor<WorkOrderWebhookOptions> options,
|
|
TimeProvider timeProvider,
|
|
ILogger<AwsWorkOrderWebhookSecretProvider> logger)
|
|
{
|
|
_client = client;
|
|
_options = options;
|
|
_timeProvider = timeProvider;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<WorkOrderWebhookSecretResult> GetSecretAsync(
|
|
string keyId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var options = _options.CurrentValue;
|
|
if (!options.KeySecrets.TryGetValue(keyId, out var secretArn)
|
|
|| string.IsNullOrWhiteSpace(secretArn))
|
|
{
|
|
if (_cache.TryRemove(keyId, out var removed))
|
|
CryptographicOperations.ZeroMemory(removed.Secret);
|
|
return new WorkOrderWebhookSecretResult(WorkOrderWebhookSecretStatus.UnknownKey);
|
|
}
|
|
|
|
var now = _timeProvider.GetUtcNow();
|
|
if (_cache.TryGetValue(keyId, out var cached)
|
|
&& cached.SecretArn == secretArn
|
|
&& cached.ExpiresAt > now)
|
|
return FoundCopy(cached.Secret);
|
|
|
|
var keyLock = _locks.GetOrAdd(keyId, _ => new SemaphoreSlim(1, 1));
|
|
await keyLock.WaitAsync(cancellationToken);
|
|
try
|
|
{
|
|
now = _timeProvider.GetUtcNow();
|
|
if (_cache.TryGetValue(keyId, out cached)
|
|
&& cached.SecretArn == secretArn
|
|
&& cached.ExpiresAt > now)
|
|
return FoundCopy(cached.Secret);
|
|
|
|
var response = await _client.GetSecretValueAsync(
|
|
new GetSecretValueRequest { SecretId = secretArn },
|
|
cancellationToken);
|
|
var secret = ReadSecret(response);
|
|
if (secret.Length == 0)
|
|
return new WorkOrderWebhookSecretResult(WorkOrderWebhookSecretStatus.Unavailable);
|
|
|
|
var ttl = TimeSpan.FromSeconds(Math.Clamp(options.SecretCacheSeconds, 1, 3600));
|
|
var replacement = new CacheEntry(secretArn, secret, now.Add(ttl));
|
|
_cache.AddOrUpdate(
|
|
keyId,
|
|
replacement,
|
|
(_, prior) =>
|
|
{
|
|
CryptographicOperations.ZeroMemory(prior.Secret);
|
|
return replacement;
|
|
});
|
|
return FoundCopy(secret);
|
|
}
|
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// AWS exceptions can contain the configured secret ARN. Keep
|
|
// diagnostics intentionally generic for this secret-bearing path.
|
|
_logger.LogError("Work-order webhook signing secret retrieval failed.");
|
|
return new WorkOrderWebhookSecretResult(WorkOrderWebhookSecretStatus.Unavailable);
|
|
}
|
|
finally
|
|
{
|
|
keyLock.Release();
|
|
}
|
|
}
|
|
|
|
private static byte[] ReadSecret(GetSecretValueResponse response)
|
|
{
|
|
if (!string.IsNullOrEmpty(response.SecretString))
|
|
return Encoding.UTF8.GetBytes(response.SecretString);
|
|
|
|
return response.SecretBinary?.ToArray() ?? Array.Empty<byte>();
|
|
}
|
|
|
|
private static WorkOrderWebhookSecretResult FoundCopy(byte[] secret) =>
|
|
new(WorkOrderWebhookSecretStatus.Found, (byte[])secret.Clone());
|
|
|
|
private sealed record CacheEntry(
|
|
string SecretArn,
|
|
byte[] Secret,
|
|
DateTimeOffset ExpiresAt);
|
|
}
|
|
}
|