using ZB.MOM.WW.Configuration; using ZB.MOM.WW.OtOpcUa.OpcUaServer; namespace ZB.MOM.WW.OtOpcUa.Host.Configuration; /// /// Fail-fast startup validator for , built on the /// shared ZB.MOM.WW.Configuration . The C# /// defaults are all valid, so a host with no explicit "OpcUa" section passes untouched; /// the validator exists to reject explicit prod/env overrides before the OPC UA SDK boots. /// Identity/transport essentials (ApplicationName, ApplicationUri, /// PublicHostname, PkiStoreRoot, OpcUaPort) must be present/valid and at /// least one security profile must be enabled. Optional fields — ApplicationConfigPath, /// PeerApplicationUris, AutoAcceptUntrustedClientCertificates, and /// ProductUri — are intentionally not validated. Failure messages carry the real /// "OpcUa:" section prefix matching the bound configuration section. /// public sealed class OpcUaApplicationHostOptionsValidator : OptionsValidatorBase { /// protected override void Validate(ValidationBuilder builder, OpcUaApplicationHostOptions o) { builder.Required(o.ApplicationName, "OpcUa:ApplicationName"); builder.Required(o.ApplicationUri, "OpcUa:ApplicationUri"); builder.Required(o.PublicHostname, "OpcUa:PublicHostname"); builder.Required(o.PkiStoreRoot, "OpcUa:PkiStoreRoot"); builder.Port(o.OpcUaPort, "OpcUa:OpcUaPort"); // EnabledSecurityProfiles is declared as IList — that interface does not derive from // IReadOnlyCollection, so it can't bind to MinCount's IReadOnlyCollection parameter // directly. ToList() bridges to the shared primitive while preserving the count (and message). builder.MinCount(o.EnabledSecurityProfiles?.ToList(), 1, "OpcUa:EnabledSecurityProfiles"); // #833 review: an enabled credential lockout needs positive durations — a zero LockoutSeconds would lock // nothing, a zero FailureWindowSeconds would never accumulate. MaxFailedAttempts <= 0 disables it. var lockout = o.CredentialLockout; builder.RequireThat(lockout is null || lockout.MaxFailedAttempts <= 0 || (lockout.LockoutSeconds > 0 && lockout.FailureWindowSeconds > 0 && lockout.SlotWaitSeconds > 0), "OpcUa:CredentialLockout:LockoutSeconds, OpcUa:CredentialLockout:FailureWindowSeconds and " + "OpcUa:CredentialLockout:SlotWaitSeconds must all be " + "greater than 0 while OpcUa:CredentialLockout:MaxFailedAttempts is greater than 0 (set " + "MaxFailedAttempts to 0 to disable the lockout)"); // #844: the per-client cap (which a success never resets) below the per-credential threshold would make every // per-credential lockout a client-wide one; 0 means 2 x MaxFailedAttempts. builder.RequireThat(lockout is null || lockout.MaxFailedAttempts <= 0 || lockout.MaxFailedAttemptsPerClient <= 0 || lockout.MaxFailedAttemptsPerClient >= lockout.MaxFailedAttempts, "OpcUa:CredentialLockout:MaxFailedAttemptsPerClient must be 0 (= 2 x MaxFailedAttempts) or at least " + "OpcUa:CredentialLockout:MaxFailedAttempts"); } }