// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Text.Json; using System.Threading.Tasks; using Amazon.SecretsManager; using Amazon.SecretsManager.Model; using Azure; using Azure.Identity; using Azure.Security.KeyVault.Secrets; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Jupiter.Common.Implementation { public interface ISecretResolver { string Resolve(string value); } public class SecretResolver : ISecretResolver { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; public SecretResolver(IServiceProvider serviceProvider, ILogger logger) { _serviceProvider = serviceProvider; _logger = logger; } public string Resolve(string value) { int providerSeparator = value.IndexOf("!", StringComparison.Ordinal); if (providerSeparator != -1) { string providerId = value.Substring(0, providerSeparator); string providerPath = value.Substring(providerSeparator + 1); return ResolveUsingProvider(providerId, providerPath, value); } return value; } private string ResolveUsingProvider(string providerId, string providerPath, string originalValue) { switch (providerId.ToUpperInvariant()) { case "AWS": return ResolveAWSSecret(providerPath); case "AKV": return ResolveAKVSecret(providerPath); default: // no provider matches so just return the original value return originalValue; } } private string ResolveAWSSecret(string providerPath) { SplitByFirstSeparator(providerPath, '|', out string arn, out string? key); IAmazonSecretsManager? secretsManager = _serviceProvider.GetService(); if (secretsManager == null) { throw new Exception($"Unable to get AWSSecretsManager when resolving aws secret resource: {providerPath}"); } string secretValue; try { Task? response = secretsManager.GetSecretValueAsync(new GetSecretValueRequest { SecretId = arn, }); secretValue = response.Result.SecretString; } catch (ResourceNotFoundException e) { _logger.LogError(e, "Failed to find AWS secret: {Arn}", arn); throw; } if (key == null) { return secretValue; } Dictionary? keyCollection = JsonSerializer.Deserialize>(secretValue); if (keyCollection == null) { throw new Exception($"Unable to deserialize secret to a json payload for path: {providerPath}"); } if (keyCollection.TryGetValue(key, out string? s)) { return s; } throw new Exception($"Unable to find key {key} in blob returned for secret {arn}"); } /// /// AKV = Azure Key Vault. /// Uses to get a secret from Azure Key Vault. /// Using lets us debug using /// and deploy to an app service that uses . /// /// /// /// private static string ResolveAKVSecret(string providerPath) { if (!SplitByFirstSeparator(providerPath, '|', out string vaultName, out string? secretName)) { throw new InvalidDataException("Azure Key Vault secret path must be of the form vaultName|secretName"); } string vaultUrl = $"https://{vaultName}.vault.azure.net/"; SecretClient client = new SecretClient(new Uri(vaultUrl), new DefaultAzureCredential()); Response response = client.GetSecret(secretName); return response.Value.Value; } private static bool SplitByFirstSeparator(string fullPath, char sep, out string left, out string? right) { int keySeparator = fullPath.IndexOf(sep, StringComparison.OrdinalIgnoreCase); left = fullPath; right = null; if (keySeparator != -1) { left = fullPath.Substring(0, keySeparator); right = fullPath.Substring(keySeparator + 1); return true; } return false; } } }