diff --git a/samples/grpc-tomcat-secure/README.md b/samples/grpc-tomcat-secure/README.md new file mode 100644 index 0000000..975f063 --- /dev/null +++ b/samples/grpc-tomcat-secure/README.md @@ -0,0 +1,61 @@ +# Spring Boot gRPC Sample + +This project is a copy one of the samples from the [gRPC Spring Boot Starter](https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/examples/local-grpc-server/build.gradle). Build and run any way you like to run Spring Boot. E.g: + +``` +$ ./mvnw spring-boot:run +... + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v3.0.0) + +2022-12-08T05:32:24.934-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 17.0.5 with PID 551632 (/home/dsyer/dev/scratch/demo/target/classes started by dsyer in /home/dsyer/dev/scratch/demo) +2022-12-08T05:32:24.938-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default" +2022-12-08T05:32:25.377-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcHealthServiceAutoConfiguration +2022-12-08T05:32:25.416-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration +2022-12-08T05:32:25.425-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration +2022-12-08T05:32:25.427-08:00 INFO 551632 --- [ main] g.s.a.GrpcServerFactoryAutoConfiguration : Detected grpc-netty: Creating NettyGrpcServerFactory +2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: Simple, bean: grpcServerService, class: com.example.demo.GrpcServerService +2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.health.v1.Health, bean: grpcHealthService, class: io.grpc.protobuf.services.HealthServiceImpl +2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.reflection.v1alpha.ServerReflection, bean: protoReflectionService, class: io.grpc.protobuf.services.ProtoReflectionService +2022-12-08T05:32:25.820-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.GrpcServerLifecycle : gRPC Server started, listening on address: *, port: 9090 +2022-12-08T05:32:25.831-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.264 seconds (process running for 1.623) +``` + +The server starts by default on port 9090. Test with [gRPCurl](https://github.com/fullstorydev/grpcurl): + +``` +$ grpcurl -d '{"name":"Hi"}' -plaintext localhost:9090 Simple.SayHello +{ + "message": "Hello ==\u003e Hi" +} +``` + +## Native Image + +The app compiles to a native image if the JVM is GraalVM: + +``` +$ ./mvnw -Pnative native:compile +$ ./target/demo + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v3.0.0) + +2022-12-08T05:36:54.365-08:00 INFO 554359 --- [ main] com.example.demo.DemoApplication : Starting AOT-processed DemoApplication using Java 17.0.5 with PID 554359 (/home/dsyer/dev/scratch/demo/target/demo started by dsyer in /home/dsyer/dev/scratch/demo) +2022-12-08T05:36:54.366-08:00 INFO 554359 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default" +2022-12-08T05:36:54.377-08:00 INFO 554359 --- [ main] g.s.a.GrpcServerFactoryAutoConfiguration : Detected grpc-netty: Creating NettyGrpcServerFactory +2022-12-08T05:36:54.392-08:00 INFO 554359 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: Simple, bean: grpcServerService, class: com.example.demo.GrpcServerService +2022-12-08T05:36:54.392-08:00 INFO 554359 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.health.v1.Health, bean: grpcHealthService, class: io.grpc.protobuf.services.HealthServiceImpl +2022-12-08T05:36:54.392-08:00 INFO 554359 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.reflection.v1alpha.ServerReflection, bean: protoReflectionService, class: io.grpc.protobuf.services.ProtoReflectionService +2022-12-08T05:36:54.396-08:00 INFO 554359 --- [ main] n.d.b.g.s.s.GrpcServerLifecycle : gRPC Server started, listening on address: *, port: 9090 +2022-12-08T05:36:54.396-08:00 INFO 554359 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 0.046 seconds (process running for 0.052) +``` diff --git a/samples/grpc-tomcat-secure/pom.xml b/samples/grpc-tomcat-secure/pom.xml new file mode 100644 index 0000000..1c70226 --- /dev/null +++ b/samples/grpc-tomcat-secure/pom.xml @@ -0,0 +1,183 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.0 + + + org.springframework.grpc + grpc-tomcat-secure-sample + 0.3.0-SNAPSHOT + Spring gRPC Server Sample + Demo project for Spring gRPC + + + + + + + + + + + + + + + 17 + 0.0.39 + 3.25.5 + 1.63.2 + + + + + org.springframework.grpc + spring-grpc-dependencies + 0.3.0-SNAPSHOT + pom + import + + + + + + org.springframework.grpc + spring-grpc-spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + io.grpc + grpc-services + + + io.grpc + grpc-servlet-jakarta + + + org.springframework.grpc + spring-grpc-test + test + + + + + + + kr.motd.maven + os-maven-plugin + 1.7.1 + + + + + org.graalvm.buildtools + native-maven-plugin + + + --verbose + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + io.spring.javaformat + spring-javaformat-maven-plugin + ${spring-javaformat-maven-plugin.version} + + + + validate + true + + validate + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + + + compile + compile-custom + + + + + + jakarta_omit,@generated=omit + + + com.google.protobuf:protoc:${protobuf-java.version}:exe:${os.detected.classifier} + grpc-java + + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + + \ No newline at end of file diff --git a/samples/grpc-tomcat-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java b/samples/grpc-tomcat-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java new file mode 100644 index 0000000..007947e --- /dev/null +++ b/samples/grpc-tomcat-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java @@ -0,0 +1,25 @@ +package org.springframework.grpc.sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@SpringBootApplication +public class GrpcServerApplication { + + public static void main(String[] args) { + SpringApplication.run(GrpcServerApplication.class, args); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.httpBasic(Customizer.withDefaults()) + .authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()) + .csrf(csrf -> csrf.disable()) + .build(); + } + +} diff --git a/samples/grpc-tomcat-secure/src/main/java/org/springframework/grpc/sample/GrpcServerService.java b/samples/grpc-tomcat-secure/src/main/java/org/springframework/grpc/sample/GrpcServerService.java new file mode 100644 index 0000000..06d0526 --- /dev/null +++ b/samples/grpc-tomcat-secure/src/main/java/org/springframework/grpc/sample/GrpcServerService.java @@ -0,0 +1,48 @@ +package org.springframework.grpc.sample; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.grpc.sample.proto.HelloReply; +import org.springframework.grpc.sample.proto.HelloRequest; +import org.springframework.grpc.sample.proto.SimpleGrpc; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import io.grpc.stub.StreamObserver; + +@Service +public class GrpcServerService extends SimpleGrpc.SimpleImplBase { + + private static Log log = LogFactory.getLog(GrpcServerService.class); + + @Override + public void sayHello(HelloRequest req, StreamObserver responseObserver) { + log.info("Hello " + req.getName()); + log.info("Security " + SecurityContextHolder.getContext().getAuthentication()); + HelloReply reply = HelloReply.newBuilder().setMessage("Hello ==> " + req.getName()).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + + @Override + public void streamHello(HelloRequest req, StreamObserver responseObserver) { + log.info("Hello " + req.getName()); + int count = 0; + while (count < 10) { + HelloReply reply = HelloReply.newBuilder().setMessage("Hello(" + count + ") ==> " + req.getName()).build(); + responseObserver.onNext(reply); + count++; + try { + Thread.sleep(1000L); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + responseObserver.onError(e); + return; + } + } + responseObserver.onCompleted(); + } + +} \ No newline at end of file diff --git a/samples/grpc-tomcat-secure/src/main/proto/hello.proto b/samples/grpc-tomcat-secure/src/main/proto/hello.proto new file mode 100644 index 0000000..731679c --- /dev/null +++ b/samples/grpc-tomcat-secure/src/main/proto/hello.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "org.springframework.grpc.sample.proto"; +option java_outer_classname = "HelloWorldProto"; + +// The greeting service definition. +service Simple { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) { + } + rpc StreamHello(HelloRequest) returns (stream HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} \ No newline at end of file diff --git a/samples/grpc-tomcat-secure/src/main/resources/META-INF/native-image/org.springframework.samples/grpc-tomcat-sample/native-image.properties b/samples/grpc-tomcat-secure/src/main/resources/META-INF/native-image/org.springframework.samples/grpc-tomcat-sample/native-image.properties new file mode 100644 index 0000000..5c43451 --- /dev/null +++ b/samples/grpc-tomcat-secure/src/main/resources/META-INF/native-image/org.springframework.samples/grpc-tomcat-sample/native-image.properties @@ -0,0 +1,2 @@ +# Ignored unless building in Nix (https://github.com/oracle/graal/issues/8639) +Args = -ENIX_LDFLAGS -ENIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu diff --git a/samples/grpc-tomcat-secure/src/main/resources/application.properties b/samples/grpc-tomcat-secure/src/main/resources/application.properties new file mode 100644 index 0000000..dc54139 --- /dev/null +++ b/samples/grpc-tomcat-secure/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.application.name=grpc-tomcat-secure +server.port=9090 +server.http2.enabled=true +spring.security.user.name=user +spring.security.user.password=user \ No newline at end of file diff --git a/samples/grpc-tomcat-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-tomcat-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java new file mode 100644 index 0000000..f08b750 --- /dev/null +++ b/samples/grpc-tomcat-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -0,0 +1,108 @@ +package org.springframework.grpc.sample; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Base64; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Lazy; +import org.springframework.grpc.client.ChannelBuilderOptions; +import org.springframework.grpc.client.GrpcChannelFactory; +import org.springframework.grpc.sample.proto.HelloReply; +import org.springframework.grpc.sample.proto.HelloRequest; +import org.springframework.grpc.sample.proto.SimpleGrpc; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.grpc.test.LocalGrpcPort; +import org.springframework.test.annotation.DirtiesContext; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; +import io.grpc.Status.Code; +import io.grpc.MethodDescriptor; +import io.grpc.StatusRuntimeException; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class GrpcServerApplicationTests { + + private static Log log = LogFactory.getLog(GrpcServerApplicationTests.class); + + public static void main(String[] args) { + new SpringApplicationBuilder(GrpcServerApplication.class, ExtraConfiguration.class).run(args); + } + + @Autowired + @Qualifier("stub") + private SimpleGrpc.SimpleBlockingStub stub; + + @Autowired + @Qualifier("basic") + private SimpleGrpc.SimpleBlockingStub basic; + + @Test + @DirtiesContext + void contextLoads() { + } + + @Test + @DirtiesContext + void unauthenticated() { + StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, + () -> stub.sayHello(HelloRequest.newBuilder().setName("Alien").build())); + assertEquals(Code.UNAUTHENTICATED, exception.getStatus().getCode()); + } + + @Test + @DirtiesContext + void authenticated() { + log.info("Testing"); + HelloReply response = basic.sayHello(HelloRequest.newBuilder().setName("Alien").build()); + assertEquals("Hello ==> Alien", response.getMessage()); + } + + @TestConfiguration + static class ExtraConfiguration { + + @Bean + @Lazy + SimpleGrpc.SimpleBlockingStub basic(GrpcChannelFactory channels, @LocalServerPort int port) { + return SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:" + port, + ChannelBuilderOptions.defaults().withCustomizer((__, channel) -> { + channel.intercept(new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new SimpleForwardingClientCall(next.newCall(method, callOptions)) { + public void start(ClientCall.Listener responseListener, + io.grpc.Metadata headers) { + headers.put(GrpcSecurity.AUTHORIZATION_KEY, + "Basic " + Base64.getEncoder().encodeToString("user:user".getBytes())); + super.start(responseListener, headers); + }; + }; + } + }); + }))); + } + + @Bean + @Lazy + SimpleGrpc.SimpleBlockingStub stub(GrpcChannelFactory channels, @LocalServerPort int port) { + return SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:" + port)); + } + + } + +} diff --git a/samples/grpc-tomcat/src/main/resources/application.properties b/samples/grpc-tomcat/src/main/resources/application.properties index 28f590b..735467d 100644 --- a/samples/grpc-tomcat/src/main/resources/application.properties +++ b/samples/grpc-tomcat/src/main/resources/application.properties @@ -2,4 +2,5 @@ spring.application.name=grpc-tomcat server.port=9090 server.http2.enabled=true logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}] +logging.level.org.springframework.security=DEBUG management.endpoints.web.exposure.include=* diff --git a/samples/pom.xml b/samples/pom.xml index 32d1733..cd413f8 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -19,6 +19,7 @@ grpc-server grpc-server-netty-shaded grpc-tomcat + grpc-tomcat-secure grpc-webflux diff --git a/spring-grpc-core/pom.xml b/spring-grpc-core/pom.xml index cb80f54..08bee50 100644 --- a/spring-grpc-core/pom.xml +++ b/spring-grpc-core/pom.xml @@ -27,6 +27,11 @@ org.springframework spring-context + + org.springframework.security + spring-security-web + true + io.grpc grpc-netty diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/GrpcSecurity.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/GrpcSecurity.java new file mode 100644 index 0000000..34bcdf2 --- /dev/null +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/GrpcSecurity.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.grpc.server.security; + +import io.grpc.Metadata; + +/** + * Defines constants and utilities for working with gRPC security. + * + * @author Dave Syer + */ +public final class GrpcSecurity { + + /** + * Constant for the Authorization header. + */ + public static final Metadata.Key AUTHORIZATION_KEY = Metadata.Key.of("Authorization", + Metadata.ASCII_STRING_MARSHALLER); + + private GrpcSecurity() { + } + +} diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/SecurityContextServerInterceptor.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/SecurityContextServerInterceptor.java new file mode 100644 index 0000000..f879d80 --- /dev/null +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/SecurityContextServerInterceptor.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.grpc.server.security; + +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +public class SecurityContextServerInterceptor implements ServerInterceptor { + + @Override + public Listener interceptCall(ServerCall call, Metadata headers, + ServerCallHandler next) { + SecurityContext securityContext = SecurityContextHolder.getContext(); + return new SecurityContextHandlerListener(next.startCall(call, headers), securityContext); + } + + static class SecurityContextHandlerListener extends SimpleForwardingServerCallListener { + + private SecurityContext securityContext; + + SecurityContextHandlerListener(ServerCall.Listener delegate, SecurityContext securityContext) { + super(delegate); + this.securityContext = securityContext; + } + + @Override + public void onMessage(ReqT message) { + SecurityContextHolder.setContext(this.securityContext); + try { + super.onMessage(message); + } + finally { + SecurityContextHolder.clearContext(); + } + } + + @Override + public void onHalfClose() { + SecurityContextHolder.setContext(this.securityContext); + try { + super.onHalfClose(); + } + finally { + SecurityContextHolder.clearContext(); + } + } + + @Override + public void onReady() { + SecurityContextHolder.setContext(this.securityContext); + try { + super.onReady(); + } + finally { + SecurityContextHolder.clearContext(); + } + } + + @Override + public void onCancel() { + super.onCancel(); + SecurityContextHolder.clearContext(); + } + + @Override + public void onComplete() { + super.onComplete(); + SecurityContextHolder.clearContext(); + } + + } + +} diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/SecurityGrpcExceptionHandler.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/SecurityGrpcExceptionHandler.java new file mode 100644 index 0000000..24b1f38 --- /dev/null +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/SecurityGrpcExceptionHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.grpc.server.security; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.grpc.server.exception.GrpcExceptionHandler; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; + +import io.grpc.Status; + +public class SecurityGrpcExceptionHandler implements GrpcExceptionHandler { + + private static final Log logger = LogFactory.getLog(SecurityGrpcExceptionHandler.class); + + @Override + public Status handleException(Throwable exception) { + if (exception instanceof AuthenticationException) { + if (logger.isDebugEnabled()) { + logger.error("Failed to authenticate", exception); + } + return Status.UNAUTHENTICATED.withDescription(exception.getMessage()); + } + if (exception instanceof AccessDeniedException) { + if (logger.isDebugEnabled()) { + logger.error("Failed to authorize", exception); + } + return Status.PERMISSION_DENIED.withDescription(exception.getMessage()); + } + return null; + } + +} diff --git a/spring-grpc-dependencies/pom.xml b/spring-grpc-dependencies/pom.xml index 21c783c..dab5cdb 100644 --- a/spring-grpc-dependencies/pom.xml +++ b/spring-grpc-dependencies/pom.xml @@ -50,6 +50,7 @@ 6.2.1 + 6.4.1 4.1.115.Final 1.69.0 3.25.5 @@ -87,6 +88,13 @@ pom import + + org.springframework.security + spring-security-bom + ${spring-security.version} + pom + import + io.netty netty-bom diff --git a/spring-grpc-spring-boot-autoconfigure/pom.xml b/spring-grpc-spring-boot-autoconfigure/pom.xml index 5e90e66..b702d6e 100644 --- a/spring-grpc-spring-boot-autoconfigure/pom.xml +++ b/spring-grpc-spring-boot-autoconfigure/pom.xml @@ -105,6 +105,16 @@ micrometer-core true + + org.springframework.security + spring-security-config + true + + + org.springframework.security + spring-security-web + true + org.springframework.boot diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryAutoConfiguration.java index e8755e8..cbed6c8 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryAutoConfiguration.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryAutoConfiguration.java @@ -99,7 +99,7 @@ public class GrpcServerFactoryAutoConfiguration { } - static class OnGrpcServletCondition extends AllNestedConditions { + public static class OnGrpcServletCondition extends AllNestedConditions { OnGrpcServletCondition() { super(ConfigurationPhase.PARSE_CONFIGURATION); @@ -123,7 +123,7 @@ public class GrpcServerFactoryAutoConfiguration { } - static class OnNativeGrpcServerCondition extends AnyNestedCondition { + public static class OnNativeGrpcServerCondition extends AnyNestedCondition { OnNativeGrpcServerCondition() { super(ConfigurationPhase.PARSE_CONFIGURATION); diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcSecurityAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcSecurityAutoConfiguration.java new file mode 100644 index 0000000..bb1da18 --- /dev/null +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcSecurityAutoConfiguration.java @@ -0,0 +1,75 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.grpc.autoconfigure.server.security; + +import java.util.concurrent.Executors; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.autoconfigure.server.GrpcServerFactoryAutoConfiguration; +import org.springframework.grpc.autoconfigure.server.exception.GrpcExceptionHandlerAutoConfiguration; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.grpc.server.exception.GrpcExceptionHandler; +import org.springframework.grpc.server.security.SecurityContextServerInterceptor; +import org.springframework.grpc.server.security.SecurityGrpcExceptionHandler; +import org.springframework.scheduling.concurrent.CustomizableThreadFactory; +import org.springframework.security.concurrent.DelegatingSecurityContextExecutor; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.web.SecurityFilterChain; + +import io.grpc.ServerBuilder; + +@ConditionalOnClass(ObjectPostProcessor.class) +@AutoConfiguration(before = GrpcExceptionHandlerAutoConfiguration.class, after = SecurityAutoConfiguration.class) +public class GrpcSecurityAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + static class ExceptionHandlerAutoConfiguration { + + @Bean + public GrpcExceptionHandler accessExceptionHandler() { + return new SecurityGrpcExceptionHandler(); + } + + } + + @ConditionalOnBean(SecurityFilterChain.class) + @Configuration(proxyBeanMethods = false) + @Conditional(GrpcServerFactoryAutoConfiguration.OnGrpcServletCondition.class) + static class GrpcSecurityConfigurerAutoConfiguration { + + @Bean + @GlobalServerInterceptor + public SecurityContextServerInterceptor securityContextInterceptor() { + return new SecurityContextServerInterceptor(); + } + + @Bean + public > ServerBuilderCustomizer securityContextCustomizer() { + // TODO: configure the thread pool via GrpcServerProperties? + return (serverBuilder) -> serverBuilder.executor(new DelegatingSecurityContextExecutor( + Executors.newCachedThreadPool(new CustomizableThreadFactory("grpc-server-")))); + } + + } + +} \ No newline at end of file diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index cee7caf..27ce109 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -6,3 +6,4 @@ org.springframework.grpc.autoconfigure.server.health.GrpcServerHealthAutoConfigu org.springframework.grpc.autoconfigure.server.GrpcServerObservationAutoConfiguration org.springframework.grpc.autoconfigure.server.GrpcServerReflectionAutoConfiguration org.springframework.grpc.autoconfigure.server.exception.GrpcExceptionHandlerAutoConfiguration +org.springframework.grpc.autoconfigure.server.security.GrpcSecurityAutoConfiguration