using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Server.Security; namespace ZB.MOM.WW.OtOpcUa.Server.Tests; /// /// Deterministic guards for Active Directory compatibility of the internal helpers /// relies on. We can't live-bind against AD in unit /// tests — instead, we pin the behaviors AD depends on (DN-parsing of AD-style /// memberOf values, filter escaping with case-preserving RDN extraction) so a /// future refactor can't silently break the AD path while the GLAuth live-smoke stays /// green. /// [Trait("Category", "Unit")] public sealed class LdapUserAuthenticatorAdCompatTests { [Fact] public void ExtractFirstRdnValue_parses_AD_memberOf_group_name_from_CN_dn() { // AD's memberOf values use uppercase CN=… and full domain paths. The extractor // returns the first RDN's value regardless of attribute-type case, so operators' // GroupToRole keys stay readable ("OPCUA-Operators" not "CN=OPCUA-Operators,..."). var dn = "CN=OPCUA-Operators,OU=OPC UA Security Groups,OU=Groups,DC=corp,DC=example,DC=com"; LdapUserAuthenticator.ExtractFirstRdnValue(dn).ShouldBe("OPCUA-Operators"); } [Fact] public void ExtractFirstRdnValue_handles_mixed_case_and_spaces_in_group_name() { var dn = "CN=Domain Users,CN=Users,DC=corp,DC=example,DC=com"; LdapUserAuthenticator.ExtractFirstRdnValue(dn).ShouldBe("Domain Users"); } [Fact] public void ExtractFirstRdnValue_also_works_for_OpenLDAP_ou_style_memberOf() { // GLAuth + some OpenLDAP deployments expose memberOf as ou=,ou=groups,... // The authenticator needs one extractor that tolerates both shapes since directories // in the field mix them depending on schema. var dn = "ou=WriteOperate,ou=groups,dc=lmxopcua,dc=local"; LdapUserAuthenticator.ExtractFirstRdnValue(dn).ShouldBe("WriteOperate"); } [Fact] public void EscapeLdapFilter_prevents_injection_via_samaccountname_lookup() { // AD login names can contain characters that are meaningful to LDAP filter syntax // (parens, backslashes). The authenticator builds filters as // ($"({UserNameAttribute}={EscapeLdapFilter(username)})") so injection attempts must // not break out of the filter. The RFC 4515 escape set is: \ → \5c, * → \2a, ( → \28, // ) → \29, \0 → \00. LdapUserAuthenticator.EscapeLdapFilter("admin)(cn=*") .ShouldBe("admin\\29\\28cn=\\2a"); LdapUserAuthenticator.EscapeLdapFilter("domain\\user") .ShouldBe("domain\\5cuser"); } [Fact] public void LdapOptions_default_UserNameAttribute_is_uid_for_rfc2307_compat() { // Regression guard: PR 31 introduced UserNameAttribute with a default of "uid" so // existing deployments (pre-AD config) keep working. Changing the default breaks // everyone's config silently; require an explicit review. new LdapOptions().UserNameAttribute.ShouldBe("uid"); } }