0f6a607fa1
Mark A2 handoff and stillpending §2 adopted; note the host-side design (GatewayBrowseScopeProvider, dashboard summary projector), the lib 0.2.0 upstream changes, and caveats (NSSM deploy config, pre-existing NU1903 + IntegrationTests EventStreamService breaks). Point CLAUDE.md at the package.
216 lines
15 KiB
Markdown
216 lines
15 KiB
Markdown
# A2 — Adopt the shared `ZB.MOM.WW.GalaxyRepository` library
|
||
|
||
> **✅ ADOPTED 2026-06-25 (branch `feat/galaxyrepository-adoption`).** mxaccessgw now consumes
|
||
> `ZB.MOM.WW.GalaxyRepository` **0.2.0** and the inline Galaxy code is deleted (27 files, −2959 LOC).
|
||
> What shipped:
|
||
> - **Lib 0.2.0** (published to Gitea) closed the two real upstream gaps: **alarm-attribute discovery**
|
||
> (`GalaxyAlarmAttributeRow` + `IGalaxyRepository.GetAlarmAttributesAsync` + `AlarmAttributesSql`) and an
|
||
> injectable **`IGalaxyBrowseScopeProvider`** (default `NullGalaxyBrowseScopeProvider` = no scoping;
|
||
> HistorianGateway @ 0.1.0 unaffected) wired into the lib's gRPC service. Lib test suite: 64 green.
|
||
> - **mxaccessgw** registers `GatewayBrowseScopeProvider : IGalaxyBrowseScopeProvider` (reads the API key's
|
||
> `EffectiveConstraints.BrowseSubtrees`) before `AddZbGalaxyRepository(config,"MxGateway:Galaxy")`, maps
|
||
> `MapZbGalaxyRepository()`, and switched `GatewayGrpcScopeResolver` galaxy request types to the lib
|
||
> `.Grpc` namespace (the global authz interceptor now intercepts the lib service). The **dashboard summary
|
||
> stays host-side** (`DashboardGalaxySummaryProjector`, memoized by cache `Sequence`) since the lib entry
|
||
> doesn't carry it. Server build zero-warning; 327 targeted tests green.
|
||
> - The end-to-end host authz chain is covered (`GalaxyRepositoryHostWiringTests`), and the lib gained the
|
||
> ported browse-projector / deploy-notifier / refresh-service (Server-005 timeout guard) tests.
|
||
>
|
||
> **Caveats / follow-ups** (see "Post-adoption notes" at the bottom).
|
||
> Original handoff (now historical) follows.
|
||
|
||
> Handoff note. Written 2026-06-25 from the HistorianGateway side, where the shared lib is
|
||
> already consumed in production. This is the mxaccessgw half of the cross-repo
|
||
> "Galaxy-browse normalization" follow-on (HistorianGateway `pending.md` A2 /
|
||
> scadaproj component-normalization).
|
||
|
||
## Goal
|
||
|
||
Replace mxaccessgw's **inline** Galaxy-browse code (`src/ZB.MOM.WW.MxGateway.Server/Galaxy/**`
|
||
plus `Grpc/GalaxyRepositoryGrpcService.cs` + `Grpc/GalaxyProtoMapper.cs`) with a
|
||
`PackageReference` to the shared **`ZB.MOM.WW.GalaxyRepository`** library, so there is **one**
|
||
Galaxy-browse implementation shared by both sidecars instead of two copies that can drift.
|
||
|
||
The wire contract is **unchanged** — both repos already serve the same `galaxy_repository.v1`
|
||
proto — so **no client change is required**. This is an internal refactor.
|
||
|
||
## Why this is safe to do now
|
||
|
||
The shared lib was **extracted from this very code**: the file names under
|
||
`src/ZB.MOM.WW.MxGateway.Server/Galaxy/**` map ~1:1 onto
|
||
`scadaproj/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/**`. HistorianGateway
|
||
consumes it (`PackageReference @ 0.1.0`) and runs it live against `wonder-sql-vd03`. So the SQL
|
||
provider, hierarchy cache, deploy notifier, snapshot store, background refresh service, projectors,
|
||
glob matcher, tag lookup, and the gRPC service are **already proven** in the package — the work here
|
||
is wiring + reconciling the mxaccessgw-only pieces, not reimplementing browse.
|
||
|
||
## The clean part — 1:1 swap
|
||
|
||
These inline files are superseded by the package (delete on adoption; rebind references to namespace
|
||
`ZB.MOM.WW.GalaxyRepository`):
|
||
|
||
| mxaccessgw inline (`…Server/Galaxy/`) | Shared lib equivalent |
|
||
|---|---|
|
||
| `GalaxyRepository.cs` / `IGalaxyRepository.cs` | same |
|
||
| `GalaxyHierarchyCache.cs` / `IGalaxyHierarchyCache.cs` / `GalaxyHierarchyCacheEntry.cs` | same |
|
||
| `GalaxyHierarchyRefreshService.cs` | same (registered as `HostedService` by the lib's DI ext) |
|
||
| `GalaxyDeployNotifier.cs` / `IGalaxyDeployNotifier.cs` / `GalaxyDeployEventInfo.cs` | same |
|
||
| `GalaxyHierarchySnapshot*.cs` / `IGalaxyHierarchySnapshotStore.cs` | same |
|
||
| `GalaxyHierarchyProjector.cs` / `GalaxyBrowseProjector.cs` / `GalaxyHierarchyIndex.cs` | same |
|
||
| `GalaxyObjectView.cs` / `GalaxyHierarchyRow.cs` / `GalaxyAttributeRow.cs` | same |
|
||
| `GalaxyBrowseChildrenResult.cs` / `GalaxyHierarchyQueryResult.cs` | same |
|
||
| `GalaxyTagLookup.cs` / `GalaxyGlobMatcher.cs` / `GalaxyCacheStatus.cs` | same |
|
||
| `GalaxyRepositoryOptions.cs` | `GalaxyRepositoryOptions` (lib ships **no validator** — see below) |
|
||
| `Grpc/GalaxyRepositoryGrpcService.cs` / `Grpc/GalaxyProtoMapper.cs` | lib bundles its own `Grpc/GalaxyRepositoryGrpcService.cs` + `GalaxyProtoMapper.cs`, mapped by `MapZbGalaxyRepository()` — **but verify authz parity first, see ⚠️ below** |
|
||
| `Galaxy/GalaxyRepositoryServiceCollectionExtensions.cs` | replaced by the lib's `AddZbGalaxyRepository` / `MapZbGalaxyRepository` |
|
||
|
||
## ⚠️ The hard part — mxaccessgw-only consumers the shared lib does NOT cover
|
||
|
||
HistorianGateway was a greenfield consumer (browse + dashboard tree only). mxaccessgw has **extra
|
||
consumers of the Galaxy cache/hierarchy that the shared lib was not extracted with.** These are the
|
||
real work and the real risk:
|
||
|
||
1. **Alarm watch-list / alarm-attribute discovery — capability GAP in the lib.**
|
||
- mxaccessgw has `Galaxy/GalaxyAlarmAttributeRow.cs` and `GalaxyRepository.GetAlarmAttributesAsync()`,
|
||
consumed by `Alarms/AlarmWatchListResolver.cs` (+ `IAlarmWatchListResolver`, `GatewayAlarmMonitor`).
|
||
- The shared lib has **`GalaxyAttributeRow` + `GetAttributesAsync()` only** — there is **no**
|
||
`GalaxyAlarmAttributeRow` / `GetAlarmAttributesAsync` in `0.1.0`.
|
||
- **Decision needed:** either (a) push the alarm-attributes SQL projection **upstream** into the
|
||
shared lib (a `0.2.0` additive feature — preferred, keeps it shared) and re-consume, or
|
||
(b) keep `AlarmWatchListResolver` + the alarm-attributes query **inline** in mxaccessgw, layered
|
||
**on top of** the shared `IGalaxyRepository`/`IGalaxyHierarchyCache`. (a) is the spirit of A2.
|
||
|
||
2. **Dashboard consumers** — `Dashboard/DashboardGalaxyProjector.cs`, `DashboardBrowseService.cs`
|
||
(+ `IDashboardBrowseService`, `DashboardBrowseModel`), `DashboardSnapshotService.cs`,
|
||
`DashboardGalaxySummary.cs`, and the Razor pages `GalaxyPage.razor` / `DashboardHome.razor` all
|
||
`using ZB.MOM.WW.MxGateway.Server.Galaxy`. Mechanical rebind to `ZB.MOM.WW.GalaxyRepository`, but
|
||
confirm every type they touch is `public` in the lib (most are; double-check projector/snapshot
|
||
types).
|
||
|
||
3. **Security / authorization** — `Security/Authorization/ConstraintEnforcer.cs` and
|
||
`GatewayGrpcScopeResolver.cs` consume Galaxy hierarchy to enforce per-key constraints. Rebind, and
|
||
see the gRPC-service question below.
|
||
|
||
⚠️ **gRPC-service authz parity — VERIFIED 2026-06-25: NOT safe to swap wholesale.** The authorization
|
||
is **split across two layers**, and the part that matters is **baked into the service body**:
|
||
|
||
1. **Authentication + scope gating IS interceptor-based ✅ (safe).** `Security/Authorization/GatewayGrpcAuthorizationInterceptor.cs`
|
||
authenticates the API key and enforces the required scope; all five Galaxy RPCs map to the `metadata`
|
||
scope (`GatewayGrpcScopeResolver.cs:23-27` → `MetadataRead`). The interceptor also pushes the identity
|
||
into an ambient `IGatewayRequestIdentityAccessor` for the call. This mirrors HistorianGateway and is
|
||
shared-lib compatible.
|
||
2. **Per-key browse-subtree CONSTRAINT FILTERING is baked into the service body ❌ (the blocker).**
|
||
mxaccessgw's `Grpc/GalaxyRepositoryGrpcService.cs` injects `IGatewayRequestIdentityAccessor` and
|
||
`ResolveBrowseSubtrees()` (`:287-291`) reads `identityAccessor.Current.EffectiveConstraints.BrowseSubtrees`,
|
||
threading those globs into the projectors for `DiscoverHierarchy` (`:78`), `BrowseChildren` (`:138`),
|
||
and the `WatchDeployEvents` count scoping (`:180,196,230-237`) — i.e. it restricts **which Galaxy
|
||
objects each API key can see**. The shared lib's service injects no identity accessor and passes
|
||
`browseSubtreeGlobs: null` everywhere; its own XML doc states it *"applies no per-identity
|
||
browse-subtree filtering … Authorization (including any subtree scoping) is the responsibility of the
|
||
hosting gateway's interceptor layer."*
|
||
|
||
**Severity — silent per-key data exposure.** Glob semantics (`GalaxyHierarchyProjector.cs:225-227`):
|
||
`null`/empty → match everything; non-empty → restrict. So keys with **no** browse constraint behave
|
||
identically in both services (no regression), but a key **with** a `BrowseSubtrees` constraint would, on
|
||
the shared service, receive the **entire** Galaxy hierarchy — a `metadata`-scoped key silently bypassing
|
||
its restriction. The boundary is locked by tests that would fail against the shared service:
|
||
`Tests/Gateway/Grpc/GalaxyRepositoryGrpcServiceTests.cs` → `BrowseChildren_BrowseSubtreesConstraint_FiltersChildren`
|
||
(`:437`) and `DiscoverHierarchy_WithSubtreeRootAndDepth_FiltersDescendants` (`:90`).
|
||
|
||
**Recommendation.** Do **not** call `MapZbGalaxyRepository()` directly. The shared projectors already
|
||
accept a `browseSubtreeGlobs` param, so either fix is small plumbing:
|
||
- **Preferred (spirit of A2 — push upstream):** give the shared `GalaxyRepositoryGrpcService` an optional
|
||
browse-subtree-constraint provider (e.g. `IBrowseSubtreeConstraintProvider`/delegate returning globs —
|
||
`null` in HistorianGateway, identity-derived in mxaccessgw). One shared service, both behaviors; bundle
|
||
with the same `0.2.0` bump as the alarm-attributes gap above.
|
||
- **Fallback (isolate to mxaccessgw):** keep a thin mxaccessgw service that resolves subtrees from
|
||
identity and calls the shared (public) projectors, and map **that** instead of `MapZbGalaxyRepository()`.
|
||
|
||
## Concrete wiring steps (template from the HistorianGateway side)
|
||
|
||
1. **`nuget.config`** — mxaccessgw's `packageSourceMapping` for the `dohertj2-gitea` source currently
|
||
lists `ZB.MOM.WW.*` up to `Theme` but **NOT** GalaxyRepository. Add:
|
||
```xml
|
||
<package pattern="ZB.MOM.WW.GalaxyRepository" />
|
||
```
|
||
under the `dohertj2-gitea` `<packageSource>`. (Easy to miss — restore will fail without it.)
|
||
|
||
2. **Server `.csproj`** — add:
|
||
```xml
|
||
<PackageReference Include="ZB.MOM.WW.GalaxyRepository" Version="0.1.0" />
|
||
```
|
||
(or whatever version ships the alarm-attributes add, if you go route 1(a) above.)
|
||
|
||
3. **DI + endpoint wiring** (in mxaccessgw's host composition — `GatewayApplication.cs` / its
|
||
`Program`), mirroring HistorianGateway's `Program.cs`:
|
||
```csharp
|
||
using ZB.MOM.WW.GalaxyRepository;
|
||
using ZB.MOM.WW.GalaxyRepository.DependencyInjection;
|
||
|
||
// service registration — bind from mxaccessgw's section path (NOT top-level "Galaxy"):
|
||
builder.Services.AddZbGalaxyRepository(builder.Configuration, "MxGateway:Galaxy"); // confirm the exact section path mxaccessgw uses today
|
||
|
||
// endpoint pipeline (after AddGrpc):
|
||
app.MapZbGalaxyRepository();
|
||
```
|
||
Delete mxaccessgw's own `Galaxy/GalaxyRepositoryServiceCollectionExtensions.cs` registrations.
|
||
|
||
4. **Option validation** — the shared lib **binds only, ships no validator** (deliberate). mxaccessgw
|
||
already validates Galaxy options via `Configuration/GatewayOptionsValidator.cs` — **keep that**; it
|
||
stays the owner of fail-fast validation, exactly as HistorianGateway's `ConfigPreflight` does.
|
||
|
||
5. **Health check** — keep mxaccessgw's existing Galaxy-SQL readiness check; read the connection
|
||
string from the same `MxGateway:Galaxy` section the lib binds (HistorianGateway does this with a raw
|
||
`SELECT 1` `SqlConnectionHealthCheck` — see `Program.cs:196`).
|
||
|
||
## Tests
|
||
|
||
mxaccessgw has a large `Tests/Galaxy/**` suite that **duplicates the shared lib's own tests**
|
||
(`GalaxyHierarchyCacheTests`, `GalaxyHierarchyProjectorTests`, `GalaxyHierarchySnapshotStoreTests`,
|
||
`GalaxyProtoMapperTests`, `GalaxyHierarchyIndexTests`). On adoption these are covered upstream and can
|
||
be **deleted**. **Keep** the mxaccessgw-specific ones that exercise behavior the lib doesn't own:
|
||
`GalaxyAlarmAttributeMappingTests`, `GalaxyFilterInputSafetyTests`, `GalaxyRepositoryGrpcServiceTests`
|
||
(unless their subjects move upstream too), and the live `IntegrationTests/Galaxy/**`.
|
||
|
||
## Suggested order — all DONE (2026-06-25)
|
||
|
||
1. ~~Verify the **gRPC-service authz parity** question~~ **DONE:** wholesale swap was unsafe — per-key
|
||
browse-subtree filtering was baked into the service body. Resolved by pushing an injectable
|
||
`IGalaxyBrowseScopeProvider` hook **upstream** into the lib (option preferred in the ⚠️ block).
|
||
2. ~~Decide alarm-attributes~~ **DONE:** upstreamed into the lib as part of `0.2.0`.
|
||
3. ~~Upstream + publish + bump~~ **DONE:** lib `0.1.0 → 0.2.0`, packed and pushed to the Gitea feed (verified live).
|
||
4. ~~`nuget.config` + `csproj` + DI/endpoint wiring; delete inline; rebind~~ **DONE.**
|
||
5. ~~Delete duplicated tests; build zero-warning; run the suite~~ **DONE** (lib 64 green; gateway 327 targeted green).
|
||
Live-validate browse + alarm watch-list is the one remaining **manual** step (needs Galaxy SQL + a running
|
||
gateway — opt-in `MXGATEWAY_RUN_LIVE_GALAXY_TESTS=1`); not runnable from the dev Mac.
|
||
6. **Remaining:** propagate to the scadaproj umbrella index + HistorianGateway's `pending.md` §A2 (mark adopted)
|
||
— cross-repo, do in those repos.
|
||
|
||
## Post-adoption notes / caveats
|
||
|
||
- **Deployment config (NSSM):** the deployed services (`MxAccessGw` on 10.100.0.48; the wonder host) read
|
||
config from **NSSM environment variables, not `appsettings.json`**. The lib's `SnapshotCachePath` default
|
||
is empty (persistence no-ops). `appsettings.json` sets `MxGateway:Galaxy:SnapshotCachePath` +
|
||
`PersistSnapshot`, but the deployments must carry `MxGateway__Galaxy__SnapshotCachePath` and
|
||
`MxGateway__Galaxy__PersistSnapshot` in their NSSM env on redeploy, or snapshot persistence silently
|
||
no-ops in production.
|
||
- **Pre-existing NU1903 (unrelated):** adding the package surfaced a transitive `SQLitePCLRaw.lib.e_sqlite3`
|
||
2.1.11 advisory (GHSA-2m69-gcr7-jv3q, no upstream patch) that breaks the build under `TreatWarningsAsErrors`
|
||
— already red on `main`. Resolved with a targeted `NuGetAuditSuppress` in `src/Directory.Build.props`
|
||
(its own commit). Remove the suppression once a patched e_sqlite3 ships.
|
||
- **Pre-existing IntegrationTests break (unrelated, NOT fixed here):** `IntegrationTests/WorkerLiveMxAccessSmokeTests.cs`
|
||
constructs `EventStreamService` with 6 ctor args, but a prior event-stream refactor reduced that ctor — so
|
||
the IntegrationTests project does not compile (already broken on `main`, independent of Galaxy). The Galaxy
|
||
live tests there were rebound to the lib and compile in isolation, but the project won't build until that
|
||
unrelated call site is fixed. Track separately.
|
||
- **No republish needed for the lib test additions:** the browse-projector / deploy-notifier / refresh-service
|
||
tests were added to the lib AFTER 0.2.0 was published; tests aren't shipped, so 0.2.0 is unchanged.
|
||
|
||
## Reference pointers
|
||
|
||
- **Working consumer (copy this):** `~/Desktop/HistorianGateway/src/ZB.MOM.WW.HistorianGateway.Server/Program.cs`
|
||
(`AddZbGalaxyRepository` @ ~line 174, `MapZbGalaxyRepository` @ ~line 314) + its `nuget.config` + Server `.csproj`.
|
||
- **Shared lib source:** `~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository/` (DI ext at
|
||
`src/ZB.MOM.WW.GalaxyRepository/DependencyInjection/GalaxyRepositoryServiceCollectionExtensions.cs`).
|
||
- **Tracked in:** HistorianGateway `pending.md` §A2 + `CLAUDE.md` → Known Follow-Ons; scadaproj component normalization.
|