fix(host): LocalDb sync endpoint opts out of the cookie fallback via an empty-prefix anon group
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).
This commit is contained in:
@@ -64,9 +64,16 @@ public static class MeshGrpcEndpoints
|
|||||||
// default-OFF or admin-only graph carries no sync surface whatsoever. Mapping it would be
|
// default-OFF or admin-only graph carries no sync surface whatsoever. Mapping it would be
|
||||||
// safe even unauthenticated — LocalDbSyncAuthInterceptor fail-closes with no ApiKey
|
// safe even unauthenticated — LocalDbSyncAuthInterceptor fail-closes with no ApiKey
|
||||||
// configured — but not mapping it at all is the stronger default.
|
// 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)
|
if (hasDriver && syncListenPort > 0)
|
||||||
{
|
{
|
||||||
app.MapZbLocalDbSync();
|
app.MapGroup(string.Empty).AllowAnonymous().MapZbLocalDbSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Central config-serve artifact endpoint (per-cluster mesh Phase 3). Mapped only on
|
// Central config-serve artifact endpoint (per-cluster mesh Phase 3). Mapped only on
|
||||||
|
|||||||
+31
-16
@@ -26,11 +26,13 @@ namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
|
|||||||
/// <para>
|
/// <para>
|
||||||
/// <b>Filters out grpc-dotnet's own catch-all infrastructure.</b> Every <c>MapGrpcService</c>
|
/// <b>Filters out grpc-dotnet's own catch-all infrastructure.</b> Every <c>MapGrpcService</c>
|
||||||
/// call also registers a per-service "Unimplemented method for X" fallback, and the FIRST
|
/// call also registers a per-service "Unimplemented method for X" fallback, and the FIRST
|
||||||
/// such call in an app additionally registers one shared "Unimplemented service" catch-all —
|
/// such call in an app additionally registers one shared "Unimplemented service" catch-all.
|
||||||
/// neither carries a route pattern beginning with <c>/</c>, and neither ever reaches real
|
/// Neither ever reaches real logic (they always answer <c>Unimplemented</c> regardless of
|
||||||
/// logic (they always answer <c>Unimplemented</c> regardless of caller identity), so their
|
/// caller identity), so their authorization metadata carries no security meaning.
|
||||||
/// authorization metadata carries no security meaning. <see cref="RealMethodEndpoints"/>
|
/// <see cref="RealMethodEndpoints"/> keeps only the five real, callable RPC methods, matched
|
||||||
/// keeps only the five real, callable RPC methods.
|
/// against the exact known route-pattern set rather than a route-pattern-shape heuristic —
|
||||||
|
/// see that method's remarks for why a shape-based filter is NOT safe once LocalDbSync's
|
||||||
|
/// empty-prefix <c>MapGroup</c> (T3) is in play.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <b>Guard-of-the-guard.</b> Every assertion below pins an EXACT expected count and the
|
/// <b>Guard-of-the-guard.</b> Every assertion below pins an EXACT expected count and the
|
||||||
@@ -140,19 +142,30 @@ public sealed class MeshGrpcAnonymousMetadataTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Fused_AllFourGrpcServiceMapsCarryAllowAnonymous_LocalDbSyncIsNotYetOwnedHere()
|
public void Fused_AllFiveMeshEndpoints_CarryAllowAnonymous()
|
||||||
{
|
{
|
||||||
// T3 owns the LocalDbSync map (MapZbLocalDbSync returns IEndpointRouteBuilder, not
|
// Includes LocalDbSync (T3): MapZbLocalDbSync returns IEndpointRouteBuilder, not an
|
||||||
// IEndpointConventionBuilder, so .AllowAnonymous() cannot be chained onto it the same way).
|
// IEndpointConventionBuilder, so .AllowAnonymous() cannot be chained onto it the way the
|
||||||
// Asserting its ABSENCE here — rather than skipping it — is what makes this test go red the
|
// other four are — it needs the empty-prefix MapGroup wrapper instead.
|
||||||
// moment T3's fix lands with the wrong scope (e.g. anonymising nothing, or the group leaking
|
|
||||||
// beyond LocalDbSync) instead of silently starting to pass for the wrong reason.
|
|
||||||
var endpoints = RealMethodEndpoints(BuildApp(
|
var endpoints = RealMethodEndpoints(BuildApp(
|
||||||
hasAdmin: true, hasDriver: true,
|
hasAdmin: true, hasDriver: true,
|
||||||
syncListenPort: 4054, configServeGrpcPort: 4055, telemetryListenPort: 4056));
|
syncListenPort: 4054, configServeGrpcPort: 4055, telemetryListenPort: 4056));
|
||||||
|
|
||||||
AnonymousPatternsOf(endpoints).ShouldBe(
|
AnonymousPatternsOf(endpoints).ShouldBe(AllFivePrefixes, ignoreOrder: true);
|
||||||
[ArtifactFetch, CentralReportApply, TelemetrySubscribe, NodeExecute], ignoreOrder: true);
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LocalDbSync_AllowAnonymousGroup_LeavesTheRoutePatternByteIdentical()
|
||||||
|
{
|
||||||
|
// The empty-prefix MapGroup wrapper (T3) must be pure convention-carrying — it must not
|
||||||
|
// change the gRPC route pattern LocalDbSyncAuthInterceptor and the sync client both key on.
|
||||||
|
var endpoints = RealMethodEndpoints(BuildApp(
|
||||||
|
hasAdmin: false, hasDriver: true,
|
||||||
|
syncListenPort: 4054, configServeGrpcPort: 0, telemetryListenPort: 0));
|
||||||
|
|
||||||
|
var sync = endpoints.ShouldHaveSingleItem();
|
||||||
|
sync.RoutePattern.RawText.ShouldBe(LocalDbSyncSync);
|
||||||
|
sync.Metadata.GetMetadata<IAllowAnonymous>().ShouldNotBeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -205,14 +218,16 @@ public sealed class MeshGrpcAnonymousMetadataTests
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The real, callable RPC method endpoints — excludes grpc-dotnet's per-app "Unimplemented
|
/// The real, callable RPC method endpoints — excludes grpc-dotnet's per-app "Unimplemented
|
||||||
/// service" catch-all and per-service "Unimplemented method for X" fallback (see class
|
/// service" catch-all and per-service "Unimplemented method for X" fallback (see class
|
||||||
/// remarks). Both carry a route pattern that never starts with <c>/</c>, which is what this
|
/// remarks). Filtered against the exact set of known method route patterns rather than a
|
||||||
/// filters on.
|
/// "starts with /" heuristic: inside the empty-prefix <c>MapGroup</c> the LocalDbSync map
|
||||||
|
/// (T3) uses, grpc-dotnet's own fallback patterns ALSO gain a leading <c>/</c>, so a
|
||||||
|
/// prefix-shaped filter would let them back in.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static List<RouteEndpoint> RealMethodEndpoints(WebApplication app) =>
|
private static List<RouteEndpoint> RealMethodEndpoints(WebApplication app) =>
|
||||||
((IEndpointRouteBuilder)app).DataSources
|
((IEndpointRouteBuilder)app).DataSources
|
||||||
.SelectMany(d => d.Endpoints)
|
.SelectMany(d => d.Endpoints)
|
||||||
.OfType<RouteEndpoint>()
|
.OfType<RouteEndpoint>()
|
||||||
.Where(e => e.RoutePattern.RawText?.StartsWith('/') == true)
|
.Where(e => AllFivePrefixes.Contains(e.RoutePattern.RawText))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
private static string RoutePatternOf(RouteEndpoint e) => e.RoutePattern.RawText!;
|
private static string RoutePatternOf(RouteEndpoint e) => e.RoutePattern.RawText!;
|
||||||
|
|||||||
Reference in New Issue
Block a user