diff options
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Loading/src')
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs | 20 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs | 40 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/IAsyncLazy.cs | 148 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs | 175 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/ManagedPasswordHashing.cs | 44 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/Secrets/ISecretResult.cs | 39 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/Secrets/PrivateKey.cs (renamed from lib/VNLib.Plugins.Extensions.Loading/src/PrivateKey.cs) | 4 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs (renamed from lib/VNLib.Plugins.Extensions.Loading/src/SecretResult.cs) | 9 | ||||
-rw-r--r-- | lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs (renamed from lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs) | 140 |
9 files changed, 516 insertions, 103 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs b/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs index 5de6103..e01b32d 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs @@ -159,29 +159,19 @@ namespace VNLib.Plugins.Extensions.Loading /// </summary> /// <param name="assemblyName">The name of the assmbly within the current plugin directory</param> /// <param name="unloadToken">The plugin unload token</param> - /// <param name="explicitContext">Explicitly set an assembly load context to load the requested assembly into</param> + /// <param name="loadContext">The assembly load context to load the assmbly into</param> /// <exception cref="FileNotFoundException"></exception> - internal static AssemblyLoader<T> Load(string assemblyName, AssemblyLoadContext? explicitContext, CancellationToken unloadToken) + internal static AssemblyLoader<T> Load(string assemblyName, AssemblyLoadContext loadContext, CancellationToken unloadToken) { + _ = loadContext ?? throw new ArgumentNullException(nameof(loadContext)); + //Make sure the file exists if (!FileOperations.FileExists(assemblyName)) { throw new FileNotFoundException($"The desired assembly {assemblyName} could not be found at the file path"); } - - if(explicitContext == null) - { - /* - * Dynamic assemblies are loaded directly to the exe assembly context. - * This should always be the plugin isolated context. - */ - - Assembly executingAsm = Assembly.GetExecutingAssembly(); - explicitContext = AssemblyLoadContext.GetLoadContext(executingAsm) ?? throw new InvalidOperationException("Could not get default assembly load context"); - } - - return new(assemblyName, explicitContext, unloadToken); + return new(assemblyName, loadContext, unloadToken); } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs index 190c153..fbe8d48 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs @@ -23,11 +23,14 @@ */ using System; +using System.IO; using System.Text.Json; using System.Reflection; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using VNLib.Utils.Extensions; + namespace VNLib.Plugins.Extensions.Loading { /// <summary> @@ -66,6 +69,9 @@ namespace VNLib.Plugins.Extensions.Loading { public const string S3_CONFIG = "s3_config"; public const string S3_SECRET_KEY = "s3_secret"; + public const string PLUGIN_ASSET_KEY = "assets"; + public const string PLUGINS_HOST_KEY = "plugins"; + public const string PLUGIN_PATH_KEY = "path"; /// <summary> /// Retrieves a top level configuration dictionary of elements for the specified type. @@ -356,5 +362,39 @@ namespace VNLib.Plugins.Extensions.Loading IConfigScope? s3conf = plugin.TryGetConfig(S3_CONFIG); return s3conf?.Deserialze<S3Config>(); } + + /// <summary> + /// Trys to get the optional assets directory from the plugin configuration + /// </summary> + /// <param name="plugin"></param> + /// <returns>The absolute path to the assets directory if defined, null otherwise</returns> + public static string? GetAssetsPath(this PluginBase plugin) + { + //Get global plugin config element + IConfigScope config = plugin.GetConfig(PLUGINS_HOST_KEY); + + //Try to get the assets path if its defined + string? assetsPath = config.GetPropString(PLUGIN_ASSET_KEY); + + //Try to get the full path for the assets if we can + return assetsPath != null ? Path.GetFullPath(assetsPath) : null; + } + + /// <summary> + /// Gets the absolute path to the plugins directory as defined in the host configuration + /// </summary> + /// <param name="plugin"></param> + /// <returns>The absolute path to the directory containing all plugins</returns> + public static string GetPluginsPath(this PluginBase plugin) + { + //Get global plugin config element + IConfigScope config = plugin.GetConfig(PLUGINS_HOST_KEY); + + //Get the plugins path or throw because it should ALWAYS be defined if this method is called + string pluginsPath = config[PLUGIN_PATH_KEY].GetString()!; + + //Get absolute path + return Path.GetFullPath(pluginsPath); + } } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/IAsyncLazy.cs b/lib/VNLib.Plugins.Extensions.Loading/src/IAsyncLazy.cs new file mode 100644 index 0000000..98e0ebe --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/IAsyncLazy.cs @@ -0,0 +1,148 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: IAsyncLazy.cs +* +* IAsyncLazy.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.Threading.Tasks; +using System.Runtime.CompilerServices; + +namespace VNLib.Plugins.Extensions.Loading +{ + /// <summary> + /// Represents an asynchronous lazy operation. with non-blocking access to the target value. + /// </summary> + /// <typeparam name="T">The result type</typeparam> + public interface IAsyncLazy<T> + { + /// <summary> + /// Gets a value indicating whether the asynchronous operation has completed. + /// </summary> + bool Completed { get; } + + /// <summary> + /// Gets a task that represents the asynchronous operation. + /// </summary> + /// <returns></returns> + TaskAwaiter<T> GetAwaiter(); + + /// <summary> + /// Gets the target value of the asynchronous operation without blocking. + /// If the operation failed, throws an exception that caused the failure. + /// If the operation has not completed, throws an exception. + /// </summary> + T Value { get; } + } + + /// <summary> + /// Extension methods for <see cref="IAsyncLazy{T}"/> + /// </summary> + public static class AsyncLazyExtensions + { + /// <summary> + /// Gets an <see cref="IAsyncLazy{T}"/> wrapper for the specified <see cref="Task{T}"/> + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="task"></param> + /// <returns>The async operation task wrapper</returns> + public static IAsyncLazy<T> AsLazy<T>(this Task<T> task) => new AsyncLazy<T>(task); + + /// <summary> + /// Tranforms one lazy operation into another using the specified handler + /// </summary> + /// <typeparam name="T"></typeparam> + /// <typeparam name="TResult">The resultant type</typeparam> + /// <param name="lazy"></param> + /// <param name="handler">The function that will peform the transformation of the lazy result</param> + /// <returns>A new <see cref="IAsyncLazy{T}"/> that returns the transformed type</returns> + public static IAsyncLazy<TResult> Transform<T, TResult>(this IAsyncLazy<T> lazy, Func<T, TResult> handler) + { + _ = lazy ?? throw new ArgumentNullException(nameof(lazy)); + _ = handler ?? throw new ArgumentNullException(nameof(handler)); + + //Await the lazy task, then pass the result to the handler + static async Task<TResult> OnResult(IAsyncLazy<T> lazy, Func<T, TResult> cb) + { + T result = await lazy; + return cb(result); + } + + return OnResult(lazy, handler).AsLazy(); + } + +#nullable disable + + private sealed class AsyncLazy<T> : IAsyncLazy<T> + { + private readonly Task<T> _task; + + private T _result; + + public AsyncLazy(Task<T> task) + { + _task = task ?? throw new ArgumentNullException(nameof(task)); + _ = task.ContinueWith(SetResult, TaskScheduler.Default); + } + + ///<inheritdoc/> + public bool Completed => _task.IsCompleted; + + ///<inheritdoc/> + public T Value + { + get + { + if (_task.IsCompletedSuccessfully) + { + return _result; + } + else if(_task.IsFaulted) + { + //Compress and raise exception from result + return _task.GetAwaiter().GetResult(); + } + else + { + throw new InvalidOperationException("The asynchronous operation has not completed."); + } + } + } + + /* + * Only set the result if the task completed successfully. + */ + private void SetResult(Task<T> task) + { + if (task.IsCompletedSuccessfully) + { + _result = task.Result; + } + } + + ///<inheritdoc/> + public TaskAwaiter<T> GetAwaiter() => _task.GetAwaiter(); + } +#nullable enable + + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs index 5511398..8f7dee8 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs @@ -37,6 +37,50 @@ using VNLib.Utils.Extensions; namespace VNLib.Plugins.Extensions.Loading { + /// <summary> + /// Base class for concrete type loading exceptions. Raised when searching + /// for a concrete type fails. + /// </summary> + public class ConcreteTypeException : TypeLoadException + { + public ConcreteTypeException() : base() + { } + + public ConcreteTypeException(string? message) : base(message) + { } + + public ConcreteTypeException(string? message, Exception? innerException) : base(message, innerException) + { } + } + + /// <summary> + /// Raised when a concrete type is found but is ambiguous because more than one + /// type implements the desired abstract type. + /// </summary> + public sealed class ConcreteTypeAmbiguousMatchException : ConcreteTypeException + { + public ConcreteTypeAmbiguousMatchException(string message) : base(message) + { } + + public ConcreteTypeAmbiguousMatchException(string message, Exception innerException) : base(message, innerException) + { } + + public ConcreteTypeAmbiguousMatchException() + { } + } + + /// <summary> + /// The requested concrete type was not found in the assembly + /// </summary> + public sealed class ConcreteTypeNotFoundException : ConcreteTypeException + { + public ConcreteTypeNotFoundException(string message) : base(message) + { } + public ConcreteTypeNotFoundException(string message, Exception innerException) : base(message, innerException) + { } + public ConcreteTypeNotFoundException() + { } + } /// <summary> /// Provides common loading (and unloading when required) extensions for plugins @@ -47,7 +91,7 @@ namespace VNLib.Plugins.Extensions.Loading /// A key in the 'plugins' configuration object that specifies /// an asset search directory /// </summary> - public const string PLUGIN_ASSET_KEY = "assets"; + public const string DEBUG_CONFIG_KEY = "debug"; public const string SECRETS_CONFIG_KEY = "secrets"; public const string PASSWORD_HASHING_KEY = "passwords"; @@ -121,31 +165,79 @@ namespace VNLib.Plugins.Extensions.Loading { plugin.ThrowIfUnloaded(); _ = assemblyName ?? throw new ArgumentNullException(nameof(assemblyName)); - - //get plugin directory from config - IConfigScope config = plugin.GetConfig("plugins"); + /* * Allow an assets directory to limit the scope of the search for the desired * assembly, otherwise search all plugins directories */ - - string? assetDir = config.GetPropString(PLUGIN_ASSET_KEY); - assetDir ??= config["path"].GetString(); + + string? assetDir = plugin.GetAssetsPath(); + assetDir ??= plugin.GetPluginsPath(); /* * This should never happen since this method can only be called from a * plugin context, which means this path was used to load the current plugin */ - _ = assetDir ?? throw new ArgumentNullException(PLUGIN_ASSET_KEY, "No plugin path is defined for the current host configuration, this is likely a bug"); + _ = assetDir ?? throw new ArgumentNullException(ConfigurationExtensions.PLUGIN_ASSET_KEY, "No plugin path is defined for the current host configuration, this is likely a bug"); //Get the first file that matches the search file string? asmFile = Directory.EnumerateFiles(assetDir, assemblyName, dirSearchOption).FirstOrDefault(); _ = asmFile ?? throw new FileNotFoundException($"Failed to load custom assembly {assemblyName} from plugin directory"); - + + //Get the plugin's load context if not explicitly supplied + explictAlc ??= GetPluginLoadContext(); + //Load the assembly return AssemblyLoader<T>.Load(asmFile, explictAlc, plugin.UnloadToken); - } + } + + /// <summary> + /// Gets the current plugin's <see cref="AssemblyLoadContext"/>. + /// </summary> + /// <returns></returns> + /// <exception cref="InvalidOperationException"></exception> + public static AssemblyLoadContext GetPluginLoadContext() + { + /* + * Since this library should only be used in a plugin context, the executing assembly + * will be loaded into the plugin's isolated load context. So we can get the load + * context for the executing assembly and use that as the plugin's load context. + */ + + Assembly executingAsm = Assembly.GetExecutingAssembly(); + return AssemblyLoadContext.GetLoadContext(executingAsm) ?? throw new InvalidOperationException("Could not get plugin's assembly load context"); + } + + /// <summary> + /// Gets a single type implemenation of the abstract type from the current assembly. If multiple + /// concrete types are found, an exception is raised, if no concrete types are found, an exception + /// is raised. + /// </summary> + /// <param name="abstractType">The abstract type to get the concrete type from</param> + /// <returns>The concrete type if found</returns> + /// <exception cref="ConcreteTypeNotFoundException"></exception> + /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception> + public static Type GetTypeImplFromCurrentAssembly(Type abstractType) + { + //Get all types from the current assembly that implement the abstract type + Assembly executingAsm = Assembly.GetExecutingAssembly(); + Type[] concreteTypes = executingAsm.GetTypes().Where(t => !t.IsAbstract && abstractType.IsAssignableFrom(t)).ToArray(); + + if(concreteTypes.Length == 0) + { + throw new ConcreteTypeNotFoundException($"Failed to load implemenation of abstract type {abstractType} because no concrete implementations were found in this assembly"); + } + + if(concreteTypes.Length > 1) + { + throw new ConcreteTypeAmbiguousMatchException( + $"Failed to load implemenation of abstract type {abstractType} because multiple concrete implementations were found in this assembly"); + } + + //Get the only concrete type + return concreteTypes[0]; + } /// <summary> /// Determintes if the current plugin config has a debug propety set @@ -285,11 +377,13 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="EntryPointNotFoundException"></exception> + /// <exception cref="ConcreteTypeNotFoundException"></exception> + /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception> public static T GetOrCreateSingleton<T>(this PluginBase plugin) { //Add service to service continer return GetOrCreateSingleton(plugin, CreateService<T>); - } + } /// <summary> /// <para> @@ -311,6 +405,8 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="EntryPointNotFoundException"></exception> + /// <exception cref="ConcreteTypeNotFoundException"></exception> + /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception> public static T GetOrCreateSingleton<T>(this PluginBase plugin, string configName) { //Add service to service continer @@ -357,6 +453,8 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="EntryPointNotFoundException"></exception> + /// <exception cref="ConcreteTypeNotFoundException"></exception> + /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception> public static T CreateService<T>(this PluginBase plugin) { if (plugin.HasConfigForType<T>()) @@ -390,6 +488,8 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="EntryPointNotFoundException"></exception> + /// <exception cref="ConcreteTypeNotFoundException"></exception> + /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception> public static T CreateService<T>(this PluginBase plugin, string configName) { IConfigScope config = plugin.GetConfig(configName); @@ -416,30 +516,67 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="EntryPointNotFoundException"></exception> + /// <exception cref="ConcreteTypeNotFoundException"></exception> + /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception> public static T CreateService<T>(this PluginBase plugin, IConfigScope? config) { + return (T)CreateService(plugin, typeof(T), config); + } + + /// <summary> + /// <para> + /// Creates and configures a new instance of the desired type, with the specified configuration scope + /// </para> + /// <para> + /// If the type derrives <see cref="IAsyncConfigurable"/> the <see cref="IAsyncConfigurable.ConfigureServiceAsync"/> + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// </para> + /// <para> + /// If the type derrives <see cref="IAsyncBackgroundWork"/> the <see cref="IAsyncBackgroundWork.DoWorkAsync(ILogProvider, System.Threading.CancellationToken)"/> + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// </para> + /// </summary> + /// <param name="plugin"></param> + /// <param name="serviceType">The service type to instantiate</param> + /// <param name="config">The configuration scope to pass directly to the new instance</param> + /// <returns>The a new instance configured service</returns> + /// <exception cref="KeyNotFoundException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="EntryPointNotFoundException"></exception> + /// <exception cref="ConcreteTypeNotFoundException"></exception> + /// <exception cref="ConcreteTypeAmbiguousMatchException"></exception> + public static object CreateService(this PluginBase plugin, Type serviceType, IConfigScope? config) + { + _ = plugin ?? throw new ArgumentNullException(nameof(plugin)); + _ = serviceType ?? throw new ArgumentNullException(nameof(serviceType)); + plugin.ThrowIfUnloaded(); - Type serviceType = typeof(T); + //The requested sesrvice is not a class, so see if we can find a default implementation in assembly + if (serviceType.IsAbstract || serviceType.IsInterface) + { + //Overwrite the service type with the default implementation + serviceType = GetTypeImplFromCurrentAssembly(serviceType); + } - T service; + object service; //Determin configuration requirments if (ConfigurationExtensions.ConfigurationRequired(serviceType) || config != null) { - if(config == null) + if (config == null) { ConfigurationExtensions.ThrowConfigNotFoundForType(serviceType); } //Get the constructor for required or available config - ConstructorInfo? constructor = serviceType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IConfigScope) }); + ConstructorInfo? constructor = serviceType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IConfigScope) }); //Make sure the constructor exists _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {serviceType.Name}"); //Call constructore - service = (T)constructor.Invoke(new object[2] { plugin, config }); + service = constructor.Invoke(new object[2] { plugin, config }); } else { @@ -450,8 +587,8 @@ namespace VNLib.Plugins.Extensions.Loading _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {serviceType.Name}"); //Call constructore - service = (T)constructor.Invoke(new object[1] { plugin }); - } + service = constructor.Invoke(new object[1] { plugin }); + } Task? loading = null; @@ -475,7 +612,7 @@ namespace VNLib.Plugins.Extensions.Loading #pragma warning restore CA5394 // Do not use insecure randomness //If the instances supports async loading, dont start work until its loaded - if(loading != null) + if (loading != null) { _ = loading.ContinueWith(t => ObserveWork(plugin, bw, randomDelay), TaskScheduler.Default); } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ManagedPasswordHashing.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ManagedPasswordHashing.cs index 522bfae..3489789 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/ManagedPasswordHashing.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/ManagedPasswordHashing.cs @@ -25,7 +25,6 @@ using System; using System.Linq; using System.Text.Json; -using System.Threading.Tasks; using System.Collections.Generic; using VNLib.Utils; @@ -121,10 +120,9 @@ namespace VNLib.Plugins.Extensions.Loading public bool Verify(ReadOnlySpan<byte> passHash, ReadOnlySpan<byte> password) => _loader.Resource.Verify(passHash, password); } - private sealed class SecretProvider : VnDisposeable, ISecretProvider, IAsyncConfigurable + private sealed class SecretProvider : VnDisposeable, ISecretProvider { - private byte[]? _pepper; - private Exception? _error; + private readonly IAsyncLazy<byte[]> _pepper; public SecretProvider(PluginBase plugin, IConfigScope config) { @@ -146,11 +144,19 @@ namespace VNLib.Plugins.Extensions.Loading { Passwords = new(this); } + + //Get the pepper from secret storage + _pepper = plugin.GetSecretAsync(LoadingExtensions.PASSWORD_HASHING_KEY) + .ToLazy(static sr => sr.GetFromBase64()); } public SecretProvider(PluginBase plugin) { Passwords = new(this); + + //Get the pepper from secret storage + _pepper = plugin.GetSecretAsync(LoadingExtensions.PASSWORD_HASHING_KEY) + .ToLazy(static sr => sr.GetFromBase64()); } @@ -162,7 +168,7 @@ namespace VNLib.Plugins.Extensions.Loading get { Check(); - return _pepper!.Length; + return _pepper.Value.Length; } } @@ -170,41 +176,21 @@ namespace VNLib.Plugins.Extensions.Loading { Check(); //Coppy pepper to buffer - _pepper.CopyTo(buffer); + _pepper.Value.CopyTo(buffer); //Return pepper length - return _pepper!.Length; + return _pepper.Value.Length; } protected override void Check() { base.Check(); - if (_error != null) - { - throw _error; - } + _ = _pepper.Value; } protected override void Free() { //Clear the pepper if set - MemoryUtil.InitializeBlock(_pepper.AsSpan()); - } - - public async Task ConfigureServiceAsync(PluginBase plugin) - { - try - { - //Get the pepper from secret storage - _pepper = await plugin.TryGetSecretAsync(LoadingExtensions.PASSWORD_HASHING_KEY).ToBase64Bytes(); - } - catch (Exception ex) - { - //Store exception for re-propagation - _error = ex; - - //Propagate exception to system - throw; - } + MemoryUtil.InitializeBlock(_pepper.Value.AsSpan()); } } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/ISecretResult.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/ISecretResult.cs new file mode 100644 index 0000000..b3c8737 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/ISecretResult.cs @@ -0,0 +1,39 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: ISecretResult.cs +* +* ISecretResult.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; + +namespace VNLib.Plugins.Extensions.Loading +{ + /// <summary> + /// The result of a secret fetch operation + /// </summary> + public interface ISecretResult : IDisposable + { + /// <summary> + /// The protected raw result value + /// </summary> + ReadOnlySpan<char> Result { get; } + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/PrivateKey.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/PrivateKey.cs index 2e5fb7f..08637cd 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/PrivateKey.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/PrivateKey.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -90,7 +90,7 @@ namespace VNLib.Plugins.Extensions.Loading return alg; } - internal PrivateKey(SecretResult secret) + internal PrivateKey(ISecretResult secret) { //Alloc and get utf8 byte[] buffer = new byte[secret.Result.Length]; diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/SecretResult.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs index 6c1c5f8..f2cbd28 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/SecretResult.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -30,16 +30,15 @@ using VNLib.Utils.Memory; namespace VNLib.Plugins.Extensions.Loading { + /// <summary> /// The result of a secret fetch operation /// </summary> - public sealed class SecretResult : VnDisposeable + public sealed class SecretResult : VnDisposeable, ISecretResult { private readonly char[] _secretChars; - /// <summary> - /// The protected raw result value - /// </summary> + ///<inheritdoc/> public ReadOnlySpan<char> Result => _secretChars; diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs index 9e3c222..711ae50 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs @@ -61,7 +61,27 @@ namespace VNLib.Plugins.Extensions.Loading public const string VAULT_URL_KEY = "url"; public const string VAULT_URL_SCHEME = "vault://"; - + + + /// <summary> + /// <para> + /// Gets a 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="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> + /// <exception cref="KeyNotFoundException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + public static async Task<ISecretResult> GetSecretAsync(this PluginBase plugin, string secretName) + { + ISecretResult? res = await TryGetSecretAsync(plugin, secretName).ConfigureAwait(false); + return res ?? throw new KeyNotFoundException($"Missing required secret {secretName}"); + } /// <summary> /// <para> @@ -77,21 +97,24 @@ namespace VNLib.Plugins.Extensions.Loading /// <returns>The element from the configuration file with the given name, or null if the configuration or property does not exist</returns> /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> - public static Task<SecretResult?> TryGetSecretAsync(this PluginBase plugin, string secretName) + 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<SecretResult?>(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 Task.FromResult<SecretResult?>(new(rawSecret.AsSpan())); + return Task.FromResult<ISecretResult?>(new SecretResult(rawSecret.AsSpan())); } + return GetSecretFromVaultAsync(plugin, rawSecret); } @@ -104,7 +127,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="UriFormatException"></exception> /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> - public static Task<SecretResult?> GetSecretFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath) + public static Task<ISecretResult?> GetSecretFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath) { //print the path for debug if (plugin.IsDebug()) @@ -130,7 +153,7 @@ namespace VNLib.Plugins.Extensions.Loading string mount = path[..lastSep].ToString(); string secret = path[(lastSep + 1)..].ToString(); - async Task<SecretResult?> execute() + async Task<ISecretResult?> execute() { //Try load client IVaultClient? client = plugin.GetVault(); @@ -332,7 +355,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <returns>The base64 decoded secret as a byte[]</returns> /// <exception cref="ArgumentNullException"></exception> /// <exception cref="InternalBufferTooSmallException"></exception> - public static byte[] GetFromBase64(this SecretResult secret) + public static byte[] GetFromBase64(this ISecretResult secret) { _ = secret ?? throw new ArgumentNullException(nameof(secret)); @@ -355,28 +378,12 @@ namespace VNLib.Plugins.Extensions.Loading } /// <summary> - /// Converts the secret recovery task to - /// </summary> - /// <param name="secret"></param> - /// <returns>A task whos result the base64 decoded secret as a byte[]</returns> - /// <exception cref="ArgumentNullException"></exception> - /// <exception cref="InternalBufferTooSmallException"></exception> - public static async Task<byte[]?> ToBase64Bytes(this Task<SecretResult?> secret) - { - _ = secret ?? throw new ArgumentNullException(nameof(secret)); - - using SecretResult? sec = await secret.ConfigureAwait(false); - - return sec?.GetFromBase64(); - } - - /// <summary> /// Recovers a certificate from a PEM encoded secret /// </summary> /// <param name="secret"></param> /// <returns>The <see cref="X509Certificate2"/> parsed from the PEM encoded data</returns> /// <exception cref="ArgumentNullException"></exception> - public static X509Certificate2 GetCertificate(this SecretResult secret) + public static X509Certificate2 GetCertificate(this ISecretResult secret) { _ = secret ?? throw new ArgumentNullException(nameof(secret)); return X509Certificate2.CreateFromPem(secret.Result); @@ -387,7 +394,7 @@ namespace VNLib.Plugins.Extensions.Loading /// </summary> /// <param name="secret"></param> /// <returns>The document parsed from the secret value</returns> - public static JsonDocument GetJsonDocument(this SecretResult secret) + public static JsonDocument GetJsonDocument(this ISecretResult secret) { _ = secret ?? throw new ArgumentNullException(nameof(secret)); @@ -409,7 +416,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <param name="secret"></param> /// <returns>The <see cref="PublicKey"/> parsed from the SPKI public key</returns> /// <exception cref="ArgumentNullException"></exception> - public static PublicKey GetPublicKey(this SecretResult secret) + public static PublicKey GetPublicKey(this ISecretResult secret) { _ = secret ?? throw new ArgumentNullException(nameof(secret)); @@ -431,7 +438,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <returns>The <see cref="PrivateKey"/> from the secret value</returns> /// <exception cref="FormatException"></exception> /// <exception cref="ArgumentNullException"></exception> - public static PrivateKey GetPrivateKey(this SecretResult secret) + public static PrivateKey GetPrivateKey(this ISecretResult secret) { _ = secret ?? throw new ArgumentNullException(nameof(secret)); return new PrivateKey(secret); @@ -445,7 +452,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="JsonException"></exception> /// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentNullException"></exception> - public static ReadOnlyJsonWebKey GetJsonWebKey(this SecretResult secret) + public static ReadOnlyJsonWebKey GetJsonWebKey(this ISecretResult secret) { _ = secret ?? throw new ArgumentNullException(nameof(secret)); @@ -458,6 +465,24 @@ namespace VNLib.Plugins.Extensions.Loading return new ReadOnlyJsonWebKey(buffer.Span[..count]); } +#nullable disable + + /// <summary> + /// Converts the secret recovery task to return the base64 decoded secret as a byte[] + /// </summary> + /// <param name="secret"></param> + /// <returns>A task whos result the base64 decoded secret as a byte[]</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="InternalBufferTooSmallException"></exception> + public static async Task<byte[]> ToBase64Bytes(this Task<ISecretResult> secret) + { + _ = secret ?? throw new ArgumentNullException(nameof(secret)); + + using ISecretResult sec = await secret.ConfigureAwait(false); + + return sec?.GetFromBase64(); + } + /// <summary> /// Gets a task that resolves a <see cref="ReadOnlyJsonWebKey"/> /// from a <see cref="SecretResult"/> task @@ -465,11 +490,11 @@ namespace VNLib.Plugins.Extensions.Loading /// <param name="secret"></param> /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the secret, or null if the secret was not found</returns> /// <exception cref="ArgumentNullException"></exception> - public static async Task<ReadOnlyJsonWebKey?> ToJsonWebKey(this Task<SecretResult?> secret) + public static async Task<ReadOnlyJsonWebKey> ToJsonWebKey(this Task<ISecretResult> secret) { _ = secret ?? throw new ArgumentNullException(nameof(secret)); - using SecretResult? sec = await secret.ConfigureAwait(false); + using ISecretResult sec = await secret.ConfigureAwait(false); return sec?.GetJsonWebKey(); } @@ -483,16 +508,65 @@ namespace VNLib.Plugins.Extensions.Loading /// A value that inidcates that a value is required from the result, /// or a <see cref="KeyNotFoundException"/> is raised /// </param> - /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the secret, or null if the secret was not found</returns> + /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the secret, or throws <see cref="KeyNotFoundException"/> if the key was not found</returns> /// <exception cref="ArgumentNullException"></exception> - public static async Task<ReadOnlyJsonWebKey> ToJsonWebKey(this Task<SecretResult?> secret, bool required) + /// <exception cref="KeyNotFoundException"></exception> + public static async Task<ReadOnlyJsonWebKey> ToJsonWebKey(this Task<ISecretResult> secret, bool required) { _ = secret ?? throw new ArgumentNullException(nameof(secret)); - using SecretResult? sec = await secret.ConfigureAwait(false); + using ISecretResult sec = await secret.ConfigureAwait(false); //If required is true and result is null, raise an exception return required && sec == null ? throw new KeyNotFoundException("A required secret was missing") : (sec?.GetJsonWebKey()!); } + + /// <summary> + /// Converts a <see cref="SecretResult"/> async operation to a lazy result that can be awaited, that transforms the result + /// to your desired type. If the result is null, the default value of <typeparamref name="TResult"/> is returned + /// </summary> + /// <typeparam name="TResult"></typeparam> + /// <param name="result"></param> + /// <param name="transformer">Your function to transform the secret to its output form</param> + /// <returns>A <see cref="IAsyncLazy{T}"/> </returns> + /// <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)); + + //standard secret transformer + static async Task<TResult> Run(Task<ISecretResult> tr, Func<ISecretResult, TResult> transformer) + { + using ISecretResult res = await tr.ConfigureAwait(false); + return res == null ? default : transformer(res); + } + + return Run(result, transformer).AsLazy(); + } + + /// <summary> + /// Converts a <see cref="SecretResult"/> async operation to a lazy result that can be awaited, that transforms the result + /// to your desired type. If the result is null, the default value of <typeparamref name="TResult"/> is returned + /// </summary> + /// <typeparam name="TResult"></typeparam> + /// <param name="result"></param> + /// <param name="transformer">Your function to transform the secret to its output form</param> + /// <returns>A <see cref="IAsyncLazy{T}"/> </returns> + /// <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)); + + //Transform with task transformer + static async Task<TResult> Run(Task<ISecretResult?> tr, Func<ISecretResult, Task<TResult>> transformer) + { + using ISecretResult res = await tr.ConfigureAwait(false); + return res == null ? default : await transformer(res).ConfigureAwait(false); + } + + return Run(result, transformer).AsLazy(); + } } } |