Compare commits
	
		
			6 Commits
		
	
	
		
			feat/condi
			...
			feature/im
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a56818bcf8 | |||
|   | 5410a9f9a0 | ||
| 140eab163a | |||
|   | b90bb23f27 | ||
|   | d2a8ced972 | ||
|   | f8c6db55e9 | 
| @@ -16,17 +16,34 @@ namespace Core.Blueprint.KeyVault.Configuration | ||||
|     { | ||||
|         public static IServiceCollection AddKeyVault(this IServiceCollection services, IConfiguration configuration) | ||||
|         { | ||||
|             var keyVaultUriString = configuration["ConnectionStrings:KeyVaultDAL"]; | ||||
|  | ||||
|             if (string.IsNullOrEmpty(keyVaultUriString)) | ||||
|             var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty; | ||||
|  | ||||
|             if(environment ==  "Local") | ||||
|             { | ||||
|                 throw new ArgumentNullException("ConnectionStrings:KeyVault", "KeyVault URI is missing in the configuration."); | ||||
|                 var vaultSettings = configuration.GetSection("Vault").Get<VaultOptions>(); | ||||
|  | ||||
|                 if (string.IsNullOrEmpty(vaultSettings?.Address) || string.IsNullOrEmpty(vaultSettings.Token) | ||||
|                     || string.IsNullOrEmpty(vaultSettings.SecretMount)) | ||||
|                 { | ||||
|                     throw new ArgumentNullException("Vault options are not configured correctly."); | ||||
|                 } | ||||
|  | ||||
|                 services.AddSingleton(vaultSettings); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 var keyVaultUriString = configuration["ConnectionStrings:KeyVaultDAL"]; | ||||
|  | ||||
|             var keyVaultUri = new Uri(keyVaultUriString); | ||||
|                 if (string.IsNullOrEmpty(keyVaultUriString)) | ||||
|                 { | ||||
|                     throw new ArgumentNullException("ConnectionStrings:KeyVault", "KeyVault URI is missing in the configuration."); | ||||
|                 } | ||||
|  | ||||
|             // Register SecretClient as a singleton | ||||
|             services.AddSingleton(_ => new SecretClient(keyVaultUri, new DefaultAzureCredential())); | ||||
|                 var keyVaultUri = new Uri(keyVaultUriString); | ||||
|  | ||||
|                 services.AddSingleton(_ => new SecretClient(keyVaultUri, new DefaultAzureCredential())); | ||||
|             } | ||||
|  | ||||
|             services.AddSingleton<IKeyVaultProvider, KeyVaultProvider>(); | ||||
|             return services; | ||||
|   | ||||
							
								
								
									
										15
									
								
								Core.Blueprint.KeyVault/Configuration/VaultOptions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Core.Blueprint.KeyVault/Configuration/VaultOptions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Core.Blueprint.KeyVault.Configuration | ||||
| { | ||||
|     public class VaultOptions | ||||
|     { | ||||
|         public string Address { get; set; } = string.Empty; | ||||
|         public string Token { get; set; } = string.Empty; | ||||
|         public string SecretMount { get; set; } = string.Empty; | ||||
|     } | ||||
| } | ||||
| @@ -10,7 +10,9 @@ | ||||
|     <PackageReference Include="Azure.Identity" Version="1.13.1" /> | ||||
|     <PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.7.0" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> | ||||
|     <PackageReference Include="VaultSharp" Version="1.17.5.1" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
|   | ||||
| @@ -1,93 +1,188 @@ | ||||
| using Azure; | ||||
| using Azure.Security.KeyVault.Secrets; | ||||
| 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 | ||||
| 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 | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Provides operations for managing secrets in Azure Key Vault. | ||||
|     /// </summary> | ||||
|     public sealed class KeyVaultProvider(SecretClient keyVaultProvider): IKeyVaultProvider | ||||
|     private readonly string environment; | ||||
|     private readonly SecretClient? azureClient; | ||||
|     private readonly IVaultClient? hashiClient; | ||||
|     private readonly VaultOptions? hashiOptions; | ||||
|  | ||||
|     public KeyVaultProvider(IConfiguration configuration) | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Creates a new secret in Azure Key Vault. | ||||
|         /// </summary> | ||||
|         /// <param name="keyVaultRequest">The request containing the name and value of the secret.</param> | ||||
|         /// <param name="cancellationToken">The cancellation token to cancel the operation.</param> | ||||
|         /// <returns>A <see cref="KeyVaultResponse"/> containing the details of the created secret.</returns> | ||||
|         public async ValueTask<KeyVaultResponse> CreateSecretAsync(KeyVaultRequest keyVaultRequest, CancellationToken cancellationToken) | ||||
|         environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"; | ||||
|  | ||||
|         if (environment == "Local") | ||||
|         { | ||||
|             KeyVaultResponse _response = new(); | ||||
|             KeyVaultSecret azureResponse = await keyVaultProvider.SetSecretAsync(new KeyVaultSecret(keyVaultRequest.Name, keyVaultRequest.Value), cancellationToken); | ||||
|  | ||||
|             _response.Value = azureResponse.Value; | ||||
|             _response.Name = azureResponse.Name; | ||||
|  | ||||
|             return _response; | ||||
|             hashiOptions = configuration.GetSection("Vault").Get<VaultOptions>(); | ||||
|             hashiClient = new VaultClient(new VaultClientSettings( | ||||
|                 hashiOptions?.Address, | ||||
|                 new TokenAuthMethodInfo(hashiOptions?.Token) | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Deletes a secret from Azure Key Vault if it exists. | ||||
|         /// </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) | ||||
|         else | ||||
|         { | ||||
|             var existingSecret = await this.GetSecretAsync(secretName, cancellationToken); | ||||
|             if (existingSecret != null) | ||||
|             { | ||||
|                 await keyVaultProvider.StartDeleteSecretAsync(secretName, cancellationToken); | ||||
|                 return new("Key Deleted", true); | ||||
|             } | ||||
|  | ||||
|             return new("Key Not Found", false); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Retrieves a secret from Azure Key Vault. | ||||
|         /// </summary> | ||||
|         /// <param name="secretName">The name of the secret to retrieve.</param> | ||||
|         /// <param name="cancellationToken">The cancellation token to cancel the operation.</param> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Tuple"/> containing the <see cref="KeyVaultResponse"/> with secret details  | ||||
|         /// and an optional error message if the secret was not found. | ||||
|         /// </returns> | ||||
|         public async ValueTask<Tuple<KeyVaultResponse, string?>> GetSecretAsync(string secretName, CancellationToken cancellationToken) | ||||
|         { | ||||
|             KeyVaultSecret azureResponse = await keyVaultProvider.GetSecretAsync(secretName, cancellationToken: cancellationToken); | ||||
|  | ||||
|             if (azureResponse == null) | ||||
|             { | ||||
|                 return new(new KeyVaultResponse(), "Key Not Found"); | ||||
|             } | ||||
|  | ||||
|             return new(new KeyVaultResponse { Name = secretName, Value = azureResponse.Value }, string.Empty); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Updates an existing secret in Azure Key Vault. If the secret does not exist, an error is returned. | ||||
|         /// </summary> | ||||
|         /// <param name="newSecret">The updated secret information.</param> | ||||
|         /// <param name="cancellationToken">The cancellation token to cancel the operation.</param> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Tuple"/> containing the updated <see cref="KeyVaultResponse"/> and an optional error message if the secret was not found. | ||||
|         /// </returns> | ||||
|         public async ValueTask<Tuple<KeyVaultResponse, string>> UpdateSecretAsync(KeyVaultRequest newSecret, CancellationToken cancellationToken) | ||||
|         { | ||||
|             KeyVaultResponse _response = new(); | ||||
|             var existingSecret = await this.GetSecretAsync(newSecret.Name, cancellationToken); | ||||
|             if (existingSecret == null) | ||||
|             { | ||||
|                 return new(new KeyVaultResponse(), "Key Not Found"); | ||||
|             } | ||||
|             KeyVaultSecret azureResponse = await keyVaultProvider.SetSecretAsync(new KeyVaultSecret(newSecret.Name, newSecret.Value), cancellationToken); | ||||
|  | ||||
|             _response.Value = azureResponse.Value; | ||||
|             _response.Name = azureResponse.Name; | ||||
|  | ||||
|             return new(new KeyVaultResponse { Name = newSecret.Name, Value = azureResponse.Value }, string.Empty); | ||||
|             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."); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -148,5 +148,13 @@ namespace Core.Blueprint.Mongo | ||||
|         /// <param name="filterExpression">An expression used to filter the documents to delete.</param> | ||||
|         /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> | ||||
|         Task DeleteManyAsync(Expression<Func<TDocument, bool>> filterExpression); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Executes an aggregation pipeline and returns the first document in the result asynchronously. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="TOutput">The type of the output document you expect from the pipeline.</typeparam> | ||||
|         /// <param name="pipeline">The aggregation pipeline definition to execute.</param> | ||||
|         /// <returns>The first document from the aggregation result, or null if none found.</returns> | ||||
|         Task<TOutput> FindOnePipelineAsync<TOutput>(PipelineDefinition<TDocument, TOutput> pipeline); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -248,5 +248,16 @@ namespace Core.Blueprint.Mongo | ||||
|         { | ||||
|             return Task.Run(() => _collection.DeleteManyAsync(filterExpression)); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Executes an aggregation pipeline and returns the first document in the result asynchronously. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="TOutput">The type of the output document you expect from the pipeline.</typeparam> | ||||
|         /// <param name="pipeline">The aggregation pipeline definition to execute.</param> | ||||
|         /// <returns>The first document from the aggregation result, or null if none found.</returns> | ||||
|         public virtual Task<TOutput> FindOnePipelineAsync<TOutput>(PipelineDefinition<TDocument, TOutput> pipeline) | ||||
|         { | ||||
|             return Task.Run(() => _collection.Aggregate(pipeline).FirstOrDefaultAsync()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -7,13 +7,13 @@ | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.Azure.StackExchangeRedis" Version="3.2.0" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" /> | ||||
|     <PackageReference Include="StackExchange.Redis" Version="2.8.22" /> | ||||
|     <PackageReference Include="Microsoft.Azure.StackExchangeRedis" Version="3.2.1" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.5" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.5" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.5" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" /> | ||||
|     <PackageReference Include="StackExchange.Redis" Version="2.8.37" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user