using ZB.MOM.WW.OtOpcUa.Core.Authorization; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Security; /// /// Validates OPC UA UserName tokens. The SDK already decrypts the token (using the server /// application cert) and hands the cleartext username + password to this seam. Implementations /// decide whether the credentials are valid and what roles to attach for downstream ACL checks. /// /// Production implementation lives in the Host project (wraps ILdapAuthService); the /// default rejects every attempt so misconfigured /// dev nodes don't silently accept credentials. /// public interface IOpcUaUserAuthenticator { /// /// Resolves cleartext UserName credentials against the configured backing store. Must not /// throw — callers turn results into ImpersonateEventArgs.IdentityValidationError /// reject codes, and a thrown exception escapes into the OPC UA SDK's session-activation /// path where it surfaces as a generic BadInternalError. /// /// The username to authenticate. /// The cleartext password to authenticate. /// The cancellation token. /// An OpcUaUserAuthResult indicating success or failure. Task AuthenticateUserNameAsync(string username, string password, CancellationToken ct); /// /// with the activating client's /// remote endpoint (lmxopcua#751), so an implementation can account pre-authentication failures per /// client rather than process-wide. This is the overload the server calls; the default ignores the /// endpoint and delegates, so an implementation that does not need it is unchanged. /// /// /// The endpoint uses the SDK's public transport peer address during activation and is null when unknown — an /// implementation must treat null as "no per-client information", never as a distinct client. Same /// must-not-throw contract as the three-argument overload. /// /// The username to authenticate. /// The cleartext password to authenticate. /// The client's IP endpoint, or null when it could not be resolved. /// The cancellation token. /// An OpcUaUserAuthResult indicating success or failure. Task AuthenticateUserNameAsync( string username, string password, System.Net.IPEndPoint? remoteEndpoint, CancellationToken ct) => AuthenticateUserNameAsync(username, password, ct); /// /// Resolves what an anonymous session is allowed to do. Anonymous is a normal /// principal — — not a special case, so it can be granted /// read or write through ordinary ACL rows. /// /// /// /// This is asked only AFTER the host has decided anonymous sessions are admissible at all /// (OpcUaApplicationHostOptions.AllowAnonymous). Admission is the host's decision; /// authority is this seam's. /// /// /// The default implementation admits anonymous with no roles, which is exactly the /// behaviour before per-key ACLs: an anonymous session connects, and every role-gated operation /// (write, alarm ack) denies. Implementors that can resolve grants override it; the rest /// keep the safe status quo rather than being forced to reimplement it. /// /// /// The cancellation token. /// The roles to attach to the anonymous session, or a denial. Task AuthenticateAnonymousAsync(CancellationToken ct) => Task.FromResult(OpcUaUserAuthResult.Allow(OpcUaPrincipals.Anonymous, Array.Empty())); } /// Outcome of a UserName authentication attempt. populates the session identity's role set. /// /// /// ⚠️ is not the principal, and assuming it was is a live trap. /// The authenticator passes the principal as the display name, but /// RoleCarryingUserIdentity derives UserIdentity.DisplayName from the presented /// token — which for a key-bearing client is the static username apikey, the same /// for every key in the fleet. So the principal was being dropped on the floor at the identity /// boundary, and the audit ClientUserId on a rejected write recorded apikey for /// everyone. carries it explicitly; nothing should infer it from a name. /// /// public sealed record OpcUaUserAuthResult( bool Success, string? DisplayName, IReadOnlyList Roles, string? Error) { /// /// The ACL principal the session authenticates as — the name NodeAcl rows are keyed on. /// Empty on a denial, and on a legacy authenticator that predates the principal model, which the /// ACL gate treats as unauthorized rather than guessing. /// public string Principal { get; init; } = string.Empty; /// /// Public id of the API key that authenticated, or null for anonymous. Carried so an open session /// can be re-checked against a later deployment that revoked the key. /// public string? ApiKeyId { get; init; } /// Creates a successful authentication result with the specified display name and roles. /// The display name for the authenticated user — also taken as the principal. /// The roles to assign to the authenticated user. /// A successful OpcUaUserAuthResult. public static OpcUaUserAuthResult Allow(string displayName, IReadOnlyList roles) => new(true, displayName, roles, null) { Principal = displayName ?? string.Empty }; /// Creates a successful result for a key-bearing client, carrying the key id for revocation re-checks. /// The ACL principal the key maps to. /// The interim server-wide roles. /// The public key id that authenticated. /// A successful OpcUaUserAuthResult. public static OpcUaUserAuthResult AllowKey(string principal, IReadOnlyList roles, string apiKeyId) => new(true, principal, roles, null) { Principal = principal, ApiKeyId = apiKeyId }; /// Creates a failed authentication result with the specified error message. /// The error message describing why authentication failed. /// A failed OpcUaUserAuthResult. public static OpcUaUserAuthResult Deny(string error) => new(false, null, Array.Empty(), error); } /// /// Default deny-all authenticator. Wired by OpcUaApplicationHost when no production /// authenticator is registered in DI — keeps the server safe-by-default rather than accepting /// arbitrary UserName credentials. Production Host DI overrides this with the LDAP adapter. /// public sealed class NullOpcUaUserAuthenticator : IOpcUaUserAuthenticator { public static readonly NullOpcUaUserAuthenticator Instance = new(); private NullOpcUaUserAuthenticator() { } /// public Task AuthenticateUserNameAsync(string username, string password, CancellationToken ct) => Task.FromResult(OpcUaUserAuthResult.Deny("No UserName authenticator is configured on this server.")); }