// Copyright (C) 2025, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. #nullable enable using System; using System.Collections.Generic; using System.Linq; using Duplicati.Library.Interface; using Duplicati.Library.SecretProvider; namespace Duplicati.Library.DynamicLoader; public class SecretProviderLoader { /// /// Loader for secret providers /// private class SecretProviderLoaderSub : DynamicLoader { /// /// Gets the key for the secret provider /// /// The item to get the key for /// The key protected override string GetInterfaceKey(ISecretProvider item) => item.Key; /// /// Returns the subfolders searched for secret providers /// protected override string[] Subfolders => ["secretproviders"]; /// /// The built-in modules /// protected override IEnumerable BuiltInModules => SecretProviderModules.Modules; /// /// Gets the supported commands for a certain key /// /// The key to find commands for /// The supported commands or null if the key was not found public IReadOnlyList GetSupportedCommands(string key) { if (string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); LoadInterfaces(); lock (m_lock) { if (m_interfaces.TryGetValue(key, out var b) && b != null) return GetSupportedCommandsCached(b).ToList(); else return []; } } } /// /// The loader instance /// private static readonly Lazy _loader = new(() => new SecretProviderLoaderSub()); /// /// The secret provider modules /// public static ISecretProvider[] Modules => _loader.Value.Interfaces; /// /// The keys for the secret providers /// public static string[] Keys { get { return _loader.Value.Keys; } } /// /// Gets the supported commands for a certain key /// /// The key to find commands for /// The supported commands or null if the key was not found public static IReadOnlyList GetSupportedCommands(string key) => _loader.Value.GetSupportedCommands(key); /// /// Returns the metadata for a provider /// /// The key to get metadata for /// The key, description, and supported commands public static (string Key, string DisplayName, string Description, IReadOnlyList SupportedCommands) GetProviderMetadata(string key) { var provider = _loader.Value.Interfaces.FirstOrDefault(p => p.Key == key); if (provider == null) throw new ArgumentException($"No secret provider found for key {key}"); return (provider.Key, provider.DisplayName, provider.Description, provider.SupportedCommands.AsReadOnly()); } /// /// Creates an instance of a secret provider /// /// The configuration string /// The secret provider instance public static ISecretProvider CreateInstance(string config) { if (string.IsNullOrEmpty(config)) throw new ArgumentNullException(nameof(config)); // Translate from environment variables string? envName = null; if (config.StartsWith("$")) { envName = config[1..]; if (envName.StartsWith("{") && envName.EndsWith("}")) envName = envName[1..^1]; } else if (config.StartsWith("%") && config.EndsWith("%")) { envName = config[1..^1]; } if (envName != null) { var result = Environment.GetEnvironmentVariable(envName.ToUpperInvariant()); if (string.IsNullOrEmpty(result)) throw new ArgumentException($"The environment variable {envName} was not found"); config = result; } var uri = new Uri(config); var key = uri.Scheme; var providerType = Modules.FirstOrDefault(p => p.Key == key) ?? throw new ArgumentException($"No secret provider found for key {key}"); if (Activator.CreateInstance(providerType.GetType()) is not ISecretProvider provider) throw new InvalidOperationException($"Failed to create an instance of {providerType}"); return provider; } }