Files
mxaccessgw/A2-galaxyrepository-adoption-handoff.md
T
Joseph Doherty 0f6a607fa1 docs: record Galaxy library adoption (0.2.0) complete + caveats
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.
2026-06-25 12:36:15 -04:00

15 KiB
Raw Blame History

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 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 — 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.