diff --git a/CLAUDE.md b/CLAUDE.md index 72e3783d..07c01e17 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -180,7 +180,7 @@ The server supports non-transparent warm/hot redundancy via the `Redundancy` sec The server uses LDAP-based user authentication via the `Security:Ldap` section in `appsettings.json`. When enabled, credentials are validated by LDAP bind against a GLAuth server, and LDAP group membership maps to OPC UA permissions: `ReadOnly` (browse/read), `WriteOperate` (write FreeAccess/Operate attributes), `WriteTune` (write Tune attributes), `WriteConfigure` (write Configure attributes), `AlarmAck` (alarm acknowledgment). `LdapOpcUaUserAuthenticator` (`src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/LdapOpcUaUserAuthenticator.cs`) implements `IOpcUaUserAuthenticator`, delegating the LDAP bind + group lookup to `OtOpcUaLdapAuthService` (`src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/OtOpcUaLdapAuthService.cs`, an `ILdapAuthService`). See `docs/security.md` for the full guide. -Dev/test LDAP is the **shared GLAuth** running on the Linux Docker host at `10.100.0.35:3893` (baseDN `dc=zb,dc=local`, plaintext/`Transport=None`). It is managed via `scadaproj/infra/glauth/` (source of truth + deploy runbook). Single bind account `cn=serviceaccount,dc=zb,dc=local` / `serviceaccount123`; all test users password `password`. The docker-dev compose binds this shared instance directly — `DevStubMode` is no longer used. The per-VM NSSM GLAuth at `C:\publish\glauth\` and the old base DNs `dc=lmxopcua,dc=local` / `dc=otopcua,dc=local` are obsolete. (The integration-test harness under `tests/.../Host.IntegrationTests/` uses a separate ephemeral bitnami/openldap on port 3894 for automated tests — that is distinct from the shared dev GLAuth.) +Dev/test LDAP is the **shared GLAuth** running on the Linux Docker host at `10.100.0.35:3893` (baseDN `dc=zb,dc=local`, plaintext/`Transport=None`). It is managed via `scadaproj/infra/glauth/` (source of truth + deploy runbook). Single bind account `cn=serviceaccount,dc=zb,dc=local` / `serviceaccount123`; all test users password `password`. The docker-dev compose binds this shared instance directly — `DevStubMode` is no longer used. The per-VM NSSM GLAuth at `C:\publish\glauth\` and the old base DNs `dc=lmxopcua,dc=local` / `dc=otopcua,dc=local` are obsolete. (The integration-test harness under `tests/.../Host.IntegrationTests/` runs its own ephemeral **GLAuth** container on port 3894 — seeded by `tests/.../Host.IntegrationTests/glauth/config.toml` with dev users alice/bob + the `cn=serviceaccount` bind account — for the opt-in `OTOPCUA_HARNESS_USE_LDAP=1` real-bind mode; distinct from the shared dev GLAuth on 3893. It replaced the retired `bitnami/openldap:2.6` in PR #451, 2026-07-15.) ## Library Preferences diff --git a/archreview/plans/INTEGRATION-SWEEP-STATUS.md b/archreview/plans/INTEGRATION-SWEEP-STATUS.md index 5398b8b1..abe26829 100644 --- a/archreview/plans/INTEGRATION-SWEEP-STATUS.md +++ b/archreview/plans/INTEGRATION-SWEEP-STATUS.md @@ -21,7 +21,7 @@ need sudo to create new ones — I deployed FOCAS to `~/otopcua-focas` on the ho | **OpcUaClient.Browser** | ✅ 1/1 | — | | **S7** | ✅ 3/3 (2 skip) | skips = outage tests needing `S7_TIMEOUT_OUTAGE_*` env | | **Modbus** (profile=standard) | ✅ 6/6 | — | -| **Host** | ⚠️ **105/117** (7F, 5 skip) | 7 heavy 2-node deploy/failover E2E time out (20–45 s) under amd64-emulated SQL; 5 LDAP-backed skip | +| **Host** | ⚠️ **105/117** (7F, 5 skip) | 7 heavy E2E time out under amd64-emulated SQL (OPEN); LDAP image swapped OpenLDAP→GLAuth 2026-07-15 (PR #451). The 5 skips are driver-fixture-gated E2E, not LDAP | | **TwinCAT** | ⏭️ 13 skip | no TC3 XAR fixture exists (expected clean-skip) | | **Modbus** (dl205 / mitsubishi / s7_1500 / exception_injection) | ✅ 16/13/10/17 (was 8F/5F/3F/8F) | **NOT drift** — fixture-cycling artifact, re-verified GREEN 2026-07-15 — see below | | **AbCip** | ✅ 10P/3skip (was 6F/1P/3skip) | static-init-order bug FIXED 2026-07-15 (`326b22a7`) — see below | @@ -38,9 +38,11 @@ and all driver **unit** suites + the live `/run` gates pass. These integration s and never run, so they'd bit-rotted in several distinct ways. **Follow-up progress (2026-07-15):** #1 AbCip (PR #446), #5 OpcUaServer (PR #447), #2 Modbus (PR #448 — false -alarm), #4 AbLegacy (PR #449 — fixture gap), #3 FOCAS (PR #450 — topology + mock-timer-endianness), and #7 -(fixture deploys) all closed. **6 of 7 done; every driver + server suite now verified GREEN.** Remaining: -**#6 Host only** — swap the gone `bitnami/openldap:2.6` image + relax deadlines for amd64-emulated SQL. +alarm), #4 AbLegacy (PR #449 — fixture gap), #3 FOCAS (PR #450 — topology + mock-timer), #7 (fixture deploys), +and the #6 **LDAP-image** leg (PR #451 — swapped OpenLDAP→GLAuth) all closed. **The only thing left is the #6 +amd64-emulated-SQL timing leg** (7 heavy E2E tests time out under emulated `mssql:2022` on arm64 — run against +real SQL or raise deadlines; a local-emulation artifact, not a product defect). Every driver + server suite is +verified GREEN. --- @@ -105,14 +107,24 @@ alarm), #4 AbLegacy (PR #449 — fixture gap), #3 FOCAS (PR #450 — topology + `ServiceResultException(BadNodeIdUnknown)` for a removed node rather than returning a bad `DataValue`; the two subscription-survival assertions now use `Should.ThrowAsync`. Suite **4F → 4/4 green**. -- **Host** — largely healthy (105/117). 7 failures are the heavy 2-node deploy/failover/reconnect E2E tests - (`DeployHappyPathTests`, `FailoverDuringDeployTests`, `DriverReconnectE2eTests`, - `FleetDiagnosticsRoundTripTests`, `EquipmentNamespaceMaterializationTests`, - `RoslynVirtualTagEvaluatorTests`) taking 20–45 s — likely **timing under the amd64-emulated SQL** on the - arm64 Mac (mssql:2022 has no arm64 image). 5 LDAP-backed tests skip because **`bitnami/openldap:2.6` is gone - from Bitnami's catalog** (manifest unknown — Bitnami deprecated their free image catalog in 2025). - **Fix:** replace the openldap image with a maintained one; run Host E2E against real (non-emulated) SQL or - raise deadlines. +- **Host** — largely healthy (105/117). Two independent issues: + - **LDAP image — ✅ FIXED 2026-07-15 (PR #451).** The opt-in real-LDAP mode (`OTOPCUA_HARNESS_USE_LDAP=1`) + used `bitnami/openldap:2.6`, gone since Bitnami deprecated their free catalog in 2025. **Replaced it with + GLAuth** — the same LDAP server the rest of the project uses (docker-dev + the live OPC UA data-plane auth, + both against the shared GLAuth on `:3893`), removing the lone OpenLDAP outlier. Added a harness + `glauth/config.toml` (base DN `dc=zb,dc=local`, search-capable `serviceaccount`, dev users alice=full / + bob=read-only, role groups with the same gidnumbers as `scadaproj/infra/glauth`) + repointed the harness's + service-account override to `cn=serviceaccount`. Live-verified all three bind legs against a deployed + container on `:3894` (serviceaccount search returns alice + 5 memberOf groups; alice/bob bind OK; wrong + password → `Invalid credentials (49)`). NB: the real-LDAP mode is **opt-in** — the default Host run uses + `StubLdapAuthService`, so this never blocked the default suite (the sweep's "5 LDAP skips" were actually the + driver-fixture-gated E2E tests, not LDAP). + - **amd64-emulated-SQL timing (still open).** 7 heavy 2-node deploy/failover/reconnect E2E tests + (`DeployHappyPathTests`, `FailoverDuringDeployTests`, `DriverReconnectE2eTests`, + `FleetDiagnosticsRoundTripTests`, `EquipmentNamespaceMaterializationTests`, `RoslynVirtualTagEvaluatorTests`) + take 20–45 s and time out under the amd64-emulated `mssql:2022` (no arm64 image) on the arm64 Mac. **Fix:** + run Host E2E against real (non-emulated) SQL — e.g. the shared SQL on the Linux Docker host — or raise the + per-test deadlines. Not a product defect; a local-emulation timing artifact. - **TwinCAT** — 13 skip cleanly (no TC3 XAR fixture exists; unit-only per the driver's design). Expected. @@ -144,7 +156,11 @@ alarm), #4 AbLegacy (PR #449 — fixture gap), #3 FOCAS (PR #450 — topology + `SubscriptionSurvivalTests`. That unmasked a **second** drift: the 1.5.378 node-cache read path *throws* `ServiceResultException(BadNodeIdUnknown)` for a removed node instead of returning a bad `DataValue` — updated the two subscription-survival assertions to `Should.ThrowAsync`. Suite **4F → 4/4**. -6. **Host** — replace `bitnami/openldap:2.6` (image gone); run E2E on real SQL or raise deadlines. +6. **Host** — ⬐ split into two: + - ~~replace `bitnami/openldap:2.6` (image gone)~~ ✅ **DONE 2026-07-15 (PR #451)** — swapped the harness LDAP + to **GLAuth** (unifies on the LDAP the rest of the project uses); live-verified the binds on `:3894`. + - **[OPEN] amd64-emulated-SQL timing** — the 7 heavy E2E tests time out under emulated `mssql:2022` on arm64. + Run them against real (non-emulated) SQL or raise deadlines. Local-emulation artifact, not a product defect. 7. ~~**Fixture deployment gap** — FOCAS + AbLegacy fixtures aren't on the host.~~ ✅ **RESOLVED 2026-07-15.** Both now deployed under the home dir: `~/otopcua-ablegacy` (PR #449) + `~/otopcua-focas` (PR #450). (`/opt` still needs sudo; the home-dir deploys work fine.) @@ -174,7 +190,7 @@ Run per suite (env → project): | AbCip | `AB_SERVER_ENDPOINT=10.100.0.35:44818` `AB_SERVER_PROFILE=controllogix` | `tests/Drivers/…AbCip.IntegrationTests` | | AbLegacy | `AB_LEGACY_ENDPOINT=10.100.0.35:44818` `AB_LEGACY_COMPOSE_PROFILE=` (**must** match the running PCCC container — else the `[Theory]` runs all 3 families and 2 fail) | `tests/Drivers/…AbLegacy.IntegrationTests` | | FOCAS | `OTOPCUA_FOCAS_SIM_ENDPOINT=10.100.0.35:8193` (tests now honour this — no longer hardcoded to 127.0.0.1) | `tests/Drivers/…FOCAS.IntegrationTests` | -| Host | `OTOPCUA_HARNESS_USE_SQL=1` (+ SQL `:14331` via `tests/Server/…Host.IntegrationTests/docker-compose.yml up sql`) `OTOPCUA_HARNESS_USE_LDAP=0` | `tests/Server/…Host.IntegrationTests` | +| Host | `OTOPCUA_HARNESS_USE_SQL=1` (+ SQL `:14331` via the harness `docker-compose.yml up sql`) — set `OTOPCUA_HARNESS_USE_LDAP=1` to exercise real binds against the **GLAuth** service (`up ldap`, host `:3894`; users alice/bob, svc `cn=serviceaccount`/`serviceaccount123`) | `tests/Server/…Host.IntegrationTests` | **Modbus profile cycling:** the sim services share `:5020`, so to run a profile: `ssh … 'cd /opt/otopcua-modbus && docker compose down && docker compose --profile

