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);
///
/// Resolves what an anonymous session is allowed to do (#520). 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 #520: 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.
public sealed record OpcUaUserAuthResult(
bool Success,
string? DisplayName,
IReadOnlyList Roles,
string? Error)
{
/// Creates a successful authentication result with the specified display name and roles.
/// The display name for the authenticated user.
/// The roles to assign to the authenticated user.
/// A successful OpcUaUserAuthResult.
public static OpcUaUserAuthResult Allow(string displayName, IReadOnlyList roles) =>
new(true, displayName, roles, null);
/// 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."));
}