using IceImportDAL.DTO.IceMapFtpSource; using IceImportDAL.DTO.RemoteFileSources; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using VaultSharp; namespace IceImportBLL.IceImportSecretResolver; // Reads secrets from HashiCorp Vault KV v2 — same construction/caching pattern as // GB5Solution/PAY/PAYBLL/Vault/VaultService.cs (the only existing "Vault-secret-by-key" // abstraction found anywhere in the repo; see IIceImportSecretResolver's doc comment for the full // design-decision writeup on why this isn't a shared GB5Shared abstraction reused as-is). // // NEVER log a resolved secret value — only the *VaultRef path is safe to log (see // MicemapFtpSourceDTO's doc comment). The mount point is always "secret", matching PAYBLL's // convention; each *VaultRef is the mount-relative key exactly as stored on MICEMAPFTPSOURCE. public class IceImportSecretResolver : IIceImportSecretResolver { private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(5); private readonly IVaultClient _vault; private readonly IMemoryCache _cache; private readonly ILogger _logger; public IceImportSecretResolver(IVaultClient vault, IMemoryCache cache, ILogger logger) { _vault = vault; _cache = cache; _logger = logger; } public async Task ResolveAsync(MicemapFtpSourceDTO config, CancellationToken ct) { var host = await GetSecretAsync(config.HostVaultRef, ct).ConfigureAwait(false); var username = await GetSecretAsync(config.UsernameVaultRef, ct).ConfigureAwait(false); var credential = await GetSecretAsync(config.CredentialVaultRef, ct).ConfigureAwait(false); return new RemoteFileSourceConfigDTO { IceMapId = config.IceMapId, ConnectionType = config.ConnectionType, Host = host, Username = username, Credential = credential, RemoteFolderPath = config.RemoteFolderPath, FileNamePattern = config.FileNamePattern, PostProcessAction = config.PostProcessAction, ArchiveFolderPath = config.ArchiveFolderPath }; } private async Task GetSecretAsync(string vaultKeyPath, CancellationToken ct) { if (_cache.TryGetValue(vaultKeyPath, out string? cached) && cached is not null) return cached; _logger.LogDebug("IceImport: Vault cache miss for path {VaultPath} — fetching from Vault", vaultKeyPath); // KV v2: path format is "secret/data/{vaultKeyPath}" — ReadSecretAsync handles the /data/ // segment internally when mountPoint is provided. var secret = await _vault.V1.Secrets.KeyValue.V2 .ReadSecretAsync(path: vaultKeyPath, mountPoint: "secret") .ConfigureAwait(false); var value = secret?.Data?.Data?.Values.FirstOrDefault()?.ToString() ?? throw new InvalidOperationException( $"Vault secret at path '{vaultKeyPath}' returned empty or null data."); var options = new MemoryCacheEntryOptions().SetSlidingExpiration(CacheTtl); _cache.Set(vaultKeyPath, value, options); return value; } }