Commit Graph

1243 Commits

Author SHA1 Message Date
Joseph Doherty 0ec9ec29ef docs(audit): Driver.AbLegacy.Cli.md — verb/flag accuracy
CODE-REALITY: all four verbs (probe/read/write/subscribe), all common flags
(-g/--gateway, -P/--plc-type, --timeout-ms, --verbose), AssemblyName
otopcua-ablegacy-cli — all match code exactly. PCCC type table (Bit/Int/Long/
Float/AnalogInt/String/TimerElement/CounterElement/ControlElement) confirmed
against AbLegacy ReadCommand.cs:25 and WriteCommand.cs:24.

INLINE COMPLETENESS: corrected CLI roster count from "third of four" to
"third of six" to match DriverClis.md (S7, TwinCAT, FOCAS are also shipped).
Evidence: docs/Driver.AbLegacy.Cli.md:7 vs docs/DriverClis.md roster table.

STRUCTURAL: ../tests/.../Docker/README.md link confirmed present on disk.
check_links.py: 0 rows for this file.
2026-06-03 16:30:11 -04:00
Joseph Doherty 72de494b9f docs(audit): Client.UI.md — accuracy pass
CODE-REALITY fixes (file:line evidence):
- Read/Write tab write description was wrong: claimed the service reads
  current value first to determine the target type before writing.
  ReadWriteViewModel.WriteAsync (ReadWriteViewModel.cs:97-113) calls
  WriteValueAsync directly with the raw string — no pre-read.
  The type-inferring read-before-write lives only in the Subscriptions
  tab write dialog (SubscriptionsViewModel.ValidateAndWriteAsync).
  Button label is also "Write", not "Send" (ReadWriteView.axaml:35).
- Settings save timing was incomplete: MainWindowViewModel.DisconnectAsync
  (MainWindowViewModel.cs:309) calls SaveSettings() on disconnect too;
  doc said only "after successful connect and on window close".

STRUCTURAL: no rows in links-report.md for this file.
STALE-STATUS: no stale-status language found.
INLINE COMPLETENESS: no inventory gaps found.
2026-06-03 16:30:07 -04:00
Joseph Doherty de6a8974c1 docs(audit): Driver.Modbus.Cli.md — verb/flag accuracy
CODE-REALITY: all four verbs (probe/read/write/subscribe), all common flags
(-h/--host, -p/--port, -U/--unit-id, --timeout-ms, --disable-reconnect,
--verbose), AssemblyName otopcua-modbus-cli — all match code exactly.

INLINE COMPLETENESS: corrected CLI roster count from "four" to "six" to
match DriverClis.md which lists all six shipped CLIs (Modbus, AB CIP,
AB Legacy, S7, TwinCAT, FOCAS); also added FOCAS to the explicit list.
Evidence: docs/Driver.Modbus.Cli.md:8 vs docs/DriverClis.md roster table.

STRUCTURAL: no link rows for this doc in links-report.md; v2/modbus-addressing.md
target confirmed present. check_links.py: 0 rows for this file.
2026-06-03 16:29:57 -04:00
Joseph Doherty 84e225e0ad docs(audit): Client.CLI.md — verb/flag accuracy pass
CODE-REALITY: verified all 8 verbs + flags against src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/.
Verb set confirmed exact (connect/browse/read/write/subscribe/historyread/alarms/redundancy);
common options (-u/-U/-P/-S/-F/--verbose) match CommandBase.cs:32-64; per-command flags all match.

Fixes:
- Aggregate map: StandardDeviation maps to AggregateFunction_StandardDeviationPopulation,
  not ...Sample (AggregateTypeMapper.cs:26). Doc table corrected.
- STALE: test count 52 -> 77 (77 [Fact] across tests/Client/...CLI.Tests, no Theory).

STRUCTURAL: links-report.md had no rows for docs/Client.CLI.md; check_links.py clean.
Executable name otopcua-cli is the CliFx SetExecutableName (Program.cs:12); csproj has no
AssemblyName, so dotnet-run invocation in CLAUDE.md is correct — no change.
2026-06-03 16:29:23 -04:00
Joseph Doherty b9bdfee189 docs(audit): security.md — accuracy pass (profiles, LDAP, ACL, analyzer)
STRUCTURAL (links-report.md):
- Repointed missing src/.../Security/Ldap/LdapAuthService.cs -> the real
  OtOpcUaLdapAuthService.cs (Ldap/OtOpcUaLdapAuthService.cs implements
  ILdapAuthService). Class was reorganized as a wrapper over shared
  ZB.MOM.WW.Auth.Ldap. check_links now clean for docs/security.md.

CODE-REALITY — transport profiles (OpcUaApplicationHost.cs:15-23,59-64,374-409):
- Only THREE profiles exist: None, Basic256Sha256Sign,
  Basic256Sha256SignAndEncrypt (NO hyphens, NO underscores). Removed the four
  fabricated Aes128/Aes256 rows. Config binds by enum-member name; hyphenated
  form does NOT bind. Documented this + the empty-list fallback to None.
- Config section is OpcUa (not OpcUaServer); key is the LIST
  EnabledSecurityProfiles (not singular SecurityProfile). Program.cs:120 binds
  'OpcUa'; Certificates.razor:80 reads OpcUa:PkiStoreRoot.
- No SecurityProfileResolver class exists — stated so explicitly.

CODE-REALITY — LDAP (LdapOptions.cs:21, OtOpcUaLdapAuthService.cs):
- Section is Security:Ldap (LdapOptions.SectionName), not OpcUaServer:Ldap.
- Authenticator is OtOpcUaLdapAuthService (wrapper) + LdapOpcUaUserAuthenticator
  (IOpcUaUserAuthenticator.AuthenticateUserNameAsync), not bespoke
  LdapUserAuthenticator/IUserAuthenticator.
- UseTls bool -> Transport enum (Ldaps/StartTls/None); AllowInsecureLdap ->
  AllowInsecure. Added Enabled master switch + DevStubMode.
- Group->role mapping is downstream via IGroupRoleMapper<string>
  (OtOpcUaGroupRoleMapper), NOT in the auth service. ILdapGroupsBearer and
  DenyAllUserAuthenticator do not exist (fallback is NullOpcUaUserAuthenticator).
- GroupToRole values corrected to canonical roles (Viewer/Designer/
  Administrator/Operator).

CODE-REALITY — ACL trie (TriePermissionEvaluator.cs, PermissionTrieCache.cs,
NodeScope.cs, NodePermissions.cs):
- NodePermissions backing type is int (not uint); lives in Configuration/Enums.
- Authorize(UserAuthorizationState, OpcUaOperation, NodeScope) returns
  AuthorizationDecision.
- Evaluator is strictly fail-CLOSED. Removed the fabricated
  'fail-open-during-transition' + Authorization:StrictMode key (no StrictMode
  anywhere in source).
