189 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			189 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using Azure.Security.KeyVault.Secrets;
 | |
| using VaultSharp;
 | |
| using VaultSharp.V1.AuthMethods.Token;
 | |
| using Core.Blueprint.KeyVault.Configuration;
 | |
| using Microsoft.Extensions.Configuration;
 | |
| using System.Net.Http.Json;
 | |
| using VaultSharp.Core;
 | |
| 
 | |
| namespace Core.Blueprint.KeyVault;
 | |
| 
 | |
| /// <summary>
 | |
| /// Provides operations for managing secrets in Azure Key Vault or HashiCorp Vault transparently based on the environment.
 | |
| /// </summary>
 | |
| public sealed class KeyVaultProvider : IKeyVaultProvider
 | |
| {
 | |
|     private readonly string environment;
 | |
|     private readonly SecretClient? azureClient;
 | |
|     private readonly IVaultClient? hashiClient;
 | |
|     private readonly VaultOptions? hashiOptions;
 | |
| 
 | |
|     public KeyVaultProvider(IConfiguration configuration)
 | |
|     {
 | |
|         environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
 | |
| 
 | |
|         if (environment == "Local")
 | |
|         {
 | |
|             hashiOptions = configuration.GetSection("Vault").Get<VaultOptions>();
 | |
|             hashiClient = new VaultClient(new VaultClientSettings(
 | |
|                 hashiOptions?.Address,
 | |
|                 new TokenAuthMethodInfo(hashiOptions?.Token)
 | |
|             ));
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             var keyVaultUri = new Uri(configuration["ConnectionStrings:KeyVaultDAL"]!);
 | |
|             azureClient = new SecretClient(keyVaultUri, new Azure.Identity.DefaultAzureCredential());
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Creates a new secret in Azure Key Vault or HashiCorp Vault.
 | |
|     /// </summary>
 | |
|     public async ValueTask<KeyVaultResponse> CreateSecretAsync(KeyVaultRequest keyVaultRequest, CancellationToken cancellationToken)
 | |
|     {
 | |
|         if (environment == "Local")
 | |
|         {
 | |
|             await hashiClient!.V1.Secrets.KeyValue.V2.WriteSecretAsync(
 | |
|                 path: keyVaultRequest.Name,
 | |
|                 data: new Dictionary<string, object> { { "value", keyVaultRequest.Value } },
 | |
|                 mountPoint: hashiOptions!.SecretMount
 | |
|             );
 | |
|             return new KeyVaultResponse { Name = keyVaultRequest.Name, Value = keyVaultRequest.Value };
 | |
|         }
 | |
| 
 | |
|         KeyVaultSecret azureResponse = await azureClient!.SetSecretAsync(
 | |
|             new KeyVaultSecret(keyVaultRequest.Name, keyVaultRequest.Value), cancellationToken
 | |
|         );
 | |
| 
 | |
|         return new KeyVaultResponse { Name = azureResponse.Name, Value = azureResponse.Value };
 | |
|     }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Permanently deletes a secret from Azure Key Vault or HashiCorp Vault (hard delete for Vault).
 | |
|     /// </summary>
 | |
|     /// <param name="secretName">The name of the secret to delete.</param>
 | |
|     /// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
 | |
|     /// <returns>
 | |
|     /// A <see cref="Tuple"/> containing a status message and a boolean indicating whether the secret was successfully deleted.
 | |
|     /// </returns>
 | |
|     public async ValueTask<Tuple<string, bool>> DeleteSecretAsync(string secretName, CancellationToken cancellationToken)
 | |
|     {
 | |
|         if (environment == "Local")
 | |
|         {
 | |
|             await DestroyAllSecretVersionsAsync(secretName, cancellationToken);
 | |
|         }
 | |
| 
 | |
|         var existingSecret = await this.GetSecretAsync(secretName, cancellationToken);
 | |
|         if (existingSecret.Item2 == string.Empty)
 | |
|         {
 | |
|             await azureClient!.StartDeleteSecretAsync(secretName, cancellationToken);
 | |
|             return new("Key Deleted", true);
 | |
|         }
 | |
| 
 | |
|         return new("Key Not Found", false);
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Retrieves a secret from Azure Key Vault or HashiCorp Vault.
 | |
|     /// </summary>
 | |
|     public async ValueTask<Tuple<KeyVaultResponse, string?>> GetSecretAsync(string secretName, CancellationToken cancellationToken)
 | |
|     {
 | |
|         if (environment == "Local")
 | |
|         {
 | |
|             try
 | |
|             {
 | |
|                 var secret = await hashiClient!.V1.Secrets.KeyValue.V2.ReadSecretAsync(
 | |
|                     path: secretName,
 | |
|                     mountPoint: hashiOptions!.SecretMount
 | |
|                 );
 | |
| 
 | |
|                 if (secret.Data.Data.TryGetValue("value", out var value))
 | |
|                 {
 | |
|                     return new(new KeyVaultResponse { Name = secretName, Value = value?.ToString() ?? "" }, string.Empty);
 | |
|                 }
 | |
| 
 | |
|                 return new(new KeyVaultResponse(), "Key Not Found");
 | |
|             }
 | |
|             catch (VaultSharp.Core.VaultApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound)
 | |
|             {
 | |
|                 return new(new KeyVaultResponse(), "Key Not Found");
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         try
 | |
|         {
 | |
|             KeyVaultSecret azureResponse = await azureClient!.GetSecretAsync(secretName, cancellationToken: cancellationToken);
 | |
|             return new(new KeyVaultResponse { Name = secretName, Value = azureResponse.Value }, string.Empty);
 | |
|         }
 | |
|         catch (Azure.RequestFailedException ex) when (ex.Status == 404)
 | |
|         {
 | |
|             return new(new KeyVaultResponse(), "Key Not Found");
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Updates an existing secret in Azure Key Vault or HashiCorp Vault. If the secret does not exist, an error is returned.
 | |
|     /// </summary>
 | |
|     public async ValueTask<Tuple<KeyVaultResponse, string>> UpdateSecretAsync(KeyVaultRequest newSecret, CancellationToken cancellationToken)
 | |
|     {
 | |
|         var existingSecret = await this.GetSecretAsync(newSecret.Name, cancellationToken);
 | |
|         if (!string.IsNullOrEmpty(existingSecret.Item2))
 | |
|         {
 | |
|             return new(new KeyVaultResponse(), "Key Not Found");
 | |
|         }
 | |
| 
 | |
|         var updated = await CreateSecretAsync(newSecret, cancellationToken);
 | |
|         return new(updated, string.Empty);
 | |
|     }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// Permanently deletes all versions of a given secret in HashiCorp Vault.
 | |
|     /// Returns a tuple indicating the result status and a message.
 | |
|     /// </summary>
 | |
|     /// <param name="secretName">The secret name/path.</param>
 | |
|     /// <param name="cancellationToken">A cancellation token.</param>
 | |
|     /// <returns>
 | |
|     /// A tuple:
 | |
|     /// - <c>bool?</c>: <c>true</c> if deleted, <c>false</c> if no versions, <c>null</c> if not found.
 | |
|     /// - <c>string</c>: message explaining the result.
 | |
|     /// </returns>
 | |
|     private async Task<(bool? WasDeleted, string Message)> DestroyAllSecretVersionsAsync(string secretName, CancellationToken cancellationToken)
 | |
|     {
 | |
|         Dictionary<string, object> versions;
 | |
| 
 | |
|         try
 | |
|         {
 | |
|             var metadata = await hashiClient!.V1.Secrets.KeyValue.V2.ReadSecretMetadataAsync(
 | |
|                 path: secretName,
 | |
|                 mountPoint: hashiOptions!.SecretMount
 | |
|             );
 | |
| 
 | |
|             versions = metadata.Data.Versions.Keys.ToDictionary(k => k, _ => (object)0);
 | |
|             if (versions.Count == 0)
 | |
|                 return (false, "Key exists but contains no versions.");
 | |
|         }
 | |
|         catch (VaultApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound)
 | |
|         {
 | |
|             return (null, "Key Not Found.");
 | |
|         }
 | |
| 
 | |
|         using var httpClient = new HttpClient { BaseAddress = new Uri(hashiOptions.Address) };
 | |
|         var request = new HttpRequestMessage(HttpMethod.Post, $"/v1/{hashiOptions.SecretMount}/destroy/{secretName}")
 | |
|         {
 | |
|             Content = JsonContent.Create(new { versions = versions.Keys.ToArray() })
 | |
|         };
 | |
|         request.Headers.Add("X-Vault-Token", hashiOptions.Token);
 | |
|         var response = await httpClient.SendAsync(request, cancellationToken);
 | |
|         response.EnsureSuccessStatusCode();
 | |
| 
 | |
|         await hashiClient.V1.Secrets.KeyValue.V2.DeleteMetadataAsync(
 | |
|             path: secretName,
 | |
|             mountPoint: hashiOptions.SecretMount
 | |
|         );
 | |
| 
 | |
|         return (true, "Key Permanently Deleted.");
 | |
|     }
 | |
| }
 | 
