15 KiB
A2 — Adopt the shared ZB.MOM.WW.GalaxyRepository library
✅ ADOPTED 2026-06-25 (branch
feat/galaxyrepository-adoption). mxaccessgw now consumesZB.MOM.WW.GalaxyRepository0.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 injectableIGalaxyBrowseScopeProvider(defaultNullGalaxyBrowseScopeProvider= 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'sEffectiveConstraints.BrowseSubtrees) beforeAddZbGalaxyRepository(config,"MxGateway:Galaxy"), mapsMapZbGalaxyRepository(), and switchedGatewayGrpcScopeResolvergalaxy request types to the lib.Grpcnamespace (the global authz interceptor now intercepts the lib service). The dashboard summary stays host-side (DashboardGalaxySummaryProjector, memoized by cacheSequence) 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.mdA2 / 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:
-
Alarm watch-list / alarm-attribute discovery — capability GAP in the lib.
- mxaccessgw has
Galaxy/GalaxyAlarmAttributeRow.csandGalaxyRepository.GetAlarmAttributesAsync(), consumed byAlarms/AlarmWatchListResolver.cs(+IAlarmWatchListResolver,GatewayAlarmMonitor). - The shared lib has
GalaxyAttributeRow+GetAttributesAsync()only — there is noGalaxyAlarmAttributeRow/GetAlarmAttributesAsyncin0.1.0. - Decision needed: either (a) push the alarm-attributes SQL projection upstream into the
shared lib (a
0.2.0additive feature — preferred, keeps it shared) and re-consume, or (b) keepAlarmWatchListResolver+ the alarm-attributes query inline in mxaccessgw, layered on top of the sharedIGalaxyRepository/IGalaxyHierarchyCache. (a) is the spirit of A2.
- mxaccessgw has
-
Dashboard consumers —
Dashboard/DashboardGalaxyProjector.cs,DashboardBrowseService.cs(+IDashboardBrowseService,DashboardBrowseModel),DashboardSnapshotService.cs,DashboardGalaxySummary.cs, and the Razor pagesGalaxyPage.razor/DashboardHome.razorallusing ZB.MOM.WW.MxGateway.Server.Galaxy. Mechanical rebind toZB.MOM.WW.GalaxyRepository, but confirm every type they touch ispublicin the lib (most are; double-check projector/snapshot types). -
Security / authorization —
Security/Authorization/ConstraintEnforcer.csandGatewayGrpcScopeResolver.csconsume 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:
- Authentication + scope gating IS interceptor-based ✅ (safe).
Security/Authorization/GatewayGrpcAuthorizationInterceptor.csauthenticates the API key and enforces the required scope; all five Galaxy RPCs map to themetadatascope (GatewayGrpcScopeResolver.cs:23-27→MetadataRead). The interceptor also pushes the identity into an ambientIGatewayRequestIdentityAccessorfor the call. This mirrors HistorianGateway and is shared-lib compatible. - Per-key browse-subtree CONSTRAINT FILTERING is baked into the service body ❌ (the blocker).
mxaccessgw's
Grpc/GalaxyRepositoryGrpcService.csinjectsIGatewayRequestIdentityAccessorandResolveBrowseSubtrees()(:287-291) readsidentityAccessor.Current.EffectiveConstraints.BrowseSubtrees, threading those globs into the projectors forDiscoverHierarchy(:78),BrowseChildren(:138), and theWatchDeployEventscount 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 passesbrowseSubtreeGlobs: nulleverywhere; 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
GalaxyRepositoryGrpcServicean optional browse-subtree-constraint provider (e.g.IBrowseSubtreeConstraintProvider/delegate returning globs —nullin HistorianGateway, identity-derived in mxaccessgw). One shared service, both behaviors; bundle with the same0.2.0bump 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)
-
nuget.config— mxaccessgw'spackageSourceMappingfor thedohertj2-giteasource currently listsZB.MOM.WW.*up toThemebut NOT GalaxyRepository. Add:<package pattern="ZB.MOM.WW.GalaxyRepository" />under the
dohertj2-gitea<packageSource>. (Easy to miss — restore will fail without it.) -
Server
.csproj— add:<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.)
-
DI + endpoint wiring (in mxaccessgw's host composition —
GatewayApplication.cs/ itsProgram), mirroring HistorianGateway'sProgram.cs: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.csregistrations. -
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'sConfigPreflightdoes. -
Health check — keep mxaccessgw's existing Galaxy-SQL readiness check; read the connection string from the same
MxGateway:Galaxysection the lib binds (HistorianGateway does this with a rawSELECT 1SqlConnectionHealthCheck— seeProgram.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)
Verify the gRPC-service authz parity questionDONE: wholesale swap was unsafe — per-key browse-subtree filtering was baked into the service body. Resolved by pushing an injectableIGalaxyBrowseScopeProviderhook upstream into the lib (option preferred in the ⚠️ block).Decide alarm-attributesDONE: upstreamed into the lib as part of0.2.0.Upstream + publish + bumpDONE: lib0.1.0 → 0.2.0, packed and pushed to the Gitea feed (verified live).DONE.nuget.config+csproj+ DI/endpoint wiring; delete inline; rebindDelete duplicated tests; build zero-warning; run the suiteDONE (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-inMXGATEWAY_RUN_LIVE_GALAXY_TESTS=1); not runnable from the dev Mac.- 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 (
MxAccessGwon 10.100.0.48; the wonder host) read config from NSSM environment variables, notappsettings.json. The lib'sSnapshotCachePathdefault is empty (persistence no-ops).appsettings.jsonsetsMxGateway:Galaxy:SnapshotCachePath+PersistSnapshot, but the deployments must carryMxGateway__Galaxy__SnapshotCachePathandMxGateway__Galaxy__PersistSnapshotin their NSSM env on redeploy, or snapshot persistence silently no-ops in production. - Pre-existing NU1903 (unrelated) — ✅ RESOLVED (2026-07-18, commit
2f0cfe3): adding the package surfaced a transitiveSQLitePCLRaw.lib.e_sqlite32.1.11 advisory (GHSA-2m69-gcr7-jv3q, at the time no upstream patch) that breaks the build underTreatWarningsAsErrors— already red onmain. Initially resolved with a targetedNuGetAuditSuppressinsrc/Directory.Build.props(its own commit). The patched e_sqlite3 (2.1.12) has since shipped: the suppression was removed and the patched native lib pinned intentionally (src/Directory.Build.propsnow documents this in place of the suppression). - Pre-existing IntegrationTests break (unrelated, NOT fixed here) — ✅ RESOLVED since:
IntegrationTests/WorkerLiveMxAccessSmokeTests.csconstructedEventStreamServicewith 6 ctor args, but a prior event-stream refactor reduced that ctor — so the IntegrationTests project did not compile (already broken onmain, independent of Galaxy). The call site has since been fixed to match the 3-arg ctor (sessionManager, options, metrics—Grpc/EventStreamService.cs:11-14; call site atWorkerLiveMxAccessSmokeTests.cs:~1580) and the project compiles (verified 2026-08-07). - 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) + itsnuget.config+ Server.csproj. - Shared lib source:
~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository/(DI ext atsrc/ZB.MOM.WW.GalaxyRepository/DependencyInjection/GalaxyRepositoryServiceCollectionExtensions.cs). - Tracked in: HistorianGateway
pending.md§A2 +CLAUDE.md→ Known Follow-Ons; scadaproj component normalization.