Files
lmxopcua/docs/v2/lmx-followups.md
Joseph Doherty 4886a5783f 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.
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>
2026-04-18 15:23:22 -04:00

5.8 KiB

LMX Galaxy bridge — remaining follow-ups

State after PR 19: the Galaxy driver is functionally at v1 parity through the IDriver abstraction; the OPC UA server runs with LDAP-authenticated Basic256Sha256 endpoints and alarms are observable through AlarmConditionState.ReportEvent. The items below are what remains LMX- specific before the stack can fully replace the v1 deployment, in rough priority order.

1. Proxy-side IHistoryProvider for ReadAtTime / ReadEvents

Status: Host-side IPC shipped (PR 10 + PR 11). Proxy consumer not written.

PR 10 added HistoryReadAtTimeRequest/Response on the IPC wire and MxAccessGalaxyBackend.HistoryReadAtTimeAsync delegates to HistorianDataSource.ReadAtTimeAsync. PR 11 did the same for events (HistoryReadEventsRequest/Response + GalaxyHistoricalEvent). The Proxy side (GalaxyProxyDriver) doesn't call those yet — Core.Abstractions.IHistoryProvider only exposes ReadRawAsync + ReadProcessedAsync.

To do:

  • Extend IHistoryProvider with ReadAtTimeAsync(string, DateTime[], …) and ReadEventsAsync(string?, DateTime, DateTime, int, …).
  • GalaxyProxyDriver calls the new IPC message kinds.
  • DriverNodeManager wires the new capability methods onto HistoryRead AtTime + Events service handlers.
  • Integration test: OPC UA client calls HistoryReadAtTime / HistoryReadEvents, value flows through IPC to the Host's HistorianDataSource, back to the client.

2. Write-gating by role — DONE (PR 26)

Landed in PR 26. WriteAuthzPolicy in Server/Security/ maps SecurityClassification → required role (FreeAccess → no role required, Operate/SecuredWriteWriteOperate, TuneWriteTune, Configure/VerifiedWriteWriteConfigure, ViewOnly → deny regardless). DriverNodeManager caches the classification per variable during discovery and checks the session's roles (via IRoleBearer) in OnWriteValue before calling IWritable.WriteAsync. Roles do not cascade — a session with WriteOperate can't write a Tune attribute unless it also carries WriteTune.

See feedback_acl_at_server_layer.md in memory for the architectural directive that authz stays at the server layer and never delegates to driver-specific auth.

3. Admin UI client-cert trust management — DONE (PR 28)

PR 28 shipped /certificates in the Admin UI. CertTrustService reads the OPC UA server's PKI store root (OpcUaServerOptions.PkiStoreRoot — default %ProgramData%\OtOpcUa\pki) and lists rejected + trusted certs by parsing the .der files directly, so it has no Opc.Ua dependency and runs on any Admin host that can reach the shared PKI directory.

Operator actions: Trust (moves rejected/certs/*.dertrusted/certs/*.der), Delete rejected, Revoke trust. The OPC UA stack re-reads the trusted store on each new client handshake, so no explicit reload signal is needed — operators retry the rejected client's connection after trusting.

Deferred: flipping AutoAcceptUntrustedClientCertificates to false as the deployment default. That's a production-hardening config change, not a code gap — the Admin UI is now ready to be the trust gate.

4. Live-LDAP integration test — DONE (PR 31)

PR 31 shipped Server.Tests/LdapUserAuthenticatorLiveTests.cs — 6 live-bind tests against the dev GLAuth instance at localhost:3893, skipped cleanly when the port is unreachable. Covers: valid bind, wrong password, unknown user, empty credentials, single-group → WriteOperate mapping, multi-group admin user surfacing all mapped roles.

Also added UserNameAttribute to LdapOptions (default uid for RFC 2307 compat) so Active Directory deployments can configure sAMAccountName / userPrincipalName without code changes. LdapUserAuthenticatorAdCompatTests (5 unit guards) pins the AD-shape DN parsing + filter escape behaviors. See docs/security.md §"Active Directory configuration" for the AD appsettings snippet.

Deferred: asserting session.Identity end-to-end on the server side (i.e. drive a full OPC UA session with username/password, then read an IHostConnectivityProbe-style "whoami" node to verify the role surfaced). That needs a test-only address-space node and is a separate PR.

5. Full Galaxy live-service smoke test against the merged v2 stack

Status: Individual pieces have live smoke tests (PR 5 MXAccess, PR 13 probe manager, PR 14 alarm tracker), but the full loop — OPC UA client → OtOpcUaServerGalaxyProxyDriver (in-process) → named-pipe to Galaxy.Host subprocess → live MXAccess runtime → real Galaxy objects — has no single end-to-end smoke test.

To do:

  • Test that spawns the full topology, discovers a deployed Galaxy object, subscribes to one of its attributes, writes a value back, and asserts the write round-tripped through MXAccess. Skip when ArchestrA isn't running.

6. Second driver instance on the same server

Status: DriverHost.RegisterAsync supports multiple drivers; the OPC UA server creates one DriverNodeManager per driver and isolates their subtrees under distinct namespace URIs. Not proven with two active GalaxyProxyDriver instances pointing at different Galaxies.

To do:

  • Integration test that registers two driver instances, each with a distinct DriverInstanceId + endpoint in its own session, asserts nodes from both appear under the correct subtrees, alarm events land on the correct instance's condition nodes.

7. Host-status per-AppEngine granularity → Admin UI dashboard

Status: PR 13 ships per-platform/per-AppEngine ScanState probing; PR 17 surfaces the resulting OnHostStatusChanged events through OPC UA. Admin UI doesn't render a per-host dashboard yet.

To do:

  • SignalR hub push of HostStatusChangedEventArgs to the Admin UI.
  • Dashboard page showing each tracked host, current state, last transition time, failure count.