| | | 1 | | namespace Envilder.Infrastructure; |
| | | 2 | | |
| | | 3 | | using Amazon; |
| | | 4 | | using Amazon.Runtime.CredentialManagement; |
| | | 5 | | using Amazon.SimpleSystemsManagement; |
| | | 6 | | using Envilder.Domain; |
| | | 7 | | using Envilder.Domain.Ports; |
| | | 8 | | using Envilder.Infrastructure.Aws; |
| | | 9 | | using Envilder.Infrastructure.Azure; |
| | | 10 | | using global::Azure.Identity; |
| | | 11 | | using global::Azure.Security.KeyVault.Secrets; |
| | | 12 | | using System; |
| | | 13 | | |
| | | 14 | | /// <summary> |
| | | 15 | | /// Creates the appropriate <see cref="ISecretProvider"/> implementation |
| | | 16 | | /// based on the map file configuration and optional runtime overrides. |
| | | 17 | | /// </summary> |
| | | 18 | | internal static class SecretProviderFactory |
| | | 19 | | { |
| | 1 | 20 | | private static readonly RegionEndpoint FallbackRegion = RegionEndpoint.USEast1; |
| | | 21 | | |
| | | 22 | | /// <summary> |
| | | 23 | | /// Creates an <see cref="ISecretProvider"/> for the provider specified in |
| | | 24 | | /// <paramref name="config"/>. When <paramref name="options"/> is provided, |
| | | 25 | | /// its values take precedence over <paramref name="config"/>. |
| | | 26 | | /// </summary> |
| | | 27 | | /// <param name="config">Configuration from the <c>$config</c> section of a map file.</param> |
| | | 28 | | /// <param name="options">Optional runtime overrides (e.g. CLI flags).</param> |
| | | 29 | | /// <returns>A ready-to-use secret provider.</returns> |
| | | 30 | | /// <exception cref="InvalidOperationException"> |
| | | 31 | | /// Thrown when Azure is selected but no Vault URL is provided. |
| | | 32 | | /// </exception> |
| | | 33 | | public static ISecretProvider Create(MapFileConfig config, EnvilderOptions? options = null) |
| | | 34 | | { |
| | 1 | 35 | | if (config is null) |
| | | 36 | | { |
| | 1 | 37 | | throw new ArgumentNullException(nameof(config)); |
| | | 38 | | } |
| | | 39 | | |
| | 1 | 40 | | var provider = options?.Provider ?? config.Provider; |
| | 1 | 41 | | var profile = options?.Profile ?? config.Profile; |
| | 1 | 42 | | var vaultUrl = options?.VaultUrl ?? config.VaultUrl; |
| | | 43 | | |
| | 1 | 44 | | ValidateCrossProviderConfig(provider, profile, vaultUrl); |
| | | 45 | | |
| | 1 | 46 | | return provider switch |
| | 1 | 47 | | { |
| | 1 | 48 | | SecretProviderType.Azure => CreateAzureSecretProvider(vaultUrl), |
| | 1 | 49 | | _ => CreateAwsSecretProvider(profile), |
| | 1 | 50 | | }; |
| | | 51 | | } |
| | | 52 | | |
| | | 53 | | private static void ValidateCrossProviderConfig( |
| | | 54 | | SecretProviderType? provider, |
| | | 55 | | string? profile, |
| | | 56 | | string? vaultUrl) |
| | | 57 | | { |
| | 1 | 58 | | var isAzure = provider == SecretProviderType.Azure; |
| | | 59 | | |
| | 1 | 60 | | if (isAzure && !string.IsNullOrWhiteSpace(profile)) |
| | | 61 | | { |
| | 1 | 62 | | throw new InvalidOperationException( |
| | 1 | 63 | | "AWS profile cannot be used with Azure Key Vault provider."); |
| | | 64 | | } |
| | | 65 | | |
| | 1 | 66 | | if (!isAzure && !string.IsNullOrWhiteSpace(vaultUrl)) |
| | | 67 | | { |
| | 1 | 68 | | throw new InvalidOperationException( |
| | 1 | 69 | | "Vault URL cannot be used with AWS SSM provider."); |
| | | 70 | | } |
| | 1 | 71 | | } |
| | | 72 | | |
| | | 73 | | private static AzureKeyVaultSecretProvider CreateAzureSecretProvider(string? vaultUrl) |
| | | 74 | | { |
| | 1 | 75 | | if (string.IsNullOrWhiteSpace(vaultUrl)) |
| | | 76 | | { |
| | 1 | 77 | | throw new InvalidOperationException("Vault URL must be provided for Azure Key Vault provider."); |
| | | 78 | | } |
| | | 79 | | |
| | 1 | 80 | | var secretClient = new SecretClient(new Uri(vaultUrl), new DefaultAzureCredential()); |
| | 1 | 81 | | return new(secretClient); |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | private static AwsSsmSecretProvider CreateAwsSecretProvider(string? profile) |
| | | 85 | | { |
| | 1 | 86 | | if (!string.IsNullOrWhiteSpace(profile)) |
| | | 87 | | { |
| | 1 | 88 | | var profilesLocation = Environment.GetEnvironmentVariable("AWS_SHARED_CREDENTIALS_FILE"); |
| | 1 | 89 | | var chain = new CredentialProfileStoreChain(profilesLocation); |
| | 1 | 90 | | if (chain.TryGetAWSCredentials(profile, out var credentials)) |
| | | 91 | | { |
| | 1 | 92 | | var region = ResolveProfileRegion(chain, profile!); |
| | 1 | 93 | | return new(new AmazonSimpleSystemsManagementClient(credentials, region)); |
| | | 94 | | } |
| | | 95 | | |
| | 1 | 96 | | throw new InvalidOperationException( |
| | 1 | 97 | | $"AWS profile '{profile}' was not found in the credential store."); |
| | | 98 | | } |
| | | 99 | | |
| | 1 | 100 | | return new(new AmazonSimpleSystemsManagementClient()); |
| | | 101 | | } |
| | | 102 | | |
| | | 103 | | private static RegionEndpoint ResolveProfileRegion(CredentialProfileStoreChain chain, string profile) |
| | | 104 | | { |
| | 1 | 105 | | if (chain.TryGetProfile(profile, out var credentialProfile) && credentialProfile.Region != null) |
| | | 106 | | { |
| | 0 | 107 | | return credentialProfile.Region; |
| | | 108 | | } |
| | | 109 | | |
| | 1 | 110 | | return ResolveRegion(); |
| | | 111 | | } |
| | | 112 | | |
| | | 113 | | /// <summary> |
| | | 114 | | /// Resolves the AWS region from environment variables (<c>AWS_REGION</c> or |
| | | 115 | | /// <c>AWS_DEFAULT_REGION</c>), falling back to <c>us-east-1</c> when neither is set. |
| | | 116 | | /// </summary> |
| | | 117 | | private static RegionEndpoint ResolveRegion() |
| | | 118 | | { |
| | 1 | 119 | | var regionName = Environment.GetEnvironmentVariable("AWS_REGION") |
| | 1 | 120 | | ?? Environment.GetEnvironmentVariable("AWS_DEFAULT_REGION"); |
| | | 121 | | |
| | 1 | 122 | | return string.IsNullOrWhiteSpace(regionName) |
| | 1 | 123 | | ? FallbackRegion |
| | 1 | 124 | | : RegionEndpoint.GetBySystemName(regionName); |
| | | 125 | | } |
| | | 126 | | } |