using ZB.MOM.WW.Configuration; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; namespace ZB.MOM.WW.ScadaBridge.AuditLog.Configuration; /// /// Validates on startup. The caps drive payload /// truncation in the M2+ writers, so an unset/zero cap would let arbitrarily /// large blobs into the central AuditLog table. /// must be at least as large as /// because the error cap is meant to capture more detail than the /// happy-path summary, not less. is /// bounded to [30, 3650] to keep purge windows sane: too short would /// drop in-flight investigations, too long would defeat the partition-switch /// purge's purpose. /// public sealed class AuditLogOptionsValidator : OptionsValidatorBase { /// Inclusive lower bound for . public const int MinRetentionDays = 30; /// Inclusive upper bound for . public const int MaxRetentionDays = 3650; /// Inclusive lower bound for (8 KiB). public const int MinInboundMaxBytes = 8_192; /// Inclusive upper bound for (16 MiB). public const int MaxInboundMaxBytes = 16_777_216; /// protected override void Validate(ValidationBuilder builder, AuditLogOptions options) { builder.RequireThat(options.DefaultCapBytes > 0, $"AuditLog:{nameof(AuditLogOptions.DefaultCapBytes)} ({options.DefaultCapBytes}) " + "must be > 0; it drives payload-summary truncation in audit writers."); builder.RequireThat(options.ErrorCapBytes >= options.DefaultCapBytes, $"AuditLog:{nameof(AuditLogOptions.ErrorCapBytes)} ({options.ErrorCapBytes}) " + $"must be >= {nameof(AuditLogOptions.DefaultCapBytes)} ({options.DefaultCapBytes}); " + "the error-row cap is intended to capture more detail than the happy-path summary."); // Valid when RetentionDays is within [Min, Max] inclusive. The De Morgan'd // guard !(below Min OR above Max) is equivalent to (>= Min AND <= Max). builder.RequireThat( !(options.RetentionDays < MinRetentionDays || options.RetentionDays > MaxRetentionDays), $"AuditLog:{nameof(AuditLogOptions.RetentionDays)} ({options.RetentionDays}) " + $"must be in [{MinRetentionDays}, {MaxRetentionDays}] days."); // Valid when InboundMaxBytes is within [Min, Max] inclusive. The De Morgan'd // guard !(below Min OR above Max) is equivalent to (>= Min AND <= Max). builder.RequireThat( !(options.InboundMaxBytes < MinInboundMaxBytes || options.InboundMaxBytes > MaxInboundMaxBytes), $"AuditLog:{nameof(AuditLogOptions.InboundMaxBytes)} ({options.InboundMaxBytes}) " + $"must be in [{MinInboundMaxBytes}, {MaxInboundMaxBytes}] bytes."); // M5.5 (T3): per-channel retention overrides. Each entry must be keyed by a // recognized AuditChannel name and carry a window in [MinRetentionDays, // RetentionDays] — i.e. SHORTER than or equal to the global window. A longer // per-channel window is meaningless under month-partition switch-out (governed // by the global window), so it is rejected rather than silently ignored. foreach (var (channelKey, days) in options.PerChannelRetentionDays) { builder.RequireThat( Enum.TryParse(channelKey, ignoreCase: false, out _), $"AuditLog:{nameof(AuditLogOptions.PerChannelRetentionDays)} key '{channelKey}' " + $"is not a recognized channel name. Valid keys: {string.Join(", ", Enum.GetNames())}."); // Valid when days is within [MinRetentionDays, RetentionDays] inclusive. // The lower bound matches the global RetentionDays floor; the upper bound // is the configured global window (longer is meaningless — see remarks). builder.RequireThat( !(days < MinRetentionDays || days > options.RetentionDays), $"AuditLog:{nameof(AuditLogOptions.PerChannelRetentionDays)}['{channelKey}'] ({days}) " + $"must be in [{MinRetentionDays}, {nameof(AuditLogOptions.RetentionDays)}={options.RetentionDays}] days " + "— a per-channel window must be shorter than or equal to the global retention window."); } } }