using System; using System.Collections.Generic; using System.Configuration; // For ConfigurationManager (Web.config) using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace SecretServerConfigProvider { // ================================================================================== // 1. Settings Provider Interface & Models // ================================================================================== /// /// Represents the settings required to connect to Secret Server and identify the secret to fetch. /// public class SecretServerConnectionSettings { public string BaseUrl { get; set; } public string RuleName { get; set; } public string RuleKey { get; set; } // Alternatively, use Username/Password for simple OAuth2 if Rule is not used public string Username { get; set; } public string Password { get; set; } /// /// The ID of the secret to fetch configuration values from. /// public int SecretId { get; set; } } /// /// Interface for providing Secret Server connection settings. /// This allows settings to come from Files, Databases, or Web.config. /// public interface ISecretServerSettingsProvider { SecretServerConnectionSettings GetSettings(); } // ================================================================================== // 2. Settings Provider Implementations // ================================================================================== public class FileSecretServerSettingsProvider : ISecretServerSettingsProvider { private readonly string _filePath; private readonly ILogger _logger; public FileSecretServerSettingsProvider(string filePath, ILogger logger = null) { _filePath = filePath; _logger = logger; } public SecretServerConnectionSettings GetSettings() { _logger?.LogInformation($"Loading Secret Server settings from file: {_filePath}"); // MOCK: In a real scenario, read and deserialize the JSON/XML file. if (!File.Exists(_filePath)) { _logger?.LogWarning("Settings file not found, returning default mock settings."); } return new SecretServerConnectionSettings { BaseUrl = "https://secrets.yourcompany.com", RuleName = "AppServerRule", RuleKey = "FileLoadedKey", SecretId = 1001 }; } } public class DatabaseSecretServerSettingsProvider : ISecretServerSettingsProvider { private readonly string _connectionString; private readonly ILogger _logger; public DatabaseSecretServerSettingsProvider(string connectionString, ILogger logger = null) { _connectionString = connectionString; _logger = logger; } public SecretServerConnectionSettings GetSettings() { _logger?.LogInformation("Loading Secret Server settings from Database."); // MOCK: In a real scenario, use ADO.NET/Entity Framework to fetch settings. // using (var conn = new SqlConnection(_connectionString)) { ... } return new SecretServerConnectionSettings { BaseUrl = "https://secrets.yourcompany.com", Username = "db_user", Password = "db_password", SecretId = 2002 }; } } public class WebConfigSecretServerSettingsProvider : ISecretServerSettingsProvider { private readonly ILogger _logger; public WebConfigSecretServerSettingsProvider(ILogger logger = null) { _logger = logger; } public SecretServerConnectionSettings GetSettings() { _logger?.LogInformation("Loading Secret Server settings from Web.config/App.config."); // Uses the legacy ConfigurationManager to read bootstrap settings return new SecretServerConnectionSettings { BaseUrl = ConfigurationManager.AppSettings["TssUrl"] ?? "https://default-url", RuleName = ConfigurationManager.AppSettings["TssRuleName"], RuleKey = ConfigurationManager.AppSettings["TssRuleKey"], SecretId = int.TryParse(ConfigurationManager.AppSettings["TssSecretId"], out int id) ? id : 0 }; } } // ================================================================================== // 3. Secret Server API Client (The Logic) // ================================================================================== /// /// Encapsulates the logic to authenticate and fetch a secret as a dictionary. /// Matches the requirement: "Use the api that fetches a dictionary of key value." /// public class SecretServerApiClient { private readonly ILogger _logger; private readonly HttpClient _httpClient; public SecretServerApiClient(ILogger logger = null) { _logger = logger; _httpClient = new HttpClient(); } /// /// Authenticates and fetches the specified secret, returning it as a flat dictionary. /// public IDictionary FetchSecretValues(SecretServerConnectionSettings settings) { try { // 1. Authenticate (Simplified OAuth2 Password Grant for demonstration) // In a real SDK usage, you might use 'RuleName' and 'RuleKey' specific endpoints. string token = GetAccessToken(settings); // 2. Fetch the Secret JSON string secretJson = GetSecretJson(settings.BaseUrl, settings.SecretId, token); // 3. Parse and convert to Dictionary return ParseSecretItemsToDictionary(secretJson); } catch (Exception ex) { _logger?.LogError(ex, "Failed to fetch configuration from Secret Server."); throw; } } private string GetAccessToken(SecretServerConnectionSettings settings) { _logger?.LogDebug($"Authenticating to {settings.BaseUrl}..."); // MOCK: Real implementation would POST to /oauth2/token // var content = new FormUrlEncodedContent(new[] // { // new KeyValuePair("username", settings.Username), // new KeyValuePair("password", settings.Password), // new KeyValuePair("grant_type", "password") // }); // var response = _httpClient.PostAsync($"{settings.BaseUrl}/oauth2/token", content).Result; // Return a mock token for this example return "mock_bearer_token_abc123"; } private string GetSecretJson(string baseUrl, int secretId, string token) { _logger?.LogDebug($"Fetching Secret ID {secretId}..."); // MOCK: Real implementation would GET /api/v1/secrets/{id} // _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // var response = _httpClient.GetAsync($"{baseUrl}/api/v1/secrets/{secretId}").Result; // return response.Content.ReadAsStringAsync().Result; // Returning a mock JSON response representing the Delinea Secret Server response structure return JsonConvert.SerializeObject(new { id = secretId, name = "ProductionDatabaseConfig", items = new[] { new { slug = "connection_string", itemValue = "Server=prod;Database=DB;User Id=sa;Password=secret;" }, new { slug = "api_key", itemValue = "12345-ABCDE-67890" }, new { slug = "max_retries", itemValue = "5" } } }); } private IDictionary ParseSecretItemsToDictionary(string json) { var result = new Dictionary(StringComparer.OrdinalIgnoreCase); JObject secretObj = JObject.Parse(json); JArray items = (JArray)secretObj["items"]; if (items != null) { foreach (var item in items) { // Delinea secrets have a "slug" (internal name) and "itemValue" string key = item["slug"]?.ToString(); string value = item["itemValue"]?.ToString(); if (!string.IsNullOrWhiteSpace(key)) { result[key] = value; } } } return result; } } // ================================================================================== // 4. Custom Configuration Source & Provider // ================================================================================== /// /// The Configuration Source that hooks into the .NET ConfigurationBuilder. /// public class SecretServerConfigurationSource : IConfigurationSource { private readonly ISecretServerSettingsProvider _settingsProvider; private readonly ILogger _logger; public SecretServerConfigurationSource(ISecretServerSettingsProvider settingsProvider, ILogger logger = null) { _settingsProvider = settingsProvider; _logger = logger; } public IConfigurationProvider Build(IConfigurationBuilder builder) { return new SecretServerConfigurationProvider(_settingsProvider, _logger); } } /// /// The Provider that actually loads the data. /// public class SecretServerConfigurationProvider : ConfigurationProvider { private readonly ISecretServerSettingsProvider _settingsProvider; private readonly ILogger _logger; private readonly SecretServerApiClient _apiClient; public SecretServerConfigurationProvider(ISecretServerSettingsProvider settingsProvider, ILogger logger = null) { _settingsProvider = settingsProvider; _logger = logger; _apiClient = new SecretServerApiClient(logger); } public override void Load() { try { _logger?.LogInformation("Started loading configuration from Secret Server..."); // 1. Get the Bootstrap settings (URL, Creds, SecretID) var connectionSettings = _settingsProvider.GetSettings(); if (connectionSettings == null || string.IsNullOrEmpty(connectionSettings.BaseUrl)) { _logger?.LogWarning("Secret Server connection settings are missing. Skipping source."); return; } // 2. Fetch the configuration data as a Dictionary // This uses the API helper to get the KVP dictionary directly IDictionary secretData = _apiClient.FetchSecretValues(connectionSettings); // 3. Populate the internal Data dictionary of the ConfigurationProvider foreach (var kvp in secretData) { // We handle key collision or prefixing here if necessary if (Data.ContainsKey(kvp.Key)) { Data[kvp.Key] = kvp.Value; } else { Data.Add(kvp.Key, kvp.Value); } } _logger?.LogInformation("Successfully loaded configuration from Secret Server."); } catch (Exception ex) { _logger?.LogError(ex, "Critical error loading config from Secret Server."); // In a production environment, you might decide whether to throw or swallow this exception // depending on whether the app can start without these secrets. throw; } } } // ================================================================================== // 5. Usage Example (Extensions) // ================================================================================== public static class SecretServerConfigurationExtensions { /// /// Extension method to easily add Secret Server to the config builder. /// public static IConfigurationBuilder AddSecretServer( this IConfigurationBuilder builder, ISecretServerSettingsProvider settingsProvider, ILogger logger = null) { return builder.Add(new SecretServerConfigurationSource(settingsProvider, logger)); } } }