dd0a846b64
Secrets were per-node SQLite, so a secret written on one node was invisible to the rest of a cluster. G-7's design resolved the "shared SQL store vs Akka replicator" fork to build only the former; both are built here so the choice is a deployment decision (availability vs partition tolerance) rather than a library limitation. Two new packages — ZB.MOM.WW.Secrets.Replicator.SqlServer (shared store, plus a local-store-with-hub mode) and .Replicator.AkkaDotNet (peer-to-peer over distributed pub/sub). Core gains ISecretsStoreMigrator, one shared SecretLastWriterWins predicate so no two stores can disagree on a tie, the transport-agnostic reconciler, and ReplicatingSecretStore — which closes a real gap: nothing had ever called ISecretReplicator.PublishAsync, so the seam was inert and local writes would not have propagated at all. Verified 182 pass / 1 skip / 0 warnings, including 15 live tests against a real SQL Server 2022 (the SQLite suite ported case-for-case, so any behavioural divergence between the stores fails) and a 9-test in-process 2-node Akka cluster over real remoting. A post-build review caught six defects, all fixed and now covered: both replication modes could not resolve from the container (no test had built one), an unbounded fetch that broke past SQL Server's 2100-parameter cap, a poison row that aborted the rest of its batch forever, Enum.Parse on peer input that could restart the actor in a loop, null crypto blobs crossing the trust boundary, and a silently dropped pull-read failure. Packed at 0.2.0 and vulnerability-scanned clean; not yet published to the feed. Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
194 lines
12 KiB
Markdown
194 lines
12 KiB
Markdown
# scadaproj
|
||
|
||
Umbrella workspace for a family of related **SCADA / OT** (operational-technology) projects
|
||
built around **AVEVA System Platform (Wonderware)**, **OPC UA**, and **Akka.NET**.
|
||
|
||
This repository is part **index** — a map of the sister projects, which are their own
|
||
repositories — and part **home** for cross-project *normalization* work and the shared code
|
||
it produces.
|
||
|
||
> Working in here with Claude Code? See [`CLAUDE.md`](CLAUDE.md) for the machine-oriented
|
||
> index and guidance. This README is the human-facing overview.
|
||
|
||
## What's in here
|
||
|
||
| Path | What it is |
|
||
|---|---|
|
||
| [`CLAUDE.md`](CLAUDE.md) | High-level index of the sister projects + working guidance |
|
||
| [`components/`](components/) | Component-normalization framework (per concern: target spec, current state, gaps) — 7 components |
|
||
| [`docs/plans/`](docs/plans/) | Implementation + design plans (incl. Galaxy Repository and LocalDb, which have no `components/` entry) |
|
||
| [`infra/glauth/`](infra/glauth/) | The shared GLAuth every app's dev/test LDAP points at (`10.100.0.35:3893`) |
|
||
| [`ZB.MOM.WW.Auth/`](ZB.MOM.WW.Auth/) | **Built shared library** (.NET 10) — login / identity / API keys |
|
||
| [`ZB.MOM.WW.Theme/`](ZB.MOM.WW.Theme/) | **Built shared RCL** (.NET 10) — Technical-Light design system |
|
||
| [`ZB.MOM.WW.Audit/`](ZB.MOM.WW.Audit/) | **Built shared library** — canonical audit event + writer seam |
|
||
| [`ZB.MOM.WW.Health/`](ZB.MOM.WW.Health/) | **Built shared library** — readiness / liveness / active-node probes |
|
||
| [`ZB.MOM.WW.Telemetry/`](ZB.MOM.WW.Telemetry/) | **Built shared library** — OTel metrics / traces + Serilog |
|
||
| [`ZB.MOM.WW.Configuration/`](ZB.MOM.WW.Configuration/) | **Built shared library** — options validation + startup preflight |
|
||
| [`ZB.MOM.WW.Secrets/`](ZB.MOM.WW.Secrets/) | **Built shared library** — AES-256-GCM secret store + `${secret:}` resolution + CLI + SQL-Server / Akka cluster replication |
|
||
| [`ZB.MOM.WW.GalaxyRepository/`](ZB.MOM.WW.GalaxyRepository/) | **Built shared library** — Galaxy object-hierarchy SQL browse + gRPC service |
|
||
| [`ZB.MOM.WW.LocalDb/`](ZB.MOM.WW.LocalDb/) | **Built shared library** — embedded SQLite cache + 2-node gRPC replication |
|
||
|
||
## The sister projects
|
||
|
||
Independent repositories, coupled only at **runtime over wire protocols** (gRPC + OPC UA) —
|
||
not by a shared build. scadaproj *indexes* them; it does not contain them.
|
||
|
||
- **OtOpcUa** (`lmxopcua`) — OPC UA server (.NET 10) that republishes Wonderware Galaxy tags as
|
||
an OPC UA address space.
|
||
- **MxAccessGateway** (`mxaccessgw`) — gRPC gateway giving modern x64 / .NET-10 clients full
|
||
MXAccess parity without loading 32-bit COM. The linchpin both others depend on.
|
||
- **ScadaBridge** — distributed SCADA platform on Akka.NET (hub-and-spoke: one central cluster
|
||
+ N site clusters); its Data Connection Layer ingests OPC UA and MxAccess.
|
||
- **HistorianGateway** (`historiangw`) — gRPC sidecar over the AVEVA Historian (read/write) plus
|
||
read-only Galaxy browse; no COM, no x86 worker. OtOpcUa's sole historian backend.
|
||
|
||
Data flow:
|
||
|
||
```
|
||
Wonderware Galaxy ──gRPC──▶ mxaccessgw ──▶ OtOpcUa (republishes as OPC UA) ─┐
|
||
└────────────────────────────────────┴─▶ ScadaBridge clusters
|
||
(or ScadaBridge's MxGateway adapter, direct)
|
||
```
|
||
|
||
Full relationship map: [`CLAUDE.md`](CLAUDE.md#cross-project-relationships).
|
||
|
||
## Component normalization
|
||
|
||
The sister repos kept re-implementing the same cross-cutting concerns and drifting apart.
|
||
[`components/`](components/) captures, per component, the **one target** spec, each project's
|
||
**code-verified current state**, and a **gaps / adoption backlog**. Convention + workflow in
|
||
[`components/README.md`](components/README.md).
|
||
|
||
Status verified 2026-07-18 against the feed + consumer branches (see [Roadmap](#roadmap)).
|
||
"All four" = OtOpcUa, MxAccessGateway, ScadaBridge, HistorianGateway.
|
||
|
||
| Component | Feed | Adoption | Folder |
|
||
|---|---|---|---|
|
||
| Auth (login / identity / authz) | `0.1.0`–`0.1.5` | mxaccessgw / ScadaBridge / HistorianGateway @ `0.1.5`; OtOpcUa @ `0.1.1` | [`components/auth/`](components/auth/) |
|
||
| UI Theme (layout / tokens / components) | `0.2.0`–`0.3.1` | all four @ `0.3.1` | [`components/ui-theme/`](components/ui-theme/) |
|
||
| Audit (event model + writer seam) | `0.1.0` | all four (DEEP) | [`components/audit/`](components/audit/) |
|
||
| Config + validation | `0.1.0` | all four | [`components/configuration/`](components/configuration/) |
|
||
| Observability (metrics / traces / logs) | `0.1.0` | all four | [`components/observability/`](components/observability/) |
|
||
| Health (readiness / liveness / active-node) | `0.1.0` | all four | [`components/health/`](components/health/) |
|
||
| Secrets (encrypted store + `${secret:}`) | `0.1.0`–`0.1.3` (`0.2.0` packed, unpublished) | all four @ `0.1.2` (`0.1.3` published, not yet consumed) | [`components/secrets/`](components/secrets/) |
|
||
| Galaxy Repository (SQL browse + gRPC) | `0.1.0`, `0.2.0` | HistorianGateway + mxaccessgw | _(design in `docs/plans/`)_ |
|
||
| LocalDb (embedded cache + 2-node sync) | `0.1.0` | none yet | _(design in `docs/plans/`)_ |
|
||
|
||
## `ZB.MOM.WW.Theme` — the shared UI kit
|
||
|
||
The UI-theme component, realized as a single-package .NET 10 Razor Class Library that replaced
|
||
each app's copy-pasted Technical-Light design system. **Adopted by all four apps, currently on
|
||
`0.3.1`** (feed carries `0.2.0`–`0.3.1`); adoption history in
|
||
[`components/ui-theme/GAPS.md`](components/ui-theme/GAPS.md).
|
||
|
||
| Asset / Component | Purpose | Used by |
|
||
|---|---|---|
|
||
| `_content/ZB.MOM.WW.Theme/css/theme.css` | Design tokens, IBM Plex typography, Bootstrap overrides | all |
|
||
| `_content/ZB.MOM.WW.Theme/css/layout.css` | Side-rail shell layout, nav CSS, chip/card helpers | all |
|
||
| `_content/ZB.MOM.WW.Theme/fonts/*.woff2` | IBM Plex Sans 400/600 + Mono 500, vendored | all |
|
||
| `ThemeHead`, `ThemeShell`, `BrandBar` | Shell entry point and chassis components | all |
|
||
| `NavRailItem`, `NavRailSection` | Rail nav components | all |
|
||
| `StatusPill` (`StatusState`) | Inline status chip — replaces per-app `StatusBadge` | all |
|
||
| `LoginCard` | Static form-POST sign-in card | OtOpcUa, ScadaBridge (MxGateway when login page added) |
|
||
| `TechButton`, `TechCard`, `TechField` | Common controls (Bootstrap 5 wrappers) | all |
|
||
|
||
**Consumer matrix:** all four apps consume the single `ZB.MOM.WW.Theme` package —
|
||
OtOpcUa `AdminUI`, MxAccessGateway `Server`, ScadaBridge `Host` + `CentralUI`, HistorianGateway `Server`.
|
||
|
||
### Build & test
|
||
|
||
```bash
|
||
cd ZB.MOM.WW.Theme
|
||
dotnet build -c Release # 0 warnings (TreatWarningsAsErrors)
|
||
dotnet test # 48 bUnit tests (verified 2026-07-18)
|
||
./build/pack.sh # → ./artifacts/ZB.MOM.WW.Theme.<version>.nupkg (feed is at 0.3.1)
|
||
```
|
||
|
||
Stack: .NET 10 · Razor Class Library · bUnit · xUnit · central package management.
|
||
More detail: [`ZB.MOM.WW.Theme/README.md`](ZB.MOM.WW.Theme/README.md).
|
||
|
||
---
|
||
|
||
## `ZB.MOM.WW.Auth` — the shared library
|
||
|
||
The auth component, realized as a four-package .NET 10 library that replaced each app's
|
||
re-implemented authentication. **Adopted by all four apps** — pins drift deliberately
|
||
(OtOpcUa `0.1.1`, ScadaBridge `0.1.3`, MxAccessGateway + HistorianGateway `0.1.4`); history in
|
||
[`components/auth/GAPS.md`](components/auth/GAPS.md).
|
||
|
||
| Package | Purpose | Used by |
|
||
|---|---|---|
|
||
| `ZB.MOM.WW.Auth.Abstractions` | Contracts, `CanonicalRole`, API-key types (zero deps) | all |
|
||
| `ZB.MOM.WW.Auth.Ldap` | Bind-then-search LDAP authentication (fail-closed) | all |
|
||
| `ZB.MOM.WW.Auth.ApiKeys` | Peppered-HMAC API keys + SQLite store + verifier + admin commands | MxAccessGateway, ScadaBridge |
|
||
| `ZB.MOM.WW.Auth.AspNetCore` | Canonical claim types, cookie defaults, LDAP DI helpers | all (web UIs) |
|
||
|
||
**Consumer matrix:** OtOpcUa → `Abstractions + Ldap + AspNetCore` (no ApiKeys / SQLite);
|
||
MxAccessGateway, ScadaBridge & HistorianGateway → all four packages.
|
||
|
||
Identity (LDAP / GLAuth, the canonical role set) is normalized; each app keeps its own
|
||
**authorization enforcement** (OPC-UA permissions / gRPC scopes / roles + site-scoping) and maps
|
||
it onto the canonical roles. Design: [`components/auth/spec/SPEC.md`](components/auth/spec/SPEC.md)
|
||
and [`CANONICAL-ROLES.md`](components/auth/spec/CANONICAL-ROLES.md).
|
||
|
||
### Build & test
|
||
|
||
```bash
|
||
cd ZB.MOM.WW.Auth
|
||
dotnet build -c Release # 0 warnings
|
||
dotnet test # 215 passing, 1 skipped (opt-in LDAP integration test) — verified 2026-07-18
|
||
dotnet pack -c Release -o ./artifacts # → 4 nupkgs (feed is at 0.1.4)
|
||
```
|
||
|
||
Stack: .NET 10 · xUnit · `Novell.Directory.Ldap.NETStandard` · `Microsoft.Data.Sqlite` · central
|
||
package management. The packages are **libraries linked into each consumer** — there is **no
|
||
central auth service**. More detail: [`ZB.MOM.WW.Auth/README.md`](ZB.MOM.WW.Auth/README.md).
|
||
|
||
The optional LDAP integration test is opt-in:
|
||
|
||
```bash
|
||
ZB_LDAP_IT=1 dotnet test # requires a reachable GLAuth (e.g. a sister repo's infra/glauth)
|
||
```
|
||
|
||
## Roadmap
|
||
|
||
> **Verified 2026-07-18** against the Gitea feed (`nuget/query?q=ZB.MOM.WW`) and the actual
|
||
> `PackageReference`s on each consumer's default branch — not from status claims in docs.
|
||
> Per-component detail in each `components/<name>/GAPS.md`.
|
||
|
||
**Shipped**
|
||
|
||
- ✅ Auth normalized + `ZB.MOM.WW.Auth` built (feed: `0.1.0`–`0.1.5`) and **adopted by all four apps**.
|
||
`0.1.5` (2026-07-18) pins patched SQLitePCLRaw; **mxaccessgw, ScadaBridge and HistorianGateway bumped to it**.
|
||
OtOpcUa stays at `0.1.1` — it takes no `ApiKeys`/SQLite dependency, so it was never exposed.
|
||
- ✅ UI Theme normalized + `ZB.MOM.WW.Theme` built (feed: `0.2.0`–`0.3.1`); **all four apps on `0.3.1`**.
|
||
- ✅ Audit — `ZB.MOM.WW.Audit` `0.1.0` on the feed; adopted by all four (DEEP: canonical record everywhere).
|
||
- ✅ Configuration — `ZB.MOM.WW.Configuration` `0.1.0` on the feed; adopted by all four.
|
||
- ✅ Observability — `ZB.MOM.WW.Telemetry` + `.Serilog` `0.1.0` on the feed; adopted by all four.
|
||
- ✅ Health — `ZB.MOM.WW.Health` (+ `.Akka`, `.EntityFrameworkCore`) `0.1.0` on the feed; **adopted by all four**
|
||
(`MapZbHealth` wired in each; previously documented as "not yet adopted" — that was stale).
|
||
- ✅ Galaxy Repository — `ZB.MOM.WW.GalaxyRepository` `0.2.0` on the feed; consumed by HistorianGateway
|
||
**and mxaccessgw** (which wires `AddZbGalaxyRepository` in its Server; previously documented as a
|
||
pending follow-on — also stale).
|
||
- ✅ Secrets — `ZB.MOM.WW.Secrets` (+ `.Abstractions`, `.Ui`) published through **`0.1.3`** (KEK rotation,
|
||
2026-07-18); all four apps still pinned at `0.1.2`.
|
||
- ✅ Secrets G-7 clustered replication — **built 2026-07-18 at `0.2.0`**, both fork options:
|
||
`ZB.MOM.WW.Secrets.Replicator.SqlServer` (shared store *and* local-plus-hub) and
|
||
`ZB.MOM.WW.Secrets.Replicator.AkkaDotNet` (peer-to-peer, partition-tolerant). Verified against a
|
||
real SQL Server 2022 and an in-process 2-node Akka cluster. Packed, **not yet published**.
|
||
- ✅ LocalDb — `ZB.MOM.WW.LocalDb` (+ `.Contracts`, `.Replication`) built at `0.1.0` and **published to the
|
||
feed 2026-07-18** (restore-verified from a scratch consumer).
|
||
|
||
**Open**
|
||
|
||
- ⬜ Publish Secrets `0.2.0` (5 packages) to the feed, then adopt a clustered topology in
|
||
ScadaBridge / OtOpcUa — config + a shared KEK, no code change. [`components/secrets/GAPS.md`](components/secrets/GAPS.md).
|
||
- ⬜ Bump the four Secrets consumers `0.1.2` → `0.1.3`/`0.2.0` (KEK rotation + the same security fix).
|
||
- ⬜ Clear the pre-existing `AngleSharp` NU1902 build break in ScadaBridge `CentralUI.Tests`
|
||
and HistorianGateway `Tests` — both fail full-solution builds under `TreatWarningsAsErrors`.
|
||
- ⬜ Adopt `ZB.MOM.WW.LocalDb` in an app — no consumer references it yet.
|
||
- ⬜ Give LocalDb a `components/localdb/` entry; it is the one shared lib whose design lives only in
|
||
[`docs/plans/2026-07-17-localdb-design.md`](docs/plans/2026-07-17-localdb-design.md).
|
||
- ⬜ Converge the Auth version pins (OtOpcUa is three minors behind at `0.1.1`).
|
||
- ⬜ Normalize the next cross-cutting component.
|