Verified the A2 gRPC-service authz-parity question: a wholesale swap to MapZbGalaxyRepository() is unsafe because per-key browse-subtree filtering is baked into mxaccessgw's service body. Records the verdict in the A2 handoff + stillpending §2. Adds the approved design for closing the upstream gaps in ZB.MOM.WW.GalaxyRepository 0.2.0 (alarm-attribute discovery + an injectable browse-subtree scope provider; dashboard summary stays host-side) and the full mxaccessgw adoption.
12 KiB
A2 — Adopt the shared ZB.MOM.WW.GalaxyRepository library
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
Verify the gRPC-service authz parity questionDONE (2026-06-25): wholesale swap is unsafe — per-key browse-subtree filtering is baked into the service body. The service must keep an mxaccessgw subtree-scoping hook (push the provider upstream, or wrap). See the ⚠️ block above.- Decide alarm-attributes: upstream into the lib (
0.2.0) vs keep inline on top of shared interfaces. - If upstreaming: do that in
scadaproj/ZB.MOM.WW.GalaxyRepositoryfirst, publish, bump the version. nuget.config+csproj+ DI/endpoint wiring; delete the superseded inline files; rebind namespaces in dashboard/alarms/security consumers.- Delete duplicated tests; build zero-warning; run the suite; live-validate browse + alarm watch-list.
- Propagate to the scadaproj umbrella index + HistorianGateway's
pending.mdA2 (mark adopted).
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.