Phase 3 PR 31 — Live-LDAP integration test + Active Directory compatibility #30

Merged
dohertj2 merged 1 commits from phase-3-pr31-live-ldap-ad-compat into v2 2026-04-18 15:27:55 -04:00
Owner

Closes LMX follow-up #4 with 6 live-bind tests against the dev GLAuth instance (skipped cleanly when unreachable) and makes the authenticator configurable for Active Directory along the way — normal deployment target for this project.

Live-bind coverage (LdapUserAuthenticatorLiveTests)

  • Valid credentials bind and surface DisplayName.
  • Wrong password fails.
  • Unknown user fails.
  • Empty credentials fail pre-flight (no directory round-trip).
  • writeop user's memberOf maps through GroupToRole to WriteOperate — the exact string WriteAuthzPolicy.IsAllowed expects.
  • admin user surfaces all four mapped roles (WriteOperate + WriteTune + WriteConfigure + AlarmAck), proving memberOf parsing doesn't stop after the first match.

All 6 pass against a running GLAuth at localhost:3893; skip cleanly with an operator-facing message when the port is unreachable.

Active Directory compatibility

Wiring the live-smoke surfaced a real gap: the authenticator's hard-coded (uid=<name>) filter didn't match GLAuth (keys users by cn), and wouldn't match AD either (keys users by sAMAccountName). Fix: new LdapOptions.UserNameAttribute (default uid for RFC 2307 backcompat) drives the filter. Regression guard in LdapUserAuthenticatorAdCompatTests pins the default so a future 'helpful' rename can't silently break anyone.

Also:

  • LdapOptions xml-doc expanded with an AD cheat-sheet covering DC FQDN, LDAPS port 636 under LDAP-signing enforcement, dedicated read-only service account, sAMAccountName vs userPrincipalName vs cn trade-offs, AD memberOf DN shape (CN=Group,OU=...,DC=... with the CN= RDN stripped for GroupToRole lookup), and an explicit 'nested groups NOT expanded' call-out (LDAP_MATCHING_RULE_IN_CHAIN / tokenGroups is a future enhancement, not a config change).
  • docs/security.md §"Active Directory configuration" adds a complete AD appsettings.json snippet with realistic group names (OPCUA-OperatorsWriteOperate, etc.), LDAPS on, AllowInsecureLdap: false, and operator-facing notes on each field.
  • LdapUserAuthenticatorAdCompatTests (5 deterministic 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) — verifies a 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 Category=LiveLdap: 6 pass / 0 fail against running GLAuth (skips cleanly without).
  • Server build clean — 0 errors, 0 warnings.

Deferred

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.

Closes LMX follow-up #4 with 6 live-bind tests against the dev GLAuth instance (skipped cleanly when unreachable) and makes the authenticator configurable for Active Directory along the way — normal deployment target for this project. ## Live-bind coverage (`LdapUserAuthenticatorLiveTests`) - Valid credentials bind and surface DisplayName. - Wrong password fails. - Unknown user fails. - Empty credentials fail pre-flight (no directory round-trip). - `writeop` user's `memberOf` maps through `GroupToRole` to `WriteOperate` — the exact string `WriteAuthzPolicy.IsAllowed` expects. - `admin` user surfaces all four mapped roles (WriteOperate + WriteTune + WriteConfigure + AlarmAck), proving `memberOf` parsing doesn't stop after the first match. All 6 pass against a running GLAuth at `localhost:3893`; skip cleanly with an operator-facing message when the port is unreachable. ## Active Directory compatibility Wiring the live-smoke surfaced a real gap: the authenticator's hard-coded `(uid=<name>)` filter didn't match GLAuth (keys users by `cn`), and wouldn't match AD either (keys users by `sAMAccountName`). Fix: new `LdapOptions.UserNameAttribute` (default `uid` for RFC 2307 backcompat) drives the filter. Regression guard in `LdapUserAuthenticatorAdCompatTests` pins the default so a future 'helpful' rename can't silently break anyone. Also: - `LdapOptions` xml-doc expanded with an AD cheat-sheet covering DC FQDN, LDAPS port 636 under LDAP-signing enforcement, dedicated read-only service account, `sAMAccountName` vs `userPrincipalName` vs `cn` trade-offs, AD `memberOf` DN shape (`CN=Group,OU=...,DC=...` with the `CN=` RDN stripped for `GroupToRole` lookup), and an explicit 'nested groups NOT expanded' call-out (`LDAP_MATCHING_RULE_IN_CHAIN` / `tokenGroups` is a future enhancement, not a config change). - `docs/security.md` §"Active Directory configuration" adds a complete AD `appsettings.json` snippet with realistic group names (`OPCUA-Operators` → `WriteOperate`, etc.), LDAPS on, `AllowInsecureLdap: false`, and operator-facing notes on each field. - `LdapUserAuthenticatorAdCompatTests` (5 deterministic 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`) — verifies a 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 `Category=LiveLdap`: **6 pass / 0 fail** against running GLAuth (skips cleanly without). - Server build clean — 0 errors, 0 warnings. ## Deferred 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.
dohertj2 added 1 commit 2026-04-18 15:27:53 -04:00
Phase 3 PR 31 — Live-LDAP integration test + Active Directory compatibility. Closes LMX follow-up #4 with 6 live-bind tests in Server.Tests/LdapUserAuthenticatorLiveTests.cs against the dev GLAuth instance at localhost:3893 (skipped cleanly when unreachable via Assert.Skip + a clear SkipReason — matches the GalaxyRepositoryLiveSmokeTests pattern). Coverage: valid credentials bind + surface DisplayName; wrong password fails; unknown user fails; empty credentials fail pre-flight without touching the directory; writeop user's memberOf maps through GroupToRole to WriteOperate (the exact string WriteAuthzPolicy.IsAllowed expects); admin user surfaces all four mapped roles (WriteOperate + WriteTune + WriteConfigure + AlarmAck) proving memberOf parsing doesn't stop after the first match. While wiring this up, the authenticator's hard-coded user-lookup filter 'uid=<name>' didn't match GLAuth (which keys users by cn and doesn't populate uid) — AND it doesn't match Active Directory either, which uses sAMAccountName. Added UserNameAttribute to LdapOptions (default 'uid' for RFC 2307 backcompat) so deployments override to 'cn' / 'sAMAccountName' / 'userPrincipalName' as the directory requires; authenticator filter now interpolates the configured attribute. The default stays 'uid' so existing test fixtures and OpenLDAP installs keep working without a config change — a regression guard in LdapUserAuthenticatorAdCompatTests.LdapOptions_default_UserNameAttribute_is_uid_for_rfc2307_compat pins this so a future 'helpful' default change can't silently break anyone. 4886a5783f
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>
dohertj2 merged commit 5d5e1f9650 into v2 2026-04-18 15:27:55 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#30