up
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.WebService.Security;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Validation helpers for <see cref="ScannerWebServiceOptions"/>.
|
||||
/// </summary>
|
||||
public static class ScannerWebServiceOptionsValidator
|
||||
{
|
||||
private static readonly HashSet<string> SupportedStorageDrivers = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"mongo"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> SupportedQueueDrivers = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"redis",
|
||||
"nats",
|
||||
"rabbitmq"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> SupportedArtifactDrivers = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"minio"
|
||||
};
|
||||
|
||||
public static void Validate(ScannerWebServiceOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
if (options.SchemaVersion <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Scanner configuration requires a positive schemaVersion.");
|
||||
}
|
||||
|
||||
options.Storage ??= new ScannerWebServiceOptions.StorageOptions();
|
||||
ValidateStorage(options.Storage);
|
||||
|
||||
options.Queue ??= new ScannerWebServiceOptions.QueueOptions();
|
||||
ValidateQueue(options.Queue);
|
||||
|
||||
options.ArtifactStore ??= new ScannerWebServiceOptions.ArtifactStoreOptions();
|
||||
ValidateArtifactStore(options.ArtifactStore);
|
||||
|
||||
options.Features ??= new ScannerWebServiceOptions.FeatureFlagOptions();
|
||||
options.Plugins ??= new ScannerWebServiceOptions.PluginOptions();
|
||||
options.Telemetry ??= new ScannerWebServiceOptions.TelemetryOptions();
|
||||
ValidateTelemetry(options.Telemetry);
|
||||
|
||||
options.Authority ??= new ScannerWebServiceOptions.AuthorityOptions();
|
||||
ValidateAuthority(options.Authority);
|
||||
|
||||
options.Signing ??= new ScannerWebServiceOptions.SigningOptions();
|
||||
ValidateSigning(options.Signing);
|
||||
|
||||
options.Api ??= new ScannerWebServiceOptions.ApiOptions();
|
||||
if (string.IsNullOrWhiteSpace(options.Api.BasePath))
|
||||
{
|
||||
throw new InvalidOperationException("API basePath must be configured.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateStorage(ScannerWebServiceOptions.StorageOptions storage)
|
||||
{
|
||||
if (!SupportedStorageDrivers.Contains(storage.Driver))
|
||||
{
|
||||
throw new InvalidOperationException($"Unsupported storage driver '{storage.Driver}'. Supported drivers: mongo.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(storage.Dsn))
|
||||
{
|
||||
throw new InvalidOperationException("Storage DSN must be configured.");
|
||||
}
|
||||
|
||||
if (storage.CommandTimeoutSeconds <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Storage commandTimeoutSeconds must be greater than zero.");
|
||||
}
|
||||
|
||||
if (storage.HealthCheckTimeoutSeconds <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Storage healthCheckTimeoutSeconds must be greater than zero.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateQueue(ScannerWebServiceOptions.QueueOptions queue)
|
||||
{
|
||||
if (!SupportedQueueDrivers.Contains(queue.Driver))
|
||||
{
|
||||
throw new InvalidOperationException($"Unsupported queue driver '{queue.Driver}'. Supported drivers: redis, nats, rabbitmq.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(queue.Dsn))
|
||||
{
|
||||
throw new InvalidOperationException("Queue DSN must be configured.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(queue.Namespace))
|
||||
{
|
||||
throw new InvalidOperationException("Queue namespace must be configured.");
|
||||
}
|
||||
|
||||
if (queue.VisibilityTimeoutSeconds <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Queue visibilityTimeoutSeconds must be greater than zero.");
|
||||
}
|
||||
|
||||
if (queue.LeaseHeartbeatSeconds <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Queue leaseHeartbeatSeconds must be greater than zero.");
|
||||
}
|
||||
|
||||
if (queue.MaxDeliveryAttempts <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Queue maxDeliveryAttempts must be greater than zero.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateArtifactStore(ScannerWebServiceOptions.ArtifactStoreOptions artifactStore)
|
||||
{
|
||||
if (!SupportedArtifactDrivers.Contains(artifactStore.Driver))
|
||||
{
|
||||
throw new InvalidOperationException($"Unsupported artifact store driver '{artifactStore.Driver}'. Supported drivers: minio.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(artifactStore.Endpoint))
|
||||
{
|
||||
throw new InvalidOperationException("Artifact store endpoint must be configured.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(artifactStore.Bucket))
|
||||
{
|
||||
throw new InvalidOperationException("Artifact store bucket must be configured.");
|
||||
}
|
||||
|
||||
if (artifactStore.EnableObjectLock && artifactStore.ObjectLockRetentionDays <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Artifact store objectLockRetentionDays must be greater than zero when object lock is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateTelemetry(ScannerWebServiceOptions.TelemetryOptions telemetry)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(telemetry.MinimumLogLevel))
|
||||
{
|
||||
throw new InvalidOperationException("Telemetry minimumLogLevel must be configured.");
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(telemetry.MinimumLogLevel, ignoreCase: true, out LogLevel _))
|
||||
{
|
||||
throw new InvalidOperationException($"Telemetry minimumLogLevel '{telemetry.MinimumLogLevel}' is invalid.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint) && !Uri.TryCreate(telemetry.OtlpEndpoint, UriKind.Absolute, out _))
|
||||
{
|
||||
throw new InvalidOperationException("Telemetry OTLP endpoint must be an absolute URI when specified.");
|
||||
}
|
||||
|
||||
foreach (var attribute in telemetry.ResourceAttributes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(attribute.Key))
|
||||
{
|
||||
throw new InvalidOperationException("Telemetry resource attribute keys must be non-empty.");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var header in telemetry.OtlpHeaders)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(header.Key))
|
||||
{
|
||||
throw new InvalidOperationException("Telemetry OTLP header keys must be non-empty.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateAuthority(ScannerWebServiceOptions.AuthorityOptions authority)
|
||||
{
|
||||
authority.Resilience ??= new ScannerWebServiceOptions.AuthorityOptions.ResilienceOptions();
|
||||
NormalizeList(authority.Audiences, toLower: false);
|
||||
NormalizeList(authority.RequiredScopes, toLower: true);
|
||||
NormalizeList(authority.BypassNetworks, toLower: false);
|
||||
NormalizeList(authority.ClientScopes, toLower: true);
|
||||
NormalizeResilience(authority.Resilience);
|
||||
|
||||
if (authority.RequiredScopes.Count == 0)
|
||||
{
|
||||
authority.RequiredScopes.Add(ScannerAuthorityScopes.ScansEnqueue);
|
||||
}
|
||||
|
||||
if (authority.ClientScopes.Count == 0)
|
||||
{
|
||||
foreach (var scope in authority.RequiredScopes)
|
||||
{
|
||||
authority.ClientScopes.Add(scope);
|
||||
}
|
||||
}
|
||||
|
||||
if (authority.BackchannelTimeoutSeconds <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Authority backchannelTimeoutSeconds must be greater than zero.");
|
||||
}
|
||||
|
||||
if (authority.TokenClockSkewSeconds < 0 || authority.TokenClockSkewSeconds > 300)
|
||||
{
|
||||
throw new InvalidOperationException("Authority tokenClockSkewSeconds must be between 0 and 300 seconds.");
|
||||
}
|
||||
|
||||
if (!authority.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authority.Issuer))
|
||||
{
|
||||
throw new InvalidOperationException("Authority issuer must be configured when authority is enabled.");
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(authority.Issuer, UriKind.Absolute, out var issuerUri))
|
||||
{
|
||||
throw new InvalidOperationException("Authority issuer must be an absolute URI.");
|
||||
}
|
||||
|
||||
if (authority.RequireHttpsMetadata && !issuerUri.IsLoopback && !string.Equals(issuerUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException("Authority issuer must use HTTPS when requireHttpsMetadata is enabled.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(authority.MetadataAddress) && !Uri.TryCreate(authority.MetadataAddress, UriKind.Absolute, out _))
|
||||
{
|
||||
throw new InvalidOperationException("Authority metadataAddress must be an absolute URI when specified.");
|
||||
}
|
||||
|
||||
if (authority.Audiences.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Authority audiences must include at least one entry when authority is enabled.");
|
||||
}
|
||||
|
||||
if (!authority.AllowAnonymousFallback)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(authority.ClientId))
|
||||
{
|
||||
throw new InvalidOperationException("Authority clientId must be configured when anonymous fallback is disabled.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authority.ClientSecret))
|
||||
{
|
||||
throw new InvalidOperationException("Authority clientSecret must be configured when anonymous fallback is disabled.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSigning(ScannerWebServiceOptions.SigningOptions signing)
|
||||
{
|
||||
if (signing.EnvelopeTtlSeconds <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Signing envelopeTtlSeconds must be greater than zero.");
|
||||
}
|
||||
|
||||
if (!signing.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(signing.KeyId))
|
||||
{
|
||||
throw new InvalidOperationException("Signing keyId must be configured when signing is enabled.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(signing.Algorithm))
|
||||
{
|
||||
throw new InvalidOperationException("Signing algorithm must be configured when signing is enabled.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(signing.KeyPem) && string.IsNullOrWhiteSpace(signing.KeyPemFile))
|
||||
{
|
||||
throw new InvalidOperationException("Signing requires keyPem or keyPemFile when enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void NormalizeList(IList<string> values, bool toLower)
|
||||
{
|
||||
if (values is null || values.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
for (var i = values.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var entry = values[i];
|
||||
if (string.IsNullOrWhiteSpace(entry))
|
||||
{
|
||||
values.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalized = toLower ? entry.Trim().ToLowerInvariant() : entry.Trim();
|
||||
if (!seen.Add(normalized))
|
||||
{
|
||||
values.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
values[i] = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
private static void NormalizeResilience(ScannerWebServiceOptions.AuthorityOptions.ResilienceOptions resilience)
|
||||
{
|
||||
if (resilience.RetryDelays is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var delay in resilience.RetryDelays.ToArray())
|
||||
{
|
||||
if (delay <= TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("Authority resilience retryDelays must be greater than zero.");
|
||||
}
|
||||
}
|
||||
|
||||
if (resilience.OfflineCacheTolerance.HasValue && resilience.OfflineCacheTolerance.Value < TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("Authority resilience offlineCacheTolerance must be greater than or equal to zero.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user