aboutsummaryrefslogtreecommitdiff
path: root/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs')
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs338
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
+
}
}