Commit Graph

560 Commits

Author SHA1 Message Date
Joseph Doherty 1b7f995aea feat(runtime): DriverHost spawns + subscribes only its own ClusterId's drivers 2026-06-07 03:19:22 -04:00
Joseph Doherty 4fca4e1aca feat(runtime): node-scoped ParseComposition filters address space by ClusterId 2026-06-07 03:15:46 -04:00
Joseph Doherty 7b2f64fdb8 refactor(runtime): case-insensitive ClusterId/NodeId match + suppress short-circuit + edge tests (review) 2026-06-07 03:12:09 -04:00
Joseph Doherty 24796f2c12 feat(runtime): ClusterId scope resolution + node-scoped driver-spec parse 2026-06-07 03:05:02 -04:00
Joseph Doherty ad7f9e731f feat(admin): headless POST /api/deployments REST endpoint (API-key gated)
v2-ci / build (push) Failing after 50s
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
A thin gateway over the admin-operations cluster singleton so CI/scripts can trigger a
deployment without the Blazor button. Forwards to the same IAdminOperationsClient.
StartDeploymentAsync; mounted on admin-role nodes. Auth is a fixed-time X-Api-Key check
against Security:DeployApiKey (orthogonal to the cookie-only web auth); AllowAnonymous so the
auth fallback doesn't 401 it, self-disabling (503) until the key is set. Outcome->status:
202/200/409/422. Unit tests for the key check + outcome mapping; HTTP E2E (real auth + real
deploy via the 2-node harness). Documented in docs/security.md.
2026-06-06 15:54:51 -04:00
Joseph Doherty a5d857d5b2 test(opcua): E2E deploy of an Equipment namespace through the real ConfigComposer
v2-ci / build (push) Failing after 46s
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
Seed a 1-area/1-line/1-equipment/1-tag Equipment namespace, StartDeployment via the
in-process 2-node harness, and assert the persisted artifact decodes (ParseComposition)
to the equipment signal (FullName from TagConfig) + friendly UNS folder names. Covers the
ConfigComposer -> ArtifactBlob -> ParseComposition.EquipmentTags seam the unit tests only
approximated with hand-built JSON. (OPC UA browse is covered against a real SDK node manager
in Phase7ApplierHierarchyTests; the cluster harness binds the no-op sink.)
2026-06-06 15:22:25 -04:00
Joseph Doherty a79ed5fff1 test(opcua): full equipment-namespace structure pipeline against a real SDK node manager
v2-ci / build (push) Failing after 35s
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
Entities -> Phase7Composer.Compose -> MaterialiseHierarchy + MaterialiseEquipmentTags ->
real OtOpcUaNodeManager, asserting the Area/Line/Equipment folders + the equipment-signal
Variable land in a live OPC UA address space (structure-only). Also covers compose-side
EquipmentTags extraction. The cluster-level deploy + network-browse E2E + scadaproj loader
need the docker-dev fixture (not runnable on this dev box) and are tracked as a follow-up.
2026-06-06 15:07:45 -04:00
Joseph Doherty aaf869145a fix(opcua): equipment-tag planner diff + folder-scoped NodeIds (review findings)
Two bundle-review fixes + idempotency coverage:
- CRITICAL: the planner ignored EquipmentTags, so an incremental deploy changing only
  equipment tags produced an empty plan and HandleRebuild short-circuited before
  materialising them. Add TagId to EquipmentTagPlan + Added/Removed/ChangedEquipmentTags
  to Phase7Plan (diffed by TagId, in IsEmpty, driving Apply's needsRebuild) — mirroring
  the GalaxyTags treatment.
- IMPORTANT: equipment variable NodeId was the raw driver FullName, which collides across
  identical machines (e.g. two PLCs both exposing register 40001) — the second variable
  was silently dropped. NodeId is now folder-scoped (parent/Name); FullName stays on
  EquipmentTagPlan for the later values-routing milestone.
- Task 4: SDK-backed idempotency test (double-apply -> single variable); restart-safety
  confirmed (RestoreApplied reuses the same RebuildAddressSpace -> HandleRebuild path).
- Minor: align composer equipment-tag sort with the artifact decoder (coalesce FolderPath).
2026-06-06 15:02:50 -04:00
Joseph Doherty 08cddfe128 fix(opcua): UNS equipment folders browse by friendly Name, NodeId stays the logical Id
Equipment folder DisplayName was the colloquial MachineCode; the live rebuild (artifact
ReadEquipmentNode) + composer now use the UNS level-5 Name segment, matching Area/Line
folders + EquipmentNodeWalker. NodeId stays the logical EquipmentId so browse-path
resolution + ACLs are unaffected.
2026-06-06 14:51:12 -04:00
Joseph Doherty df0dc516c3 feat(opcua): materialise Equipment-namespace tags in the live rebuild
Add Phase7Applier.MaterialiseEquipmentTags — a sink-based pass (Task-0 decision A) that
ensures each EquipmentTagPlan's Variable (NodeId = FullName) under its existing equipment
folder, nesting any FolderPath as a sub-folder. Wire it into OpcUaPublishActor.HandleRebuild
after the Galaxy pass. Variables start BadWaitingForInitialData; never re-creates equipment
folders (decision #4).
2026-06-06 14:46:38 -04:00
Joseph Doherty febe462750 feat(opcua): carry Equipment-namespace tags through the deployment composition
Add EquipmentTagPlan + an init-only EquipmentTags member on Phase7CompositionResult
(mirror of GalaxyTags). Populate it compose-side (Tag.EquipmentId != null AND owning
namespace Kind == Equipment) and artifact-decode-side via BuildEquipmentTagPlans, with
FullName extracted from Tag.TagConfig. Init-only member (not a 7th positional param) so
existing convenience constructors + call sites are untouched.
2026-06-06 14:42:38 -04:00
Joseph Doherty c1ce5833e9 feat(runtime): wire driver SubscribeBulk pass so tag values stream
v2-ci / build (push) Failing after 51s
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
Materialised SystemPlatform/Galaxy variables previously stayed
BadWaitingForInitialData because nothing told the driver to subscribe
(OpcUaPublishActor TODO 'on a future SubscribeBulk pass') and published
values were only forwarded to the VirtualTag mux, never the OPC UA sink.

DriverHostActor now, after each apply, groups the deployment's galaxy tag
MXAccess refs by driver and sends DriverInstanceActor.SetDesiredSubscriptions;
the actor retains the set and (re)subscribes on every Connected entry, so
values resume after reconnects/redeploys (closes the F8b/#113 gap). Published
values are also forwarded to OpcUaPublishActor as AttributeValueUpdate
(NodeId == galaxy MxAccessRef) so the materialised variable shows live data.

Verified live in docker-dev: galaxy TestMachine_001 tags go Good with a
changing TestChangingInt. +1 unit test.
2026-06-06 12:31:55 -04:00
Joseph Doherty fae960c157 fix(opcua): migrate OPC UA server to Opc.Ua SDK 1.5.378 (resolves startup TypeLoadException)
v2-ci / build (push) Failing after 47s
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
Opc.Ua.Server was pinned 1.5.374.126 while Client/Configuration were 1.5.378.106, so the
published Host unified Opc.Ua.Core to 1.5.378 (which dropped Opc.Ua.INodeIdFactory that Server
1.5.374 referenced). Every driver-role node (and the fused site nodes) failed to start the OPC
UA server with TypeLoadException, leaving the OPC data plane dead and the site UIs at 503.

Align all OPC UA packages to 1.5.378.106 (bump Server; drop the Opc.Ua.Configuration/Client
VersionOverrides in OpcUaServer + its integration tests) and port the server host to the
1.5.378 async API:
- ApplicationInstance requires an ITelemetryContext ctor (DefaultTelemetry.Create)
- Start/Stop/LoadApplicationConfiguration/Validate -> async; CheckApplicationInstanceCertificate
  -> CheckApplicationInstanceCertificatesAsync
- ImpersonateEventHandler is now (ISession, ImpersonateEventArgs)
- UserNameIdentityToken.DecryptedPassword is now byte[] (UTF-8 decode)
- tests ported (byte[] passwords; async discovery/session/read client API)

Verified: full solution builds, OpcUaServer unit tests 52/52, and in docker-dev all six OPC
endpoints (4840-4845) listen and the site UIs return 302 (were 503). End-to-end OPC behaviour
(read/write/subscribe/security under 1.5.378) still needs a functional client test.
2026-06-04 12:56:18 -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
Joseph Doherty 2844180865 fix: honor LdapOptions.Enabled at runtime; dedupe ILdapAuthService registration; +SearchBase test, doc fix
v2-ci / build (push) Failing after 41s
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-01 23:03:12 -04:00
Joseph Doherty d3ab2bfbaf fix: bind OtOpcUa LdapOptions from real Security:Ldap section; gate validator on DevStubMode 2026-06-01 22:46:09 -04:00
Joseph Doherty 88e773af36 feat: validate OpcUa host options at startup (route through IOptions + ValidateOnStart) 2026-06-01 18:45:55 -04:00
Joseph Doherty f35ebd7aaf feat: add fail-fast LDAP options validation in OtOpcUa via ZB.MOM.WW.Configuration 2026-06-01 18:32:44 -04:00
Joseph Doherty 695fa6408b docs(alarms): record native alarms verified working; add D.1 smoke
v2-ci / build (push) Failing after 47s
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 2026-04-30 alarm plan banners claimed worker-side native alarm
subscription was blocked on a COM-bitness finding. That's stale: the
mxaccessgw .NET client now has true MxAccess alarm-event support, and a
live StreamAlarms check (+ new Skip-gated GatewayGalaxyAlarmFeedLiveTests
through the lmxopcua consumer) confirms native alarms — operator comment,
category, severity, timestamps — flow end-to-end. Reconcile both plan docs
to reality and add docs/plans/alarms-d1-smoke-artifact.md as the D.1
alarm-source deliverable. Historian-write live smoke + full server->A&C
round-trip remain (Windows parity rig only).
2026-05-31 09:59:01 -04:00
Joseph Doherty 61193629b6 fix(adminui): wire Test Connect probes + live panels on admin-only nodes
v2-ci / build (push) Failing after 36s
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
Both bugs surfaced only on split-role deployments (the MAIN cluster's
admin-only nodes), where the AdminUI runs without the driver role.

- Test Connect returned "No probe registered" for every driver: the
  IDriverProbe set was registered only under the driver role, but the
  admin-operations singleton that consumes it is pinned to admin. Extract
  AddOtOpcUaDriverProbes() (idempotent via TryAddEnumerable) and call it
  in the hasAdmin path too.

- Live driver-status/alerts/script-log panels showed "SignalR error:
  Connection refused": these Blazor Server components opened a HubConnection
  to their own hub via the browser's public URL, which server-side code
  can't reach behind Traefik (host :9200 -> container :9000). Read the
  in-process source directly instead -- DriverStatus via
  IDriverStatusSnapshotStore.SnapshotChanged, Alerts/ScriptLog via a new
  IInProcessBroadcaster<T>. Fleet status was unaffected (reads DB/ActorSystem).

Adds unit tests for probe registration, the snapshot-store event, and the
broadcaster.
2026-05-29 16:38:32 -04:00
Joseph Doherty e3a27422a1 fix(adminui): Galaxy editor 500 — read DriverConfig case-insensitively + null-safe FromRecord
v2-ci / build (push) Failing after 39s
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
GalaxyDriverPage deserialized DriverConfig with case-sensitive camelCase opts, but the
persisted/seeded config is PascalCase (the runtime reads it case-insensitively). So all four
nested option records read as null -> FromRecord NRE (HTTP 500) on edit, and the form would
have shown defaults instead of the real config (risking a clobber on save). Fix: add
PropertyNameCaseInsensitive=true (matches the runtime) so real values load, plus null-coalesce
the nested records in FromRecord as defense-in-depth. Regression test asserts the seeded
PascalCase config loads its real values.
2026-05-29 12:45:44 -04:00
Joseph Doherty 32d7fd7cc9 fix(galaxy): complete PR 7.2 rename — use canonical GalaxyMxGateway driver type
v2-ci / build (push) Failing after 48s
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 driver/factory/seed use 'GalaxyMxGateway' (legacy 'Galaxy' was retired),
but the AdminUI editor router, GalaxyDriverPage, address picker, identity
dropdown, the Galaxy browser/probe, and DraftValidator still keyed on 'Galaxy'.
Result: the seeded GalaxyMxGateway driver couldn't be edited ('no editor
registered'), UI-created Galaxy drivers wrote a type with no factory, and a
SystemPlatform-bound GalaxyMxGateway driver failed publish validation.
Align all stragglers to GalaxyMxGateway (+ failing-test-first DraftValidator
coverage). ShouldStub's 'Galaxy' legacy safety-net left intact.
2026-05-29 12:31:55 -04:00
Joseph Doherty de666b24c3 test: fix Galaxy-tag Phase7 test fixtures + S7 CLI enum; add MaterialiseGalaxyTags coverage
v2-ci / build (push) Failing after 38s
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
Completes the test side of the in-progress Galaxy-tag workstream:
- Phase7ApplierTests / Phase7ApplierHierarchyTests: supply the now-required
  Galaxy-tag args to Phase7Plan / Phase7CompositionResult.
- Add genuine coverage for Phase7Applier.MaterialiseGalaxyTags (folder-per-distinct-path,
  variable-per-tag node-id derivation, folder dedupe) + added-Galaxy-tags-trigger-rebuild.
- S7.Cli.Tests: use the project's S7CpuType (CLI option type) instead of S7.Net.CpuType.
Whole solution now builds 0/0; OpcUaServer.Tests 52, S7.Cli.Tests 36 green.
2026-05-29 12:18:01 -04:00
Joseph Doherty da4634d67e fix(tests,cli): implement IOpcUaAddressSpaceSink.EnsureVariable in test fakes; fix CLI CS1587
v2-ci / build (push) Failing after 44s
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
Resolves the 12 reported build errors (7 CS0535 sink fakes + 5 CLI CS1587).
Runtime.Tests green (74). NOTE: OpcUaServer.Tests still has pre-existing CS7036
errors from the in-progress Galaxy-tag workstream (Phase7Plan/Phase7CompositionResult
new required params) — separate, test-only, not addressed here.
2026-05-29 10:19:32 -04:00
Joseph Doherty 1fd093d95d test(config): global LdapGroupRoleMapping CRUD 2026-05-29 09:52:47 -04:00
Joseph Doherty f210f09caf feat(security): merge DB-backed LDAP role grants into login claims 2026-05-29 09:51:22 -04:00
Joseph Doherty b719194046 feat(security): RoleMapper.Merge — additive DB-backed role grants 2026-05-29 09:43:12 -04:00
Joseph Doherty 7570df76d3 feat(adminui): editable OpcUaClient endpoint URL list via CollectionEditor 2026-05-29 09:41:09 -04:00
Joseph Doherty 244949caa3 feat(adminui): editable S7 tag list via CollectionEditor 2026-05-29 09:37:12 -04:00
Joseph Doherty a5a0d06dbe feat(adminui): editable FOCAS device + tag lists via CollectionEditor 2026-05-29 09:33:53 -04:00
Joseph Doherty 6882761f4c feat(adminui): editable TwinCAT device + tag lists via CollectionEditor 2026-05-29 09:29:57 -04:00
Joseph Doherty 15f3797f1e feat(adminui): editable AbLegacy device + tag lists via CollectionEditor 2026-05-29 09:26:25 -04:00
Joseph Doherty 534d670b21 feat(adminui): editable AbCip device + tag lists via CollectionEditor 2026-05-29 09:22:51 -04:00
Joseph Doherty b351a81c8f fix(adminui): preserve un-edited Modbus tag fields across edit (review)
Capture the original ModbusTagDefinition as _source in ModbusTagRow and
rewrite ToDefinition() to use 'with {}', so StringByteOrder, ArrayCount,
Deadband, UnitId, and CoalesceProhibited survive a load→edit→save cycle.
2026-05-29 09:18:36 -04:00
Joseph Doherty c4116e54c9 feat(adminui): editable Modbus tag list via CollectionEditor 2026-05-29 09:14:06 -04:00
Joseph Doherty c3fec1426c fix(adminui): case-insensitive resilience policy keys + malformed-json test (review) 2026-05-29 09:10:41 -04:00
Joseph Doherty 4a469fbe06 feat(adminui): typed resilience override form model + tests 2026-05-29 09:06:45 -04:00
Joseph Doherty 9e479ce675 test(security): fix Logout_clears_the_cookie
v2-ci / build (push) Failing after 44s
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
Two pre-existing test bugs surfaced by the auth-alignment branch:
 - Test wanted the 204/JSON contract but never sent Accept:
   application/json — endpoint correctly returned 302 (form POST).
 - Cookie-name assertion still used OtOpcUa.Auth= (now
   ZB.MOM.WW.OtOpcUa.Auth= since the Task 1 default change).

Endpoint behavior is intentional and untouched.
2026-05-29 08:01:26 -04:00
Joseph Doherty af691f3291 fix(security): correct challenge tests to match framework reality
ASP.NET Core's cookie-handler IsAjaxRequest heuristic only checks
X-Requested-With (not Accept). Drop the third test (Accept: application/json
was assumed to → 401 but actually → 302) and the Location.ShouldBeNull
assertion on the XHR test (framework still writes Location alongside 401;
clients ignore it). Renamed _ajax_ → _xhr_ for accuracy. Design doc
updated to match.
2026-05-29 07:58:18 -04:00
Joseph Doherty 453340e71e test(security): add browser-vs-AJAX challenge tests for root path
Adds protected MapGet("/") in the test host plus three [Fact] methods
exercising the cookie scheme's challenge heuristic for the root route:
browser (Accept: text/html), AJAX (X-Requested-With: XMLHttpRequest),
and JSON (Accept: application/json) callers. Also adds a no-redirect
HttpClient helper so the 302 + Location can be asserted directly.
2026-05-29 07:56:15 -04:00
Joseph Doherty 74b9218a92 refactor(security): drop JwtBearer parallel scheme, externalize cookie config
Single Cookie auth scheme; framework default challenge restores 302 → /login
for browsers + 401 for AJAX. OtOpcUaCookieOptions now flows through to
CookieAuthenticationOptions via PostConfigure (fixes a latent bug where the
options class was bound but ignored). Cookie name moves to
ZB.MOM.WW.OtOpcUa.Auth; existing sessions get a one-time forced sign-out.
2026-05-29 07:47:58 -04:00