feat(opcua,host): F13c LDAP-bound UserName validator
Adds IOpcUaUserAuthenticator seam in OpcUaServer.Security with a deny-all NullOpcUaUserAuthenticator default. OpcUaApplicationHost subscribes to SessionManager.ImpersonateUser after _application.Start so UserName tokens flow through the authenticator and either attach a UserIdentity to the session (Allow) or set IdentityValidationError = BadIdentityTokenRejected (Deny / authenticator exception). Anonymous + X509 tokens fall through to SDK defaults. LdapOpcUaUserAuthenticator (Host project) bridges to the same ILdapAuthService that AddOtOpcUaAuth uses for Admin cookies / JWT, so a single LDAP source-of-truth governs both Admin control plane and OPC UA data plane. Program.cs registers LdapOptions + LdapAuthService + IOpcUaUserAuthenticator on driver-role hosts; admin-only nodes are unchanged. OtOpcUaServerHostedService threads the resolved authenticator into OpcUaApplicationHost so the seam respects Host DI. 10 new tests: 6 in OpcUaServer.Tests cover the pure HandleImpersonation static method (success / denial / anonymous fallthrough / authenticator- throw / null-username / Null authenticator); 4 in Host.IntegrationTests cover the LdapOpcUaUserAuthenticator adapter (LDAP allow → Allow with roles, LDAP deny → Deny, exception → backend-error denial, display-name fallback). OpcUaServer suite is 40 / 40 green. Closes #104. Unblocks Task 60 (dual-endpoint + ServiceLevel tests) once #81 residual lands.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>ILdapAuthService</c>); the
|
||||
/// <see cref="NullOpcUaUserAuthenticator"/> default rejects every attempt so misconfigured
|
||||
/// dev nodes don't silently accept credentials.
|
||||
/// </summary>
|
||||
public interface IOpcUaUserAuthenticator
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves cleartext UserName credentials against the configured backing store. Must not
|
||||
/// throw — callers turn results into <c>ImpersonateEventArgs.IdentityValidationError</c>
|
||||
/// reject codes, and a thrown exception escapes into the OPC UA SDK's session-activation
|
||||
/// path where it surfaces as a generic <c>BadInternalError</c>.
|
||||
/// </summary>
|
||||
Task<OpcUaUserAuthResult> AuthenticateUserNameAsync(string username, string password, CancellationToken ct);
|
||||
}
|
||||
|
||||
/// <summary>Outcome of a UserName authentication attempt. <see cref="Roles"/> populates the session identity's role set.</summary>
|
||||
public sealed record OpcUaUserAuthResult(
|
||||
bool Success,
|
||||
string? DisplayName,
|
||||
IReadOnlyList<string> Roles,
|
||||
string? Error)
|
||||
{
|
||||
public static OpcUaUserAuthResult Allow(string displayName, IReadOnlyList<string> roles) =>
|
||||
new(true, displayName, roles, null);
|
||||
|
||||
public static OpcUaUserAuthResult Deny(string error) =>
|
||||
new(false, null, Array.Empty<string>(), error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default deny-all authenticator. Wired by <c>OpcUaApplicationHost</c> 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.
|
||||
/// </summary>
|
||||
public sealed class NullOpcUaUserAuthenticator : IOpcUaUserAuthenticator
|
||||
{
|
||||
public static readonly NullOpcUaUserAuthenticator Instance = new();
|
||||
private NullOpcUaUserAuthenticator() { }
|
||||
|
||||
public Task<OpcUaUserAuthResult> AuthenticateUserNameAsync(string username, string password, CancellationToken ct) =>
|
||||
Task.FromResult(OpcUaUserAuthResult.Deny("No UserName authenticator is configured on this server."));
|
||||
}
|
||||
Reference in New Issue
Block a user