Active Directory compatibility. LdapOptions xml-doc expanded with a cheat-sheet covering Server (DC FQDN), Port 389 vs 636, UseTls=true under AD LDAP-signing enforcement, dedicated read-only service account DN, sAMAccountName vs userPrincipalName vs cn trade-offs, memberOf DN shape (CN=Group,OU=...,DC=... with the CN= RDN stripped to become the GroupToRole key), and the explicit 'nested groups NOT expanded' call-out (LDAP_MATCHING_RULE_IN_CHAIN / tokenGroups is a future authenticator enhancement, not a config change). docs/security.md §'Active Directory configuration' adds a complete appsettings.json snippet with realistic AD group names (OPCUA-Operators → WriteOperate, OPCUA-Engineers → WriteConfigure, OPCUA-AlarmAck → AlarmAck, OPCUA-Tuners → WriteTune), LDAPS port 636, TLS on, insecure-LDAP off, and operator-facing notes on each field. LdapUserAuthenticatorAdCompatTests (5 unit guards): ExtractFirstRdnValue parses AD-style 'CN=OPCUA-Operators,OU=...,DC=...' DNs correctly (case-preserving — operators' GroupToRole keys stay readable); also handles mixed case and spaces in group names ('Domain Users'); also works against the OpenLDAP ou=<group>,ou=groups shape (GLAuth) so one extractor tolerates both memberOf formats common in the field; EscapeLdapFilter escapes the RFC 4515 injection set (\, *, (, ), \0) so a malicious login like 'admin)(cn=*' can't break out of the filter; default UserNameAttribute regression guard.
Test posture — Server.Tests Unit: 43 pass / 0 fail (38 prior + 5 new AD-compat guards). Server.Tests LiveLdap category: 6 pass / 0 fail against running GLAuth (would skip cleanly without). Server build clean, 0 errors, 0 warnings.
Deferred: the session-identity end-to-end check (drive a full OPC UA UserName session, then read a 'whoami' node to verify the role landed on RoleBasedIdentity). That needs a test-only address-space node and is scoped for a separate PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
73 lines
4.8 KiB
C#
73 lines
4.8 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Server.Security;
|
|
|
|
/// <summary>
|
|
/// LDAP settings for the OPC UA server's UserName token validator. Bound from
|
|
/// <c>appsettings.json</c> <c>OpcUaServer:Ldap</c>. Defaults target the GLAuth dev instance
|
|
/// (localhost:3893, <c>dc=lmxopcua,dc=local</c>) for the stock inner-loop setup. Production
|
|
/// deployments are expected to point at Active Directory; see <see cref="UserNameAttribute"/>
|
|
/// and the per-field xml-docs for the AD-specific overrides.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para><b>Active Directory cheat-sheet</b>:</para>
|
|
/// <list type="bullet">
|
|
/// <item><see cref="Server"/>: one of the domain controllers, or the domain FQDN (will round-robin DCs).</item>
|
|
/// <item><see cref="Port"/>: <c>389</c> (LDAP) or <c>636</c> (LDAPS); use 636 + <see cref="UseTls"/> in production.</item>
|
|
/// <item><see cref="UseTls"/>: <c>true</c>. AD increasingly rejects plain-LDAP bind under LDAP-signing enforcement.</item>
|
|
/// <item><see cref="AllowInsecureLdap"/>: <c>false</c>. Dev escape hatch only.</item>
|
|
/// <item><see cref="SearchBase"/>: <c>DC=corp,DC=example,DC=com</c> — your domain's base DN.</item>
|
|
/// <item><see cref="ServiceAccountDn"/>: a dedicated service principal with read access to user + group entries
|
|
/// (e.g. <c>CN=OpcUaSvc,OU=Service Accounts,DC=corp,DC=example,DC=com</c>). Never a privileged admin.</item>
|
|
/// <item><see cref="UserNameAttribute"/>: <c>sAMAccountName</c> (classic login name) or <c>userPrincipalName</c>
|
|
/// (user@domain form). Default is <c>uid</c> which AD does <b>not</b> populate, so this override is required.</item>
|
|
/// <item><see cref="DisplayNameAttribute"/>: <c>displayName</c> gives the human name; <c>cn</c> works too but is less rich.</item>
|
|
/// <item><see cref="GroupAttribute"/>: <c>memberOf</c> — matches AD's default. Values are full DNs
|
|
/// (<c>CN=<Group>,OU=...,DC=...</c>); the authenticator strips the leading <c>CN=</c> RDN value and uses
|
|
/// that as the lookup key in <see cref="GroupToRole"/>.</item>
|
|
/// <item><see cref="GroupToRole"/>: maps your AD group common-names to OPC UA roles — e.g.
|
|
/// <c>{"OPCUA-Operators" : "WriteOperate", "OPCUA-Engineers" : "WriteConfigure"}</c>.</item>
|
|
/// </list>
|
|
/// <para>
|
|
/// Nested groups are <b>not</b> expanded — AD's <c>tokenGroups</c> / <c>LDAP_MATCHING_RULE_IN_CHAIN</c>
|
|
/// membership-chain filter isn't used. Assign users directly to the role-mapped groups, or pre-flatten
|
|
/// membership in your directory. If nested expansion becomes a requirement, it's an authenticator
|
|
/// enhancement (not a config change).
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class LdapOptions
|
|
{
|
|
public bool Enabled { get; init; } = false;
|
|
public string Server { get; init; } = "localhost";
|
|
public int Port { get; init; } = 3893;
|
|
public bool UseTls { get; init; } = false;
|
|
|
|
/// <summary>Dev-only escape hatch — must be false in production.</summary>
|
|
public bool AllowInsecureLdap { get; init; } = true;
|
|
|
|
public string SearchBase { get; init; } = "dc=lmxopcua,dc=local";
|
|
public string ServiceAccountDn { get; init; } = string.Empty;
|
|
public string ServiceAccountPassword { get; init; } = string.Empty;
|
|
public string DisplayNameAttribute { get; init; } = "cn";
|
|
public string GroupAttribute { get; init; } = "memberOf";
|
|
|
|
/// <summary>
|
|
/// LDAP attribute used to match a login name against user entries in the directory.
|
|
/// Defaults to <c>uid</c> (RFC 2307). Common overrides:
|
|
/// <list type="bullet">
|
|
/// <item><c>sAMAccountName</c> — Active Directory, classic NT-style login names (e.g. <c>jdoe</c>).</item>
|
|
/// <item><c>userPrincipalName</c> — Active Directory, email-style (e.g. <c>jdoe@corp.example.com</c>).</item>
|
|
/// <item><c>cn</c> — GLAuth + some OpenLDAP deployments where users are keyed by common-name.</item>
|
|
/// </list>
|
|
/// Used only when <see cref="ServiceAccountDn"/> is non-empty (search-then-bind path) —
|
|
/// direct-bind fallback constructs the DN as <c>cn=<name>,<SearchBase></c>
|
|
/// regardless of this setting and is not a production-grade path against AD.
|
|
/// </summary>
|
|
public string UserNameAttribute { get; init; } = "uid";
|
|
|
|
/// <summary>
|
|
/// LDAP group → OPC UA role. Each authenticated user gets every role whose source group
|
|
/// is in their membership list. Recognized role names (CLAUDE.md): <c>ReadOnly</c> (browse
|
|
/// + read), <c>WriteOperate</c>, <c>WriteTune</c>, <c>WriteConfigure</c>, <c>AlarmAck</c>.
|
|
/// </summary>
|
|
public Dictionary<string, string> GroupToRole { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
|
}
|