diff options
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs')
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs | 338 |
1 files changed, 37 insertions, 301 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs index 6ac1c0b..a5ba550 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs @@ -1,12 +1,12 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading -* File: VaultSecrets.cs +* File: PluginSecretLoading.cs * -* VaultSecrets.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. +* PluginSecretLoading.cs is part of VNLib.Plugins.Extensions.Loading which +* is part of the larger VNLib collection of libraries and utilities. * * VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -23,26 +23,14 @@ */ using System; -using System.IO; -using System.Linq; using System.Text; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; -using VaultSharp; -using VaultSharp.V1.Commons; -using VaultSharp.V1.AuthMethods; -using VaultSharp.V1.AuthMethods.Token; -using VaultSharp.V1.AuthMethods.AppRole; -using VaultSharp.V1.SecretsEngines.PKI; - using VNLib.Utils; using VNLib.Utils.Memory; -using VNLib.Utils.Logging; -using VNLib.Utils.Extensions; using VNLib.Hashing.IdentityUtility; namespace VNLib.Plugins.Extensions.Loading @@ -53,19 +41,12 @@ namespace VNLib.Plugins.Extensions.Loading /// </summary> public static class PluginSecretLoading { - public const string VAULT_OBJECT_NAME = "hashicorp_vault"; - public const string SECRETS_CONFIG_KEY = "secrets"; - public const string VAULT_TOKEN_KEY = "token"; - public const string VAULT_ROLE_KEY = "role"; - public const string VAULT_SECRET_KEY = "secret"; - public const string VAULT_TOKNE_ENV_NAME = "VNLIB_PLUGINS_VAULT_TOKEN"; - - public const string VAULT_URL_KEY = "url"; - - public const string VAULT_URL_SCHEME = "vault://"; - public const string ENV_URL_SCHEME = "env://"; - public const string FILE_URL_SCHEME = "file://"; - + /// <summary> + /// Gets a wrapper for the secret store for the current plugin + /// </summary> + /// <param name="plugin"></param> + /// <returns>The secret store structure</returns> + public static PluginSecretStore Secrets(this PluginBase plugin) => new(plugin); /// <summary> /// <para> @@ -103,291 +84,43 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="ObjectDisposedException"></exception> public static Task<ISecretResult?> TryGetSecretAsync(this PluginBase plugin, string secretName) { - plugin.ThrowIfUnloaded(); - - //Get the secret from the config file raw - string? rawSecret = TryGetSecretInternal(plugin, secretName); - - if (rawSecret == null) - { - return Task.FromResult<ISecretResult?>(null); - } - - //Secret is a vault path, or return the raw value - if (rawSecret.StartsWith(VAULT_URL_SCHEME, StringComparison.OrdinalIgnoreCase)) - { - return GetSecretFromVaultAsync(plugin, rawSecret); - } - - //See if the secret is an environment variable path - if (rawSecret.StartsWith(ENV_URL_SCHEME, StringComparison.OrdinalIgnoreCase)) - { - //try to get the environment variable - string envVar = rawSecret[ENV_URL_SCHEME.Length..]; - string? envVal = Environment.GetEnvironmentVariable(envVar); - - return Task.FromResult<ISecretResult?>(envVal == null ? null : new SecretResult(envVal)); - } - - //See if the secret is a file path - if (rawSecret.StartsWith(FILE_URL_SCHEME, StringComparison.OrdinalIgnoreCase)) - { - string filePath = rawSecret[FILE_URL_SCHEME.Length..]; - return GetSecretFromFileAsync(filePath, plugin.UnloadToken); - } - - //Finally, return the raw value - return Task.FromResult<ISecretResult?>(new SecretResult(rawSecret.AsSpan())); - } - - private static async Task<ISecretResult?> GetSecretFromFileAsync(string filePath, CancellationToken ct) - { - //read the file data - byte[] secretFileData = await File.ReadAllBytesAsync(filePath, ct); - - //recover the character data from the file data - int chars = Encoding.UTF8.GetCharCount(secretFileData); - char[] secretFileChars = new char[chars]; - Encoding.UTF8.GetChars(secretFileData, secretFileChars); - - //Create secret from the file data - SecretResult sr = SecretResult.ToSecret(secretFileChars); - - //Clear file data buffer - MemoryUtil.InitializeBlock(secretFileData.AsSpan()); - return sr; - } - - /// <summary> - /// Gets a secret at the given vault url (in the form of "vault://[mount-name]/[secret-path]?secret=[secret_name]") - /// </summary> - /// <param name="plugin"></param> - /// <param name="vaultPath">The raw vault url to lookup</param> - /// <returns>The string of the object at the specified vault path</returns> - /// <exception cref="UriFormatException"></exception> - /// <exception cref="KeyNotFoundException"></exception> - /// <exception cref="ObjectDisposedException"></exception> - public static Task<ISecretResult?> GetSecretFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath) - { - //print the path for debug - if (plugin.IsDebug()) - { - plugin.Log.Debug("Retrieving secret {s} from vault", vaultPath.ToString()); - } - - //Slice off path - ReadOnlySpan<char> paq = vaultPath.SliceAfterParam(VAULT_URL_SCHEME); - ReadOnlySpan<char> path = paq.SliceBeforeParam('?'); - ReadOnlySpan<char> query = paq.SliceAfterParam('?'); - - if (paq.IsEmpty) - { - throw new UriFormatException("Vault secret location not valid/empty "); - } - //Get the secret - string secretTableKey = query.SliceAfterParam("secret=").SliceBeforeParam('&').ToString(); - string vaultType = query.SliceBeforeParam("vault_type=").SliceBeforeParam('&').ToString(); - - //get mount and path - int lastSep = path.IndexOf('/'); - string mount = path[..lastSep].ToString(); - string secret = path[(lastSep + 1)..].ToString(); - - async Task<ISecretResult?> execute() - { - //Try load client - IVaultClient? client = plugin.GetVault(); - - _ = client ?? throw new KeyNotFoundException("Vault client not found"); - //run read async - Secret<SecretData> result = await client.V1.Secrets.KeyValue.V2.ReadSecretAsync(path:secret, mountPoint:mount); - //Read the secret - return SecretResult.ToSecret(result.Data.Data[secretTableKey].ToString()); - } - - return Task.Run(execute); + return plugin.Secrets().TryGetSecretAsync(secretName); } /// <summary> /// <para> - /// Gets a Certicate from the "secrets" element. + /// Gets a required secret from the "secrets" element. /// </para> /// <para> /// Secrets elements are merged from the host config and plugin local config 'secrets' element. /// before searching. The plugin config takes precedence over the host config. /// </para> /// </summary> - /// <param name="plugin"></param> + /// <param name="secrets"></param> /// <param name="secretName">The name of the secret propery to get</param> - /// <returns>The element from the configuration file with the given name, or null if the configuration or property does not exist</returns> + /// <returns>The element from the configuration file with the given name, raises an exception if the secret does not exist</returns> /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> - public static Task<X509Certificate?> TryGetCertificateAsync(this PluginBase plugin, string secretName) - { - plugin.ThrowIfUnloaded(); - //Get the secret from the config file raw - string? rawSecret = TryGetSecretInternal(plugin, secretName); - if (rawSecret == null) - { - return Task.FromResult<X509Certificate?>(null); - } - - //Secret is a vault path, or return the raw value - if (!rawSecret.StartsWith(VAULT_URL_SCHEME, StringComparison.OrdinalIgnoreCase)) - { - return Task.FromResult<X509Certificate?>(new (rawSecret)); - } - - return GetCertFromVaultAsync(plugin, rawSecret); - } - - public static Task<X509Certificate?> GetCertFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath, CertificateCredentialsRequestOptions? options = null) + public static async Task<ISecretResult> GetSecretAsync(this PluginSecretStore secrets, string secretName) { - //print the path for debug - if (plugin.IsDebug()) - { - plugin.Log.Debug("Retrieving certificate {s} from vault", vaultPath.ToString()); - } - - //Slice off path - ReadOnlySpan<char> paq = vaultPath.SliceAfterParam(VAULT_URL_SCHEME); - ReadOnlySpan<char> path = paq.SliceBeforeParam('?'); - ReadOnlySpan<char> query = paq.SliceAfterParam('?'); - - if (paq.IsEmpty) - { - throw new UriFormatException("Vault secret location not valid/empty "); - } - - //Get the secret - string role = query.SliceAfterParam("role=").SliceBeforeParam('&').ToString(); - string vaultType = query.SliceBeforeParam("vault_type=").SliceBeforeParam('&').ToString(); - string commonName = query.SliceBeforeParam("cn=").SliceBeforeParam('&').ToString(); - - //get mount and path - int lastSep = path.IndexOf('/'); - string mount = path[..lastSep].ToString(); - string secret = path[(lastSep + 1)..].ToString(); - - async Task<X509Certificate?> execute() - { - //Try load client - IVaultClient? client = plugin.GetVault(); - - _ = client ?? throw new KeyNotFoundException("Vault client not found"); - - options ??= new() - { - CertificateFormat = CertificateFormat.pem, - PrivateKeyFormat = PrivateKeyFormat.pkcs8, - CommonName = commonName, - }; - - //run read async - Secret<CertificateCredentials> result = await client.V1.Secrets.PKI.GetCredentialsAsync(pkiRoleName:secret, certificateCredentialRequestOptions:options, pkiBackendMountPoint:mount); - //Read the secret - byte[] pemCertData = Encoding.UTF8.GetBytes(result.Data.CertificateContent); - - return new (pemCertData); - } - - return Task.Run(execute); + ISecretResult? res = await secrets.TryGetSecretAsync(secretName).ConfigureAwait(false); + return res ?? throw new KeyNotFoundException($"Missing required secret {secretName}"); } /// <summary> - /// Gets the ambient vault client for the current plugin - /// if the configuration is loaded, null otherwise + /// Gets a secret at the given vault url (in the form of "vault://[mount-name]/[secret-path]?secret=[secret_name]") /// </summary> /// <param name="plugin"></param> - /// <returns>The ambient <see cref="IVaultClient"/> if loaded, null otherwise</returns> + /// <param name="vaultPath">The raw vault url to lookup</param> + /// <returns>The string of the object at the specified vault path</returns> + /// <exception cref="UriFormatException"></exception> /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> - public static IVaultClient? GetVault(this PluginBase plugin) => LoadingExtensions.GetOrCreateSingleton(plugin, TryGetVaultLoader); - - private static string? TryGetSecretInternal(PluginBase plugin, string secretName) - { - bool local = plugin.PluginConfig.TryGetProperty(SECRETS_CONFIG_KEY, out JsonElement localEl); - bool host = plugin.HostConfig.TryGetProperty(SECRETS_CONFIG_KEY, out JsonElement hostEl); - - //total config - IReadOnlyDictionary<string, JsonElement>? conf; - - if (local && host) - { - //Load both config objects to dict - Dictionary<string, JsonElement> localConf = localEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value); - Dictionary<string, JsonElement> hostConf = hostEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value); - - //merge the two configs - foreach(KeyValuePair<string, JsonElement> lc in localConf) - { - //Overwrite any host config keys, plugin conf takes priority - hostConf[lc.Key] = lc.Value; - } - //set the merged config - conf = hostConf; - } - else if(local) - { - //Store only local config - conf = localEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value); - } - else if(host) - { - //store only host config - conf = hostEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value); - } - else - { - conf = null; - } - - //Get the value or default json element - return conf != null && conf.TryGetValue(secretName, out JsonElement el) ? el.GetString() : null; - } - - private static IVaultClient? TryGetVaultLoader(PluginBase pbase) + [Obsolete("Deprecated in favor of Secrets")] + public static Task<ISecretResult?> GetSecretFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath) { - //Get vault config - IConfigScope? conf = pbase.TryGetConfig(VAULT_OBJECT_NAME); - - if (conf == null) - { - return null; - } - - //try get servre address creds from config - string? serverAddress = conf[VAULT_URL_KEY].GetString() ?? throw new KeyNotFoundException($"Failed to load the key {VAULT_URL_KEY} from object {VAULT_OBJECT_NAME}"); - - IAuthMethodInfo authMethod; - - //Get authentication method from config - if (conf.TryGetValue(VAULT_TOKEN_KEY, out JsonElement tokenEl)) - { - //Init token - authMethod = new TokenAuthMethodInfo(tokenEl.GetString()); - } - else if (conf.TryGetValue(VAULT_ROLE_KEY, out JsonElement roleEl) && conf.TryGetValue(VAULT_SECRET_KEY, out JsonElement secretEl)) - { - authMethod = new AppRoleAuthMethodInfo(roleEl.GetString(), secretEl.GetString()); - } - //Try to get the token as an environment variable - else if(Environment.GetEnvironmentVariable(VAULT_TOKNE_ENV_NAME) != null) - { - string tokenValue = Environment.GetEnvironmentVariable(VAULT_TOKNE_ENV_NAME)!; - authMethod = new TokenAuthMethodInfo(tokenValue); - } - else - { - throw new KeyNotFoundException($"Failed to load the vault authentication method from {VAULT_OBJECT_NAME}"); - } - - //Settings - VaultClientSettings settings = new(serverAddress, authMethod); - - //create vault client - return new VaultClient(settings); - } + throw new NotSupportedException("This method is not supported in this context"); + } /// <summary> /// Gets the Secret value as a byte buffer @@ -398,7 +131,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="InternalBufferTooSmallException"></exception> public static byte[] GetFromBase64(this ISecretResult secret) { - _ = secret ?? throw new ArgumentNullException(nameof(secret)); + ArgumentNullException.ThrowIfNull(secret); //Temp buffer using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAlloc(secret.Result.Length); @@ -517,7 +250,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="InternalBufferTooSmallException"></exception> public static async Task<byte[]> ToBase64Bytes(this Task<ISecretResult> secret) { - _ = secret ?? throw new ArgumentNullException(nameof(secret)); + ArgumentNullException.ThrowIfNull(secret); using ISecretResult sec = await secret.ConfigureAwait(false); @@ -533,8 +266,8 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="ArgumentNullException"></exception> public static async Task<ReadOnlyJsonWebKey> ToJsonWebKey(this Task<ISecretResult> secret) { - _ = secret ?? throw new ArgumentNullException(nameof(secret)); - + ArgumentNullException.ThrowIfNull(secret); + using ISecretResult sec = await secret.ConfigureAwait(false); return sec?.GetJsonWebKey(); @@ -554,7 +287,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="KeyNotFoundException"></exception> public static async Task<ReadOnlyJsonWebKey> ToJsonWebKey(this Task<ISecretResult> secret, bool required) { - _ = secret ?? throw new ArgumentNullException(nameof(secret)); + ArgumentNullException.ThrowIfNull(secret); using ISecretResult sec = await secret.ConfigureAwait(false); @@ -573,8 +306,8 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="ArgumentNullException"></exception> public static IAsyncLazy<TResult> ToLazy<TResult>(this Task<ISecretResult> result, Func<ISecretResult, TResult> transformer) { - _ = result ?? throw new ArgumentNullException(nameof(result)); - _ = transformer ?? throw new ArgumentNullException(nameof(transformer)); + ArgumentNullException.ThrowIfNull(result); + ArgumentNullException.ThrowIfNull(transformer); //standard secret transformer static async Task<TResult> Run(Task<ISecretResult> tr, Func<ISecretResult, TResult> transformer) @@ -597,8 +330,8 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="ArgumentNullException"></exception> public static IAsyncLazy<TResult> ToLazy<TResult>(this Task<ISecretResult> result, Func<ISecretResult, Task<TResult>> transformer) { - _ = result ?? throw new ArgumentNullException(nameof(result)); - _ = transformer ?? throw new ArgumentNullException(nameof(transformer)); + ArgumentNullException.ThrowIfNull(result); + ArgumentNullException.ThrowIfNull(transformer); //Transform with task transformer static async Task<TResult> Run(Task<ISecretResult?> tr, Func<ISecretResult, Task<TResult>> transformer) @@ -609,5 +342,8 @@ namespace VNLib.Plugins.Extensions.Loading return Run(result, transformer).AsLazy(); } + +#nullable enable + } } |