using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Auth.Client; using StellaOps.Zastava.Core.Configuration; using StellaOps.Zastava.Core.Diagnostics; using StellaOps.Zastava.Core.Security; namespace Microsoft.Extensions.DependencyInjection; public static class ZastavaServiceCollectionExtensions { public static IServiceCollection AddZastavaRuntimeCore( this IServiceCollection services, IConfiguration configuration, string componentName) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configuration); if (string.IsNullOrWhiteSpace(componentName)) { throw new ArgumentException("Component name is required.", nameof(componentName)); } services.AddOptions() .Bind(configuration.GetSection(ZastavaRuntimeOptions.SectionName)) .ValidateDataAnnotations() .Validate(static options => !string.IsNullOrWhiteSpace(options.Tenant), "Tenant is required.") .Validate(static options => !string.IsNullOrWhiteSpace(options.Environment), "Environment is required.") .PostConfigure(options => { if (string.IsNullOrWhiteSpace(options.Component)) { options.Component = componentName; } }) .ValidateOnStart(); services.TryAddEnumerable(ServiceDescriptor.Singleton, ZastavaLoggerFactoryOptionsConfigurator>()); services.TryAddSingleton(); services.TryAddSingleton(); ConfigureAuthorityServices(services, configuration); services.TryAddSingleton(); return services; } private static void ConfigureAuthorityServices(IServiceCollection services, IConfiguration configuration) { var authoritySection = configuration.GetSection($"{ZastavaRuntimeOptions.SectionName}:authority"); var authorityOptions = new ZastavaAuthorityOptions(); authoritySection.Bind(authorityOptions); services.AddStellaOpsAuthClient(options => { options.Authority = authorityOptions.Issuer.ToString(); options.ClientId = authorityOptions.ClientId; options.ClientSecret = authorityOptions.ClientSecret; options.AllowOfflineCacheFallback = authorityOptions.AllowStaticTokenFallback; options.ExpirationSkew = TimeSpan.FromSeconds(Math.Clamp(authorityOptions.RefreshSkewSeconds, 0, 300)); options.DefaultScopes.Clear(); var normalized = new SortedSet(StringComparer.Ordinal); if (authorityOptions.Audience is not null) { foreach (var audience in authorityOptions.Audience) { if (string.IsNullOrWhiteSpace(audience)) { continue; } normalized.Add($"aud:{audience.Trim().ToLowerInvariant()}"); } } if (authorityOptions.Scopes is not null) { foreach (var scope in authorityOptions.Scopes) { if (!string.IsNullOrWhiteSpace(scope)) { normalized.Add(scope.Trim()); } } } foreach (var scope in normalized) { options.DefaultScopes.Add(scope); } }); } }