Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -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
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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