e3cf363703
MapZbLocalDbSync returns IEndpointRouteBuilder, not an IEndpointConventionBuilder, so .AllowAnonymous() cannot be chained onto it the way the other four mesh maps are (T2). app.MapGroup(string.Empty).AllowAnonymous().MapZbLocalDbSync() carries the same convention while leaving the gRPC route pattern byte-identical - verified directly (LocalDbSync_AllowAnonymousGroup_LeavesTheRoutePatternByteIdentical). The census's RealMethodEndpoints filter had to move off a "route pattern starts with /" heuristic to an exact-route-pattern-set match: inside the empty-prefix group, grpc-dotnet's own "Unimplemented service"/"Unimplemented method" fallback endpoints also gain a leading "/", so the heuristic let them back in and over-counted (caught by DriverOnly_WithASyncPort_AlsoMapsTheLocalDbSyncEndpoint and LocalDbSync_AllowAnonymousGroup_LeavesTheRoutePatternByteIdentical going red against the naive filter before this fix). All five mesh gRPC endpoints now carry IAllowAnonymous (archreview 03/S14, T3).
126 lines
7.5 KiB
C#
126 lines
7.5 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Maps the mesh gRPC ingress endpoints (artifact serve, central ack, telemetry stream, node
|
|
/// command) and the LocalDb sync endpoint.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Why AllowAnonymous (archreview 03/S14).</b> On an admin-role node
|
|
/// <c>AddOtOpcUaAuth</c> installs a global <c>FallbackPolicy = RequireAuthenticatedUser</c>
|
|
/// 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Do not "tidy" this into Program.cs.</b> A standing census guard
|
|
/// (<c>MeshGrpcAnonymousMetadataTests</c>) 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 <c>FallbackPolicy</c> silently blocked every mesh gRPC call on a
|
|
/// login-enabled admin node for seven phases of live gates.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class MeshGrpcEndpoints
|
|
{
|
|
/// <summary>
|
|
/// Maps the mesh gRPC ingress endpoints and the LocalDb sync endpoint, gated on the same
|
|
/// role + dedicated-port checks <c>Program.cs</c> used inline before this extraction.
|
|
/// </summary>
|
|
/// <param name="app">The built application to map endpoints on.</param>
|
|
/// <param name="hasAdmin">Whether this node carries the <c>admin</c> role.</param>
|
|
/// <param name="hasDriver">Whether this node carries the <c>driver</c> role.</param>
|
|
/// <param name="syncListenPort">
|
|
/// The dedicated h2c port bound for the LocalDb passive sync endpoint, or <c>0</c> when it is
|
|
/// not bound.
|
|
/// </param>
|
|
/// <param name="configServeGrpcPort">
|
|
/// The dedicated h2c port bound for the ConfigServe artifact + central command endpoints, or
|
|
/// <c>0</c> when it is not bound.
|
|
/// </param>
|
|
/// <param name="telemetryListenPort">
|
|
/// The dedicated h2c port bound for the telemetry stream + node command endpoints, or
|
|
/// <c>0</c> when it is not bound.
|
|
/// </param>
|
|
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<DeploymentArtifactGrpcService>().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<CentralCommandGrpcService>().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<TelemetryStreamGrpcService>().AllowAnonymous();
|
|
app.MapGrpcService<NodeCommandGrpcService>().AllowAnonymous();
|
|
}
|
|
}
|
|
}
|