using ZB.MOM.WW.LocalDb.Replication; using ZB.MOM.WW.OtOpcUa.AdminUI.Grpc; using ZB.MOM.WW.OtOpcUa.Host.Grpc; namespace ZB.MOM.WW.OtOpcUa.Host.Configuration; /// /// Maps the mesh gRPC ingress endpoints (artifact serve, central ack, telemetry stream, node /// command) and the LocalDb sync endpoint. /// /// /// /// Why AllowAnonymous (archreview 03/S14). On an admin-role node /// AddOtOpcUaAuth installs a global FallbackPolicy = RequireAuthenticatedUser /// under the cookie scheme, which the authorization middleware applies to EVERY endpoint /// without explicit authorization metadata — gRPC included. These endpoints authenticate /// with a per-cluster PSK bearer inside a gRPC interceptor, which runs INSIDE the endpoint /// invocation, i.e. after that middleware. Without this opt-out a correctly-keyed node call /// is challenged by the cookie handler before the interceptor's verdict matters: artifact /// fetches fail, deploy acks are dropped, deployments seal TimedOut. Anonymous is safe here /// because all five interceptors fail CLOSED — an unset key rejects every call and the /// presented token is compared with FixedTimeEquals. /// /// /// Do not "tidy" this into Program.cs. A standing census guard /// (MeshGrpcAnonymousMetadataTests) drives THIS method, so a map that moves back /// inline leaves the guard asserting a shape production no longer has — which is exactly /// how the AdminUI cookie FallbackPolicy silently blocked every mesh gRPC call on a /// login-enabled admin node for seven phases of live gates. /// /// public static class MeshGrpcEndpoints { /// /// Maps the mesh gRPC ingress endpoints and the LocalDb sync endpoint, gated on the same /// role + dedicated-port checks Program.cs used inline before this extraction. /// /// The built application to map endpoints on. /// Whether this node carries the admin role. /// Whether this node carries the driver role. /// /// The dedicated h2c port bound for the LocalDb passive sync endpoint, or 0 when it is /// not bound. /// /// /// The dedicated h2c port bound for the ConfigServe artifact + central command endpoints, or /// 0 when it is not bound. /// /// /// The dedicated h2c port bound for the telemetry stream + node command endpoints, or /// 0 when it is not bound. /// public static void MapOtOpcUaMeshGrpcServices( this WebApplication app, bool hasAdmin, bool hasDriver, int syncListenPort, int configServeGrpcPort, int telemetryListenPort) { ArgumentNullException.ThrowIfNull(app); // Passive LocalDb sync endpoint. Gated on the same port check that bound the listener, so a // default-OFF or admin-only graph carries no sync surface whatsoever. Mapping it would be // safe even unauthenticated — LocalDbSyncAuthInterceptor fail-closes with no ApiKey // configured — but not mapping it at all is the stronger default. // // AllowAnonymous (03/S14) via an empty-prefix MapGroup: MapZbLocalDbSync returns // IEndpointRouteBuilder, NOT an IEndpointConventionBuilder, so .AllowAnonymous() cannot be // chained onto it the way the other four maps below are. An empty-prefix group carries the // AllowAnonymous convention to whatever it maps while leaving the gRPC route pattern // byte-identical to the ungrouped map (verified by // LocalDbSync_AllowAnonymousGroup_LeavesTheRoutePatternByteIdentical). if (hasDriver && syncListenPort > 0) { app.MapGroup(string.Empty).AllowAnonymous().MapZbLocalDbSync(); } // Central config-serve artifact endpoint (per-cluster mesh Phase 3). Mapped only on // admin-role nodes (which hold the central SQL connection) and only when a dedicated h2c // port is bound. The ConfigServeAuthInterceptor is the ONLY inbound auth and fail-closes // with no ConfigServe:ApiKey. if (hasAdmin && configServeGrpcPort > 0) { // AllowAnonymous (03/S14): this is every FetchAndCache node's artifact fetch. Without the // opt-out the AdminUI cookie FallbackPolicy 401s/redirects it before // ConfigServeAuthInterceptor's verdict matters — a fleet-wide deploy-plane outage on any // login-enabled admin node. app.MapGrpcService().AllowAnonymous(); // Central command ingress (mesh gRPC command plane): the node→central deployment ack. It // rides the SAME listener rather than claiming a port of its own — a second cleartext h2c // port would buy nothing but another firewall rule, and both endpoints are admin-role and // dialled by the same driver nodes. Deliberately NOT gated on MeshTransport:Mode: mapping // it under Dps costs nothing (nothing dials it) whereas gating it would mean flipping the // mode on a running fleet requires a central restart before any node can ack — the same // reasoning that registers CentralCommunicationActor in both modes. // CentralCommandAuthInterceptor (added at AddGrpc in Program.cs) is the ONLY inbound auth // and fail-closes per cluster. AllowAnonymous (03/S14): this is every node's deploy ack, // from every cluster — same reasoning as the artifact fetch above. app.MapGrpcService().AllowAnonymous(); } // Driver-node live-telemetry stream (per-cluster mesh Phase 5). Central dials in; gated by // TelemetryStreamAuthInterceptor (fail-closed on empty Telemetry:ApiKey). // // The node-side command endpoint (mesh gRPC command plane, Phase 1) rides the SAME // driver-role h2c listener rather than claiming a fourth port: central already dials this // node here, both directions are per-cluster-keyed, and a separate port would be one more // thing to open on every site firewall for no isolation gain. Mapping it is what makes // MeshCommandAuthInterceptor live — it is registered in Program.cs and is a pass-through // until a call matches this service's method prefix. It fail-closes with no // MeshTransport:GrpcPsk, so an operator who maps the service without setting a key gets a // closed endpoint, not an open one. if (hasDriver && telemetryListenPort > 0) { // AllowAnonymous (03/S14) on both: on a fused admin+driver node (the docker-dev central // pair, and any production fused central) these sit behind the SAME cookie FallbackPolicy // as the ConfigServe endpoints above and would be 401/redirected identically without the // opt-out — central's own telemetry dialer and node-command dispatch to its own pair. app.MapGrpcService().AllowAnonymous(); app.MapGrpcService().AllowAnonymous(); } } }