From e2f187a0214579aad0268c8c62ade64f1bd5e1de Mon Sep 17 00:00:00 2001 From: Daniel O'Callaghan Date: Thu, 23 Oct 2025 14:10:28 +0200 Subject: [PATCH] Fix MethodDescriptor lookup in ManualGrpcSecurityMetadataSource for gRPC 1.75.0+ ## Problem With gRPC 1.75.0+, MethodDescriptor instances from ProtoReflectionService don't match instances from ServiceDescriptor.getMethods() when used as HashMap keys. This causes security configuration lookups to fail, resulting in ServerReflection and other services requiring authentication even when configured with AccessPredicate.permitAll(). ## Root Cause gRPC 1.75.0 changed MethodDescriptor equality/identity behavior. When ProtoReflectionService creates MethodDescriptor instances, they are not the same object instances returned by ServerReflectionGrpc.getServiceDescriptor(), causing HashMap lookups by object identity to fail. ## Solution Use method.getFullMethodName() (String) as the HashMap key instead of relying on MethodDescriptor object identity. This provides stable, reliable lookups across all MethodDescriptor instances for the same method. Changes: - Added accessMapByName HashMap using String (fullMethodName) as key - Updated getAttributes() to try name-based lookup first - Maintained backward compatibility with existing object-based lookup - Updated all set() and remove() methods to populate both maps - Added debug logging for troubleshooting ## Backward Compatibility The fix maintains both lookup mechanisms (name-based and object-based) to ensure existing code continues to work. The name-based lookup is attempted first as it's more reliable with gRPC 1.75.0+. ## Testing Verified with gRPC 1.75.0 that: - ServerReflection works without authentication when configured with permitAll() - Health checks continue to work as expected - Authenticated services still require authentication - Both lookup mechanisms function correctly --- .../ManualGrpcSecurityMetadataSource.java | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/grpc-server-spring-boot-starter/src/main/java/net/devh/boot/grpc/server/security/check/ManualGrpcSecurityMetadataSource.java b/grpc-server-spring-boot-starter/src/main/java/net/devh/boot/grpc/server/security/check/ManualGrpcSecurityMetadataSource.java index 042a3616b..0f1b4e721 100644 --- a/grpc-server-spring-boot-starter/src/main/java/net/devh/boot/grpc/server/security/check/ManualGrpcSecurityMetadataSource.java +++ b/grpc-server-spring-boot-starter/src/main/java/net/devh/boot/grpc/server/security/check/ManualGrpcSecurityMetadataSource.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.ConfigAttribute; @@ -31,6 +32,7 @@ import io.grpc.MethodDescriptor; import io.grpc.ServerCall; import io.grpc.ServiceDescriptor; +import lombok.extern.slf4j.Slf4j; /** * A {@link GrpcSecurityMetadataSource} for manual configuration. For each {@link MethodDescriptor gRPC method} a @@ -43,13 +45,34 @@ * * @author Daniel Theuke (daniel.theuke@aequitas-software.de) */ +@Slf4j public final class ManualGrpcSecurityMetadataSource extends AbstractGrpcSecurityMetadataSource { + // Use String (fullMethodName) as key to avoid MethodDescriptor identity issues with gRPC 1.75.0+ + private final Map> accessMapByName = new HashMap<>(); + // Keep legacy map for backward compatibility private final Map, Collection> accessMap = new HashMap<>(); private Collection defaultAttributes = wrap(AccessPredicate.denyAll()); private Collection getAttributes(final MethodDescriptor method) { - return this.accessMap.getOrDefault(method, this.defaultAttributes); + // Try name-based lookup first (more reliable with gRPC 1.75.0+) + final String fullMethodName = method.getFullMethodName(); + Collection attrs = this.accessMapByName.get(fullMethodName); + + if (attrs != null) { + log.debug("Found security config for method by name: {}", fullMethodName); + return attrs; + } + + // Fallback to object-based lookup for backward compatibility + attrs = this.accessMap.get(method); + if (attrs != null) { + log.debug("Found security config for method by descriptor: {}", fullMethodName); + return attrs; + } + + log.debug("No security config found for method {}, using default", fullMethodName); + return this.defaultAttributes; } @Override @@ -59,7 +82,10 @@ public Collection getAttributes(final ServerCall call) { @Override public Collection getAllConfigAttributes() { - return this.accessMap.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()); + // Combine both maps to ensure we get all config attributes + return Stream.concat( + this.accessMap.values().stream(), + this.accessMapByName.values().stream()).flatMap(Collection::stream).collect(Collectors.toSet()); } /** @@ -76,6 +102,7 @@ public ManualGrpcSecurityMetadataSource set(final ServiceDescriptor service, fin final Collection wrappedPredicate = wrap(predicate); for (final MethodDescriptor method : service.getMethods()) { this.accessMap.put(method, wrappedPredicate); + this.accessMapByName.put(method.getFullMethodName(), wrappedPredicate); } return this; } @@ -92,6 +119,7 @@ public ManualGrpcSecurityMetadataSource remove(final ServiceDescriptor service) requireNonNull(service, "service"); for (final MethodDescriptor method : service.getMethods()) { this.accessMap.remove(method); + this.accessMapByName.remove(method.getFullMethodName()); } return this; } @@ -106,7 +134,9 @@ public ManualGrpcSecurityMetadataSource remove(final ServiceDescriptor service) */ public ManualGrpcSecurityMetadataSource set(final MethodDescriptor method, final AccessPredicate predicate) { requireNonNull(method, "method"); - this.accessMap.put(method, wrap(predicate)); + final Collection wrappedPredicate = wrap(predicate); + this.accessMap.put(method, wrappedPredicate); + this.accessMapByName.put(method.getFullMethodName(), wrappedPredicate); return this; } @@ -120,6 +150,7 @@ public ManualGrpcSecurityMetadataSource set(final MethodDescriptor method, public ManualGrpcSecurityMetadataSource remove(final MethodDescriptor method) { requireNonNull(method, "method"); this.accessMap.remove(method); + this.accessMapByName.remove(method.getFullMethodName()); return this; }