Files
mxaccessgw/A2-galaxyrepository-adoption-handoff.md
T
Joseph Doherty b7e2214341 galaxy-adoption: verify gRPC authz-parity gate + design upstream gaps (0.2.0)
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.
2026-06-25 10:23:07 -04:00

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.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 consumersDashboard/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 / authorizationSecurity/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-27MetadataRead). 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.csBrowseChildren_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:

    <package pattern="ZB.MOM.WW.GalaxyRepository" />
    

    under the dohertj2-gitea <packageSource>. (Easy to miss — restore will fail without it.)

  2. 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.)

  3. DI + endpoint wiring (in mxaccessgw's host composition — GatewayApplication.cs / its Program), mirroring HistorianGateway's Program.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.cs registrations.

  4. Option validation — the shared lib binds only, ships no validator (deliberate). mxaccessgw already validates Galaxy options via Configuration/GatewayOptionsValidator.cskeep 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

  1. Verify the gRPC-service authz parity question DONE (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.
  2. Decide alarm-attributes: upstream into the lib (0.2.0) vs keep inline on top of shared interfaces.
  3. If upstreaming: do that in scadaproj/ZB.MOM.WW.GalaxyRepository first, publish, bump the version.
  4. nuget.config + csproj + DI/endpoint wiring; delete the superseded inline files; rebind namespaces in dashboard/alarms/security consumers.
  5. Delete duplicated tests; build zero-warning; run the suite; live-validate browse + alarm watch-list.
  6. Propagate to the scadaproj umbrella index + HistorianGateway's pending.md A2 (mark adopted).

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.