- Cache: generation-sealed Install/Invalidate/Prune. AclChangeNotifier does
  NOT exist — removed.
- Added the SystemPlatform (Galaxy) scope hierarchy variant.

CODE-REALITY — control plane (AdminRole.cs, ServiceCollectionExtensions.cs:
113-131):
- AdminRole members are Viewer/Designer/Administrator (Task 1.7 rename from
  ConfigViewer/ConfigEditor/FleetAdmin). DriverOperator/FleetAdmin are POLICY
  names; DriverOperator requires roles Operator|Administrator.

CODE-REALITY — analyzer (UnwrappedCapabilityCallAnalyzer.cs:99-103,
AnalyzerReleases.Shipped.md):
- Confirmed category OtOpcUa.Resilience + severity Warning (already correct).
  Corrected 'Five tests' (suite has 26 cases) and AlarmSurfaceInvoker
  wrapper-home wording.

OTHER FIXES:
- v2 header: removed false AddJwtBearer/IPostConfigureOptions<JwtBearerOptions>
  claim — auth is Cookie-only; JWT is mint-only via /auth/token for external
  consumers (JwtTokenService.cs:25-48).
- Certificates.razor is a read-only viewer; removed fabricated
  CertTrustService/CertTrustOptions promote claim.
- Audit: writer is AuditWriterActor (not AuditLogService); softened the
  unverifiable server-side 'AUDIT:' Serilog-prefix claim.
2026-06-03 16:26:00 -04:00
Joseph Doherty 1b6dedc142 docs(audit): Redundancy.md — accuracy pass 2026-06-03 16:24:33 -04:00
Joseph Doherty da074adce9 docs(audit): Reservations.md — accuracy pass
STALE-STATUS / CODE-REALITY fixes:
- Table row ReleasedAt/ReleasedBy: "FleetAdmin" → "Administrator" (AdminRole
  enum renamed in CanonicalizeAdminRoles migration).  ReleasedBy now documents
  that it is the LDAP operator name passed as explicit @ReleasedBy param — not
  SUSER_SNAME() — per migration 20260522000001_AddReleasedByToReleaseExternalIdReservation.
- §4 Release: "FleetAdmin" → "Administrator"; added @ReleasedBy required param
  requirement matching the updated stored-proc signature; replaced "SUSER_SNAME()"
  attribution claim with the correct explicit-param description.
- §The Admin page: replaced entirely.  Actual Reservations.razor uses bare
  [Authorize] (not [Authorize(Policy="FleetAdmin")] and not "CanPublish").
  The page is a read-only flat list (no Active/Released split, no Release row
  action, no Release dialog).  Redirected release-flow readers to
  docs/v2/admin-ui.md §"Release an external-ID reservation".

Evidence:
  src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Reservations.razor:2
  src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/AdminRole.cs:36
  src/Server/ZB.MOM.WW.OtOpcUa.Security/ServiceCollectionExtensions.cs:130
  src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260522000001_AddReleasedByToReleaseExternalIdReservation.cs
2026-06-03 16:22:08 -04:00
Joseph Doherty 60d2fdf25c docs(audit): ServiceHosting.md — accuracy pass (host roles, historian sidecar bitness)
Structural (broken paths):
- Line 73: ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/Contracts/
  → ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Contracts/
  (contracts extracted to their own top-level project; no Contracts/ subfolder)
- Line 73: ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Pipe/
  → ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Ipc/
  (directory renamed from Pipe/ to Ipc/)
  Verified: both new targets exist on disk.

Code-reality (bitness):
- Line 10: historian sidecar platform "x86 (32-bit)" → "x64 (64-bit)"
  Evidence: ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/
  ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.csproj
  <PlatformTarget>x64</PlatformTarget> with explicit comment:
  "x64 — AVEVA Historian 2020 ships an x64 build of aahClientManaged …
   The earlier x86 default was inherited from v1's Galaxy.Host bitness
   (MXAccess COM, retired in PR 7.2) and didn't reflect any constraint
   of the Historian SDK itself."

Stale-status:
- Line 69: removed "Task 63 traefik docs — TODO"; link retargeted to
  existing docs/v2/Architecture-v2.md (Traefik section present at line 114)
- Line 77: removed "v2 rewrite tracked as plan Task 62" — install script
  ships complete at scripts/install/Install-Services.ps1
2026-06-03 16:20:17 -04:00
Joseph Doherty a71d33be35 docs(audit): drivers/README.md — link the 7 new driver overview pages 2026-06-03 16:17:05 -04:00
Joseph Doherty f21883d607 docs(audit): G2 completeness — S7/TwinCAT/OpcUaClient/Historian overview pages 2026-06-03 16:14:40 -04:00
Joseph Doherty 974d835d08 docs(audit): G2 completeness — Modbus/AbCip/AbLegacy driver overview pages 2026-06-03 16:13:22 -04:00
Joseph Doherty 534982948a docs(audit): drivers/README.md — OpcUaClient fixture state + Galaxy tier (review fixes) 2026-06-03 16:08:18 -04:00
Joseph Doherty 200fd6b4c4 docs(audit): FOCAS-Test-Fixture.md — fix duplicated test-file bullet (review fix) 2026-06-03 16:08:15 -04:00
Joseph Doherty 897b06016c docs(audit): OpcUaClient-Test-Fixture.md — accuracy pass
STALE-STATUS (OpcPlcFixture.cs:39):
- "What the fixture is": opc.tcp://localhost:50000 → opc.tcp://10.100.0.35:50000
  (shared Docker host migrated 2026-04-28; fixture already defaults to 10.100.0.35)

CODE-REALITY (OpcUaClientSmokeTests.cs — 3 integration tests open real Secure Channels):
- "What it does NOT cover" §1 ("No UA Secure Channel is ever opened") was wrong
  for the integration suite which does open real channels. Rewritten to scope the
  no-Secure-Channel claim to the unit suite and list what the integration suite
  still doesn't exercise (non-anonymous security policies, signing/encryption,
  chunk assembly, keep-alive).
- "When to trust" table: added Integration (opc-plc) column; noted that real OPC UA
  read + subscribe ARE covered by integration tests; write not yet exercised on wire.

NOTE on IRediscoverable: OpcUaClientDriver does NOT implement IRediscoverable
(verified: no reference in src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/).
Doc makes no such claim — no change needed for that aspect.

INLINE COMPLETENESS:
- "Key fixture / config files": added OpcPlcFixture.cs, OpcUaClientSmokeTests.cs,
  and Docker/docker-compose.yml entries with correct endpoints and flags.
- Added explicit note in OpcUaClientDriver.cs entry: implements IAlarmSource +
  IHistoryProvider (unique among drivers); does NOT implement IRediscoverable.