up -d'`, then run with diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/TwoNodeClusterHarness.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/TwoNodeClusterHarness.cs index a4f120ad..93fe1e0b 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/TwoNodeClusterHarness.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/TwoNodeClusterHarness.cs @@ -38,7 +38,7 @@ namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests; /// database name (OtOpcUa_Harness_{guid}) created via EnsureCreated /// and dropped via EnsureDeleted on dispose. /// OTOPCUA_HARNESS_USE_LDAP=1 → drop the stub and point LdapAuthService -/// at OpenLDAP on localhost:3894. +/// at the GLAuth container on localhost:3894 (see Docker/glauth/config.toml). /// /// /// Why not WebApplicationFactory<Program>? Program.cs reads OTOPCUA_ROLES @@ -300,8 +300,10 @@ public sealed class TwoNodeClusterHarness : IAsyncDisposable configOverrides["Security:Ldap:Transport"] = "None"; configOverrides["Security:Ldap:AllowInsecure"] = "true"; configOverrides["Security:Ldap:SearchBase"] = "dc=zb,dc=local"; - configOverrides["Security:Ldap:ServiceAccountDn"] = "cn=admin,dc=zb,dc=local"; - configOverrides["Security:Ldap:ServiceAccountPassword"] = "ldapadmin"; + // GLAuth's search-capable bind account (see Docker/glauth/config.toml) — matches the + // shared GLAuth the data-plane binds against. Replaced the retired OpenLDAP cn=admin. + configOverrides["Security:Ldap:ServiceAccountDn"] = "cn=serviceaccount,dc=zb,dc=local"; + configOverrides["Security:Ldap:ServiceAccountPassword"] = "serviceaccount123"; } builder.Configuration.AddInMemoryCollection(configOverrides); diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/docker-compose.yml b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/docker-compose.yml index 463d03ff..5cff1dbc 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/docker-compose.yml +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/docker-compose.yml @@ -6,8 +6,8 @@ # # 1. EF behaviors that diverge between provider implementations — index uniqueness, # RowVersion concurrency, JSON column round-trips, EF migration application. -# 2. Real LDAP binds against an OpenLDAP server with the dev users from -# C:\publish\glauth\auth.md. +# 2. Real LDAP binds against a GLAuth server (./glauth/config.toml) with dev users +# alice (full access) / bob (read-only) and the cn=serviceaccount bind account. # # Activate by setting these env vars before running the suite: # @@ -43,15 +43,13 @@ services: retries: 20 ldap: - # OpenLDAP image — same one docker-dev/ uses, just on a different port. Dev users - # alice/bob match the GLAuth fixtures so AuthEndpoints contract tests share creds. - image: bitnami/openldap:2.6 - environment: - LDAP_ROOT: "dc=zb,dc=local" - LDAP_ADMIN_USERNAME: "admin" - LDAP_ADMIN_PASSWORD: "ldapadmin" - LDAP_USERS: "alice,bob" - LDAP_PASSWORDS: "alice123,bob123" - LDAP_USER_DC: "ou=FleetAdmin" + # GLAuth — the same LDAP server the rest of the project uses (docker-dev + the live + # OPC UA data-plane auth, both against the shared GLAuth on :3893). Replaced the retired + # bitnami/openldap:2.6 (Bitnami deprecated their free catalog in 2025). Seeded by + # ./glauth/config.toml with a search-capable serviceaccount + dev users alice/bob. + # Listens on :3893 in-container; mapped to host :3894 to run beside docker-dev's :3893. + image: glauth/glauth:latest + volumes: + - ./glauth/config.toml:/app/config/config.cfg:ro ports: - - "3894:1389" + - "3894:3893" diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/glauth/config.toml b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/glauth/config.toml new file mode 100644 index 00000000..d75ba23d --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/glauth/config.toml @@ -0,0 +1,78 @@ +# GLAuth config for the Host.IntegrationTests real-LDAP mode (OTOPCUA_HARNESS_USE_LDAP=1). +# +# Replaces the retired bitnami/openldap:2.6 image (Bitnami deprecated their free catalog +# in 2025) with GLAuth — the same LDAP server the rest of the project uses (docker-dev + +# the live OPC UA data-plane auth, both against the shared GLAuth on 10.100.0.35:3893). +# Unifying on GLAuth removes the lone OpenLDAP outlier and the gone-image breakage. +# +# Mirrors scadaproj/infra/glauth/config.toml (source of truth) but trimmed to the harness's +# needs: baseDN dc=zb,dc=local, a search-capable service account, and two dev users +# (alice = full-access, bob = read-only). Bind DN form is GLAuth's cn=,dc=zb,dc=local. +# +# Listens on :3893 inside the container; the compose maps host :3894 -> :3893 so this runs +# side-by-side with docker-dev's shared GLAuth (:3893) without a port clash. + +[ldap] + enabled = true + listen = "0.0.0.0:3893" + +[ldaps] + enabled = false + +[backend] + datastore = "config" + baseDN = "dc=zb,dc=local" + +[behaviors] + # Tests may bind bad passwords on purpose (outage / wrong-cred paths) — don't lock out. + LimitFailedBinds = false + +# ── Groups (gidnumbers match scadaproj/infra/glauth so GroupToRole maps identically) ── +[[groups]] + name = "ReadOnly" + gidnumber = 5601 +[[groups]] + name = "WriteOperate" + gidnumber = 5602 +[[groups]] + name = "WriteTune" + gidnumber = 5603 +[[groups]] + name = "WriteConfigure" + gidnumber = 5604 +[[groups]] + name = "AlarmAck" + gidnumber = 5605 + +# ── Users ───────────────────────────────────────────────────────────── +# Service account the harness binds first to search for the user entry. +# DN: cn=serviceaccount,dc=zb,dc=local password: serviceaccount123 +[[users]] + name = "serviceaccount" + uidnumber = 5999 + primarygroup = 5601 + passsha256 = "af29d0e5c9801ae98a999ed3915e1cf428a64b4b62b3cf221b6336cce0398419" + [[users.capabilities]] + action = "search" + object = "*" + +# alice — full access (member of every write/ack role group). password: alice123 +[[users]] + name = "alice" + givenname = "Alice" + sn = "Admin" + mail = "alice@zb.local" + uidnumber = 5010 + primarygroup = 5604 + othergroups = [5601, 5602, 5603, 5605] + passsha256 = "4e40e8ffe0ee32fa53e139147ed559229a5930f89c2204706fc174beb36210b3" + +# bob — read-only. password: bob123 +[[users]] + name = "bob" + givenname = "Bob" + sn = "Reader" + mail = "bob@zb.local" + uidnumber = 5011 + primarygroup = 5601 + passsha256 = "8d059c3640b97180dd2ee453e20d34ab0cb0f2eccbe87d01915a8e578a202b11"