From 5433d422c05f83298d601ef2bc0890fcc55b4415 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Thu, 23 Jan 2025 13:03:07 +0000 Subject: [PATCH] Add support for events and anonymous --- .../grpc/sample/GrpcServerApplication.java | 4 +- .../sample/GrpcServerApplicationTests.java | 45 ++++++++++++++++++- .../AuthenticationServerInterceptor.java | 6 ++- .../grpc/server/security/CallContext.java | 5 +++ .../security/RequestMapperConfigurer.java | 41 ++++++++++++++--- 5 files changed, 91 insertions(+), 10 deletions(-) diff --git a/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java b/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java index 7a6bc98..a875d4d 100644 --- a/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java +++ b/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java @@ -44,8 +44,10 @@ public class GrpcServerApplication { .hasAuthority("ROLE_ADMIN") .methods("Simple/SayHello") .hasAuthority("ROLE_USER") + .methods("grpc.*/*") + .permitAll() .allRequests() - .permitAll()) + .denyAll()) .httpBasic(withDefaults()) .preauth(withDefaults()) .authenticationExtractor((headers, attributes) -> { diff --git a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java index bc8bcc4..286efb7 100644 --- a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java +++ b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -4,7 +4,10 @@ import static org.junit.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -30,6 +33,10 @@ import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; import io.grpc.MethodDescriptor; import io.grpc.Status.Code; import io.grpc.StatusRuntimeException; +import io.grpc.reflection.v1.ServerReflectionGrpc; +import io.grpc.reflection.v1.ServerReflectionRequest; +import io.grpc.reflection.v1.ServerReflectionResponse; +import io.grpc.stub.StreamObserver; @SpringBootTest(properties = { "spring.grpc.server.port=0", "spring.grpc.client.channels.stub.address=static://0.0.0.0:${local.grpc.port}", @@ -45,6 +52,10 @@ public class GrpcServerApplicationTests { @Qualifier("stub") private SimpleGrpc.SimpleBlockingStub stub; + @Autowired + @Qualifier("reflect") + private ServerReflectionGrpc.ServerReflectionStub reflect; + @Autowired @Qualifier("secure") private SimpleGrpc.SimpleBlockingStub secure; @@ -66,6 +77,32 @@ public class GrpcServerApplicationTests { assertEquals(Code.UNAUTHENTICATED, exception.getStatus().getCode()); } + @Test + @DirtiesContext + void anonymous() throws Exception { + AtomicReference response = new AtomicReference<>(); + AtomicBoolean error = new AtomicBoolean(); + StreamObserver responses = new StreamObserver() { + @Override + public void onNext(ServerReflectionResponse value) { + response.set(value); + } + + @Override + public void onError(Throwable t) { + error.set(true); + } + + @Override + public void onCompleted() { + } + }; + StreamObserver request = reflect.serverReflectionInfo(responses); + request.onNext(ServerReflectionRequest.newBuilder().setListServices("").build()); + request.onCompleted(); + Awaitility.await().until(() -> response.get() != null || error.get()); + } + @Test @DirtiesContext void unauthauthorized() { @@ -114,7 +151,7 @@ public class GrpcServerApplicationTests { @Lazy SimpleGrpc.SimpleBlockingStub basic(GrpcChannelFactory channels) { return SimpleGrpc.newBlockingStub(channels.createChannel("basic", ChannelBuilderOptions.defaults() - .withInterceptors(List.of(new BasicAuthenticationInterceptor("user", "user"))))); + .withInterceptors(List.of(new BasicAuthenticationInterceptor("user", "user"))))); } @Bean @@ -123,6 +160,12 @@ public class GrpcServerApplicationTests { return SimpleGrpc.newBlockingStub(channels.createChannel("stub")); } + @Bean + @Lazy + ServerReflectionGrpc.ServerReflectionStub reflect(GrpcChannelFactory channels, @LocalGrpcPort int port) { + return ServerReflectionGrpc.newStub(channels.createChannel("stub")); + } + } } diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/AuthenticationServerInterceptor.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/AuthenticationServerInterceptor.java index 7dc4684..16ec5e7 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/AuthenticationServerInterceptor.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/AuthenticationServerInterceptor.java @@ -103,12 +103,14 @@ public class AuthenticationServerInterceptor implements ServerInterceptor, Order @Override public void onReady() { - if (this.authentication == null || !this.authentication.isAuthenticated() - || this.authentication instanceof AnonymousAuthenticationToken) { + if (this.authentication == null || !this.authentication.isAuthenticated()) { throw new BadCredentialsException("not authenticated"); } else { if (!this.authorizationManager.authorize(() -> this.authentication, this.context).isGranted()) { + if (this.authentication instanceof AnonymousAuthenticationToken) { + throw new BadCredentialsException("not authenticated"); + } throw new AccessDeniedException("not allowed"); } } diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/CallContext.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/CallContext.java index 5ce674e..aa29faa 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/CallContext.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/CallContext.java @@ -20,4 +20,9 @@ import io.grpc.Metadata; import io.grpc.MethodDescriptor; public record CallContext(Metadata headers, Attributes attributes, MethodDescriptor method) { + + @Override + public final String toString() { + return "CallContext:[" + this.method.getFullMethodName() + "]"; + } } diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/RequestMapperConfigurer.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/RequestMapperConfigurer.java index cdb4a1a..d9662ae 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/RequestMapperConfigurer.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/RequestMapperConfigurer.java @@ -22,10 +22,13 @@ import java.util.function.Supplier; import org.springframework.context.ApplicationContext; import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.authorization.AuthenticatedAuthorizationManager; import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationManagers; +import org.springframework.security.authorization.SpringAuthorizationEventPublisher; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.util.Assert; @@ -38,14 +41,22 @@ public class RequestMapperConfigurer extends SecurityConfigurerAdapter roleHierarchy; - public RequestMapperConfigurer(ApplicationContext context) { + private final AuthorizationEventPublisher publisher; + + public RequestMapperConfigurer(ApplicationContext context) throws Exception { + if (context.getBeanNamesForType(AuthorizationEventPublisher.class).length > 0) { + this.publisher = context.getBean(AuthorizationEventPublisher.class); + } else { + this.publisher = new SpringAuthorizationEventPublisher(context); + } this.roleHierarchy = SingletonSupplier.of(() -> (context.getBeanNamesForType(RoleHierarchy.class).length > 0) - ? context.getBean(RoleHierarchy.class) : new NullRoleHierarchy()); + ? context.getBean(RoleHierarchy.class) + : new NullRoleHierarchy()); } @Override public void configure(GrpcSecurity builder) throws Exception { - builder.authorizationManager(new RequestMapperAuthorizationManager(this.authorizedCalls)); + builder.authorizationManager(new RequestMapperAuthorizationManager(this.authorizedCalls, this.publisher)); } public AuthorizedCall allRequests() { @@ -111,6 +122,18 @@ public class RequestMapperConfigurer extends SecurityConfigurerAdapter manager) { Assert.notNull(manager, "manager cannot be null"); this.authorizationManager = (this.not) ? AuthorizationManagers.not(manager) : manager; @@ -127,20 +150,26 @@ public class RequestMapperConfigurer extends SecurityConfigurerAdapter { private final List authorizedCalls; + private final AuthorizationEventPublisher publisher; - public RequestMapperAuthorizationManager(List authorizedCalls) { + public RequestMapperAuthorizationManager(List authorizedCalls, + AuthorizationEventPublisher publisher) { this.authorizedCalls = authorizedCalls; + this.publisher = publisher; } @SuppressWarnings("deprecation") @Override public AuthorizationDecision check(Supplier authentication, CallContext context) { + AuthorizationDecision result = new AuthorizationDecision(false); for (AuthorizedCall authorizedCall : this.authorizedCalls) { if (authorizedCall.matcher.matches(context)) { - return authorizedCall.authorizationManager.check(authentication, context); + result = authorizedCall.authorizationManager.check(authentication, context); + break; } } - return new AuthorizationDecision(false); + this.publisher.publishAuthorizationEvent(authentication, context, result); + return result; } }