Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a configuration diagnostic emitted while analysing Authority plugin settings.
|
||||
/// </summary>
|
||||
public sealed record AuthorityConfigurationDiagnostic(
|
||||
string PluginName,
|
||||
AuthorityConfigurationDiagnosticSeverity Severity,
|
||||
string Message)
|
||||
{
|
||||
public string PluginName { get; init; } = PluginName ?? throw new ArgumentNullException(nameof(PluginName));
|
||||
|
||||
public AuthorityConfigurationDiagnosticSeverity Severity { get; init; } = Severity;
|
||||
|
||||
public string Message { get; init; } = Message ?? throw new ArgumentNullException(nameof(Message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Severity levels for configuration diagnostics.
|
||||
/// </summary>
|
||||
public enum AuthorityConfigurationDiagnosticSeverity
|
||||
{
|
||||
Info = 0,
|
||||
Warning = 1,
|
||||
Error = 2
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Analyses Authority plugin configurations for common security issues.
|
||||
/// </summary>
|
||||
public static class AuthorityPluginConfigurationAnalyzer
|
||||
{
|
||||
private const int BaselineMinimumLength = 12;
|
||||
private const bool BaselineRequireUppercase = true;
|
||||
private const bool BaselineRequireLowercase = true;
|
||||
private const bool BaselineRequireDigit = true;
|
||||
private const bool BaselineRequireSymbol = true;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates plugin contexts and returns diagnostics describing potential misconfigurations.
|
||||
/// </summary>
|
||||
/// <param name="contexts">Plugin contexts produced by <see cref="AuthorityPluginConfigurationLoader"/>.</param>
|
||||
/// <returns>Diagnostics describing any detected issues.</returns>
|
||||
public static IReadOnlyList<AuthorityConfigurationDiagnostic> Analyze(IEnumerable<AuthorityPluginContext> contexts)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(contexts);
|
||||
|
||||
var diagnostics = new List<AuthorityConfigurationDiagnostic>();
|
||||
|
||||
foreach (var context in contexts)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(context.Manifest.AssemblyName, "StellaOps.Authority.Plugin.Standard", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
AnalyzeStandardPlugin(context, diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
private static void AnalyzeStandardPlugin(AuthorityPluginContext context, ICollection<AuthorityConfigurationDiagnostic> diagnostics)
|
||||
{
|
||||
var section = context.Configuration.GetSection("passwordPolicy");
|
||||
if (!section.Exists())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int minLength = section.GetValue("minimumLength", BaselineMinimumLength);
|
||||
bool requireUppercase = section.GetValue("requireUppercase", BaselineRequireUppercase);
|
||||
bool requireLowercase = section.GetValue("requireLowercase", BaselineRequireLowercase);
|
||||
bool requireDigit = section.GetValue("requireDigit", BaselineRequireDigit);
|
||||
bool requireSymbol = section.GetValue("requireSymbol", BaselineRequireSymbol);
|
||||
|
||||
var deviations = new List<string>();
|
||||
|
||||
if (minLength < BaselineMinimumLength)
|
||||
{
|
||||
deviations.Add($"minimum length {minLength.ToString(CultureInfo.InvariantCulture)} < {BaselineMinimumLength}");
|
||||
}
|
||||
|
||||
if (!requireUppercase && BaselineRequireUppercase)
|
||||
{
|
||||
deviations.Add("uppercase requirement disabled");
|
||||
}
|
||||
|
||||
if (!requireLowercase && BaselineRequireLowercase)
|
||||
{
|
||||
deviations.Add("lowercase requirement disabled");
|
||||
}
|
||||
|
||||
if (!requireDigit && BaselineRequireDigit)
|
||||
{
|
||||
deviations.Add("digit requirement disabled");
|
||||
}
|
||||
|
||||
if (!requireSymbol && BaselineRequireSymbol)
|
||||
{
|
||||
deviations.Add("symbol requirement disabled");
|
||||
}
|
||||
|
||||
if (deviations.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var message = $"Password policy for plugin '{context.Manifest.Name}' weakens host defaults: {string.Join(", ", deviations)}.";
|
||||
diagnostics.Add(new AuthorityConfigurationDiagnostic(context.Manifest.Name, AuthorityConfigurationDiagnosticSeverity.Warning, message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NetEscapades.Configuration.Yaml;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Utility helpers for loading Authority plugin configuration manifests.
|
||||
/// </summary>
|
||||
public static class AuthorityPluginConfigurationLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads plugin configuration files based on the supplied Authority options.
|
||||
/// </summary>
|
||||
/// <param name="options">Authority configuration containing plugin descriptors.</param>
|
||||
/// <param name="basePath">Application base path used to resolve relative directories.</param>
|
||||
/// <param name="configureBuilder">Optional hook to customise per-plugin configuration builder.</param>
|
||||
public static IReadOnlyList<AuthorityPluginContext> Load(
|
||||
StellaOpsAuthorityOptions options,
|
||||
string basePath,
|
||||
Action<IConfigurationBuilder>? configureBuilder = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentNullException.ThrowIfNull(basePath);
|
||||
|
||||
var descriptorPairs = options.Plugins.Descriptors
|
||||
.OrderBy(static pair => pair.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
if (descriptorPairs.Length == 0)
|
||||
{
|
||||
return Array.Empty<AuthorityPluginContext>();
|
||||
}
|
||||
|
||||
var configurationDirectory = ResolveConfigurationDirectory(options.Plugins.ConfigurationDirectory, basePath);
|
||||
var contexts = new List<AuthorityPluginContext>(descriptorPairs.Length);
|
||||
|
||||
foreach (var (name, descriptor) in descriptorPairs)
|
||||
{
|
||||
var configPath = ResolveConfigPath(configurationDirectory, descriptor.ConfigFile);
|
||||
var optional = !descriptor.Enabled;
|
||||
|
||||
if (!optional && !File.Exists(configPath))
|
||||
{
|
||||
throw new FileNotFoundException($"Required Authority plugin configuration '{configPath}' was not found.", configPath);
|
||||
}
|
||||
|
||||
var builder = new ConfigurationBuilder();
|
||||
var builderBasePath = Path.GetDirectoryName(configPath);
|
||||
if (!string.IsNullOrEmpty(builderBasePath) && Directory.Exists(builderBasePath))
|
||||
{
|
||||
builder.SetBasePath(builderBasePath);
|
||||
}
|
||||
|
||||
configureBuilder?.Invoke(builder);
|
||||
builder.AddYamlFile(configPath, optional: optional, reloadOnChange: false);
|
||||
var configuration = builder.Build();
|
||||
|
||||
var manifest = descriptor.ToManifest(name, configPath);
|
||||
contexts.Add(new AuthorityPluginContext(manifest, configuration));
|
||||
}
|
||||
|
||||
return contexts;
|
||||
}
|
||||
|
||||
private static string ResolveConfigurationDirectory(string configurationDirectory, string basePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configurationDirectory))
|
||||
{
|
||||
return Path.GetFullPath(basePath);
|
||||
}
|
||||
|
||||
var directory = configurationDirectory;
|
||||
if (!Path.IsPathRooted(directory))
|
||||
{
|
||||
directory = Path.Combine(basePath, directory);
|
||||
}
|
||||
|
||||
return Path.GetFullPath(directory);
|
||||
}
|
||||
|
||||
private static string ResolveConfigPath(string configurationDirectory, string? configFile)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configFile))
|
||||
{
|
||||
throw new InvalidOperationException("Authority plugin descriptor must specify a configFile.");
|
||||
}
|
||||
|
||||
if (Path.IsPathRooted(configFile))
|
||||
{
|
||||
return Path.GetFullPath(configFile);
|
||||
}
|
||||
|
||||
return Path.GetFullPath(Path.Combine(configurationDirectory, configFile));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
public sealed class AuthoritySigningAdditionalKeyOptions
|
||||
{
|
||||
public string KeyId { get; set; } = string.Empty;
|
||||
|
||||
public string Path { get; set; } = string.Empty;
|
||||
|
||||
public string? Source { get; set; }
|
||||
|
||||
internal void Validate(string defaultSource)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(KeyId))
|
||||
{
|
||||
throw new InvalidOperationException("Additional signing keys require a keyId.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Path))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing key '{KeyId}' requires a path.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Source))
|
||||
{
|
||||
Source = defaultSource;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
public sealed class AuthoritySigningOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether signing is enabled for revocation exports.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Signing algorithm identifier (ES256 by default).
|
||||
/// </summary>
|
||||
public string Algorithm { get; set; } = SignatureAlgorithms.Es256;
|
||||
|
||||
/// <summary>
|
||||
/// Identifier for the signing key source (e.g. "file", "vault").
|
||||
/// </summary>
|
||||
public string KeySource { get; set; } = "file";
|
||||
|
||||
/// <summary>
|
||||
/// Active signing key identifier (kid).
|
||||
/// </summary>
|
||||
public string ActiveKeyId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Path to the private key material (PEM-encoded).
|
||||
/// </summary>
|
||||
public string KeyPath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Optional provider hint (default provider when null).
|
||||
/// </summary>
|
||||
public string? Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional passphrase protecting the private key (not yet supported).
|
||||
/// </summary>
|
||||
public string? KeyPassphrase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional signing keys retained for verification (previous rotations).
|
||||
/// </summary>
|
||||
public IList<AuthoritySigningAdditionalKeyOptions> AdditionalKeys { get; } = new List<AuthoritySigningAdditionalKeyOptions>();
|
||||
|
||||
internal void Validate()
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ActiveKeyId))
|
||||
{
|
||||
throw new InvalidOperationException("Authority signing configuration requires signing.activeKeyId.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(KeyPath))
|
||||
{
|
||||
throw new InvalidOperationException("Authority signing configuration requires signing.keyPath.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Algorithm))
|
||||
{
|
||||
Algorithm = SignatureAlgorithms.Es256;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(KeySource))
|
||||
{
|
||||
KeySource = "file";
|
||||
}
|
||||
|
||||
foreach (var key in AdditionalKeys)
|
||||
{
|
||||
key.Validate(KeySource);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="2.1.0" />
|
||||
<PackageReference Include="System.Threading.RateLimiting" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Authority.Plugins.Abstractions/StellaOps.Authority.Plugins.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Helper utilities for bootstrapping StellaOps Authority configuration.
|
||||
/// </summary>
|
||||
public static class StellaOpsAuthorityConfiguration
|
||||
{
|
||||
private static readonly string[] DefaultAuthorityYamlFiles =
|
||||
{
|
||||
"authority.yaml",
|
||||
"authority.local.yaml",
|
||||
"etc/authority.yaml",
|
||||
"etc/authority.local.yaml"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Builds <see cref="StellaOpsAuthorityOptions"/> using the shared configuration bootstrapper.
|
||||
/// </summary>
|
||||
/// <param name="configure">Optional hook to customise bootstrap behaviour.</param>
|
||||
public static StellaOpsConfigurationContext<StellaOpsAuthorityOptions> Build(
|
||||
Action<StellaOpsBootstrapOptions<StellaOpsAuthorityOptions>>? configure = null)
|
||||
{
|
||||
return StellaOpsConfigurationBootstrapper.Build<StellaOpsAuthorityOptions>(options =>
|
||||
{
|
||||
options.BindingSection ??= "Authority";
|
||||
options.EnvironmentPrefix ??= "STELLAOPS_AUTHORITY_";
|
||||
|
||||
configure?.Invoke(options);
|
||||
|
||||
AppendDefaultYamlFiles(options);
|
||||
|
||||
var previousPostBind = options.PostBind;
|
||||
options.PostBind = (authorityOptions, configuration) =>
|
||||
{
|
||||
previousPostBind?.Invoke(authorityOptions, configuration);
|
||||
authorityOptions.Validate();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static void AppendDefaultYamlFiles(StellaOpsBootstrapOptions<StellaOpsAuthorityOptions> options)
|
||||
{
|
||||
foreach (var path in DefaultAuthorityYamlFiles)
|
||||
{
|
||||
var alreadyPresent = options.YamlFiles.Any(file =>
|
||||
string.Equals(file.Path, path, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!alreadyPresent)
|
||||
{
|
||||
options.YamlFiles.Add(new YamlConfigurationFile(path, Optional: true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1005
src/__Libraries/StellaOps.Configuration/StellaOpsAuthorityOptions.cs
Normal file
1005
src/__Libraries/StellaOps.Configuration/StellaOpsAuthorityOptions.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
public sealed class StellaOpsBootstrapOptions<TOptions>
|
||||
where TOptions : class, new()
|
||||
{
|
||||
public StellaOpsBootstrapOptions()
|
||||
{
|
||||
ConfigurationOptions = new StellaOpsConfigurationOptions();
|
||||
}
|
||||
|
||||
internal StellaOpsConfigurationOptions ConfigurationOptions { get; }
|
||||
|
||||
public string? BasePath
|
||||
{
|
||||
get => ConfigurationOptions.BasePath;
|
||||
set => ConfigurationOptions.BasePath = value;
|
||||
}
|
||||
|
||||
public bool IncludeJsonFiles
|
||||
{
|
||||
get => ConfigurationOptions.IncludeJsonFiles;
|
||||
set => ConfigurationOptions.IncludeJsonFiles = value;
|
||||
}
|
||||
|
||||
public bool IncludeYamlFiles
|
||||
{
|
||||
get => ConfigurationOptions.IncludeYamlFiles;
|
||||
set => ConfigurationOptions.IncludeYamlFiles = value;
|
||||
}
|
||||
|
||||
public bool IncludeEnvironmentVariables
|
||||
{
|
||||
get => ConfigurationOptions.IncludeEnvironmentVariables;
|
||||
set => ConfigurationOptions.IncludeEnvironmentVariables = value;
|
||||
}
|
||||
|
||||
public string? EnvironmentPrefix
|
||||
{
|
||||
get => ConfigurationOptions.EnvironmentPrefix;
|
||||
set => ConfigurationOptions.EnvironmentPrefix = value;
|
||||
}
|
||||
|
||||
public IList<JsonConfigurationFile> JsonFiles => ConfigurationOptions.JsonFiles;
|
||||
|
||||
public IList<YamlConfigurationFile> YamlFiles => ConfigurationOptions.YamlFiles;
|
||||
|
||||
public string? BindingSection
|
||||
{
|
||||
get => ConfigurationOptions.BindingSection;
|
||||
set => ConfigurationOptions.BindingSection = value;
|
||||
}
|
||||
|
||||
public Action<IConfigurationBuilder>? ConfigureBuilder
|
||||
{
|
||||
get => ConfigurationOptions.ConfigureBuilder;
|
||||
set => ConfigurationOptions.ConfigureBuilder = value;
|
||||
}
|
||||
|
||||
public Action<TOptions, IConfiguration>? PostBind { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NetEscapades.Configuration.Yaml;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
public static class StellaOpsConfigurationBootstrapper
|
||||
{
|
||||
public static StellaOpsConfigurationContext<TOptions> Build<TOptions>(
|
||||
Action<StellaOpsBootstrapOptions<TOptions>>? configure = null)
|
||||
where TOptions : class, new()
|
||||
{
|
||||
var bootstrapOptions = new StellaOpsBootstrapOptions<TOptions>();
|
||||
configure?.Invoke(bootstrapOptions);
|
||||
|
||||
var configurationOptions = bootstrapOptions.ConfigurationOptions;
|
||||
var builder = new ConfigurationBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(configurationOptions.BasePath))
|
||||
{
|
||||
builder.SetBasePath(configurationOptions.BasePath!);
|
||||
}
|
||||
|
||||
if (configurationOptions.IncludeJsonFiles)
|
||||
{
|
||||
foreach (var file in configurationOptions.JsonFiles)
|
||||
{
|
||||
builder.AddJsonFile(file.Path, optional: file.Optional, reloadOnChange: file.ReloadOnChange);
|
||||
}
|
||||
}
|
||||
|
||||
if (configurationOptions.IncludeYamlFiles)
|
||||
{
|
||||
foreach (var file in configurationOptions.YamlFiles)
|
||||
{
|
||||
builder.AddYamlFile(file.Path, optional: file.Optional);
|
||||
}
|
||||
}
|
||||
|
||||
configurationOptions.ConfigureBuilder?.Invoke(builder);
|
||||
|
||||
if (configurationOptions.IncludeEnvironmentVariables)
|
||||
{
|
||||
builder.AddEnvironmentVariables(configurationOptions.EnvironmentPrefix);
|
||||
}
|
||||
|
||||
var configuration = builder.Build();
|
||||
|
||||
IConfiguration bindingSource;
|
||||
if (string.IsNullOrWhiteSpace(configurationOptions.BindingSection))
|
||||
{
|
||||
bindingSource = configuration;
|
||||
}
|
||||
else
|
||||
{
|
||||
bindingSource = configuration.GetSection(configurationOptions.BindingSection!);
|
||||
}
|
||||
|
||||
var options = new TOptions();
|
||||
bindingSource.Bind(options);
|
||||
|
||||
bootstrapOptions.PostBind?.Invoke(options, configuration);
|
||||
|
||||
return new StellaOpsConfigurationContext<TOptions>(configuration, options);
|
||||
}
|
||||
|
||||
public static IConfigurationBuilder AddStellaOpsDefaults(
|
||||
this IConfigurationBuilder builder,
|
||||
Action<StellaOpsConfigurationOptions>? configure = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
var options = new StellaOpsConfigurationOptions();
|
||||
configure?.Invoke(options);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.BasePath))
|
||||
{
|
||||
builder.SetBasePath(options.BasePath!);
|
||||
}
|
||||
|
||||
if (options.IncludeJsonFiles)
|
||||
{
|
||||
foreach (var file in options.JsonFiles)
|
||||
{
|
||||
builder.AddJsonFile(file.Path, optional: file.Optional, reloadOnChange: file.ReloadOnChange);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.IncludeYamlFiles)
|
||||
{
|
||||
foreach (var file in options.YamlFiles)
|
||||
{
|
||||
builder.AddYamlFile(file.Path, optional: file.Optional);
|
||||
}
|
||||
}
|
||||
|
||||
options.ConfigureBuilder?.Invoke(builder);
|
||||
|
||||
if (options.IncludeEnvironmentVariables)
|
||||
{
|
||||
builder.AddEnvironmentVariables(options.EnvironmentPrefix);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
public sealed class StellaOpsConfigurationContext<TOptions>
|
||||
where TOptions : class, new()
|
||||
{
|
||||
public StellaOpsConfigurationContext(IConfigurationRoot configuration, TOptions options)
|
||||
{
|
||||
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
public IConfigurationRoot Configuration { get; }
|
||||
|
||||
public TOptions Options { get; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Defines how default StellaOps configuration sources are composed.
|
||||
/// </summary>
|
||||
public sealed class StellaOpsConfigurationOptions
|
||||
{
|
||||
public string? BasePath { get; set; } = Directory.GetCurrentDirectory();
|
||||
|
||||
public bool IncludeJsonFiles { get; set; } = true;
|
||||
|
||||
public bool IncludeYamlFiles { get; set; } = true;
|
||||
|
||||
public bool IncludeEnvironmentVariables { get; set; } = true;
|
||||
|
||||
public string? EnvironmentPrefix { get; set; }
|
||||
|
||||
public IList<JsonConfigurationFile> JsonFiles { get; } = new List<JsonConfigurationFile>
|
||||
{
|
||||
new("appsettings.json", true, false),
|
||||
new("appsettings.local.json", true, false)
|
||||
};
|
||||
|
||||
public IList<YamlConfigurationFile> YamlFiles { get; } = new List<YamlConfigurationFile>
|
||||
{
|
||||
new("appsettings.yaml", true),
|
||||
new("appsettings.local.yaml", true)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Optional hook to register additional configuration sources (e.g. module-specific YAML files).
|
||||
/// </summary>
|
||||
public Action<IConfigurationBuilder>? ConfigureBuilder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional configuration section name used when binding strongly typed options.
|
||||
/// Null or empty indicates the root.
|
||||
/// </summary>
|
||||
public string? BindingSection { get; set; }
|
||||
}
|
||||
|
||||
public sealed record JsonConfigurationFile(string Path, bool Optional = true, bool ReloadOnChange = false);
|
||||
|
||||
public sealed record YamlConfigurationFile(string Path, bool Optional = true);
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
public static class StellaOpsOptionsBinder
|
||||
{
|
||||
public static TOptions BindOptions<TOptions>(
|
||||
this IConfiguration configuration,
|
||||
string? section = null,
|
||||
Action<TOptions, IConfiguration>? postConfigure = null)
|
||||
where TOptions : class, new()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
var options = new TOptions();
|
||||
var bindingSource = string.IsNullOrWhiteSpace(section)
|
||||
? configuration
|
||||
: configuration.GetSection(section);
|
||||
|
||||
bindingSource.Bind(options);
|
||||
postConfigure?.Invoke(options, configuration);
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user