STRUCTURAL: no rows in links-report.md for this doc.
VERIFY: check_links.py — 0 rows for OpcUaClient-Test-Fixture.md.
2026-06-03 16:02:14 -04:00
Joseph Doherty 2eb3ceb961 docs(audit): S7-Test-Fixture.md — accuracy pass
STALE-STATUS (Snap7ServerFixture.cs:40):
- TL;DR + "What the fixture is": localhost:1102 → 10.100.0.35:1102 (shared
  Docker host migrated 2026-04-28; fixture already defaults to 10.100.0.35)

CODE-REALITY (S7_1500SmokeTests.cs exists and sends real S7comm):
- "What it does NOT cover" §1 ("No ISO-on-TCP frame is ever sent") was
  simply wrong — the integration suite DOES send real S7comm. Rewritten
  to clarify that the unit suite uses IS7Client fakes while the integration
  suite exercises the full wire path.
- "What it does NOT cover" §2 ("successful read not tested end-to-end")
  was also wrong — Driver_reads_seeded_u16_through_real_S7comm does exactly
  that. Rewritten to scope the error-branch-only claim to unit tests.
- "When to trust" table: added Integration (python-snap7) column reflecting
  what the existing S7_1500SmokeTests actually answer.
- "Follow-up candidates" §1: removed the suggestion to build a Snap7 server
  fixture — python-snap7 fixture (task #216) already ships. Follow-ups now
  correctly list Plcsim Advanced and real lab rig only.

INLINE COMPLETENESS:
- "Key fixture / config files": was missing all integration test artefacts.
  Added Snap7ServerFixture.cs, S7_1500SmokeTests.cs, Docker/docker-compose.yml,
  and Docker/profiles/s7_1500.json with descriptions matching file contents.

STRUCTURAL: no broken links in links-report.md for this doc.
VERIFY: check_links.py — 0 rows for S7-Test-Fixture.md.
2026-06-03 16:01:58 -04:00
Joseph Doherty d686e12123 docs(audit): AbLegacy-Test-Fixture.md — accuracy pass
STALE-STATUS: TL;DR claimed "Wire-level round-trip against ab_server PCCC
mode currently fails with BadCommunicationError on read/write (verified
2026-04-20)."  Docker/README.md §Known limitations explicitly states the
root cause was ab_server's empty-CIP-path gate, not a pccc.c gap, and that
N/F/L files round-trip cleanly with the /1,0 path.  AbLegacyReadSmokeTests.cs
confirms tests pass against the fixture.  Rewrote TL;DR + What-the-fixture-is
opening to reflect current passing state; residual gap is only B3 bit-file
writes (0x803D0000).

STALE-STATUS: Lifecycle probe listed as localhost:44818.
AbLegacyServerFixture.cs:57,119 default is 10.100.0.35:44818 (shared Docker
host, migrated 2026-04-28). Fixed.

INLINE COMPLETENESS: Follow-up item 1 phrased as future work ("smoke suite
passes today for N/F/L…"); tightened to describe the current passing state
and narrowed the remaining action to the bit-file write gap.

Verified: python3 .docs-audit/check_links.py — zero rows for this doc.
2026-06-03 16:01:16 -04:00
Joseph Doherty 497d8be1d5 docs(audit): AbServer-Test-Fixture.md — accuracy pass
STRUCTURAL: links-report.md row — path MISSING src/tools/ab_server/.
ab_server is not in this repo; it lives in the upstream libplctag/libplctag
GitHub repo and is cloned + built inside Docker/Dockerfile. Rewrote Binary
bullet to describe it as an external upstream source (no local path reference
that fails the link checker).

STALE-STATUS: Lifecycle TCP-probe host was listed as 127.0.0.1:44818
(AbServer-Test-Fixture.md:21). AbServerFixture.cs:35,72 default is
10.100.0.35:44818 (shared Docker host, migrated 2026-04-28). Fixed.

CODE-REALITY: Micro800 profile Notes quoted "ab_server has no --plc micro800
— falls back to controllogix emulation." Incorrect: Docker/docker-compose.yml
micro800 service uses --plc=Micro800; AbServerProfile.cs:49 confirms
"--plc=Micro800 mode (unconnected-only, empty path)." Updated Notes quote
and summary table row to match actual compose behaviour.

Verified: python3 .docs-audit/check_links.py — zero rows for this doc.
2026-06-03 16:01:06 -04:00
Joseph Doherty 33d40901d2 docs(audit): Modbus-Test-Fixture.md — accuracy pass
STALE-STATUS: TL;DR + Lifecycle section referred to "localhost" as the
simulator address (Modbus-Test-Fixture.md:7,19). Fixture default is
10.100.0.35:5020 (shared Docker host, migrated 2026-04-28) confirmed by
ModbusSimulatorFixture.cs:36.  Updated both prose occurrences.

INLINE COMPLETENESS: Follow-up item 1 claimed MODBUS_SIM_ENDPOINT
lacked documentation; the env var is already documented in this page +
CLAUDE.md. Reworded to reflect actual gap (cross-reference to
test-data-sources.md only).

Verified: python3 .docs-audit/check_links.py — zero rows for this doc.
2026-06-03 16:00:57 -04:00
Joseph Doherty 26833073ca docs(audit): drivers/README.md — capability matrix + link fixes
CODE-REALITY (matrix corrected against driver class declarations):
- Galaxy: GalaxyDriver.cs:38-39 implements IDriver, ITagDiscovery,
  IReadable, IWritable, ISubscribable, IRediscoverable,
  IHostConnectivityProbe, IAlarmSource. Removed the bogus
  IHistoryProvider (no IHistoryProvider refs anywhere in the Galaxy
  project); added the missing IRediscoverable. Replaced the stale
  out-of-process Host/Proxy/named-pipe quirk + the dead
  `Driver.Galaxy.{Shared,Host,Proxy}` path: per CLAUDE.md PR 7.2 those
  retired; the real driver is in-process .NET 10 over gRPC to the
  external mxaccessgw gateway (GalaxyDriver.cs:20-21 doc comment).
  Project path corrected to Driver.Galaxy (+ .Browser, .Contracts).
- Modbus: ModbusDriver.cs:21-22 — added missing IPerCallHostResolver.
- FOCAS: FocasDriver.cs:20-21 — added missing IWritable (it IS
  implemented; WriteAsync returns BadNotWritable for every point,
  FocasDriver.cs:317).
- S7 (S7Driver.cs:31-32), AbCip (AbCipDriver.cs:27-28),
  AbLegacy (AbLegacyDriver.cs:13-14, no IAlarmSource confirmed),
  TwinCAT (TwinCATDriver.cs:13-14), OpcUaClient
  (OpcUaClientDriver.cs:31) verified — already correct.
- Added the 9th family Historian.Wonderware as a server-side historian
  sink (HistorianDataSource.cs:19 `: IHistorianDataSource`), and added
  IHistorianDataSource to the capability-interface list.
- Clarified OpcUaClient as the only driver-side IHistoryProvider; fixed
  the HistoricalDataAccess cross-ref accordingly (the Aveva Historian
  path is the Wonderware IHistorianDataSource sink, not a Galaxy
  IHistoryProvider).
- Added an alarm-source roster to the AlarmTracking cross-ref.

STRUCTURAL (4 dead links repointed to the docs/v1 archive, all verified
to exist):
- ../HistoricalDataAccess.md -> ../v1/HistoricalDataAccess.md (x2)
- ../Subscriptions.md -> ../v1/Subscriptions.md
- Galaxy-Repository.md -> ../v1/drivers/Galaxy-Repository.md
- Galaxy-Test-Fixture.md -> ../v1/drivers/Galaxy-Test-Fixture.md
check_links.py now reports zero rows for docs/drivers/README.md.

STALE-STATUS: removed out-of-process/named-pipe Galaxy wording; noted
native MxAccess alarms work end-to-end; dropped the FOCAS "Tier-C
two-project deployment" phrasing from the per-driver section.
2026-06-03 15:59:57 -04:00
Joseph Doherty c843abf8b1 docs(audit): drivers/FOCAS.md — accuracy pass (no changes needed)
All four dimensions verified against source:

STRUCTURAL: no rows in links-report.md; all 4 linked docs resolve:
  docs/v2/driver-specs.md, docs/v2/focas-version-matrix.md,
  docs/v2/implementation/focas-wire-protocol.md,
  docs/drivers/FOCAS-Test-Fixture.md.

STALE-STATUS: no date anchors, "blocked", "pending", "not yet", "will"
  or TODO phrases found.

CODE-REALITY (verified against src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/):
  - IAlarmSource implemented at FocasDriver.cs:21
  - IWritable correctly returns BadNotWritable (read-only design)
  - All capability interfaces (IReadable, ITagDiscovery, ISubscribable,
    IHostConnectivityProbe, IPerCallHostResolver) match FocasDriver.cs:21
  - Wire/ directory: WireFocasClient.cs present
  - FocasCapabilityMatrix.cs present

INLINE COMPLETENESS: inventory-diff.md records FOCAS as the only
  fully-covered driver (overview + CLI + fixture) — no gaps to add.
2026-06-03 15:59:31 -04:00
Joseph Doherty 6fa29c6c9a docs(audit): drivers/FOCAS-Test-Fixture.md — accuracy pass
STRUCTURAL
- docs/drivers/FOCAS-Test-Fixture.md line 140: replaced stale
  `Series/FixedTreePopulatesTests.cs` reference (file deleted) with
  `Series/WireBackendTests.cs` — the current home of all fixed-tree
  end-to-end integration tests (verified: ls Series/ shows only
  WireBackendTests.cs + WireBackendCoverageTests.cs).

STALE-STATUS
- Removed `**Status:** as of 2026-04-24` header (date-anchored, stale).
  The architecture description that followed was accurate; the date anchor
  served no purpose once the shim era is closed.

CODE-REALITY
- Line 55: TCP-probe skip gate now mentions `OTOPCUA_FOCAS_SIM_ENDPOINT`
  override (verified in FocasSimFixture.cs line 22 / 49).

FOCAS.md: no changes — all claims verified accurate against source.
- IAlarmSource implemented: FocasDriver.cs:21
- IWritable returns BadNotWritable: FocasDriver.cs (IWritable body)
- All capability interfaces listed in capability table confirmed in
  FocasDriver.cs:21 class declaration
- All linked files exist and resolve correctly
2026-06-03 15:59:14 -04:00
Joseph Doherty d731ed98fa docs(audit): drivers/Galaxy.md — interface list + accuracy pass
CODE-REALITY (known defect): the capability-surface declaration line
omitted IAlarmSource and IAsyncDisposable. GalaxyDriver.cs:39 actually
declares: IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable,
IRediscoverable, IHostConnectivityProbe, IAlarmSource, IDisposable,
IAsyncDisposable. Doc line corrected to match exactly, and an IAlarmSource
row added to the capability table (Runtime/GatewayGalaxyAlarmFeed.cs +
Runtime/GatewayGalaxyAlarmAcknowledger.cs).

STALE-STATUS: the v1-doc move note claimed Galaxy-Repository.md /
Galaxy-Test-Fixture.md 'are being moved to docs/v1/ by a parallel cleanup
track' — that move is complete; they live at docs/v1/drivers/. Rewrote to
present tense and linked the real targets.

Verified against source: deploy-watch is a gRPC stream
(GatewayGalaxyDeployWatchSource forwards WatchDeployEventsAsync via
GalaxyRepositoryClient, not a direct DB poll); contained-name<->tag-name
translation (GalaxyDiscoverer.cs:49,60); DataTypeMap at Browse/DataTypeMap.cs;
IGalaxyHierarchySource / IGalaxyDeployWatchSource / DeployWatcher all present.

check_links.py: zero rows for docs/drivers/Galaxy.md.
2026-06-03 15:58:32 -04:00
Joseph Doherty 33912694fd docs(audit): G1 completeness — driver-lifecycle + alarm-historian reference pages 2026-06-03 15:55:28 -04:00
Joseph Doherty 0a679f2c2a docs(audit): VirtualTags.md — correct write-reject mechanism (review fix) 2026-06-03 15:51:15 -04:00
Joseph Doherty f5552c23d4 docs(audit): ScriptedAlarms.md — accuracy pass
CODE-REALITY (file:line evidence)
- Definition section: removed reference to non-existent
  Phase7EngineComposer.ProjectScriptedAlarms; Phase7Composer is a pure
  data composer (entities → Phase7CompositionResult)
  (src/Server/.../OpcUaServer/Phase7Composer.cs:82-183)
- AlarmSeverity: removed "Phase7EngineComposer.MapSeverity bands it" —
  no such class exists; clarified that AlarmSeverity is defined in
  Core.Abstractions/IAlarmSource.cs not in AlarmTypes.cs
  (src/Core/.../Core.Abstractions/IAlarmSource.cs:87)
- State persistence: replaced "Stream E wires..." planning language with
  actual production class EfAlarmActorStateStore
  (src/Server/.../Runtime/ScriptedAlarms/EfAlarmActorStateStore.cs)
- Composition section: replaced Phase7EngineComposer / Phase7ComposedSources
  references (non-existent) with the actual v2 actor-system composition
  path (ScriptedAlarmEngine + ScriptedAlarmActor + driver-role host startup)
- Key source files: AlarmTypes.cs annotation corrected (adds ShelvingKind,
  names all four state enums, notes AlarmSeverity lives in Core.Abstractions)
- Key source files: Phase7Composer.cs annotation corrected to "pure data
  composer"
- Key source files: ScriptedAlarmActor.cs annotation corrected to describe
  AlarmTransitionEvent + DPS alerts topic (not "OPC UA variable reads")
- Key source files: added EfAlarmActorStateStore as the production
  IAlarmActorStateStore implementation

STALE-STATUS
- "Stream E wires the production implementation" — removed; production
  implementation ships and is named EfAlarmActorStateStore
2026-06-03 15:44:11 -04:00
Joseph Doherty 318e432d93 docs(audit): ReadWriteOperations.md — accuracy pass
STRUCTURAL: Fix broken docs/HistoricalDataAccess.md link → docs/v1/HistoricalDataAccess.md
(file moved to v1/ archive; confirmed present at docs/v1/HistoricalDataAccess.md).

CODE-REALITY: Opening paragraph incorrectly attributed OnReadValue/OnWriteValue hook wiring
to GenericDriverNodeManager. Verified: GenericDriverNodeManager is a plain IDisposable
address-space population helper, not a CustomNodeManager2; it has no read/write hooks
(src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs). The v1 DriverNodeManager
that wired those hooks was deleted at 76310b8 (2026-05-26 "chore(cleanup): delete
OtOpcUa.Server, OtOpcUa.Admin, and obsolete v1 tests"). The ADR-002 Phase 7 Stream G
DriverNodeManager replacement is planned but not yet implemented. Current v2 architecture
is a push model: OtOpcUaNodeManager (src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/
OtOpcUaNodeManager.cs) is the CustomNodeManager2; reads return the value last pushed via
WriteValue() from the Akka actor layer. Opening paragraph and Key source files section
updated to reflect present truth; behavioral-contract sections preserved for the ADR-002
planned DriverNodeManager. Flagged in .docs-audit/code-bug-flags.md as CBF-ReadWrite.
2026-06-03 15:44:03 -04:00
Joseph Doherty a32ba1f5c5 docs(audit): VirtualTags.md — accuracy pass
STRUCTURAL
- Repoint Subscriptions.md link → v1/Subscriptions.md (doc line 104;
  target confirmed at docs/v1/Subscriptions.md)

CODE-REALITY (file:line evidence)
- Intro: replace non-existent NodeScopeResolver / DriverNodeManager with
  actual EquipmentNodeWalker + GenericDriverNodeManager; NodeSourceKind
  is stamped by EquipmentNodeWalker.Walk at address-space build time
  (src/Core/.../OpcUa/EquipmentNodeWalker.cs:231,256)
- ScriptSandbox.Build: doc claimed allow-list was "System.Private.CoreLib"
  by name; actual code enumerates TRUSTED_PLATFORM_ASSEMBLIES filtered to
  System.* + netstandard + mscorlib + Microsoft.Win32.Registry
  (src/Core/.../ScriptSandbox.cs:97-127)
- Compile pipeline: doc said "three-step gate"; code has 5 steps —
  EnforceSingleRunMember injection guard (Core.Scripting-013) was missing,
  and PE emit is a distinct step before ALC load
  (src/Core/.../ScriptEvaluator.cs:80-171)
- ForbiddenTypeAnalyzer: doc listed System.Threading.Thread in
  ForbiddenNamespacePrefixes; code explicitly does NOT put it there
  ("Thread's containing namespace is System.Threading, so a prefix check
  never matches") and instead denies it via ForbiddenFullTypeNames; also
  added System.Runtime.Loader and System.Threading.ThreadPool/Timer to
  match the actual deny-list (Core.Scripting-010/-012)
  (src/Core/.../ForbiddenTypeAnalyzer.cs:60-139)
- Dispatch section: DriverNodeManager → GenericDriverNodeManager;
  NodeScopeResolver.IsWriteAllowedBySource (non-existent) removed
- Upstream reads: removed non-existent CachedTagUpstreamSource / Phase7EngineComposer
  references; describe actual DependencyMuxActor → VirtualTagActor feed
- Composition: replaced entire section; Phase7EngineComposer /
  Phase7ComposedSources / PrepareAsync / DriverSubscriptionBridge /
  CachedTagUpstreamSource do not exist in the codebase; Phase7Composer is
  a pure data composer (entities → Phase7CompositionResult)
  (src/Server/.../OpcUaServer/Phase7Composer.cs:82-183)
- Key source files: ScriptEvaluator description updated to "five-step";
  Phase7Composer description corrected; runtime actor descriptions updated

STALE-STATUS
- "Definition reload: handler is not yet wired" — removed (v2 is feature-
  complete; actor-based composition does not use VirtualTagEngine.Load
  as a reload entry point)
2026-06-03 15:42:35 -04:00
Joseph Doherty 9071a3aae0 docs(audit): AddressSpace.md — accuracy + completeness pass
STRUCTURAL: links-report.md has no rows for this doc; check_links.py clean.

STALE-STATUS / CODE-REALITY fixes (file:line evidence):
- 'Galaxy Proxy' / GalaxyProxyDriver.DiscoverAsync retired (PR 7.2) -> GalaxyDriver.DiscoverAsync delegates to GalaxyDiscoverer (Browse/GalaxyDiscoverer.cs:42); removed bogus 'AlarmExtension primitive' + 'two-pass primitive-grouping' claims (IsAlarm comes straight from the gateway hierarchy, GalaxyDiscoverer.cs:71).
- DriverNodeManager.CreateAddressSpace / DriverNodeManager.MapDataType: no such class. Root folder is created by OtOpcUaNodeManager.CreateAddressSpace (OtOpcUaNodeManager.cs:225) as a single shared 'OtOpcUa' root, EventNotifier=None (cs:234-237), not per-driver ns;s={DriverInstanceId}/urn:OtOpcUa:{id}/SubscribeToEvents|HistoryRead. Data-type resolution is OtOpcUaNodeManager.ResolveBuiltInDataType (cs:177) plus per-driver maps (Galaxy Browse/DataTypeMap.Map).
- _securityByFullRef is a Galaxy-driver-internal cache (GalaxyDriver.cs:65/682), not a node-manager field; WriteAuthzPolicy and _writeIdempotentByFullRef do not exist. Rewrote SecurityClass row to the real NodePermissions/TriePermissionEvaluator authz path (TriePermissionEvaluator.cs:78) and WriteIdempotent row to the Polly-retry semantics from DriverAttributeInfo.cs:28-35.
- NodeId scheme table rewritten: string NodeIds under one shared namespace from Config-DB ids / driver refs (Phase7Applier.cs:119-167), not ns;s={DriverInstanceId}.
- Rediscovery: OPC UA Client does NOT implement IRediscoverable (OpcUaClientDriver.cs:31); only Galaxy (DeployWatcher time_of_last_deploy) and TwinCAT (symbol-version-changed 1809) do.
- AB CIP: folder-per-device (AbCipDriver.cs:912-950), not 'per program'; UDT members fan into sub-folders, controller browse into Discovered/.

INLINE COMPLETENESS: added Source (NodeSourceKind) row; documented the two-layer builder->actor->SDK-sink architecture; added EquipmentNodeWalker.cs + Phase7Applier.cs to Key source files.

Verified DataTypeMap.cs lives at the CLAUDE.md-cited path (Driver.Galaxy/Browse/DataTypeMap.cs); contained-name/tag-name + ValueRank/ArrayDim claims cross-checked against Browse/GalaxyDiscoverer.cs:49-71.
2026-06-03 15:42:21 -04:00
Joseph Doherty 2c1dc8bb14 docs(audit): OpcUaServer.md — accuracy + completeness pass
STRUCTURAL: no broken links/paths for this doc (links-report had zero rows);
check_links.py confirms zero rows. All cited src paths verified on disk.

STALE-STATUS (v1->v2):
- Removed v1 'two separate Server/Admin processes' framing; documented the
  single role-gated Host binary + OTOPCUA_ROLES gate
  (src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs; AkkaClusterOptions.cs).
- Server class is OtOpcUaSdkServer (not 'OtOpcUaServer'); it wires ONE
  OtOpcUaNodeManager via CreateMasterNodeManager, not one DriverNodeManager
  per driver. OtOpcUaSdkServer.cs:12-26.
- Removed nonexistent OnServerStarted / LoadServerProperties overrides and
  the 'DriverNodeManagers' member (no such member; grep found none).

CODE-REALITY (doc corrected to match source; no code changed):
- Class name: OtOpcUaSdkServer : StandardServer — OtOpcUaSdkServer.cs:12.
- Address space: OtOpcUaNodeManager : CustomNodeManager2, namespace
  'https://zb.com/otopcua/ns', single 'OtOpcUa' root folder; push-driven via
  IOpcUaAddressSpaceSink — OtOpcUaNodeManager.cs:25,27,225-251.
- Impersonation lives in OpcUaApplicationHost (not the SDK server). Uses
  IOpcUaUserAuthenticator, attaches a UserIdentity (NOT RoleBasedIdentity/
  IRoleBearer — neither exists), Anonymous+X509 fall through to SDK default,
  failures -> BadIdentityTokenRejected (not BadIdentityTokenInvalid).
  OpcUaApplicationHost.cs:159-288.
- Certificate stores default to PkiStoreRoot='pki' (relative to cwd), NOT
  %LOCALAPPDATA%. Substores own/issuer/trusted/rejected.
  AutoAcceptUntrustedClientCertificates default=false (doc had
  Security.AutoAcceptClientCertificates default=true; key does not exist).
  Removed RejectSHA1Certificates claim (not present).
  OpcUaApplicationHost.cs:51,71,298-355.
- Security profiles: EnabledSecurityProfiles default = all three baseline
  profiles, one endpoint per profile; not 'resolved from ServerInstance.Security
  JSON, default None'. Endpoint path is .../OtOpcUa. OpcUaApplicationHost.cs:59-64,321.
- Dispatch: CapabilityInvoker is one per (DriverInstance, IDriver); pipeline
  keyed (DriverInstanceId, hostName, DriverCapability). Enum member is
  'Discover' (not 'Discovery'). Alarm surfaces route via AlarmSurfaceInvoker
  (SubscribeAlarmsAsync/UnsubscribeAlarmsAsync/AcknowledgeAsync), per-host
  fan-out. CapabilityInvoker.cs:7-19,61-156; AlarmSurfaceInvoker.cs:5-51;
  DriverCapability.cs:20-41. OTOPCUA0001 analyzer is category OtOpcUa.Resilience,
  severity Warning — UnwrappedCapabilityCallAnalyzer.cs:67; AnalyzerReleases.Shipped.md:10.
- Authorization: removed nonexistent AuthorizationGate / NodeScopeResolver /
  Authorization:StrictMode / lax-strict mode / WriteAuthzPolicy. Documented the
  real permission-trie infra under Core/Authorization/ (PermissionTrie,
  TriePermissionEvaluator, NodeScope, UserAuthorizationState, AuthorizationDecision).
- Config DB: optimistic concurrency is RowVersion (per-entity), not a
  'DraftRevisionToken' (no such field). sp_PublishGeneration +
  sp_ComputeGenerationDiff verified in Configuration migrations.
- Redundancy: ServiceLevel republished via SdkServiceLevelPublisher
  (IServiceLevelPublisher); ServiceLevelCalculator 0-255. Dropped invented
  'RedundantServerArray' node; standard props are RedundancySupport +
  ServerUriArray. SdkServiceLevelPublisher.cs:9-58; ServiceLevelCalculator.cs:13-23.

INLINE COMPLETENESS: documented EnabledSecurityProfiles binding key in the
Transport section (inventory-diff G3 row owner).
2026-06-03 15:41:38 -04:00
Joseph Doherty 2c938ea6f7 docs(audit): AlarmTracking.md — accuracy + orphan resolution
ORPHAN DECISION: Keep as live doc (path: keep-and-fix).
Rationale: the file carries unique v2 current content describing
the alarms-over-gateway epic architecture; docs/ScriptedAlarms.md
cross-references it explicitly. The orphan symptom is that
docs/README.md still indexes docs/v1/AlarmTracking.md — wiring
this top-level file into README.md is a follow-up task.

STRUCTURAL (dimension 2):
- docs/AlarmTracking.md line 138: Security.md → security.md (CASE-MISMATCH
  from links-report.md rows 1–2). Verified: docs/security.md exists
  (inode 77517627); docs/Security.md is the same file on APFS
  case-insensitive FS, but the checker requires exact on-disk casing.
  check_links.py: zero rows for docs/AlarmTracking.md after fix.

CODE-REALITY (dimension 4):
- line 16 table: `Phase7EngineComposer` / `Phase7EngineComposer.RouteToHistorianAsync`
  → no such class exists. Real class is `Phase7Composer`
  (src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs).
  Scripted-alarm historian routing goes through ScriptedAlarmActor →
  HistorianAdapterActor → IAlarmHistorianSink, not a RouteToHistorianAsync
  method. Fixed to: Phase7Composer / ScriptedAlarmActor transitions →
  HistorianAdapterActor → IAlarmHistorianSink.
- lines 107–123 "Historian write-back" section: referenced
  `Phase7Composer.ResolveHistorianSink` (method doesn't exist in
  current Phase7Composer.cs), `GalaxyProxyDriver` / `GalaxyHistorianWriter`
  (retired in PR 7.2 — no such class in codebase), and `aahClientManaged`
  as a direct call (now mediated through WonderwareHistorianClient).
  Current architecture: NullAlarmHistorianSink default registered in
  ServiceCollectionExtensions.AddOtOpcUaRuntime(); production override
  is SqliteStoreAndForwardSink wrapping WonderwareHistorianClient; bridge
  is HistorianAdapterActor (src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Historian/
  HistorianAdapterActor.cs). Section rewritten to match code reality.
- line 108: "Program.cs" as NullAlarmHistorianSink registration site →
  actual site is ServiceCollectionExtensions.cs, not Program.cs.

STALE-STATUS (dimension 3): no blocked/pending/not-yet banners found
in the top-level file; it was already written as current-state fact.
Galaxy native alarms work end-to-end (verified 2026-05-31) and the
doc correctly describes that as delivered.

CODE-BUG-FLAGS: none. All stale references were doc-side errors; the
production code is correct.

UNVERIFIABLE CLAIMS: AlarmConditionService, DriverNodeManager, ConditionSink,
DriverAlarmSourceAcknowledger, DriverWritableAcknowledger — these are
mentioned by name in the doc but their .cs files were not found in the
search. They may live under a path not searched, or may be internal
implementation details within existing files. These claims are plausible
given the architecture and were not changed.
2026-06-03 15:40:37 -04:00
Joseph Doherty 4cef8124fe chore: ignore .docs-audit scratch dir 2026-06-03 15:24:20 -04:00
Joseph Doherty 4893f7288d docs(plan): documentation audit implementation plan (27 tasks, Phase 0/1/2) 2026-06-03 14:06:24 -04:00
Joseph Doherty 47acdde78d docs(plan): documentation audit design (live-reference corpus, 4 dimensions, fill-every-gap) 2026-06-03 13:59:02 -04:00
Joseph Doherty c6d9b20d9f chore(adminui): prune kit-duplicate + dead shell CSS from site.css
v2-ci / build (push) Failing after 6m40s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
The ZB.MOM.WW.Theme cutover left site.css carrying a near-verbatim copy of the
kit's layout.css (.app-shell/.side-rail/.rail-link/.rail-foot/.login-*) plus two
dead rules (#sidebar-collapse — the kit emits #theme-rail; .rail-eyebrow-chevron
— rendered by the deleted NavSection.razor). Those duplicates loaded after the
kit and could silently override it. Removed them; kept only the app-only rules
the kit does not provide: .rail-eyebrow (footer Session label) and
.chip-alert/.chip-caution (domain status variants). 167 lines removed; builds clean.
2026-06-03 04:37:23 -04:00
Joseph Doherty 11de14d12e refactor(adminui): explicit ClaimTypes.Role footer filter; fix stale NavSidebar comment
v2-ci / build (push) Failing after 45s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
2026-06-03 03:18:08 -04:00
Joseph Doherty aadbf49678 feat(adminui): LoginCard sign-in; remove dead StatusBadge 2026-06-03 03:13:23 -04:00
Joseph Doherty 70d764b063 feat(adminui): MainLayout delegates to ZB.MOM.WW.Theme ThemeShell + kit nav 2026-06-03 03:10:49 -04:00
Joseph Doherty 11bcff6af5 refactor(adminui): drop vendored theme.css/fonts/nav-state.js; keep app-only CSS in site.css 2026-06-03 03:07:21 -04:00
Joseph Doherty de41963587 feat(adminui): use ZB.MOM.WW.Theme ThemeHead + ThemeScripts 2026-06-03 03:03:45 -04:00
Joseph Doherty a78b212c95 build(adminui): reference ZB.MOM.WW.Theme 0.2.0 2026-06-03 03:02:23 -04:00
Joseph Doherty 075c0e69da feat(audit): OtOpcUa IAuditActorAccessor seam + HTTP impl (audit Actor from Auth principal) (Phase 3)
v2-ci / build (push) Failing after 40s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Introduces the IAuditActorAccessor seam and HttpAuditActorAccessor impl so the
ZB.MOM.WW.Audit.AuditEvent Actor field can be sourced from the authenticated Blazor
cookie principal (ZbClaimTypes.Username) when structured emitters are added. Adds the
AuditActor.Resolve static helper (accessor value → SystemFallback/"system") as the
canonical pattern for future emit sites. Wires DI in AddOtOpcUaAuth (TryAddScoped) with
AddHttpContextAccessor(). The structured AuditEvent path remains DORMANT — no live emit
sites exist; seam is forward-looking. SP-based audit path left untouched. 9 new unit
tests all green; Security (54) and ControlPlane (45) test suites fully pass.
2026-06-02 15:25:49 -04:00
Joseph Doherty b7f5e887ee feat(audit): OtOpcUa ConfigAuditLog.Outcome column + migration + ClusterAudit visibility fix (Task 2.2)
Persist the canonical AuditOutcome and make structured audit rows visible.

- ConfigAuditLog gains a nullable Outcome column, stored as the AuditOutcome
  enum member name (nvarchar(16), mirroring how AdminRole is persisted). The
  AuditWriterActor flush now writes Outcome = evt.Outcome.ToString(). Nullable so
  legacy rows and the bespoke stored-procedure path (no derived outcome) write
  NULL.
- Migration 20260602135350_AddConfigAuditLogOutcome: additive nullable column,
  no backfill. Up adds the column, Down drops it. Chains after
  20260602112419_CanonicalizeAdminRoles; `dotnet ef migrations
  has-pending-model-changes` is clean.
- ClusterAudit visibility fix: the page filtered solely on ClusterId, but the
  structured AuditWriterActor path stamps NodeId (ClusterId null), so those rows
  were invisible. Extracted ClusterAuditQuery.ForClusterAsync (shared by the page
  and tests) which ORs in rows whose NodeId belongs to a node in the cluster —
  membership resolved from ClusterNode (NodeId -> ClusterId). SP-path
  ClusterId-stamped rows still match.

Tests: ControlPlane 45/45 (adds Outcome persistence + Denied-outcome asserts);
new Configuration ClusterAuditQueryTests 3/3 (both-paths visible, other-cluster
excluded, page-size cap); AdminUI 121/121. Configuration Unit suite is green on a
clean run (a pre-existing timing flake in ResilientConfigReaderTests, untouched
here, occasionally fails under parallel load and passes in isolation).
2026-06-02 09:59:22 -04:00
Joseph Doherty 933dd1a874 feat(audit): OtOpcUa adopt canonical ZB.MOM.WW.Audit.AuditEvent + AuditWriterActor:IAuditWriter + Outcome derivation (Task 2.1)
Deep-adopt the shared audit record. Deletes the bespoke 8-field positional
Commons AuditEvent and repoints the writer path at ZB.MOM.WW.Audit.AuditEvent
(0.1.0, feed-mapped via dohertj2-gitea). Adds the package reference to both
Commons and ControlPlane.

- AuditWriterActor now implements IAuditWriter: WriteAsync(evt, ct) is a
  best-effort, never-throwing entry point that Self.Tell()s the event onto the
  same batching/dedup/flush pipeline and returns Task.CompletedTask. Existing
  Receive<AuditEvent> + 500/5s batching + two-layer dedup unchanged.
- Flush mapping updated for the canonical field types: OccurredAtUtc is now
  DateTimeOffset (.UtcDateTime into the datetime2 column), SourceNode is string?
  (was NodeId.Value), CorrelationId is Guid? (stored null when null). Outcome is
  NOT yet persisted (column lands in Task 2.2).
- New AuditOutcomeMapper.FromAction maps the OtOpcUa action vocabulary to the
  required canonical Outcome: OpcUaAccessDenied / CrossClusterNamespaceAttempt ->
  Denied; config verbs (DraftCreated/Edited, Published, RolledBack, NodeApplied,
  ClusterCreated, NodeAdded, CredentialAdded/Disabled, ExternalIdReleased) ->
  Success. OtOpcUa emits no Failure events.

The Akka message shape changed, but the structured audit path is dormant (zero
production emit/Tell sites; all live audit flows through the bespoke SP path),
so there is no rolling-deploy wire-compat concern. Tested-not-exercised by
design.

ControlPlane.Tests: 44/44 green (AuditWriterActor suite rewritten to construct
the canonical record + assert the Outcome derivation table + the WriteAsync
best-effort/mailbox-routing contract + null SourceNode/CorrelationId handling).
2026-06-02 09:53:12 -04:00
Joseph Doherty c1619d95f5 feat(auth)!: OtOpcUa canonical control-plane roles + config-DB migration (Task 1.7)
Standardize the control-plane admin role VALUES on the canonical six
(ZB.MOM.WW.Auth CanonicalRole). OtOpcUa uses four:
  ConfigViewer   -> Viewer
  ConfigEditor   -> Designer
  FleetAdmin     -> Administrator
  DriverOperator -> Operator   (appsettings-only string role)

This is a rename, not a permission change: enforcement semantics are
preserved (whoever could deploy/administer/operate before still can).

- AdminRole enum members renamed (persisted as string names via
  HasConversion<string>); RoleGrants.razor dropdown default updated.
- EF DATA migration CanonicalizeAdminRoles rewrites existing
  LdapGroupRoleMapping.Role rows old->new (Up) and back (Down); schema /
  model snapshot byte-identical (no pending model changes).
- Enforcement role STRINGS canonicalized:
  * Security policies keep their NAMES ("DriverOperator"/"FleetAdmin")
    but require canonical roles: RequireRole("Operator","Administrator")
    and RequireRole("Administrator").
  * Deployments.razor [Authorize(Roles="Administrator,Designer")].
  * DevStub now grants "Administrator"; LdapOptions/doc-comment examples
    canonicalized.
- Data-plane authorization (NodePermissions/NodeAcl/IPermissionEvaluator/
  TriePermissionEvaluator/UserAuthorizationState) UNTOUCHED.
- New CanonicalAdminRolesTests pins canonical claim values end-to-end and
  the real registered policies; existing role-string tests updated.
2026-06-02 07:30:00 -04:00
Joseph Doherty 8ba289f975 chore(auth): OtOpcUa unify dev LDAP base DN to dc=zb,dc=local (Task 1.6)
Replace all dev-directory dc=lmxopcua,dc=local references with dc=zb,dc=local
across LdapOptions default, integration harness overrides, docker-compose LDAP_ROOT,
AclEdit placeholder DN, and dev/smoke-test docs. CN/OU prefixes preserved.
2026-06-02 06:45:23 -04:00
Joseph Doherty d0777eee29 fix(auth): OtOpcUa Task 1.5 review — pin JWT role-claim test + document issued-only JWT role key
Fix 1 (test): Token_payload_uses_canonical_zb_claim_keys now asserts that the JWT
payload carries at least one role under JwtTokenService.RoleClaimType ("Role"),
pinning the role-key contract so a future rename is caught immediately. Adds a
comment explaining why alice has roles (appsettings "ReadOnly"→"ConfigViewer"
baseline). Adds missing `using ZB.MOM.WW.OtOpcUa.Security.Jwt` to the test file.

Fix 2 (no-validation path — no AddJwtBearer in production pipeline): grep of src/
confirms no AddJwtBearer / JwtBearer scheme in ServiceCollectionExtensions or Host;
the ServiceCollectionExtensions doc comment explicitly states "no JwtBearer parallel
scheme". RoleClaimType intentionally stays the short "Role" key. Three changes:
  - RoleClaimType doc comment documents issued-only nature, the caveat that a
    JwtBearer scheme MUST use BuildValidationParameters(), and that BuildValidationParameters
    is already wired to set RoleClaimType+NameClaimType correctly.
  - Issue() inline comment at the role-mint site references RoleClaimType docs.
  - BuildValidationParameters() now sets RoleClaimType=RoleClaimType and
    NameClaimType=UsernameClaimType so that if it is ever passed to AddJwtBearer,
    role/name resolution is correct without any extra wiring. TryValidate() is
    refactored to delegate to BuildValidationParameters() so the two can never drift.

All 35 security tests green.
2026-06-02 06:30:10 -04:00
Joseph Doherty 83856b7c27 feat(auth): OtOpcUa adopt ZbClaimTypes + ZbCookieDefaults, keep cookie name (Task 1.5)
Add ZB.MOM.WW.Auth.AspNetCore package ref to Security project (version 0.1.1
from central PM). Alias JwtTokenService.UsernameClaimType and DisplayNameClaimType
to ZbClaimTypes.Username ("zb:username") and ZbClaimTypes.DisplayName ("zb:displayname")
so every mint/read site inherits the canonical spelling. AuthEndpoints login path now
emits ZbClaimTypes.Name (= ClaimTypes.Name, populates Identity.Name) instead of
ClaimTypes.NameIdentifier (no other read site used it), and references ZbClaimTypes.Role
(= ClaimTypes.Role) for role claims so [Authorize(Roles=...)] continues to resolve.
Cookie hardening now flows through ZbCookieDefaults.Apply (sets HttpOnly, SameSite=Strict,
SlidingExpiration, SecurePolicy, ExpireTimeSpan) followed by opts.Cookie.Name = v.Name to
preserve the OtOpcUa-specific "ZB.MOM.WW.OtOpcUa.Auth" cookie name. Two new tests added
to AuthEndpointsIntegrationTests assert canonical ZbClaimTypes on the cookie principal and
canonical zb: keys in the JWT payload; all 35 security tests green.
2026-06-02 06:11:00 -04:00
Joseph Doherty c4f315ec90 fix(auth): OtOpcUa 1.2 review fixes — startup insecure-transport guard + Ldaps in prod overlays, test fidelity, 0.1.1 pin 2026-06-02 01:37:29 -04:00
Joseph Doherty 257caa7bd1 feat(auth): cut OtOpcUa over to ZB.MOM.WW.Auth.Ldap; preserve DevStubMode; route roles via IGroupRoleMapper (Task 1.2/1.4) 2026-06-02 00:55:10 -04:00
Joseph Doherty 6534875476 feat(auth): add IGroupRoleMapper<string> seam (Task 1.1) 2026-06-02 00:29:45 -04:00