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));
}
}
}