From bdf7c1bc36dbcc9f66f5caa344602817f888c49d Mon Sep 17 00:00:00 2001 From: vnugent Date: Sat, 9 Mar 2024 14:52:04 -0500 Subject: Squashed commit of the following: commit 7a263bf54b7967ddeb9f6b662339ec1c74546ce8 Author: vnugent Date: Sat Mar 9 14:19:31 2024 -0500 refactor: Overhaul secret loading. Remove VaultSharp as a dep commit 766e179d110db4f955fffce55f2b0ad41c139179 Author: vnugent Date: Wed Mar 6 21:35:35 2024 -0500 refactor: changed how service constructors are invoked, moved routing --- .../src/Secrets/OnDemandSecret.cs | 240 +++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/Secrets/OnDemandSecret.cs (limited to 'lib/VNLib.Plugins.Extensions.Loading/src/Secrets/OnDemandSecret.cs') diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/OnDemandSecret.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/OnDemandSecret.cs new file mode 100644 index 0000000..6e6d560 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/OnDemandSecret.cs @@ -0,0 +1,240 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: OnDemandSecret.cs +* +* OnDemandSecret.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 +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Collections.Generic; + +using VNLib.Utils.Memory; +using VNLib.Utils.Logging; +using VNLib.Utils.Extensions; + +using static VNLib.Plugins.Extensions.Loading.PluginSecretConstants; + +namespace VNLib.Plugins.Extensions.Loading +{ + internal sealed class OnDemandSecret(PluginBase plugin, string secretName, IHCVaultClient? vault) : IOnDemandSecret + { + public string SecretName { get; } = secretName ?? throw new ArgumentNullException(nameof(secretName)); + + /// + public ISecretResult? FetchSecret() + { + plugin.ThrowIfUnloaded(); + + //Get the secret from the config file raw + string? rawSecret = TryGetSecretFromConfig(secretName); + + if (rawSecret == null) + { + return null; + } + + //Secret is a vault path, or return the raw value + if (rawSecret.StartsWith(VAULT_URL_SCHEME, StringComparison.OrdinalIgnoreCase)) + { + ValueTask res = GetSecretFromVault(rawSecret, false); + Debug.Assert(res.IsCompleted); + return res.GetAwaiter().GetResult(); + } + + //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 envVal == null ? null : SecretResult.ToSecret(envVal); + } + + //See if the secret is a file path + if (rawSecret.StartsWith(FILE_URL_SCHEME, StringComparison.OrdinalIgnoreCase)) + { + string filePath = rawSecret[FILE_URL_SCHEME.Length..]; + byte[] fileData = File.ReadAllBytes(filePath); + + return GetResultFromFileData(fileData); + } + + //Finally, return the raw value + return SecretResult.ToSecret(rawSecret); + } + + /// + public Task FetchSecretAsync(CancellationToken cancellation) + { + plugin.ThrowIfUnloaded(); + + //Get the secret from the config file raw + string? rawSecret = TryGetSecretFromConfig(secretName); + + if (rawSecret == null) + { + return Task.FromResult(null); + } + + //Secret is a vault path, or return the raw value + if (rawSecret.StartsWith(VAULT_URL_SCHEME, StringComparison.OrdinalIgnoreCase)) + { + //Exec vault async + ValueTask res = GetSecretFromVault(rawSecret, true); + return res.AsTask(); + } + + //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(envVal == null ? null : SecretResult.ToSecret(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 GetResultFromFileAsync(filePath, cancellation); + } + + //Finally, return the raw value + return Task.FromResult(SecretResult.ToSecret(rawSecret)); + + + static async Task GetResultFromFileAsync(string filePath, CancellationToken ct) + { + byte[] fileData = await File.ReadAllBytesAsync(filePath, ct); + return GetResultFromFileData(fileData); + } + } + + /// + /// Gets a secret at the given vault url (in the form of "vault://[mount-name]/[secret-path]?secret=[secret_name]") + /// + /// The raw vault url to lookup + /// + /// The string of the object at the specified vault path + /// + /// + /// + private ValueTask GetSecretFromVault(ReadOnlySpan vaultPath, bool async) + { + ArgumentNullException.ThrowIfNull(plugin); + + //print the path for debug + if (plugin.IsDebug()) + { + plugin.Log.Debug("Retrieving secret {s} from vault", vaultPath.ToString()); + } + + //Slice off path + ReadOnlySpan paq = vaultPath.SliceAfterParam(VAULT_URL_SCHEME); + ReadOnlySpan path = paq.SliceBeforeParam('?'); + ReadOnlySpan 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(); + + //Try load client + _ = vault ?? throw new KeyNotFoundException("Vault client not found"); + + if (async) + { + Task asTask = Task.Run(() => vault.ReadSecretAsync(secret, mount, secretTableKey)); + return new ValueTask(asTask); + } + else + { + ISecretResult? result = vault.ReadSecret(secret, mount, secretTableKey); + return new ValueTask(result); + } + } + + private string? TryGetSecretFromConfig(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 + Dictionary conf = new(StringComparer.OrdinalIgnoreCase); + + if (local && host) + { + //Load both config objects to dict + Dictionary localConf = localEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value); + Dictionary hostConf = hostEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value); + + //Enter all host secret objects, then follow up with plugin secert elements + hostConf.ForEach(kv => conf[kv.Key] = kv.Value); + localConf.ForEach(kv => conf[kv.Key] = kv.Value); + + } + else if (local) + { + //Store only local config + conf = localEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value, StringComparer.OrdinalIgnoreCase); + } + else if (host) + { + //store only host config + conf = hostEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value, StringComparer.OrdinalIgnoreCase); + } + + //Get the value or default json element + return conf.TryGetValue(secretName, out JsonElement el) ? el.GetString() : null; + } + + private static SecretResult GetResultFromFileData(byte[] secretFileData) + { + //recover the character data from the file data + int chars = Encoding.UTF8.GetCharCount(secretFileData); + char[] secretFileChars = new char[chars]; + Encoding.UTF8.GetChars(secretFileData, secretFileChars); + + //Clear file data buffer + MemoryUtil.InitializeBlock(secretFileData); + + //Keep the char array as a secret + return SecretResult.ToSecret(secretFileChars); + } + } +} -- cgit