From b75f8b170482b15f2ed9cb63287789595043eb0b Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 20 Aug 2026 09:14:40 -0400 Subject: [PATCH 1/2] docs(archreview): cycle-3 remediation plan + tracker (batches A-G, model assignments) --- .../2026-08-19/remediation/00-tracking.md | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 archreview/2026-08-19/remediation/00-tracking.md diff --git a/archreview/2026-08-19/remediation/00-tracking.md b/archreview/2026-08-19/remediation/00-tracking.md new file mode 100644 index 0000000..1257225 --- /dev/null +++ b/archreview/2026-08-19/remediation/00-tracking.md @@ -0,0 +1,77 @@ +# MxAccessGateway — Remediation Plan & Tracking (2026-08-19 review) + +Source review: [`../00-overall.md`](../00-overall.md) (HEAD `8b3e3b3`, committed as `f4919ba`). Generated 2026-08-20. +Tracks the 34 cycle-3 IDs (GWC-31+, WRK-32+, IPC-33+, SEC-37+, CLI-46+, TST-31+) plus the tracker-housekeeping actions the overall asked for. Cycle-1/2 trackers remain the record for their own IDs. + +**Status legend:** `Not started` · `In progress` · `Done` · `Blocked (external)` · `Deferred (P2)` · `N/A`. + +## Execution model + +This plan is executed as **seven parallel batches**, each by one subagent working in an isolated git worktree on its own `fix/c3-` branch, merged back into `main` by the coordinator in the order listed (lowest conflict surface first). Every batch must: build the affected component(s) on macOS (`NonWindows.slnx` — the x86 worker cannot build here), run only the filtered tests it touches, update docs in the same commit, and cite finding IDs in commit messages (`fix(IPC-34): …`). Worker-side changes (batches E, F) are verified afterwards on windev via `windev-worker-ci.ps1 -Mode test`. + +**Model assignment rationale:** `opus` for batches that change concurrency/authorization logic or require reasoning across several interacting files with regression tests; `sonnet` for contained, well-specified edits with a clear recipe (version bumps, doc reconciliation, a two-line worker fix); `haiku` for none — every batch either touches code invariants or needs judgement about stale prose, where the cost of a wrong mechanical sweep exceeds the saving. + +| Batch | Branch | Findings | Model | Why this model | Status | +|---|---|---|---|---|---| +| A | `fix/c3-client-versions` | CLI-46 | sonnet | Mechanical five-client bump + README note + one script guard; recipe is fully specified | Not started | +| B | `fix/c3-glauth-doc` | TST-31 | sonnet | Reconcile a table against one TOML source of truth, re-derive one default, change an assert to a log | Not started | +| C | `fix/c3-tracker-hygiene` | GWC-31 + housekeeping (close GWC-16/IPC-08/IPC-31, refresh TST-06/TST-17, register WRK-31) | sonnet | Cross-file comment/ID sweep needs care about what each ID *means*, but no logic changes | Not started | +| D | `fix/c3-dashboard-acl` | SEC-40, SEC-41, GWC-38 | opus | Authorization semantics, fail-closed decisions, claims re-resolution; each needs a regression test | Not started | +| E | `fix/c3-ipc-boundaries` | IPC-33, IPC-39, IPC-34, IPC-35 | opus | Changes the WorkerClient command lifecycle, the options validator, and the worker drain budget; race-sensitive | Not started | +| F | `fix/c3-worker-residuals` | WRK-32, WRK-33 (+WRK-34 opportunistic) | sonnet | Small, precisely located worker edits + doc prose; cannot be built on macOS so the agent's job is a careful, minimal diff | Not started | +| G | `fix/c3-session-lifecycle` | GWC-32, GWC-34, GWC-37 | opus | Distributor/dispose lifecycle races and an alarm-monitor re-resolve path; needs lock-discipline reasoning | Not started | + +**Out of scope for this execution (recorded, not dropped):** + +| ID | Status | Why | +|---|---|---| +| TST-32 | Blocked (external) | Gitea runner registration-token reset is a web-UI action on the Gitea admin page; no repo change. Operator to-do. | +| SEC-37 | Blocked (external) | Family-wide: TLS on the shared GLAuth lives in `scadaproj/infra/glauth/` (separate repo + docker host). Plan separately; after it lands, move the deployed service to `Production`. | +| CLI-47/48/49/50, IPC-36, IPC-37, TST-33/34/35/36, GWC-33/35/36, SEC-38/39, IPC-38, WRK-34 | Deferred (P2) | Roadmap tier 10; WRK-34 may be picked up by batch F if trivial. | + +## Batch specifications + +### A — CLI-46 client release-train bump (sonnet) +- Bump .NET/Go/Python/Rust clients `0.2.0 → 0.2.1`, Java `0.2.1 → 0.2.2` in every version-bearing file (`pyproject.toml` + `version.py`, `Cargo.toml` workspace + any `version =` consumers, `Directory.Build.props`/csproj ``, Java `build.gradle`/`gradle.properties`, Go module is tag-driven — update any `Version` const if one exists). Follow `docs/ClientPackaging.md` for the authoritative file list. +- `clients/python/README.md` + `docs/ClientPackaging.md`: note that published pypi 0.2.0 has a broken `connect()` (event-loop binding, fixed by `a8f86b5`) and is superseded by 0.2.1. +- Add a PR-time check to `scripts/check-codegen.ps1` (or a new `scripts/check-client-versions.ps1` wired where the other checks run): fail if any `clients/` source differs from its last publish tag while the version constant still equals that tag's version. Document in `docs/ClientPackaging.md`. +- Verify: `dotnet build clients/dotnet/...slnx`; `cargo check --workspace`; `python -m pytest` (clients/python); `JAVA_HOME=/opt/homebrew/opt/openjdk@17 gradle test` then **revert** spurious generated-proto churn; `go build ./... && go test ./...`. + +### B — TST-31 glauth.md + live-test credential (sonnet) +- Reconcile `glauth.md` against `~/Desktop/scadaproj/infra/glauth/config.toml`: ReadOnly gid, the pre-provisioned users table (drop `readonly/readonly123`, fix `admin` password), the worked example at `:124`. Keep the OPC-UA taxonomy note. +- Re-derive `MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_PASSWORD` default in `docs/GatewayTesting.md` and the `?? "admin123"` literal in `WorkerLiveMxAccessSmokeTests.cs` from the config; make the test **log** (not assert) whether `AuthenticateUser` succeeded. +- Verify: `dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnx` (IntegrationTests compiles on mac? if excluded, note it). + +### C — GWC-31 + tracker housekeeping (sonnet) +- Sweep code comments citing unregistered `GWC-30`/`GWC-31` (perf pass) in `WorkerClient.cs`, `GatewayMetrics.cs`, `WorkerClientTests.cs`, worker `WorkerFrameReader/Writer.cs` to `PERF-nn` keyed to task numbers in `docs/plans/2026-08-15-perf-review-remediation.md`; add a PERF-ID table to that plan. Do not touch the genuine GWC-30 (frame-reader prefix) citations if any exist. +- `archreview/REVIEW-PROCESS.md` (or wherever the ID rule lives): state that GWC/WRK/IPC/SEC/CLI/TST prefixes are reserved for archreview registers. +- Cycle-2 tracker (`archreview/2026-07-12/remediation/00-tracking.md` + domain docs): GWC-16 → Won't fix (rationale from cycle-3 10-gateway-core.md), IPC-08 → Done (`7c1ea12`, residual IPC-34), IPC-31 → Done (superseded by GWC-28), refresh TST-06/TST-17 rows, register WRK-31 (interim ID burned by `e913dab`) so it is never reused. +- Verify: `dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnx`; `dotnet test --filter FullyQualifiedName~WorkerClientTests`. + +### D — SEC-40, SEC-41, GWC-38 (opus) +- SEC-41: `DashboardSessionAcl` re-resolves tags from `LdapGroupClaimType` claims via `DashboardGroupTagMapping.MapGroupsToTags` against current options (as `HubTokenService` does) instead of trusting login-stamped tag claims; keep stamped claims only as fallback when no group claims exist (document). Test: principal with stale tag claim + group whose grant was removed → denied. +- SEC-40: decide **tighten**: `AlarmsHub` gets a dedicated policy requiring a real role claim (anonymous-loopback/Disabled bypass excluded) — and document in `docs/GatewayDashboardDesign.md` ACL section + `docs/GatewayConfiguration.md` GroupToTag row that alarm metadata remains galaxy-wide for authenticated Viewers (central monitor, no per-session provenance). Test for the policy. +- GWC-38: `GatewayBrowseScopeProvider` throws `RpcException(PermissionDenied)` (and logs) when `Current` is null **and** API-key auth is enabled; unchanged when auth mode is Disabled. Test both branches. +- Verify: build Server; `dotnet test --filter "FullyQualifiedName~DashboardSessionAcl|FullyQualifiedName~AlarmsHub|FullyQualifiedName~BrowseScope"` (adjust to real class names). + +### E — IPC-33, IPC-39, IPC-34, IPC-35 (opus) +- IPC-33 option (a): `GatewayOptionsValidator` adds the upper coupling `Worker:MaxMessageBytes ≤ Protocol:MaxGrpcMessageBytes + 2×reserve`; worker `ResolveDrainReplyByteBudget` becomes `MaxMessageBytes − 2×DrainReplyFrameHeadroomBytes` (floor `MaxMessageBytes/2`); update `docs/GatewayConfiguration.md`, `docs/WorkerFrameProtocol.md`. Test: `GatewayOptionsTests` case for the new bound. +- IPC-39: pre-check compares against `MaxMessageBytes − MaxSequenceEncodingBytes (11)`; fix the comment direction. +- IPC-34: caller-cancellation arms call `TrySendCancelForAbandonedCommand` (renamed) gated on `removed`, skipped when `_stopCts` is the source; new `WorkerClientErrorCode.CallerCancelled` replacing the `GatewayShutdown` mislabel, mapped to `Cancelled` in the gRPC layer; update `docs/Grpc.md` mapping line, `gateway.md:913-919`, `docs/GatewayProcessDesign.md:596-602`. Test: twin of the timeout test driven by the caller token. +- IPC-35: `ReadHandshakeEnvelopeAsync` special-cases `WorkerFault` → `WorkerClientException(WorkerFaulted, CreateWorkerFaultMessage(...))`. Test: fake writes `WorkerFault{MxaccessCreationFailed}` instead of `WorkerReady`, category text surfaces. +- Verify: build Server; `dotnet test --filter "FullyQualifiedName~WorkerClientTests|FullyQualifiedName~GatewayOptions"`. Worker half → windev afterwards. + +### F — WRK-32, WRK-33 (sonnet) +- WRK-33: `ObserveFaultWhenever(ObserveCommandTaskAsync(commandTask));` at `WorkerPipeSession.cs:1235` (or catch-all-and-log in the observer). +- WRK-32: one `pumpStep()` before capturing the `CurrentVersion` baseline in `MxAccessCommandExecutor.cs`; rewrite the executor doc comment (`:996-1003`) to state the recency-based contract; add the same to `docs/MxAccessWorkerInstanceDesign.md` write-completion section and the reply-field comment in `mxaccess_gateway.proto` (**comment-only proto edit → regenerate all generated artifacts in the same commit**, per the repo rule; if regen tooling is unavailable on macOS, skip the proto comment and say so). +- WRK-34 if trivial: `CreateFault(StaHung, "Watchdog", …)` with the correlation id moved into the message. +- Cannot build on macOS: keep diff minimal, net48-safe (no init-only/positional records). Verified on windev afterwards. + +### G — GWC-32, GWC-34, GWC-37 (opus) +- GWC-32: `DisposeAsync` transitions `_state` to `Closed` under `_syncRoot` before completing channels (or `if (_disposed || IsTerminalState()) return;` in `StageWorkerEvent`); update the XML doc to name all three completion paths. Test: dispose a Ready client mid-dispatch → no `ProtocolViolation` fault/metric. +- GWC-34: `_eventDistributorDisposed` flag set under `_syncRoot` when nulling; `EnsureDistributorCreated` throws `SessionManagerException(SessionNotReady)` when set. Test: attach after dispose → throws, no silent hang. +- GWC-37: on `ApplyProviderModeChangeAsync(toMode == Subtag)` re-resolve the watch list and re-subscribe; additionally surface "zero targets resolved while mode can need them" on the provider status (`Degraded` + reason). Update `docs/` alarm-subtag section. Test with the fake worker: GR discovery fails at start, succeeds at failover → watch list non-empty after failover. +- Verify: build Server; `dotnet test --filter "FullyQualifiedName~WorkerClientTests|FullyQualifiedName~GatewaySession|FullyQualifiedName~AlarmMonitor|FullyQualifiedName~AlarmWatchList"`. + +## Merge order +C is merged **last** (its comment sweep touches the same files E and G edit). Order: A, B, F, D, G, E, C. After merge: full `NonWindows` build + one full gateway test run; then windev worker CI for E+F. From 22d5d52c146ee639a25cdc8f7b9e444574b3aaae Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 20 Aug 2026 09:19:59 -0400 Subject: [PATCH 2/2] fix(TST-31): reconcile glauth.md with the shared GLAuth directory, fix propagated live-test credential glauth.md's OPC-perm group table and pre-provisioned-users table described a directory that no longer exists: all five OPC-perm gids were off by 100 (5501-5505 vs actual 5601-5605), and the user table listed readonly/writeop/writetune/writeconfig/alarmack accounts that scadaproj's shared GLAuth (config.toml) never provisioned. The real per-role testers are opc-readonly/opc-writeop/opc-writetune/opc-writeconfig/opc-alarmack (renamed to avoid a cn= case-collision with their group), and every dev user's password is "password" per config.toml's own comment and passsha256 (not admin123). admin itself is no longer a member of any OPC-perm group in the shared directory (only GwAdmin + OtOpcUa-Admins) - multi-role is the actual "covers every gw capability" account now. The wrong admin123 default had propagated into MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_PASSWORD (docs/GatewayTesting.md) and the WorkerLiveMxAccessSmokeTests.cs literal/comment attributing the rig's GLAuth-bridged ArchestrA identity - both corrected to password. Also made the WriteSecured parity test log (never assert) whether AuthenticateUser succeeded or was rejected: MXAccess parity means a bad credential is a legitimate failure reply, so it must not fail the test, but a stale rig credential was silently degrading the nightly live smoke's WriteSecured leg from an authenticated round-trip to a bare failure-reply-shape check with no visible signal. Verified: dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnx succeeds (IntegrationTests builds cleanly on macOS, net10.0, no COM/x86 dependency). Flips Batch B (TST-31) to Done in the cycle-3 remediation tracker. --- .../2026-08-19/remediation/00-tracking.md | 2 +- docs/GatewayTesting.md | 2 +- glauth.md | 49 ++++++++++++------- .../WorkerLiveMxAccessSmokeTests.cs | 38 +++++++++----- 4 files changed, 61 insertions(+), 30 deletions(-) diff --git a/archreview/2026-08-19/remediation/00-tracking.md b/archreview/2026-08-19/remediation/00-tracking.md index 1257225..2822cd0 100644 --- a/archreview/2026-08-19/remediation/00-tracking.md +++ b/archreview/2026-08-19/remediation/00-tracking.md @@ -14,7 +14,7 @@ This plan is executed as **seven parallel batches**, each by one subagent workin | Batch | Branch | Findings | Model | Why this model | Status | |---|---|---|---|---|---| | A | `fix/c3-client-versions` | CLI-46 | sonnet | Mechanical five-client bump + README note + one script guard; recipe is fully specified | Not started | -| B | `fix/c3-glauth-doc` | TST-31 | sonnet | Reconcile a table against one TOML source of truth, re-derive one default, change an assert to a log | Not started | +| B | `fix/c3-glauth-doc` | TST-31 | sonnet | Reconcile a table against one TOML source of truth, re-derive one default, change an assert to a log | Done | | C | `fix/c3-tracker-hygiene` | GWC-31 + housekeeping (close GWC-16/IPC-08/IPC-31, refresh TST-06/TST-17, register WRK-31) | sonnet | Cross-file comment/ID sweep needs care about what each ID *means*, but no logic changes | Not started | | D | `fix/c3-dashboard-acl` | SEC-40, SEC-41, GWC-38 | opus | Authorization semantics, fail-closed decisions, claims re-resolution; each needs a regression test | Not started | | E | `fix/c3-ipc-boundaries` | IPC-33, IPC-39, IPC-34, IPC-35 | opus | Changes the WorkerClient command lifecycle, the options validator, and the worker drain budget; race-sensitive | Not started | diff --git a/docs/GatewayTesting.md b/docs/GatewayTesting.md index d87cd63..ec8d771 100644 --- a/docs/GatewayTesting.md +++ b/docs/GatewayTesting.md @@ -116,7 +116,7 @@ Optional live smoke variables: | `MXGATEWAY_LIVE_MXACCESS_CLIENT_NAME` | `ZB.MOM.WW.MxGateway.IntegrationTests` | Client name passed to `Register`. | | `MXGATEWAY_LIVE_MXACCESS_EVENT_TIMEOUT_SECONDS` | `15` | Maximum wait for the first `OnDataChange` (also used for the `OnWriteComplete` round-trip and the abnormal-exit fault transition). | | `MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_USER` | `admin` | ArchestrA user name passed to `AuthenticateUser` before the `WriteSecured` parity step. | -| `MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_PASSWORD` | `admin123` | Password paired with the user above. Never logged; the test asserts the value does not appear in the WriteSecured diagnostic message. | +| `MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_PASSWORD` | `password` | Password paired with the user above (matches the shared GLAuth `admin` user's dev password — see `glauth.md`). Never logged; the test asserts the value does not appear in the WriteSecured diagnostic message. | The test output includes session id, worker process id, command status, HRESULT/status diagnostics, event sequence and handles, close status, and worker diff --git a/glauth.md b/glauth.md index 27db022..c0fd350 100644 --- a/glauth.md +++ b/glauth.md @@ -52,31 +52,46 @@ LmxOpcUa write rights doesn't need a second account for the gw. | Group | GID | DN | LmxOpcUa meaning | Suggested mxgw mapping | |---|---|---|---|---| -| ReadOnly | 5501 | `ou=ReadOnly,ou=groups,dc=zb,dc=local` | Browse + read OPC UA nodes | `Browse` + `Subscribe` (read paths only) | -| WriteOperate | 5502 | `ou=WriteOperate,ou=groups,dc=zb,dc=local` | Write FreeAccess / Operate attrs | `Write` (plain) | -| WriteTune | 5504 | `ou=WriteTune,ou=groups,dc=zb,dc=local` | Write Tune attrs | `WriteSecured` (Tune only) | -| WriteConfigure | 5505 | `ou=WriteConfigure,ou=groups,dc=zb,dc=local` | Write Configure attrs | `WriteSecured` (Configure) | -| AlarmAck | 5503 | `ou=AlarmAck,ou=groups,dc=zb,dc=local` | Acknowledge alarms | gw alarm-ack RPC, when added | +| ReadOnly | 5601 | `ou=ReadOnly,ou=groups,dc=zb,dc=local` | Browse + read OPC UA nodes | `Browse` + `Subscribe` (read paths only) | +| WriteOperate | 5602 | `ou=WriteOperate,ou=groups,dc=zb,dc=local` | Write FreeAccess / Operate attrs | `Write` (plain) | +| WriteTune | 5603 | `ou=WriteTune,ou=groups,dc=zb,dc=local` | Write Tune attrs | `WriteSecured` (Tune only) | +| WriteConfigure | 5604 | `ou=WriteConfigure,ou=groups,dc=zb,dc=local` | Write Configure attrs | `WriteSecured` (Configure) | +| AlarmAck | 5605 | `ou=AlarmAck,ou=groups,dc=zb,dc=local` | Acknowledge alarms | gw alarm-ack RPC, when added | + +These are the shared config's OPC-perm (`560x`) family — see +`scadaproj/infra/glauth/README.md`'s directory-layout table, which +partitions groups into non-overlapping gid ranges per sister app +(`SCADA-*` 55xx, OPC-perm 560x, `Gw*` 561x, `OtOpcUa-*` 57xx) so the +three apps coexist in one directory. **A user can be in multiple groups** — `othergroups = [...]` in the -config is a list. `admin` is the canonical example (in every role -group below). +config is a list. `multi-role` is the canonical example (in every +group in the file, not just the ones below). ## Pre-provisioned users +Per-role OPC-perm testers are named `opc-*` rather than the bare role name +(`readonly`, `writeop`, …) because a tester username must not +case-collide with a group name — see the naming rule in +`scadaproj/infra/glauth/README.md`. + | Username | Password | UID | Primary group | Other groups | Capabilities | |---|---|---|---|---|---| -| `readonly` | `readonly123` | 5001 | ReadOnly | — | Browse, read | -| `writeop` | `writeop123` | 5002 | WriteOperate | — | + plain Write | -| `writetune` | `writetune123` | 5005 | WriteTune | — | + WriteSecured (Tune) | -| `writeconfig` | `writeconfig123` | 5006 | WriteConfigure | — | + WriteSecured (Configure) | -| `alarmack` | `alarmack123` | 5003 | AlarmAck | — | Alarm acknowledgment | -| `admin` | `admin123` | 5004 | ReadOnly | WriteOperate, AlarmAck, WriteTune, WriteConfigure | All roles | +| `opc-readonly` | `password` | 5101 | ReadOnly | — | Browse, read | +| `opc-writeop` | `password` | 5102 | WriteOperate | — | + plain Write | +| `opc-writetune` | `password` | 5103 | WriteTune | — | + WriteSecured (Tune) | +| `opc-writeconfig` | `password` | 5104 | WriteConfigure | — | + WriteSecured (Configure) | +| `opc-alarmack` | `password` | 5105 | AlarmAck | — | Alarm acknowledgment | +| `multi-role` | `password` | 5005 | SCADA-Admins | every other group in the file, incl. all five OPC-perm groups + `GwAdmin`/`GwReader` | All roles, all apps (cross-app canonical login) | +| `admin` | `password` | 5001 | SCADA-Admins | `GwAdmin`, `OtOpcUa-Admins` | Dashboard Administrator + ScadaBridge/OtOpcUa admin — **not** a member of any OPC-perm group, so it has no gw-side Write/WriteSecured/AlarmAck capability | | `serviceaccount` | `` | 5999 | ReadOnly | — | LDAP search capability (for bind-then-search) | -For mxaccessgw dev, `admin` covers every gw-side capability test; -`readonly` is the right "negative" case for proving Browse-OK / -Write-denied. +For mxaccessgw dev, `multi-role` covers every gw-side capability test +(it is a member of all five OPC-perm groups as well as `GwAdmin`); +`opc-readonly` is the right "negative" case for proving Browse-OK / +Write-denied. `admin` is useful for dashboard-Administrator and +ScadaBridge/OtOpcUa-admin testing but, per the table above, carries no +OPC-perm group membership of its own. The gateway dashboard uses two gateway-specific groups beyond the LmxOpcUa taxonomy: `GwAdmin` (gid 5610 → role `Administrator`) and `GwReader` (gid 5611 → role `Viewer`). @@ -121,7 +136,7 @@ principal cannot show that. ``` DN: cn=admin,dc=zb,dc=local -Password: admin123 +Password: password ``` Construct the DN from the username; bind. Works on GLAuth because diff --git a/src/ZB.MOM.WW.MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs b/src/ZB.MOM.WW.MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs index 02bfee7..a9c2b66 100644 --- a/src/ZB.MOM.WW.MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs +++ b/src/ZB.MOM.WW.MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs @@ -514,21 +514,37 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output) // AuthenticateUser resolves an ArchestrA user id for the WriteSecured call. // Credentials are env-overridable so the test honors the gateway's "do not // log secrets" rule and works against either MXAccess's own user store or - // the LmxOpcUa-baseline GLAuth-bridged ArchestrA identity (admin/admin123). + // the LmxOpcUa-baseline GLAuth-bridged ArchestrA identity (admin/password — + // see glauth.md; the shared GLAuth's dev-user password is "password", not + // the old per-box "admin123"). MxCommandReply authReply = await fixture.Service.Invoke( CreateAuthenticateUserRequest(sessionId, serverHandle, verifyUser, verifyPassword), new TestServerCallContext()).ConfigureAwait(false); - recordedOutput.WriteLine( - $"AuthenticateUser status={authReply.ProtocolStatus.Code} hresult={authReply.Hresult} user_id={authReply.AuthenticateUser?.UserId}"); - // AuthenticateUser is allowed to fail (the underlying provider may reject - // the credential pair); we use the returned user id if non-zero and fall - // back to 0 ("operator only" / no verifier) so the parity assertion holds. - int currentUserId = authReply.ProtocolStatus.Code == ProtocolStatusCode.Ok + // AuthenticateUser is allowed to fail — MXAccess parity means a rejected + // credential is a legitimate reply, not a bug, so this must never be an + // Assert. But a silent fall-through to "operator only" (currentUserId 0) + // quietly demotes the rest of this test from an authenticated round-trip + // to a bare failure-reply-shape check, which a stale rig credential (e.g. + // TST-31) would otherwise hide indefinitely in nightly runs. Log the + // outcome loudly — never the password (verifyUser only) — so that + // degradation is visible in the test output even though it never fails + // the test by itself. + bool authenticateUserSucceeded = authReply.ProtocolStatus.Code == ProtocolStatusCode.Ok && authReply.AuthenticateUser is not null - && authReply.AuthenticateUser.UserId != 0 - ? authReply.AuthenticateUser.UserId - : 0; + && authReply.AuthenticateUser.UserId != 0; + recordedOutput.WriteLine( + authenticateUserSucceeded + ? $"AuthenticateUser SUCCEEDED for user={verifyUser} user_id={authReply.AuthenticateUser!.UserId}" + : $"AuthenticateUser FAILED (credential rejected or not recognized by the provider) " + + $"for user={verifyUser} status={authReply.ProtocolStatus.Code} hresult={authReply.Hresult} " + + $"diagnostic={authReply.DiagnosticMessage} -- WriteSecured below will run as an " + + $"unauthenticated (currentUserId=0) failure-reply-shape check only, not an " + + $"authenticated round-trip. If this is unexpected, verify " + + $"MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_USER/_PASSWORD against the rig's ArchestrA " + + $"identity (see glauth.md)."); + + int currentUserId = authenticateUserSucceeded ? authReply.AuthenticateUser!.UserId : 0; MxCommandReply writeSecuredReply = await fixture.Service.Invoke( CreateWriteSecuredRequest( @@ -1429,7 +1445,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output) string verifyUser = Environment.GetEnvironmentVariable("MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_USER") ?? "admin"; string verifyPassword = Environment.GetEnvironmentVariable("MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_PASSWORD") - ?? "admin123"; + ?? "password"; return (verifyUser, verifyPassword); }