From 8f6bca6a9303b11000447b908a54f3df67c237d7 Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Sun, 17 Oct 2021 10:34:33 +0300 Subject: [PATCH 01/22] next dev version --- README.adoc | 6 +++--- gradle.properties | 2 +- grpc-spring-boot-starter-gradle-plugin/README.adoc | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.adoc b/README.adoc index 2dc20b6a..545faf06 100644 --- a/README.adoc +++ b/README.adoc @@ -38,7 +38,7 @@ repositories { } dependencies { - compile 'io.github.lognet:grpc-spring-boot-starter:4.5.7' + compile 'io.github.lognet:grpc-spring-boot-starter:4.5.8' } @@ -48,10 +48,10 @@ By default, starter pulls `io.grpc:grpc-netty-shaded` as transitive dependency [source,groovy] ---- - compile ('io.github.lognet:grpc-spring-boot-starter:4.5.7') { + compile ('io.github.lognet:grpc-spring-boot-starter:4.5.8') { exclude group: 'io.grpc', module: 'grpc-netty-shaded' } - compile 'io.grpc:grpc-netty:1.40.0' // <1> + compile 'io.grpc:grpc-netty:1.41.0' // <1> ---- <1> Make sure to pull the version that matches the release. diff --git a/gradle.properties b/gradle.properties index 2c6b967b..60bf4760 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ gradleErrorPronePluginVersion=2.0.2 errorProneVersion=2.7.1 lombokVersion=1.18.20 -version=4.5.8 +version=4.5.9-SNAPSHOT group=io.github.lognet description=Spring Boot starter for Google RPC. gitHubUrl=https\://github.com/LogNet/grpc-spring-boot-starter diff --git a/grpc-spring-boot-starter-gradle-plugin/README.adoc b/grpc-spring-boot-starter-gradle-plugin/README.adoc index cfc1f005..3903c519 100644 --- a/grpc-spring-boot-starter-gradle-plugin/README.adoc +++ b/grpc-spring-boot-starter-gradle-plugin/README.adoc @@ -23,7 +23,7 @@ Bootstraps the project with `com.google.protobuf` gradle plugin (including `grp ---- plugins { id 'java' - id "io.github.lognet.grpc-spring-boot" version '4.5.7' + id "io.github.lognet.grpc-spring-boot" version '4.5.8' } ---- From ded2743e152a1e13c86311dbb75b241e81d02cbd Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Wed, 27 Oct 2021 16:16:13 +0300 Subject: [PATCH 02/22] closes #175 --- grpc-spring-boot-starter-demo/build.gradle | 6 +- .../grpc/auth/CustomSecurityTest.java | 41 +- .../springboot/grpc/demo/GreeterService.java | 11 - .../springboot/grpc/demo/GrpcTaskService.java | 101 +++++ .../springboot/grpc/demo/ITaskService.java | 9 + .../src/main/proto/greeter.proto | 1 - .../src/main/proto/tasks.proto | 44 ++ .../grpc/CustomInterceptorsOrderTest.java | 1 - .../grpc/auth/AllAuthConfigTest.java | 1 - .../grpc/auth/ConcurrentAuthConfigTest.java | 2 - .../grpc/auth/DisabledSecuredAnnTest.java | 2 - .../grpc/auth/JwtAuthorityTest.java | 1 - .../auth/PerCallDefaultAuthConfigTest.java | 4 - .../grpc/auth/PrePostSecurityAuthTest.java | 390 ++++++++++++++++++ .../grpc/auth/SecurityInterceptorTest.java | 33 +- .../grpc/auth/UserDetailsAuthTest.java | 19 +- .../consul/ConsulRegistrationBaseTest.java | 32 +- .../grpc/recovery/GRpcRecoveryTest.java | 6 +- .../resources/application-consul-test.yml | 2 + .../test/resources/bootstrap-consul-test.yml | 7 + .../src/test/resources/bootstrap.yml | 3 + .../grpc/FailureHandlingSupport.java | 69 ++-- .../MessageBlockingServerCallListener.java | 2 + .../GrpcSecurityEnablerConfiguration.java | 11 - .../security/SecurityAutoConfiguration.java | 9 +- .../GRpcExceptionHandlerInterceptor.java | 74 ++-- .../GRpcExceptionHandlerMethodResolver.java | 3 + .../security/AuthenticatedAttributeVoter.java | 2 +- .../grpc/security/EnableGrpcSecurity.java | 18 - .../grpc/security/GrpcSecurity.java | 76 +++- .../security/GrpcSecurityConfiguration.java | 24 +- .../GrpcSecurityConfigurerAdapter.java | 5 +- .../security/GrpcSecurityMetadataSource.java | 39 +- .../GrpcServiceAuthorizationConfigurer.java | 18 +- .../grpc/security/SecurityInterceptor.java | 147 ++++++- 35 files changed, 958 insertions(+), 255 deletions(-) create mode 100644 grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/GrpcTaskService.java create mode 100644 grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/ITaskService.java create mode 100644 grpc-spring-boot-starter-demo/src/main/proto/tasks.proto create mode 100644 grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PrePostSecurityAuthTest.java create mode 100644 grpc-spring-boot-starter-demo/src/test/resources/bootstrap-consul-test.yml delete mode 100644 grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/security/GrpcSecurityEnablerConfiguration.java delete mode 100644 grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/EnableGrpcSecurity.java diff --git a/grpc-spring-boot-starter-demo/build.gradle b/grpc-spring-boot-starter-demo/build.gradle index a2767a5a..d95f81f0 100644 --- a/grpc-spring-boot-starter-demo/build.gradle +++ b/grpc-spring-boot-starter-demo/build.gradle @@ -82,15 +82,15 @@ dependencies { testCompile 'org.springframework.boot:spring-boot-starter-test' testCompile 'com.github.stefanbirkner:system-rules:1.18.0' testCompile('org.springframework.cloud:spring-cloud-starter-consul-discovery') - testCompile 'com.pszymczyk.consul:embedded-consul:2.2.1' testCompile 'org.awaitility:awaitility:4.0.3' testCompile "org.springframework.cloud:spring-cloud-config-server" testCompile "org.springframework.cloud:spring-cloud-config-client" testCompile "org.springframework.cloud:spring-cloud-starter-bootstrap" - testCompile "com.playtika.testcontainers:embedded-keycloak:2.0.14" + testCompile "com.playtika.testcontainers:embedded-keycloak:2.0.16" + testCompile "com.playtika.testcontainers:embedded-consul:2.0.16" - testImplementation 'org.hamcrest:hamcrest:2.1' + testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'org.mockito:mockito-core:2.23.0' customSecurityTestCompile sourceSets.test.output diff --git a/grpc-spring-boot-starter-demo/src/customSecurityTest/java/org/lognet/springboot/grpc/auth/CustomSecurityTest.java b/grpc-spring-boot-starter-demo/src/customSecurityTest/java/org/lognet/springboot/grpc/auth/CustomSecurityTest.java index d56ce8f9..98d08066 100644 --- a/grpc-spring-boot-starter-demo/src/customSecurityTest/java/org/lognet/springboot/grpc/auth/CustomSecurityTest.java +++ b/grpc-spring-boot-starter-demo/src/customSecurityTest/java/org/lognet/springboot/grpc/auth/CustomSecurityTest.java @@ -11,7 +11,6 @@ import org.lognet.springboot.grpc.demo.DemoApp; import org.lognet.springboot.grpc.security.AuthCallCredentials; import org.lognet.springboot.grpc.security.AuthHeader; -import org.lognet.springboot.grpc.security.EnableGrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter; import org.springframework.boot.test.context.SpringBootTest; @@ -33,33 +32,27 @@ @RunWith(SpringRunner.class) @SpringBootTest(classes = {DemoApp.class}, webEnvironment = NONE) -@Import(CustomSecurityTest.TestConfig.class) +@Import(CustomSecurityTest.DemoGrpcSecurityConfig.class) public class CustomSecurityTest extends GrpcServerTestBase { private final static String MY_CUSTOM_SCHEME_NAME = "custom"; @TestConfiguration - static class TestConfig { - - @EnableGrpcSecurity - public class DemoGrpcSecurityConfig extends GrpcSecurityConfigurerAdapter { - - - @Override - public void configure(GrpcSecurity builder) throws Exception { - builder.authorizeRequests() - .withSecuredAnnotation() - .authenticationSchemeSelector(scheme -> - Optional.of(scheme.toString()) - .filter(s -> s.startsWith(MY_CUSTOM_SCHEME_NAME)) - .map(s -> s.substring(MY_CUSTOM_SCHEME_NAME.length() + 1)) - .map(token -> { - final String[] chunks = token.split("#"); - return new TestingAuthenticationToken(token.split("#")[0], null, "SCOPE_" + chunks[1]); - }) - ) - .authenticationProvider(new TestingAuthenticationProvider()); - } - + public static class DemoGrpcSecurityConfig extends GrpcSecurityConfigurerAdapter { + + @Override + public void configure(GrpcSecurity builder) throws Exception { + builder.authorizeRequests() + .withSecuredAnnotation() + .authenticationSchemeSelector(scheme -> + Optional.of(scheme.toString()) + .filter(s -> s.startsWith(MY_CUSTOM_SCHEME_NAME)) + .map(s -> s.substring(MY_CUSTOM_SCHEME_NAME.length() + 1)) + .map(token -> { + final String[] chunks = token.split("#"); + return new TestingAuthenticationToken(token.split("#")[0], null, "SCOPE_" + chunks[1]); + }) + ) + .authenticationProvider(new TestingAuthenticationProvider()); } } diff --git a/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/GreeterService.java b/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/GreeterService.java index c92f0214..60793eac 100644 --- a/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/GreeterService.java +++ b/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/GreeterService.java @@ -8,8 +8,6 @@ import org.lognet.springboot.grpc.GRpcService; import org.lognet.springboot.grpc.security.GrpcSecurity; import org.springframework.security.access.annotation.Secured; -import org.springframework.security.access.prepost.PostAuthorize; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; @@ -68,15 +66,6 @@ public void sayAuthHello(Empty request, StreamObserver5") - public void sayPreAuthHello(GreeterOuterClass.Person person, StreamObserver responseObserver) { - - responseObserver.onNext(person.toBuilder().setNickName("dummy").build()); - responseObserver.onCompleted(); - - } @Override @Secured({}) diff --git a/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/GrpcTaskService.java b/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/GrpcTaskService.java new file mode 100644 index 00000000..1ba7d8ad --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/GrpcTaskService.java @@ -0,0 +1,101 @@ +package org.lognet.springboot.grpc.demo; + +import io.grpc.examples.tasks.Assignment; +import io.grpc.examples.tasks.Person; +import io.grpc.examples.tasks.TaskServiceGrpc; +import io.grpc.stub.StreamObserver; +import org.lognet.springboot.grpc.GRpcService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PreAuthorize; + +import java.util.Optional; + +@GRpcService +public class GrpcTaskService extends TaskServiceGrpc.TaskServiceImplBase { + + + private ITaskService service; + + @Autowired + public void setService(Optional service) { + this.service = service.orElse(new ITaskService() { + @Override + public Assignment findAssignment(Person person) { + return null; + } + }); + } + + @Override + @PreAuthorize("hasAuthority('1') && #person.age<12") + @PostAuthorize("returnObject.description.length()>0") + public void findAssignmentUnary(Person person, StreamObserver responseObserver) { + final Assignment assignment = service.findAssignment(person); + responseObserver.onNext(assignment); + responseObserver.onCompleted(); + + } + + @Override + @PreAuthorize("#p0.age<12") + @PostAuthorize("returnObject.description.length()>0") + public StreamObserver findAssignmentsBidiStream(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(Person person) { + final Assignment assignment = service.findAssignment(person); + responseObserver.onNext(assignment); + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + + @Override + @PreAuthorize("#person.age<12") + @PostAuthorize("returnObject.description.length()>0") + public void findAssignmentOutStream(Person person, StreamObserver responseObserver) { + responseObserver.onNext(service.findAssignment(person)); + responseObserver.onNext(service.findAssignment(person)); + responseObserver.onCompleted(); + } + + @Override + @PreAuthorize("#p0.getAge()<12") + @PostAuthorize("returnObject.description.length()>0") + public StreamObserver findAssignmentInStream(StreamObserver responseObserver) { + return new StreamObserver() { + private final StringBuilder assignment = new StringBuilder(); + + @Override + public void onNext(Person person) { + if(0!=assignment.length()){ + assignment.append(System.lineSeparator()); + } + assignment.append(service.findAssignment(person).getDescription()); + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onCompleted() { + responseObserver.onNext(Assignment.newBuilder() + .setDescription(assignment.toString()) + .build()); + responseObserver.onCompleted(); + } + }; + } +} diff --git a/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/ITaskService.java b/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/ITaskService.java new file mode 100644 index 00000000..cd5b9bd1 --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/ITaskService.java @@ -0,0 +1,9 @@ +package org.lognet.springboot.grpc.demo; + + +import io.grpc.examples.tasks.Assignment; +import io.grpc.examples.tasks.Person; + +public interface ITaskService { + Assignment findAssignment(Person person); +} diff --git a/grpc-spring-boot-starter-demo/src/main/proto/greeter.proto b/grpc-spring-boot-starter-demo/src/main/proto/greeter.proto index ae1faf3f..20fb937f 100644 --- a/grpc-spring-boot-starter-demo/src/main/proto/greeter.proto +++ b/grpc-spring-boot-starter-demo/src/main/proto/greeter.proto @@ -11,7 +11,6 @@ service Greeter { rpc SayManyHellos (stream HelloRequest) returns (stream HelloReply) {} rpc SayAuthHello ( google.protobuf.Empty) returns ( HelloReply) {} rpc SayAuthOnlyHello ( google.protobuf.Empty) returns ( HelloReply) {} - rpc SayPreAuthHello ( Person) returns ( Person) {} rpc HelloPersonValidResponse ( Person) returns ( Person) {} rpc HelloPersonInvalidResponse ( Person) returns ( Person) {} diff --git a/grpc-spring-boot-starter-demo/src/main/proto/tasks.proto b/grpc-spring-boot-starter-demo/src/main/proto/tasks.proto new file mode 100644 index 00000000..6bd26d9f --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/main/proto/tasks.proto @@ -0,0 +1,44 @@ +// Copyright 2015 The gRPC 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 +// +// http://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. +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.tasks"; + + + +package task; + +service TaskService { + + rpc FindAssignmentUnary(Person) returns ( Assignment) {} + + rpc FindAssignmentsBidiStream(stream Person) returns (stream Assignment) {} + + rpc FindAssignmentOutStream( Person) returns (stream Assignment) {} + + rpc FindAssignmentInStream(stream Person) returns ( Assignment) {} + + +} + + +message Person { + string name = 1; + int32 age = 2; +} + +message Assignment { + string description = 1; +} \ No newline at end of file diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/CustomInterceptorsOrderTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/CustomInterceptorsOrderTest.java index d10f1c71..ca9812d6 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/CustomInterceptorsOrderTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/CustomInterceptorsOrderTest.java @@ -18,7 +18,6 @@ import org.lognet.springboot.grpc.demo.DemoApp; import org.lognet.springboot.grpc.security.AuthClientInterceptor; import org.lognet.springboot.grpc.security.AuthHeader; -import org.lognet.springboot.grpc.security.EnableGrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter; import org.lognet.springboot.grpc.security.SecurityInterceptor; diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/AllAuthConfigTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/AllAuthConfigTest.java index fb61f5b9..2c289ce0 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/AllAuthConfigTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/AllAuthConfigTest.java @@ -6,7 +6,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.lognet.springboot.grpc.demo.DemoApp; -import org.lognet.springboot.grpc.security.EnableGrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter; import org.lognet.springboot.grpc.security.jwt.JwtAuthProviderFactory; diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ConcurrentAuthConfigTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ConcurrentAuthConfigTest.java index 083bed22..0943ec44 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ConcurrentAuthConfigTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ConcurrentAuthConfigTest.java @@ -13,7 +13,6 @@ import org.lognet.springboot.grpc.demo.DemoApp; import org.lognet.springboot.grpc.security.AuthCallCredentials; import org.lognet.springboot.grpc.security.AuthHeader; -import org.lognet.springboot.grpc.security.EnableGrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter; import org.springframework.boot.test.context.SpringBootTest; @@ -73,7 +72,6 @@ public void configure(GrpcSecurity builder) throws Exception { @Test public void concurrentTest() throws InterruptedException { - System.out.println(); final SecuredGreeterGrpc.SecuredGreeterBlockingStub unsecuredFutureStub = SecuredGreeterGrpc .newBlockingStub(selectedChanel); diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/DisabledSecuredAnnTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/DisabledSecuredAnnTest.java index e596bc7c..fa31e87c 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/DisabledSecuredAnnTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/DisabledSecuredAnnTest.java @@ -7,7 +7,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.lognet.springboot.grpc.demo.DemoApp; -import org.lognet.springboot.grpc.security.EnableGrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter; import org.springframework.boot.test.context.SpringBootTest; @@ -16,7 +15,6 @@ import org.springframework.test.context.junit4.SpringRunner; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertNotNull; @SpringBootTest(classes = DemoApp.class) diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtAuthorityTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtAuthorityTest.java index 12ae7c92..eb0eaa7d 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtAuthorityTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtAuthorityTest.java @@ -4,7 +4,6 @@ import io.grpc.examples.GreeterGrpc; import org.junit.runner.RunWith; import org.lognet.springboot.grpc.demo.DemoApp; -import org.lognet.springboot.grpc.security.EnableGrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter; import org.lognet.springboot.grpc.security.jwt.JwtAuthProviderFactory; diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PerCallDefaultAuthConfigTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PerCallDefaultAuthConfigTest.java index 9785b4e5..5df82942 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PerCallDefaultAuthConfigTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PerCallDefaultAuthConfigTest.java @@ -10,11 +10,7 @@ import org.lognet.springboot.grpc.demo.DemoApp; import org.lognet.springboot.grpc.security.AuthCallCredentials; import org.lognet.springboot.grpc.security.AuthHeader; -import org.lognet.springboot.grpc.security.EnableGrpcSecurity; -import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PrePostSecurityAuthTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PrePostSecurityAuthTest.java new file mode 100644 index 00000000..7d22c9b0 --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PrePostSecurityAuthTest.java @@ -0,0 +1,390 @@ +package org.lognet.springboot.grpc.auth; + +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.examples.tasks.Assignment; +import io.grpc.examples.tasks.Person; +import io.grpc.examples.tasks.TaskServiceGrpc; +import io.grpc.stub.StreamObserver; +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.lognet.springboot.grpc.GrpcServerTestBase; +import org.lognet.springboot.grpc.demo.DemoApp; +import org.lognet.springboot.grpc.demo.ITaskService; +import org.lognet.springboot.grpc.security.AuthCallCredentials; +import org.lognet.springboot.grpc.security.AuthHeader; +import org.lognet.springboot.grpc.security.GrpcSecurity; +import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter; +import org.mockito.Mockito; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.test.context.junit4.SpringRunner; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; + +@SpringBootTest(classes = DemoApp.class) +@RunWith(SpringRunner.class) +@Import({PrePostSecurityAuthTest.TestCfg.class}) +public class PrePostSecurityAuthTest extends GrpcServerTestBase { + + static class AggregatingStreamObserver implements StreamObserver { + private final CompletableFuture> completion = new CompletableFuture<>(); + private final List allAssignments = new ArrayList<>(); + + @Override + public void onNext(T value) { + allAssignments.add(value); + } + + @Override + public void onError(Throwable t) { + completion.completeExceptionally(t); + } + + @Override + public void onCompleted() { + completion.complete(allAssignments); + } + + public List get(Duration duration) throws Throwable { + try { + return completion.get(duration.toMillis(), TimeUnit.MILLISECONDS); + } catch (ExecutionException exception) { + throw exception.getCause(); + } + + } + } + + @TestConfiguration + static class TestCfg extends GrpcSecurityConfigurerAdapter { + @Override + public void configure(GrpcSecurity builder) throws Exception { + builder.authorizeRequests() + .withSecuredAnnotation() + .userDetailsService(new InMemoryUserDetailsManager( + User.withDefaultPasswordEncoder() + .username("user1") + .password("user1") + .authorities("1") + .build(), + User.withDefaultPasswordEncoder() + .username("user2") + .password("user2") + .authorities("2") + .build() + )); + } + } + + + private final Person sam = Person.newBuilder() + .setName("Sam") + .setAge(13) + .build(); + + private final Person frodo = Person.newBuilder() + .setName("Frodo") + .setAge(11) + .build(); + + private final Assignment noopAssigment = Assignment.newBuilder() + .setDescription("") + .build(); + + private final Assignment saveTheWorld = Assignment.newBuilder() + .setDescription("Save the world") + .build(); + private final Assignment keepTheRing = Assignment.newBuilder() + .setDescription("Keep the ring") + .build(); + + @MockBean + private ITaskService service; + + @Test + public void unaryPreAuthorizeCallTest() { + final TaskServiceGrpc.TaskServiceBlockingStub stub = TaskServiceGrpc.newBlockingStub(getChannel()) + .withCallCredentials(user1Credentials()); + + Mockito.when(service.findAssignment(frodo)) + .thenReturn(saveTheWorld); + + final Assignment assignment = stub.findAssignmentUnary(frodo); + + assertThat(assignment, Matchers.is(saveTheWorld)); + + final StatusRuntimeException statusRuntimeException = Assert.assertThrows(StatusRuntimeException.class, () -> { + stub.findAssignmentUnary(sam); + }); + assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.PERMISSION_DENIED)); + + Mockito.verify(service, Mockito.only()).findAssignment(frodo); + } + @Test + public void unaryPreAuthorizeAccessDeniedCallTest() { + final TaskServiceGrpc.TaskServiceBlockingStub unAuthStub = TaskServiceGrpc.newBlockingStub(getChannel()) + .withCallCredentials(user2Credentials()); + + Mockito.when(service.findAssignment(frodo)) + .thenReturn(saveTheWorld); + + + final StatusRuntimeException statusRuntimeException = Assert.assertThrows(StatusRuntimeException.class, () -> { + unAuthStub.findAssignmentUnary(frodo); + }); + assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.PERMISSION_DENIED)); + + Mockito.verify(service, Mockito.never()).findAssignment(frodo); + } + @Test + public void unaryPreAuthorizeUnAuthCallTest() { + final TaskServiceGrpc.TaskServiceBlockingStub unAuthStub = TaskServiceGrpc.newBlockingStub(getChannel()) + .withCallCredentials(unAuthUserCredentials()); + + Mockito.when(service.findAssignment(frodo)) + .thenReturn(saveTheWorld); + + + final StatusRuntimeException statusRuntimeException = Assert.assertThrows(StatusRuntimeException.class, () -> { + unAuthStub.findAssignmentUnary(frodo); + }); + assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.UNAUTHENTICATED)); + + Mockito.verify(service, Mockito.never()).findAssignment(frodo); + } + + @Test + public void unaryPostAuthorizeCallTest() { + final TaskServiceGrpc.TaskServiceBlockingStub stub = TaskServiceGrpc.newBlockingStub(getChannel()) + .withCallCredentials(user1Credentials()); + + Mockito.when(service.findAssignment(frodo)) + .thenReturn(keepTheRing) + .thenReturn(noopAssigment); + + final Assignment assignment = stub.findAssignmentUnary(frodo); + assertThat(assignment, Matchers.is(keepTheRing)); + + final StatusRuntimeException statusRuntimeException = Assert.assertThrows(StatusRuntimeException.class, () -> { + stub.findAssignmentUnary(frodo); + }); + assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.PERMISSION_DENIED)); + + Mockito.verify(service, Mockito.times(2)).findAssignment(frodo); + + } + + @Test + public void bidiStreamPrePostAuthorizeOkCallTest() throws Throwable { + final TaskServiceGrpc.TaskServiceStub stub = TaskServiceGrpc.newStub(getChannel()) + .withCallCredentials(user1Credentials()); + + Mockito.when(service.findAssignment(frodo)) + .thenReturn(saveTheWorld) + .thenReturn(keepTheRing); + + + final AggregatingStreamObserver responseObserver = new AggregatingStreamObserver<>(); + final StreamObserver personsIn = stub.findAssignmentsBidiStream(responseObserver); + personsIn.onNext(frodo); + personsIn.onNext(frodo); + personsIn.onCompleted(); + + final List response = responseObserver.get(Duration.ofSeconds(10)); + assertThat(response, Matchers.contains(saveTheWorld, keepTheRing)); + Mockito.verify(service, Mockito.times(2)).findAssignment(frodo); + + + } + + @Test + public void bidiStreamPreAuthorizeFailCallTest() { + final TaskServiceGrpc.TaskServiceStub stub = TaskServiceGrpc.newStub(getChannel()) + .withCallCredentials(user1Credentials()); + + Mockito.when(service.findAssignment(sam)) + .thenReturn(keepTheRing); + + + final StatusRuntimeException statusRuntimeException = Assert.assertThrows(StatusRuntimeException.class, () -> { + + final AggregatingStreamObserver observer = new AggregatingStreamObserver<>(); + final StreamObserver personsIn = stub.findAssignmentsBidiStream(observer); + personsIn.onNext(sam); + personsIn.onCompleted(); + observer.get(Duration.ofSeconds(10)); + }); + assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.PERMISSION_DENIED)); + Mockito.verifyZeroInteractions(service); + + } + + @Test + public void outStreamPrePostAuthorizeOkCallTest() { + final TaskServiceGrpc.TaskServiceBlockingStub stub = TaskServiceGrpc.newBlockingStub(getChannel()) + .withCallCredentials(user1Credentials()); + + Mockito.when(service.findAssignment(frodo)) + .thenReturn(saveTheWorld) + .thenReturn(keepTheRing); + + final Iterator assignments = stub.findAssignmentOutStream(frodo); + List assignmentsList = new ArrayList<>(); + assignments.forEachRemaining(assignmentsList::add); + + assertThat(assignmentsList, Matchers.contains(saveTheWorld, keepTheRing)); + Mockito.verify(service, Mockito.times(2)).findAssignment(frodo); + + } + + @Test + public void outStreamPostAuthorizeCallFailTest() { + final TaskServiceGrpc.TaskServiceBlockingStub stub = TaskServiceGrpc.newBlockingStub(getChannel()) + .withCallCredentials(user1Credentials()); + + Mockito.when(service.findAssignment(frodo)) + .thenReturn(saveTheWorld) + .thenReturn(noopAssigment); + + final StatusRuntimeException statusRuntimeException = Assert.assertThrows(StatusRuntimeException.class, () -> { + stub.findAssignmentOutStream(frodo) + .forEachRemaining(a -> { + }); + }); + assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.PERMISSION_DENIED)); + + Mockito.verify(service, Mockito.times(2)).findAssignment(frodo); + } + + @Test + public void outStreamPreAuthorizeCallFailTest() { + final TaskServiceGrpc.TaskServiceBlockingStub stub = TaskServiceGrpc.newBlockingStub(getChannel()) + .withCallCredentials(user1Credentials()); + + Mockito.when(service.findAssignment(sam)) + .thenReturn(saveTheWorld) + .thenReturn(keepTheRing); + + final StatusRuntimeException statusRuntimeException = Assert.assertThrows(StatusRuntimeException.class, () -> { + stub.findAssignmentOutStream(sam) + .forEachRemaining(a -> { + }); + }); + assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.PERMISSION_DENIED)); + + Mockito.verify(service, Mockito.never()).findAssignment(sam); + } + + + @Test + public void inStreamPrePostAuthorizeOkCallTest() throws Throwable { + final TaskServiceGrpc.TaskServiceStub stub = TaskServiceGrpc.newStub(getChannel()) + .withCallCredentials(user1Credentials()); + + Mockito.when(service.findAssignment(frodo)) + .thenReturn(saveTheWorld) + .thenReturn(keepTheRing); + + final AggregatingStreamObserver responseObserver = new AggregatingStreamObserver<>(); + final StreamObserver personsIn = stub.findAssignmentInStream(responseObserver); + personsIn.onNext(frodo); + personsIn.onNext(frodo); + personsIn.onCompleted(); + + final List response = responseObserver.get(Duration.ofSeconds(10)); + + assertThat(response, Matchers.hasSize(1)); + assertThat(response.get(0).getDescription(), Matchers.is( + Stream.of(saveTheWorld, keepTheRing) + .map(Assignment::getDescription) + .collect(Collectors.joining(System.lineSeparator())) + )); + Mockito.verify(service, Mockito.times(2)).findAssignment(frodo); + + + } + @Test + public void inStreamPreAuthorizeFailCallTest() { + final TaskServiceGrpc.TaskServiceStub stub = TaskServiceGrpc.newStub(getChannel()) + .withCallCredentials(user1Credentials()); + + Mockito.when(service.findAssignment(frodo)) + .thenReturn(saveTheWorld); + + Mockito.when(service.findAssignment(sam)) + .thenReturn(keepTheRing); + + + final StatusRuntimeException statusRuntimeException = Assert.assertThrows(StatusRuntimeException.class, () -> { + final AggregatingStreamObserver responseObserver = new AggregatingStreamObserver<>(); + final StreamObserver personsIn = stub.findAssignmentInStream(responseObserver); + personsIn.onNext(frodo); + personsIn.onNext(sam); + personsIn.onCompleted(); + responseObserver.get(Duration.ofSeconds(10)); + }); + assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.PERMISSION_DENIED)); + + Mockito.verify(service, Mockito.only()).findAssignment(frodo); + + } + + @Test + public void inStreamPostAuthorizeFailCallTest() { + final TaskServiceGrpc.TaskServiceStub stub = TaskServiceGrpc.newStub(getChannel()) + .withCallCredentials(user1Credentials()); + + Mockito.when(service.findAssignment(frodo)) + .thenReturn(noopAssigment); + + + final StatusRuntimeException statusRuntimeException = Assert.assertThrows(StatusRuntimeException.class, () -> { + final AggregatingStreamObserver responseObserver = new AggregatingStreamObserver<>(); + final StreamObserver personsIn = stub.findAssignmentInStream(responseObserver); + personsIn.onNext(frodo); + personsIn.onCompleted(); + responseObserver.get(Duration.ofSeconds(10)); + }); + assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.PERMISSION_DENIED)); + + Mockito.verify(service, Mockito.only()).findAssignment(frodo); + + } + + private AuthCallCredentials user1Credentials() { + return new AuthCallCredentials( + AuthHeader.builder() + .basic("user1", "user1".getBytes(StandardCharsets.UTF_8)) + ); + } + private AuthCallCredentials user2Credentials() { + return new AuthCallCredentials( + AuthHeader.builder() + .basic("user2", "user2".getBytes(StandardCharsets.UTF_8)) + ); + } + private AuthCallCredentials unAuthUserCredentials() { + return new AuthCallCredentials( + AuthHeader.builder() + .basic("dummy", "dummy".getBytes(StandardCharsets.UTF_8)) + ); + } +} diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/SecurityInterceptorTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/SecurityInterceptorTest.java index a155fa26..e6d8fda7 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/SecurityInterceptorTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/SecurityInterceptorTest.java @@ -8,11 +8,9 @@ import io.grpc.ServerInterceptor; import io.grpc.Status; import io.grpc.StatusRuntimeException; -import io.grpc.examples.GreeterGrpc; -import io.grpc.examples.GreeterOuterClass; +import io.grpc.examples.SecuredGreeterGrpc; import org.hamcrest.Matchers; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.lognet.springboot.grpc.GRpcErrorHandler; @@ -34,7 +32,7 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import io.grpc.examples.SecuredGreeterGrpc; + import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -109,34 +107,7 @@ public void unsupportedAuthSchemeShouldThrowUnauthenticatedException() { verify(errorHandler).handle(any(),eq(Status.UNAUTHENTICATED), any(),any(),any()); } - @Test - @Ignore("@PreAuthorize is not supported yet") - public void preAuthorizeTest() { - final GreeterGrpc.GreeterBlockingStub greeterBlockingStub = GreeterGrpc.newBlockingStub(getChannel()) - .withCallCredentials(userCredentials()); - - - final GreeterOuterClass.Person personIn = GreeterOuterClass.Person.newBuilder() - .setName("Frodo") - .setAddress(GreeterOuterClass.Address.newBuilder().setCity("Shire")) - .setAge(11) - .build(); - final GreeterOuterClass.Person personOut = greeterBlockingStub.sayPreAuthHello(personIn); - assertThat(personOut.toBuilder().clearNickName().build(), - Matchers.is(personIn) - ); - - final StatusRuntimeException statusRuntimeException = Assert.assertThrows(StatusRuntimeException.class, () -> { - greeterBlockingStub.sayPreAuthHello(GreeterOuterClass.Person.newBuilder() - .setName("Aragorn") - .setAddress(GreeterOuterClass.Address.newBuilder().setCity("Isildur")) - .setAge(2) - .build()); - }); - assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.PERMISSION_DENIED)); - - } private AuthCallCredentials userCredentials(){ return new AuthCallCredentials( AuthHeader.builder() diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java index e4f550aa..9341e752 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java @@ -9,19 +9,14 @@ import io.grpc.examples.CalculatorGrpc; import io.grpc.examples.CalculatorOuterClass; import io.grpc.examples.GreeterGrpc; -import io.grpc.examples.GreeterOuterClass; import io.grpc.examples.SecuredCalculatorGrpc; -import io.grpc.examples.SecuredGreeterGrpc; import org.hamcrest.Matchers; -import org.junit.Ignore; import org.junit.Test; -import org.junit.jupiter.api.Disabled; import org.junit.runner.RunWith; import org.lognet.springboot.grpc.GrpcServerTestBase; import org.lognet.springboot.grpc.demo.DemoApp; import org.lognet.springboot.grpc.security.AuthClientInterceptor; import org.lognet.springboot.grpc.security.AuthHeader; -import org.lognet.springboot.grpc.security.EnableGrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter; import org.springframework.beans.factory.annotation.Autowired; @@ -29,7 +24,6 @@ import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -40,8 +34,6 @@ import java.util.concurrent.ExecutionException; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.emptyOrNullString; -import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -110,13 +102,14 @@ public void simpleAuthHeaderFormat() throws ExecutionException, InterruptedExcep public void shouldFailWithPermissionDenied() { final StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class, () -> { - CalculatorGrpc + final CalculatorOuterClass.CalculatorResponse response = CalculatorGrpc .newBlockingStub(selectedChanel) //auth channel .calculate(CalculatorOuterClass.CalculatorRequest.newBuilder() - .setNumber1(1) - .setNumber2(1) - .setOperation(CalculatorOuterClass.CalculatorRequest.OperationType.ADD) - .build()); + .setNumber1(1) + .setNumber2(1) + .setOperation(CalculatorOuterClass.CalculatorRequest.OperationType.ADD) + .build()); + System.out.println(response); }); assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.PERMISSION_DENIED)); } diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java index 9ef6de55..9154b3de 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java @@ -5,8 +5,6 @@ import com.ecwid.consul.v1.QueryParams; import com.ecwid.consul.v1.health.HealthServicesRequest; import com.ecwid.consul.v1.health.model.HealthService; -import com.pszymczyk.consul.ConsulProcess; -import com.pszymczyk.consul.ConsulStarterBuilder; import io.grpc.BindableService; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; @@ -17,9 +15,7 @@ import org.awaitility.Awaitility; import org.hamcrest.Matchers; import org.junit.After; -import org.junit.AfterClass; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import org.lognet.springboot.grpc.autoconfigure.GRpcServerProperties; import org.lognet.springboot.grpc.autoconfigure.consul.ServiceRegistrationMode; @@ -28,11 +24,9 @@ import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.test.context.ActiveProfiles; -import org.springframework.util.SocketUtils; import java.time.Duration; import java.util.List; -import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -41,25 +35,6 @@ @ActiveProfiles("consul-test") public abstract class ConsulRegistrationBaseTest { - private static ConsulProcess consul; - - @BeforeClass - public static void startConsul() { - int port = SocketUtils.findAvailableTcpPort(); - - - consul = ConsulStarterBuilder.consulStarter().withHttpPort(port).build().start(); - System.setProperty("spring.cloud.consul.port", String.valueOf(port)); - - } - - @AfterClass - public static void clear() { - System.clearProperty("spring.cloud.consul.port"); - Optional.ofNullable(consul).ifPresent(ConsulProcess::close); - - } - @Autowired protected DiscoveryClient discoveryClient; @@ -75,8 +50,9 @@ public static void clear() { @Before public void setUp() throws Exception { - - consulClient = new ConsulClient("localhost", Integer.parseInt(System.getProperty("spring.cloud.consul.port"))); + final String port = applicationContext.getEnvironment().getProperty("embedded.consul.port"); + final String host = applicationContext.getEnvironment().getProperty("embedded.consul.host"); + consulClient = new ConsulClient(host, Integer.parseInt(port)); List instances = discoveryClient.getInstances(serviceId); @@ -108,6 +84,8 @@ public void tearDown() throws Exception { channel.shutdownNow(); channel.awaitTermination(1, TimeUnit.SECONDS); } + // explicitly close the context to trigger services de-registration + // since we share the same instance of Consul between tests applicationContext.stop(); } diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/recovery/GRpcRecoveryTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/recovery/GRpcRecoveryTest.java index 2d8787c7..19586a58 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/recovery/GRpcRecoveryTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/recovery/GRpcRecoveryTest.java @@ -21,6 +21,8 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -150,7 +152,7 @@ public Status handleB(ExceptionB e, GRpcExceptionScope scope) { @Test - public void streamingServiceErrorHandlerTest() throws ExecutionException, InterruptedException { + public void streamingServiceErrorHandlerTest() throws ExecutionException, InterruptedException, TimeoutException { @@ -181,7 +183,7 @@ public void onCompleted() { - final Throwable actual = errorFuture.get(); + final Throwable actual = errorFuture.get(20, TimeUnit.SECONDS); assertThat(actual, notNullValue()); assertThat(actual, isA(StatusRuntimeException.class)); assertThat(((StatusRuntimeException)actual).getStatus(), is(Status.RESOURCE_EXHAUSTED)); diff --git a/grpc-spring-boot-starter-demo/src/test/resources/application-consul-test.yml b/grpc-spring-boot-starter-demo/src/test/resources/application-consul-test.yml index 8a3a5ebb..9a32458c 100644 --- a/grpc-spring-boot-starter-demo/src/test/resources/application-consul-test.yml +++ b/grpc-spring-boot-starter-demo/src/test/resources/application-consul-test.yml @@ -6,6 +6,8 @@ spring: discovery: enabled: true enabled: true + port: ${embedded.consul.port} + host: ${embedded.consul.host} service-registry: auto-registration: enabled: true \ No newline at end of file diff --git a/grpc-spring-boot-starter-demo/src/test/resources/bootstrap-consul-test.yml b/grpc-spring-boot-starter-demo/src/test/resources/bootstrap-consul-test.yml new file mode 100644 index 00000000..a8b6e2cd --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/test/resources/bootstrap-consul-test.yml @@ -0,0 +1,7 @@ +embedded: + consul: + enabled: true + containers: + enabled: true + + diff --git a/grpc-spring-boot-starter-demo/src/test/resources/bootstrap.yml b/grpc-spring-boot-starter-demo/src/test/resources/bootstrap.yml index ccb576c6..cc7460c4 100644 --- a/grpc-spring-boot-starter-demo/src/test/resources/bootstrap.yml +++ b/grpc-spring-boot-starter-demo/src/test/resources/bootstrap.yml @@ -12,6 +12,9 @@ spring: config: uri: http://localhost:${config.port:8888} embedded: + consul: + enabled: false + reuse-container: true keycloak: enabled: false wait-timeout-in-seconds: 120 diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/FailureHandlingSupport.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/FailureHandlingSupport.java index b9132294..88b3ca43 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/FailureHandlingSupport.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/FailureHandlingSupport.java @@ -3,6 +3,7 @@ import io.grpc.Metadata; import io.grpc.ServerCall; import io.grpc.Status; +import lombok.extern.slf4j.Slf4j; import org.lognet.springboot.grpc.recovery.GRpcExceptionHandlerMethodResolver; import org.lognet.springboot.grpc.recovery.GRpcExceptionScope; import org.lognet.springboot.grpc.recovery.GRpcRuntimeExceptionWrapper; @@ -10,56 +11,66 @@ import java.util.Optional; import java.util.function.Consumer; - +@Slf4j public class FailureHandlingSupport { private final GRpcExceptionHandlerMethodResolver methodResolver; + + public FailureHandlingSupport(GRpcExceptionHandlerMethodResolver methodResolver) { this.methodResolver = methodResolver; } + + + public void closeCall(RuntimeException e, ServerCall call, Metadata headers) throws RuntimeException { closeCall(e,call,headers,null); } - public void closeCall(RuntimeException e, ServerCall call, Metadata headers, Consumer customizer) throws RuntimeException { + public void closeCall( RuntimeException e, ServerCall call, Metadata headers, Consumer customizer) throws RuntimeException { - final Optional handlerMethod = methodResolver.resolveMethodByThrowable(call.getMethodDescriptor().getServiceName(), e); - if (handlerMethod.isPresent()) { - final GRpcExceptionScope.GRpcExceptionScopeBuilder exceptionScopeBuilder = GRpcExceptionScope.builder() - .callHeaders(headers) - .methodCallAttributes(call.getAttributes()) - .methodDescriptor(call.getMethodDescriptor()) - .hint(GRpcRuntimeExceptionWrapper.getHint(e)); - Optional.ofNullable(customizer) - .ifPresent(c -> c.accept(exceptionScopeBuilder)); - - final GRpcExceptionScope excScope = exceptionScopeBuilder.build(); - - final HandlerMethod handler = handlerMethod.get(); Status statusToSend = Status.INTERNAL; - try { - statusToSend = handler.invoke(GRpcRuntimeExceptionWrapper.unwrap(e), excScope); - } catch (Exception handlerException) { - - org.slf4j.LoggerFactory.getLogger(this.getClass()) - .error("Caught exception while executing handler method {}, returning {} status.", - handler.getMethod(), - statusToSend, - handlerException); - + Metadata metadataToSend = null; + + final Optional handlerMethod = methodResolver.resolveMethodByThrowable(call.getMethodDescriptor().getServiceName(), e); + if (handlerMethod.isPresent()) { + final GRpcExceptionScope.GRpcExceptionScopeBuilder exceptionScopeBuilder = GRpcExceptionScope.builder() + .callHeaders(headers) + .methodCallAttributes(call.getAttributes()) + .methodDescriptor(call.getMethodDescriptor()) + .hint(GRpcRuntimeExceptionWrapper.getHint(e)); + Optional.ofNullable(customizer) + .ifPresent(c -> c.accept(exceptionScopeBuilder)); + + final GRpcExceptionScope excScope = exceptionScopeBuilder.build(); + + final HandlerMethod handler = handlerMethod.get(); + + try { + statusToSend = handler.invoke(GRpcRuntimeExceptionWrapper.unwrap(e), excScope); + metadataToSend = excScope.getResponseHeaders(); + } catch (Exception handlerException) { + + org.slf4j.LoggerFactory.getLogger(this.getClass()) + .error("Caught exception while executing handler method {}, returning {} status.", + handler.getMethod(), + statusToSend, + handlerException); + + } } - call.close(statusToSend, excScope.getResponseHeaders()); + log.warn("Closing call with {}",statusToSend,GRpcRuntimeExceptionWrapper.unwrap(e)); + call.close(statusToSend, Optional.ofNullable(metadataToSend).orElseGet(Metadata::new)); - } else { - call.close(Status.INTERNAL, new Metadata()); - } } + + } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/MessageBlockingServerCallListener.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/MessageBlockingServerCallListener.java index 6903f306..26596de3 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/MessageBlockingServerCallListener.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/MessageBlockingServerCallListener.java @@ -4,6 +4,8 @@ import io.grpc.ServerCall; + + public class MessageBlockingServerCallListener extends ForwardingServerCallListener.SimpleForwardingServerCallListener { diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/security/GrpcSecurityEnablerConfiguration.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/security/GrpcSecurityEnablerConfiguration.java deleted file mode 100644 index 2737c34d..00000000 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/security/GrpcSecurityEnablerConfiguration.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.lognet.springboot.grpc.autoconfigure.security; - -import org.lognet.springboot.grpc.security.EnableGrpcSecurity; -import org.springframework.context.annotation.Configuration; - - -@Configuration -@EnableGrpcSecurity -public class GrpcSecurityEnablerConfiguration { - -} diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/security/SecurityAutoConfiguration.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/security/SecurityAutoConfiguration.java index 540e3b8c..88c6ef01 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/security/SecurityAutoConfiguration.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/security/SecurityAutoConfiguration.java @@ -1,10 +1,8 @@ package org.lognet.springboot.grpc.autoconfigure.security; import org.lognet.springboot.grpc.GRpcServerRunner; -import org.lognet.springboot.grpc.GRpcService; import org.lognet.springboot.grpc.autoconfigure.GRpcAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.lognet.springboot.grpc.security.GrpcSecurityConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -12,12 +10,15 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; @Configuration @AutoConfigureAfter({GRpcAutoConfiguration.class}) @ConditionalOnBean(value = {GRpcServerRunner.class}) @ConditionalOnProperty(value = "grpc.security.auth.enabled", matchIfMissing = true, havingValue = "true") @ConditionalOnClass(AuthenticationConfiguration.class) -@Import(GrpcSecurityEnablerConfiguration.class) +@Import({ GrpcSecurityConfiguration.class}) +@EnableGlobalAuthentication + public class SecurityAutoConfiguration { } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/recovery/GRpcExceptionHandlerInterceptor.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/recovery/GRpcExceptionHandlerInterceptor.java index e708fb24..de7dc690 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/recovery/GRpcExceptionHandlerInterceptor.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/recovery/GRpcExceptionHandlerInterceptor.java @@ -1,21 +1,25 @@ package org.lognet.springboot.grpc.recovery; +import io.grpc.Context; import io.grpc.ForwardingServerCall; import io.grpc.Metadata; import io.grpc.ServerCall; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; +import io.grpc.Status; import org.lognet.springboot.grpc.FailureHandlingSupport; import org.lognet.springboot.grpc.MessageBlockingServerCallListener; import org.springframework.core.Ordered; -public class GRpcExceptionHandlerInterceptor implements ServerInterceptor, Ordered { +import java.util.concurrent.atomic.AtomicBoolean; +public class GRpcExceptionHandlerInterceptor implements ServerInterceptor, Ordered { + private static final Context.Key CALL_IS_CLOSED = Context.key("CALL_IS_CLOSED"); private final GRpcExceptionHandlerMethodResolver methodResolver; private final FailureHandlingSupport failureHandlingSupport; - public GRpcExceptionHandlerInterceptor(GRpcExceptionHandlerMethodResolver methodResolver,FailureHandlingSupport failureHandlingSupport) { + public GRpcExceptionHandlerInterceptor(GRpcExceptionHandlerMethodResolver methodResolver, FailureHandlingSupport failureHandlingSupport) { this.methodResolver = methodResolver; this.failureHandlingSupport = failureHandlingSupport; } @@ -23,6 +27,10 @@ public GRpcExceptionHandlerInterceptor(GRpcExceptionHandlerMethodResolver method @Override public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + + final AtomicBoolean callIsClosed = new AtomicBoolean(false); + + if (!methodResolver.hasErrorHandlers()) { return next.startCall(call, headers); } @@ -30,6 +38,14 @@ public ServerCall.Listener interceptCall(ServerCall errorHandlingCall = new ForwardingServerCall.SimpleForwardingServerCall(call) { + @Override + public void close(Status status, Metadata trailers) { + if( callIsClosed.compareAndSet(false,true)){ + super.close(status, trailers); + } + + } + @Override public void sendMessage(RespT message) { try { @@ -39,37 +55,40 @@ public void sendMessage(RespT message) { } } }; + final ServerCall.Listener listener; try { - final ServerCall.Listener listener = next.startCall(errorHandlingCall, headers); - - return new MessageBlockingServerCallListener(listener) { - private ReqT request; - - @Override - public void onMessage(ReqT message) { - try { - request = message; - super.onMessage(message); - } catch (RuntimeException e) { - blockMessage(); - failureHandlingSupport.closeCall(e, errorHandlingCall, headers, b -> b.request(request)); - } + + listener = next.startCall( errorHandlingCall, headers); + } catch (RuntimeException e) { + failureHandlingSupport.closeCall(e, errorHandlingCall, headers); + return new ServerCall.Listener() { + + }; + } + return new MessageBlockingServerCallListener(listener) { + private ReqT request; + + @Override + public void onMessage(ReqT message) { + try { + request = message; + super.onMessage(message); + } catch (RuntimeException e) { + blockMessage(); + failureHandlingSupport.closeCall(e, errorHandlingCall, headers, b -> b.request(request)); } + } - @Override - public void onHalfClose() { - try { + @Override + public void onHalfClose() { + try { + if(!callIsClosed.get()) { super.onHalfClose(); - } catch (RuntimeException e) { - failureHandlingSupport.closeCall(e, errorHandlingCall, headers, b -> b.request(request)); } + } catch (RuntimeException e) { + failureHandlingSupport.closeCall(e, errorHandlingCall, headers, b -> b.request(request)); } - - }; - } catch (RuntimeException e) { - failureHandlingSupport.closeCall(e, errorHandlingCall, headers); - } - return new ServerCall.Listener() { + } }; @@ -77,7 +96,6 @@ public void onHalfClose() { } - @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/recovery/GRpcExceptionHandlerMethodResolver.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/recovery/GRpcExceptionHandlerMethodResolver.java index 4ad57c65..293c0eba 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/recovery/GRpcExceptionHandlerMethodResolver.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/recovery/GRpcExceptionHandlerMethodResolver.java @@ -65,6 +65,9 @@ private GRpcExceptionHandlerMethodResolver(Collection advices) { public Optional resolveMethodByThrowable(String grpcServiceName, Throwable exc) { + if(null==exc){ + return Optional.empty(); + } Throwable exception = GRpcRuntimeExceptionWrapper.unwrap(exc); Optional method = Optional.ofNullable(privateResolvers) diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/AuthenticatedAttributeVoter.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/AuthenticatedAttributeVoter.java index 163d3dbf..53d6c99d 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/AuthenticatedAttributeVoter.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/AuthenticatedAttributeVoter.java @@ -14,7 +14,7 @@ public boolean supports(ConfigAttribute attribute) { @Override public boolean supports(Class clazz) { - return io.grpc.MethodDescriptor.class.equals(clazz); + return true; } @Override diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/EnableGrpcSecurity.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/EnableGrpcSecurity.java deleted file mode 100644 index 7e6a0486..00000000 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/EnableGrpcSecurity.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.lognet.springboot.grpc.security; - -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; - -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME) -@Target(value = { java.lang.annotation.ElementType.TYPE }) -@Documented -@Import({ GrpcSecurityConfiguration.class}) -@EnableGlobalAuthentication -@Configuration -public @interface EnableGrpcSecurity { -} diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java index 02726f7e..249b5257 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java @@ -2,10 +2,23 @@ import io.grpc.Context; import io.grpc.ServerInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.lognet.springboot.grpc.GRpcServicesRegistry; import org.lognet.springboot.grpc.autoconfigure.GRpcServerProperties; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.ExpressionBasedAnnotationAttributeFactory; +import org.springframework.security.access.expression.method.ExpressionBasedPostInvocationAdvice; +import org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice; +import org.springframework.security.access.intercept.AfterInvocationManager; +import org.springframework.security.access.intercept.AfterInvocationProviderManager; +import org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource; +import org.springframework.security.access.prepost.PostInvocationAdviceProvider; +import org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter; +import org.springframework.security.access.prepost.PrePostAnnotationSecurityMetadataSource; import org.springframework.security.access.vote.AffirmativeBased; import org.springframework.security.access.vote.RoleVoter; import org.springframework.security.authentication.AuthenticationProvider; @@ -18,6 +31,7 @@ import org.springframework.security.core.userdetails.UserDetailsService; import java.util.Arrays; +import java.util.Collection; import java.util.Optional; public class GrpcSecurity extends AbstractConfiguredSecurityBuilder @@ -34,7 +48,7 @@ public GrpcSecurity(ObjectPostProcessor objectPostProcessor) { public GrpcServiceAuthorizationConfigurer.Registry authorizeRequests() throws Exception { - return getOrApply(new GrpcServiceAuthorizationConfigurer (applicationContext)) + return getOrApply(new GrpcServiceAuthorizationConfigurer (applicationContext.getBean(GRpcServicesRegistry.class))) .getRegistry(); } @@ -72,12 +86,42 @@ protected ServerInterceptor performBuild() throws Exception { - final SecurityInterceptor securityInterceptor = new SecurityInterceptor(getSharedObject(GrpcSecurityMetadataSource.class), - getAuthenticationSchemeService()); + final GrpcSecurityMetadataSource metadataSource =getSharedObject(GrpcSecurityMetadataSource.class); + final DelegatingMethodSecurityMetadataSource compositeMDS = new DelegatingMethodSecurityMetadataSource(Arrays.asList( + metadataSource, + new PrePostAnnotationSecurityMetadataSource( + new ExpressionBasedAnnotationAttributeFactory( + new DefaultMethodSecurityExpressionHandler() + ) + ) + )); + final SecurityInterceptor securityInterceptor = new SecurityInterceptor(compositeMDS,getAuthenticationSchemeService()); + securityInterceptor.setAfterInvocationManager(afterInvocationManager()); securityInterceptor.setAuthenticationManager(getSharedObject(AuthenticationManagerBuilder.class).build()); final RoleVoter scopeVoter = new RoleVoter(); scopeVoter.setRolePrefix("SCOPE_"); - securityInterceptor.setAccessDecisionManager(new AffirmativeBased(Arrays.asList(new RoleVoter(),scopeVoter, new AuthenticatedAttributeVoter()))); + + + + ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice(); + expressionAdvice.setExpressionHandler(new DefaultMethodSecurityExpressionHandler()); + + + final AffirmativeBased accessDecisionManager = new AffirmativeBased(Arrays.asList( + new RoleVoter(), + scopeVoter, + new AuthenticatedAttributeVoter(), + new PreInvocationAuthorizationAdviceVoter(expressionAdvice){ + @Override + public int vote(Authentication authentication, MethodInvocation method, Collection attributes) { + // first time invoked without arguments + return null==method.getArguments() ? ACCESS_GRANTED: super.vote(authentication, method, attributes); + } + } + + )); + + securityInterceptor.setAccessDecisionManager(accessDecisionManager); final GRpcServerProperties.SecurityProperties.Auth authCfg = Optional.of(applicationContext.getBean(GRpcServerProperties.class)) .map(GRpcServerProperties::getSecurity) .map(GRpcServerProperties.SecurityProperties::getAuth) @@ -99,4 +143,28 @@ private AuthenticationManagerBuilder getAuthenticationRegistry() { private AuthenticationSchemeService getAuthenticationSchemeService() { return getSharedObject(AuthenticationSchemeService.class); } + + protected AfterInvocationManager afterInvocationManager() { + + AfterInvocationProviderManager invocationProviderManager = new AfterInvocationProviderManager(); + ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice( + new DefaultMethodSecurityExpressionHandler()); + PostInvocationAdviceProvider postInvocationAdviceProvider = new PostInvocationAdviceProvider(postAdvice){ + @Override + public boolean supports(Class clazz) { + return MethodInvocation.class.isAssignableFrom(clazz); //todo : remove once fixed https://github.com/spring-projects/spring-security/issues/10236 + } + }; + + + + invocationProviderManager.setProviders(Arrays.asList( + postInvocationAdviceProvider + )); + invocationProviderManager.afterPropertiesSet(); + return invocationProviderManager; + + } + + } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfiguration.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfiguration.java index 6b4d692c..3a2911f4 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfiguration.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfiguration.java @@ -1,8 +1,10 @@ package org.lognet.springboot.grpc.security; +import io.grpc.BindableService; import io.grpc.ServerInterceptor; import io.grpc.Status; import lombok.extern.slf4j.Slf4j; +import org.aopalliance.intercept.MethodInterceptor; import org.lognet.springboot.grpc.GRpcErrorHandler; import org.lognet.springboot.grpc.GRpcGlobalInterceptor; import org.lognet.springboot.grpc.autoconfigure.ConditionalOnMissingErrorHandler; @@ -10,13 +12,16 @@ import org.lognet.springboot.grpc.recovery.GRpcExceptionHandler; import org.lognet.springboot.grpc.recovery.GRpcExceptionScope; import org.lognet.springboot.grpc.recovery.GRpcServiceAdvice; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.core.AuthenticationException; @@ -38,6 +43,23 @@ public class GrpcSecurityConfiguration { private GrpcSecurity grpcSecurity; + @Bean + public BeanPostProcessor bypassMethodInterceptorForGrpcMethodInvocation(){ + return new BeanPostProcessor() { + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if(bean instanceof MethodSecurityInterceptor){ + return (MethodInterceptor) invocation -> { + if (BindableService.class.isAssignableFrom(invocation.getMethod().getDeclaringClass())){ + return invocation.proceed(); + } + return ((MethodSecurityInterceptor) bean).invoke(invocation); + }; + } + return bean; + } + }; + } @ConditionalOnMissingErrorHandler(AccessDeniedException.class) @Configuration @@ -83,7 +105,7 @@ public GrpcSecurityConfigurerAdapter defaultAdapter(){ @Bean @GRpcGlobalInterceptor - public ServerInterceptor springGrpcSecurityInterceptor() throws Exception { + public ServerInterceptor springGrpcSecurityInterceptor() throws Exception { boolean hasConfigurers = grpcSecurityConfigurers != null && !grpcSecurityConfigurers.isEmpty(); if (!hasConfigurers) { GrpcSecurityConfigurerAdapter adapter = objectObjectPostProcessor.postProcess(new GrpcSecurityConfigurerAdapter() { diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfigurerAdapter.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfigurerAdapter.java index bb7a5c2e..b7b87286 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfigurerAdapter.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfigurerAdapter.java @@ -1,6 +1,7 @@ package org.lognet.springboot.grpc.security; import lombok.Getter; +import org.lognet.springboot.grpc.GRpcServicesRegistry; import org.lognet.springboot.grpc.security.jwt.JwtAuthProviderFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -9,8 +10,6 @@ import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.oauth2.jwt.JwtDecoder; -import java.util.Map; - public abstract class GrpcSecurityConfigurerAdapter extends GrpcSecurityConfigurer { private AuthenticationConfiguration authenticationConfiguration; @@ -39,7 +38,7 @@ public void setApplicationContext(ApplicationContext context) throws Exception { @Override public void init(GrpcSecurity builder) throws Exception { - builder.apply(new GrpcServiceAuthorizationConfigurer(builder.getApplicationContext())); + builder.apply(new GrpcServiceAuthorizationConfigurer(builder.getApplicationContext().getBean(GRpcServicesRegistry.class))); builder.setSharedObject(AuthenticationManagerBuilder.class, authenticationManagerBuilder); final AuthenticationSchemeService authenticationSchemeService = new AuthenticationSchemeService(); diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityMetadataSource.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityMetadataSource.java index 4bc00505..c4f8c8f0 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityMetadataSource.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityMetadataSource.java @@ -1,29 +1,46 @@ package org.lognet.springboot.grpc.security; +import io.grpc.BindableService; import io.grpc.MethodDescriptor; +import io.grpc.ServerMethodDefinition; +import org.lognet.springboot.grpc.GRpcServicesRegistry; import org.springframework.security.access.ConfigAttribute; -import org.springframework.security.access.SecurityMetadataSource; +import org.springframework.security.access.method.MethodSecurityMetadataSource; +import java.lang.reflect.Method; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; -public class GrpcSecurityMetadataSource implements SecurityMetadataSource { - private Map, List> methodsMap; +public class GrpcSecurityMetadataSource implements MethodSecurityMetadataSource { + private Map, List> methodDescriptorMap; + private Map> methodMap = new HashMap<>(); - public GrpcSecurityMetadataSource(Map, List> methodsMap) { - this.methodsMap = methodsMap; + public GrpcSecurityMetadataSource(GRpcServicesRegistry registry , Map, List> methodDescriptorMap) { + this.methodDescriptorMap = methodDescriptorMap; + + for(BindableService s:registry.getBeanNameToServiceBeanMap().values()){ + for(ServerMethodDefinition md :s.bindService().getMethods()){ + final Method method = Stream.of(s.getClass().getMethods()) + .filter(m -> md.getMethodDescriptor().getBareMethodName().equalsIgnoreCase(m.getName())) + .findFirst().get(); + methodMap.put(method,md.getMethodDescriptor()); + } + } } @Override public Collection getAttributes(Object object) throws IllegalArgumentException { - return methodsMap.get(object); + final MethodDescriptor methodDescriptor = SecurityInterceptor.GrpcMethodInvocation.class.cast(object).getCall().getMethodDescriptor(); + return methodDescriptorMap.get(methodDescriptor); } @Override public Collection getAllConfigAttributes() { - return methodsMap + return methodDescriptorMap .values() .stream() .flatMap(Collection::stream) @@ -32,6 +49,12 @@ public Collection getAllConfigAttributes() { @Override public boolean supports(Class clazz) { - return MethodDescriptor.class.isAssignableFrom(clazz); + return SecurityInterceptor.GrpcMethodInvocation.class.isAssignableFrom(clazz); + } + + @Override + public Collection getAttributes(Method method, Class targetClass) { + final MethodDescriptor methodDescriptor = methodMap.get(method); + return methodDescriptorMap.get(methodDescriptor); } } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcServiceAuthorizationConfigurer.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcServiceAuthorizationConfigurer.java index 16c75294..e9757e4f 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcServiceAuthorizationConfigurer.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcServiceAuthorizationConfigurer.java @@ -6,7 +6,7 @@ import io.grpc.ServerMethodDefinition; import io.grpc.ServerServiceDefinition; import io.grpc.ServiceDescriptor; -import org.springframework.context.ApplicationContext; +import org.lognet.springboot.grpc.GRpcServicesRegistry; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; @@ -28,8 +28,8 @@ public class GrpcServiceAuthorizationConfigurer private final GrpcServiceAuthorizationConfigurer.Registry registry; - public GrpcServiceAuthorizationConfigurer(ApplicationContext context) { - this.registry = new GrpcServiceAuthorizationConfigurer.Registry(context); + public GrpcServiceAuthorizationConfigurer(GRpcServicesRegistry registry) { + this.registry = new GrpcServiceAuthorizationConfigurer.Registry(registry); } public Registry getRegistry() { @@ -39,7 +39,7 @@ public Registry getRegistry() { @Override public void configure(GrpcSecurity builder) throws Exception { registry.processSecuredAnnotation(); - builder.setSharedObject(GrpcSecurityMetadataSource.class, new GrpcSecurityMetadataSource(registry.securedMethods)); + builder.setSharedObject(GrpcSecurityMetadataSource.class, new GrpcSecurityMetadataSource(registry.servicesRegistry,registry.securedMethods)); } @@ -86,15 +86,15 @@ public GrpcServiceAuthorizationConfigurer.Registry hasAnyAuthority(String... aut public class Registry { private MultiValueMap, ConfigAttribute> securedMethods = new LinkedMultiValueMap<>(); - private ApplicationContext context; + GRpcServicesRegistry servicesRegistry; private boolean withSecuredAnnotation = true; - Registry(ApplicationContext context) { - this.context = context; + Registry(GRpcServicesRegistry servicesRegistry) { + this.servicesRegistry = servicesRegistry; } public AuthorizedMethod anyMethod() { - ServiceDescriptor[] allServices = context.getBeansOfType(BindableService.class) + ServiceDescriptor[] allServices = servicesRegistry.getBeanNameToServiceBeanMap() .values() .stream() .map(BindableService::bindService) @@ -122,7 +122,7 @@ public GrpcSecurity withSecuredAnnotation(boolean withSecuredAnnotation) { private void processSecuredAnnotation() { if (withSecuredAnnotation) { - final Collection services = context.getBeansOfType(BindableService.class).values(); + final Collection services = servicesRegistry.getBeanNameToServiceBeanMap().values(); for (BindableService service : services) { final ServerServiceDefinition serverServiceDefinition = service.bindService(); diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java index b4d76900..c704001a 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java @@ -1,5 +1,6 @@ package org.lognet.springboot.grpc.security; +import io.grpc.BindableService; import io.grpc.Context; import io.grpc.Contexts; import io.grpc.ForwardingServerCall; @@ -9,45 +10,124 @@ import io.grpc.ServerCall; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.lognet.springboot.grpc.FailureHandlingSupport; +import org.lognet.springboot.grpc.GRpcServicesRegistry; import org.lognet.springboot.grpc.MessageBlockingServerCallListener; import org.lognet.springboot.grpc.autoconfigure.GRpcServerProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; +import org.springframework.core.MethodIntrospector; import org.springframework.core.Ordered; import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.SecurityMetadataSource; import org.springframework.security.access.intercept.AbstractSecurityInterceptor; import org.springframework.security.access.intercept.InterceptorStatusToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.util.SimpleMethodInvocation; +import org.springframework.util.ReflectionUtils; +import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.util.AbstractMap; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; +import java.util.function.Function; @Slf4j public class SecurityInterceptor extends AbstractSecurityInterceptor implements ServerInterceptor, Ordered { private static final Context.Key INTERCEPTOR_STATUS_TOKEN = Context.key("INTERCEPTOR_STATUS_TOKEN"); + private static final Context.Key> METHOD_INVOCATION = Context.key("METHOD_INVOCATION"); - private final GrpcSecurityMetadataSource securedMethods; + private final SecurityMetadataSource securityMetadataSource; private final AuthenticationSchemeSelector schemeSelector; private GRpcServerProperties.SecurityProperties.Auth authCfg; private FailureHandlingSupport failureHandlingSupport; + private Map> keyedMethods; + + static class GrpcMethodInvocation extends SimpleMethodInvocation { + final private ServerCall call; + final private Metadata headers; + final private ServerCallHandler next; + @Getter + @Setter + private Object[] arguments; + + public GrpcMethodInvocation(Map.Entry handler, ServerCall call, Metadata headers, ServerCallHandler next) { + super(handler.getKey(), handler.getValue()); + this.call = call; + this.headers = headers; + this.next = next; + } + + @Override + public Object proceed() { + return next.startCall(call, headers); + } + + ServerCall getCall() { + return call; + } + } + + @Getter + @EqualsAndHashCode + static class GrpcServiceMethodKey { + public GrpcServiceMethodKey(MethodDescriptor methodDescriptor) { + this.serviceName = methodDescriptor.getServiceName(); + this.methodName = methodDescriptor.getBareMethodName(); + } + + @EqualsAndHashCode.Include + final private String serviceName; - public SecurityInterceptor(GrpcSecurityMetadataSource securedMethods,AuthenticationSchemeSelector schemeSelector) { - this.securedMethods = securedMethods; + @EqualsAndHashCode.Include + final private String methodName; + + } + + + public SecurityInterceptor(SecurityMetadataSource securityMetadataSource, AuthenticationSchemeSelector schemeSelector) { + this.securityMetadataSource = securityMetadataSource; this.schemeSelector = schemeSelector; } + @Autowired + public void setGRpcServicesRegistry(GRpcServicesRegistry registry) { + + final Map> map = new HashMap<>(); + + Function filterFactory = name -> + method -> method.getName().equalsIgnoreCase(name); + + for (BindableService service : registry.getBeanNameToServiceBeanMap().values()) { + for (MethodDescriptor d : service.bindService().getServiceDescriptor().getMethods()) { + final Method method = MethodIntrospector + .selectMethods(service.getClass(), filterFactory.apply(d.getBareMethodName())) + .iterator().next(); + map.put(new GrpcServiceMethodKey(d), + new AbstractMap.SimpleImmutableEntry<>(service, method)); + + } + } + keyedMethods = Collections.unmodifiableMap(map); + } + @Autowired public void setFailureHandlingSupport(@Lazy FailureHandlingSupport failureHandlingSupport) { this.failureHandlingSupport = failureHandlingSupport; @@ -64,12 +144,12 @@ public int getOrder() { @Override public Class getSecureObjectClass() { - return MethodDescriptor.class; + return GrpcMethodInvocation.class; } @Override - public GrpcSecurityMetadataSource obtainSecurityMetadataSource() { - return securedMethods; + public SecurityMetadataSource obtainSecurityMetadataSource() { + return securityMetadataSource; } @Override @@ -100,7 +180,7 @@ public ServerCall.Listener interceptCall( try { final Context grpcSecurityContext; try { - grpcSecurityContext = setupGRpcSecurityContext(call, authorization); + grpcSecurityContext = setupGRpcSecurityContext(call, headers, next, authorization); } catch (AccessDeniedException | AuthenticationException e) { return fail(next, call, headers, e); } catch (Exception e) { @@ -111,8 +191,6 @@ public ServerCall.Listener interceptCall( } finally { SecurityContextHolder.getContext().setAuthentication(null); } - - } private ServerCallHandler authenticationPropagatingHandler(ServerCallHandler next) { @@ -121,7 +199,41 @@ private ServerCallHandler authenticationPropagatingHa @Override public void onMessage(ReqT message) { - propagateAuthentication(() -> super.onMessage(message)); + propagateAuthentication(() -> { + try { + switch (call.getMethodDescriptor().getType()) { + // server streaming and unary calls generated with 2 parameters, + // first one is the actual input + case SERVER_STREAMING: + case UNARY: + METHOD_INVOCATION.get().setArguments(new Object[]{message, null}); + break; + // client streaming and bidi streaming calls generated with 1 parameter + case BIDI_STREAMING: + case CLIENT_STREAMING: + case UNKNOWN: + METHOD_INVOCATION.get().setArguments(new Object[]{message}); + break; + default: + fail(next,call,headers, new AuthenticationException("Unsupported call type "+call.getMethodDescriptor().getType()) { + }); + } + + beforeInvocation(METHOD_INVOCATION.get()); + } catch (AccessDeniedException | AuthenticationException e) { + fail(next, call, headers, e); + return; + } catch (Exception e) { + fail(next, call, headers, new AuthenticationException("", e) { + }); + return; + } finally { + METHOD_INVOCATION.get().setArguments(null); + } + + super.onMessage(message); + } + ); } @Override @@ -169,7 +281,8 @@ public void sendMessage(ReqT message) { }; } - private Context setupGRpcSecurityContext(ServerCall call, CharSequence authorization) { + private Context setupGRpcSecurityContext(ServerCall call, Metadata headers, + ServerCallHandler next, CharSequence authorization) { final Authentication authentication = null == authorization ? null : schemeSelector.getAuthScheme(authorization) .orElseThrow(() -> new RuntimeException("Can't get authentication from authorization header")); @@ -178,11 +291,15 @@ private Context setupGRpcSecurityContext(ServerCall call, CharSequence aut context.setAuthentication(authentication); SecurityContextHolder.setContext(context); - final InterceptorStatusToken interceptorStatusToken = beforeInvocation(call.getMethodDescriptor()); + final Map.Entry methodHandler = keyedMethods.get(new GrpcServiceMethodKey(call.getMethodDescriptor())); + + final GrpcMethodInvocation methodInvocation = new GrpcMethodInvocation<>(methodHandler, call, headers, next); + final InterceptorStatusToken interceptorStatusToken = beforeInvocation(methodInvocation); return Context.current() - .withValues(GrpcSecurity.AUTHENTICATION_CONTEXT_KEY, SecurityContextHolder.getContext().getAuthentication(), - INTERCEPTOR_STATUS_TOKEN, interceptorStatusToken); + .withValue(GrpcSecurity.AUTHENTICATION_CONTEXT_KEY, SecurityContextHolder.getContext().getAuthentication()) + .withValue(INTERCEPTOR_STATUS_TOKEN, interceptorStatusToken) + .withValue(METHOD_INVOCATION, methodInvocation); } private ServerCall.Listener fail(ServerCallHandler next, ServerCall call, Metadata headers, RuntimeException exception) throws RuntimeException { @@ -191,10 +308,8 @@ private ServerCall.Listener fail(ServerCallHandler() { - }; } else { - return new MessageBlockingServerCallListener(next.startCall(call, headers)) { @Override public void onMessage(ReqT message) { From 1d6295cc2b3e459fc4232d791a0d68b8273fd97e Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Wed, 27 Oct 2021 17:43:48 +0300 Subject: [PATCH 03/22] closes #253 --- .../grpc/CustomInterceptorsOrderTest.java | 21 +++----- .../springboot/grpc/GrpcServerTestBase.java | 15 +++++- .../grpc/auth/UserDetailsAuthTest.java | 16 ++---- .../springboot/grpc/GRpcServicesRegistry.java | 49 ++++++++++--------- 4 files changed, 53 insertions(+), 48 deletions(-) diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/CustomInterceptorsOrderTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/CustomInterceptorsOrderTest.java index ca9812d6..4dcfc6ed 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/CustomInterceptorsOrderTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/CustomInterceptorsOrderTest.java @@ -34,8 +34,6 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.test.context.junit4.SpringRunner; @@ -56,7 +54,9 @@ "grpc.security.auth.interceptor-order=3", //third "grpc.metrics.interceptor-order=2", //second "grpc.validation.interceptor-order=1" //first - }) + } + ,webEnvironment = SpringBootTest.WebEnvironment.NONE + ) @RunWith(SpringRunner.class) @Import({CustomInterceptorsOrderTest.TestCfg.class}) public class CustomInterceptorsOrderTest extends GrpcServerTestBase { @@ -113,20 +113,13 @@ protected void afterGreeting() { @TestConfiguration static class TestCfg extends GrpcSecurityConfigurerAdapter { - - static final String pwd = "strongPassword1"; @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public UserDetails user(PasswordEncoder passwordEncoder) { - return User. - withUsername("user1") - .password(passwordEncoder.encode(pwd)) + public UserDetails user() { + return User.withDefaultPasswordEncoder() + .username("user1") + .password(pwd) .roles("reader") .build(); } diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcServerTestBase.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcServerTestBase.java index 5bfeeefb..42e108db 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcServerTestBase.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcServerTestBase.java @@ -16,18 +16,31 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.io.Resource; +import org.springframework.test.context.ContextConfiguration; import org.springframework.util.StringUtils; import java.io.IOException; import java.util.Optional; -import java.util.concurrent.ExecutionException; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +@ContextConfiguration( + initializers = GrpcServerTestBase.TessAppContextInitializer.class) public abstract class GrpcServerTestBase { + static class TessAppContextInitializer implements + ApplicationContextInitializer { + + @Override + public void initialize(GenericApplicationContext applicationContext) { + applicationContext.setAllowCircularReferences(false); + } + } + @Autowired(required = false) @Qualifier("grpcServerRunner") protected GRpcServerRunner grpcServerRunner; diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java index 9341e752..3060dcb9 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java @@ -26,8 +26,6 @@ import org.springframework.context.annotation.Import; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.test.context.junit4.SpringRunner; @@ -39,7 +37,7 @@ import static org.junit.Assert.assertTrue; -@SpringBootTest(classes = DemoApp.class) +@SpringBootTest(classes = DemoApp.class,webEnvironment = SpringBootTest.WebEnvironment.NONE) @RunWith(SpringRunner.class) @Import({UserDetailsAuthTest.TestCfg.class}) public class UserDetailsAuthTest extends GrpcServerTestBase { @@ -48,16 +46,12 @@ public class UserDetailsAuthTest extends GrpcServerTestBase { @TestConfiguration static class TestCfg extends GrpcSecurityConfigurerAdapter { static final String pwd="strongPassword1"; - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } @Bean - public UserDetails user(PasswordEncoder passwordEncoder) { - return User. - withUsername("user1") - .password(passwordEncoder.encode(pwd)) + public UserDetails user() { + return User.withDefaultPasswordEncoder() + .username("user1") + .password(pwd) .roles("reader") .build(); } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/GRpcServicesRegistry.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/GRpcServicesRegistry.java index f1782f7a..900863ad 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/GRpcServicesRegistry.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/GRpcServicesRegistry.java @@ -6,53 +6,52 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.util.function.SingletonSupplier; import java.lang.annotation.Annotation; import java.util.Collection; import java.util.Map; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; -public class GRpcServicesRegistry implements InitializingBean , ApplicationContextAware { +public class GRpcServicesRegistry implements InitializingBean, ApplicationContextAware { private ApplicationContext applicationContext; - private Map beanNameToServiceBean; + private Supplier> beanNameToServiceBean; - private Map serviceNameToServiceBean; - - private Collection grpcGlobalInterceptors; + private Supplier> serviceNameToServiceBean; + private Supplier> grpcGlobalInterceptors; /** - * * @return service name to grpc service bean */ public Map getServiceNameToServiceBeanMap() { - return serviceNameToServiceBean; + return serviceNameToServiceBean.get(); } /** - * * @return spring bean name to grpc service bean */ public Map getBeanNameToServiceBeanMap() { - return beanNameToServiceBean; + return beanNameToServiceBean.get(); } Collection getGlobalInterceptors() { - return grpcGlobalInterceptors; + return grpcGlobalInterceptors.get(); } - private Map getBeanNamesByTypeWithAnnotation(Class annotationType, Class beanType) { + private Map getBeanNamesByTypeWithAnnotation(Class annotationType, Class beanType) { return applicationContext.getBeansWithAnnotation(annotationType) .entrySet() .stream() - .filter(e-> beanType.isInstance(e.getValue())) - .collect(Collectors.toMap(Map.Entry::getKey,e->beanType.cast(e.getValue()))); + .filter(e -> beanType.isInstance(e.getValue())) + .collect(Collectors.toMap(Map.Entry::getKey, e -> beanType.cast(e.getValue()))); } @@ -60,21 +59,27 @@ private Map getBeanNamesByTypeWithAnnotation(Class + getBeanNamesByTypeWithAnnotation(GRpcService.class, BindableService.class) + ); - beanNameToServiceBean = getBeanNamesByTypeWithAnnotation(GRpcService.class, BindableService.class); - - serviceNameToServiceBean = beanNameToServiceBean - .values() - .stream() - .collect(Collectors.toMap(s->s.bindService().getServiceDescriptor().getName(),Function.identity())); + serviceNameToServiceBean = SingletonSupplier.of(() -> + beanNameToServiceBean + .get() + .values() + .stream() + .collect(Collectors.toMap(s -> s.bindService().getServiceDescriptor().getName(), Function.identity())) + ); - grpcGlobalInterceptors = getBeanNamesByTypeWithAnnotation(GRpcGlobalInterceptor.class, ServerInterceptor.class) - .values(); + grpcGlobalInterceptors = SingletonSupplier.of(() -> + getBeanNamesByTypeWithAnnotation(GRpcGlobalInterceptor.class, ServerInterceptor.class) + .values() + ); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; + this.applicationContext = applicationContext; } } From c0994d85daa6e9d8865c30c9fa9174105b2f3439 Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Wed, 27 Oct 2021 18:11:04 +0300 Subject: [PATCH 04/22] ref #175 --- .../grpc/security/SecurityInterceptor.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java index c704001a..d1f9fcec 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java @@ -215,23 +215,20 @@ public void onMessage(ReqT message) { METHOD_INVOCATION.get().setArguments(new Object[]{message}); break; default: - fail(next,call,headers, new AuthenticationException("Unsupported call type "+call.getMethodDescriptor().getType()) { - }); + throw new AuthenticationException("Unsupported call type "+call.getMethodDescriptor().getType()) {}; } beforeInvocation(METHOD_INVOCATION.get()); + super.onMessage(message); } catch (AccessDeniedException | AuthenticationException e) { - fail(next, call, headers, e); - return; + failureHandlingSupport.closeCall(e,call,headers); } catch (Exception e) { - fail(next, call, headers, new AuthenticationException("", e) { - }); - return; + failureHandlingSupport.closeCall( new AuthenticationException("", e) {},call, headers); } finally { METHOD_INVOCATION.get().setArguments(null); } - super.onMessage(message); + } ); } From d40cd6c3f17f42e5bda7f885ad548b9bd4162bb6 Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Thu, 28 Oct 2021 08:55:05 +0300 Subject: [PATCH 05/22] ref #175 --- .../java/org/lognet/springboot/grpc/security/GrpcSecurity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java index 249b5257..c3125820 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java @@ -115,7 +115,7 @@ protected ServerInterceptor performBuild() throws Exception { @Override public int vote(Authentication authentication, MethodInvocation method, Collection attributes) { // first time invoked without arguments - return null==method.getArguments() ? ACCESS_GRANTED: super.vote(authentication, method, attributes); + return null==method.getArguments() ? ACCESS_ABSTAIN: super.vote(authentication, method, attributes); } } From 0ffec28cd618f467e51d96c514525c07a1ecbbb5 Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Thu, 28 Oct 2021 12:01:18 +0300 Subject: [PATCH 06/22] ref #175 --- .../java/org/lognet/springboot/grpc/security/GrpcSecurity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java index c3125820..d0c9ba5b 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java @@ -118,8 +118,8 @@ public int vote(Authentication authentication, MethodInvocation method, Collecti return null==method.getArguments() ? ACCESS_ABSTAIN: super.vote(authentication, method, attributes); } } - )); + accessDecisionManager.setAllowIfAllAbstainDecisions(true); securityInterceptor.setAccessDecisionManager(accessDecisionManager); final GRpcServerProperties.SecurityProperties.Auth authCfg = Optional.of(applicationContext.getBean(GRpcServerProperties.class)) From a0a6583a43d344cbf51e49747a32c4eda53581fa Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Thu, 28 Oct 2021 14:27:03 +0300 Subject: [PATCH 07/22] ref #175 --- .../springboot/grpc/security/SecurityInterceptor.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java index d1f9fcec..63b491bc 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java @@ -34,6 +34,7 @@ import org.springframework.util.ReflectionUtils; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.AbstractMap; @@ -113,12 +114,17 @@ public void setGRpcServicesRegistry(GRpcServicesRegistry registry) { final Map> map = new HashMap<>(); Function filterFactory = name -> - method -> method.getName().equalsIgnoreCase(name); + method -> method.getName().equalsIgnoreCase(name) ; for (BindableService service : registry.getBeanNameToServiceBeanMap().values()) { for (MethodDescriptor d : service.bindService().getServiceDescriptor().getMethods()) { + Class abstractBaseClass = service.getClass(); + while (!Modifier.isAbstract(abstractBaseClass.getModifiers())){ + abstractBaseClass = abstractBaseClass.getSuperclass(); + } + final Method method = MethodIntrospector - .selectMethods(service.getClass(), filterFactory.apply(d.getBareMethodName())) + .selectMethods(abstractBaseClass, filterFactory.apply(d.getBareMethodName())) .iterator().next(); map.put(new GrpcServiceMethodKey(d), new AbstractMap.SimpleImmutableEntry<>(service, method)); From f4e3b5a7a95f893a256d1ba846dc1b688829473b Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Thu, 28 Oct 2021 16:30:51 +0300 Subject: [PATCH 08/22] ref #175 --- .../src/test/resources/logback-test.xml | 15 +++- .../springboot/grpc/GRpcServicesRegistry.java | 82 ++++++++++++++++++- .../security/GrpcSecurityMetadataSource.java | 35 +++----- .../grpc/security/SecurityInterceptor.java | 61 ++------------ 4 files changed, 113 insertions(+), 80 deletions(-) diff --git a/grpc-spring-boot-starter-demo/src/test/resources/logback-test.xml b/grpc-spring-boot-starter-demo/src/test/resources/logback-test.xml index 4278a644..081473d9 100644 --- a/grpc-spring-boot-starter-demo/src/test/resources/logback-test.xml +++ b/grpc-spring-boot-starter-demo/src/test/resources/logback-test.xml @@ -1,6 +1,13 @@ - - - - + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/GRpcServicesRegistry.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/GRpcServicesRegistry.java index 900863ad..19759e6b 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/GRpcServicesRegistry.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/GRpcServicesRegistry.java @@ -1,23 +1,41 @@ package org.lognet.springboot.grpc; import io.grpc.BindableService; +import io.grpc.MethodDescriptor; import io.grpc.ServerInterceptor; +import io.grpc.ServerServiceDefinition; +import lombok.Builder; +import lombok.Getter; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.core.MethodIntrospector; +import org.springframework.util.ReflectionUtils; import org.springframework.util.function.SingletonSupplier; import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; public class GRpcServicesRegistry implements InitializingBean, ApplicationContextAware { - private ApplicationContext applicationContext; + @Getter + @Builder + public static class GrpcServiceMethod { + private BindableService service; + private Method method; + + } + private ApplicationContext applicationContext; private Supplier> beanNameToServiceBean; @@ -25,6 +43,12 @@ public class GRpcServicesRegistry implements InitializingBean, ApplicationContex private Supplier> grpcGlobalInterceptors; + private Supplier, GrpcServiceMethod>> descriptorToServiceMethod; + + private Supplier< Map>> methodToDescriptor ; + + + /** * @return service name to grpc service bean @@ -45,6 +69,14 @@ Collection getGlobalInterceptors() { return grpcGlobalInterceptors.get(); } + public GrpcServiceMethod getGrpServiceMethod(MethodDescriptor descriptor) { + return descriptorToServiceMethod.get().get(descriptor); + } + + public MethodDescriptor getMethodDescriptor( Method method) { + return methodToDescriptor.get().get(method); + } + private Map getBeanNamesByTypeWithAnnotation(Class annotationType, Class beanType) { return applicationContext.getBeansWithAnnotation(annotationType) @@ -58,7 +90,14 @@ private Map getBeanNamesByTypeWithAnnotation(Class + descriptorToServiceMethod.get() + .entrySet() + .stream() + .collect(Collectors.toMap(e->e.getValue().getMethod(), Map.Entry::getKey)) + ); beanNameToServiceBean = SingletonSupplier.of(() -> getBeanNamesByTypeWithAnnotation(GRpcService.class, BindableService.class) ); @@ -78,8 +117,49 @@ public void afterPropertiesSet() throws Exception { ); } + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } + + private Map, GrpcServiceMethod> descriptorToServiceMethod (){ + final Map, GrpcServiceMethod> map = new HashMap<>(); + + Function filterFactory = name -> + method -> method.getName().equalsIgnoreCase(name) ; + + for (BindableService service : getBeanNameToServiceBeanMap().values()) { + final ServerServiceDefinition serviceDefinition = service.bindService(); + for (MethodDescriptor d : serviceDefinition.getServiceDescriptor().getMethods()) { + Class abstractBaseClass = service.getClass(); + while (!Modifier.isAbstract(abstractBaseClass.getModifiers())){ + abstractBaseClass = abstractBaseClass.getSuperclass(); + } + + final Set methods = MethodIntrospector + .selectMethods(abstractBaseClass, filterFactory.apply(d.getBareMethodName())); + + + switch (methods.size()){ + case 0: + throw new IllegalStateException("Method " +d.getBareMethodName()+ "not found in service "+ serviceDefinition.getServiceDescriptor().getName()); + case 1: + map.put(d, GrpcServiceMethod.builder() + .service(service) + .method(methods.iterator().next()) + .build()); + break; + default: + throw new IllegalStateException("Ambiguous method " +d.getBareMethodName()+ " in service "+ serviceDefinition.getServiceDescriptor().getName()); + } + + + + + + } + } + return Collections.unmodifiableMap(map); + } } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityMetadataSource.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityMetadataSource.java index c4f8c8f0..90e4992e 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityMetadataSource.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityMetadataSource.java @@ -1,46 +1,37 @@ package org.lognet.springboot.grpc.security; -import io.grpc.BindableService; import io.grpc.MethodDescriptor; -import io.grpc.ServerMethodDefinition; import org.lognet.springboot.grpc.GRpcServicesRegistry; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.method.MethodSecurityMetadataSource; import java.lang.reflect.Method; import java.util.Collection; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import java.util.stream.Stream; public class GrpcSecurityMetadataSource implements MethodSecurityMetadataSource { - private Map, List> methodDescriptorMap; - private Map> methodMap = new HashMap<>(); - - public GrpcSecurityMetadataSource(GRpcServicesRegistry registry , Map, List> methodDescriptorMap) { - this.methodDescriptorMap = methodDescriptorMap; - - for(BindableService s:registry.getBeanNameToServiceBeanMap().values()){ - for(ServerMethodDefinition md :s.bindService().getMethods()){ - final Method method = Stream.of(s.getClass().getMethods()) - .filter(m -> md.getMethodDescriptor().getBareMethodName().equalsIgnoreCase(m.getName())) - .findFirst().get(); - methodMap.put(method,md.getMethodDescriptor()); - } - } + private Map, List> methodDescriptorAttributes; + private GRpcServicesRegistry registry; + + + public GrpcSecurityMetadataSource(GRpcServicesRegistry registry , Map, List> methodDescriptorAttributes) { + this.methodDescriptorAttributes = methodDescriptorAttributes; + this.registry = registry; + + } @Override public Collection getAttributes(Object object) throws IllegalArgumentException { final MethodDescriptor methodDescriptor = SecurityInterceptor.GrpcMethodInvocation.class.cast(object).getCall().getMethodDescriptor(); - return methodDescriptorMap.get(methodDescriptor); + return methodDescriptorAttributes.get(methodDescriptor); } @Override public Collection getAllConfigAttributes() { - return methodDescriptorMap + return methodDescriptorAttributes .values() .stream() .flatMap(Collection::stream) @@ -54,7 +45,7 @@ public boolean supports(Class clazz) { @Override public Collection getAttributes(Method method, Class targetClass) { - final MethodDescriptor methodDescriptor = methodMap.get(method); - return methodDescriptorMap.get(methodDescriptor); + final MethodDescriptor methodDescriptor = registry.getMethodDescriptor(method); + return methodDescriptorAttributes.get(methodDescriptor); } } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java index 63b491bc..349a0154 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java @@ -1,16 +1,13 @@ package org.lognet.springboot.grpc.security; -import io.grpc.BindableService; import io.grpc.Context; import io.grpc.Contexts; import io.grpc.ForwardingServerCall; import io.grpc.ForwardingServerCallListener; import io.grpc.Metadata; -import io.grpc.MethodDescriptor; import io.grpc.ServerCall; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -20,7 +17,6 @@ import org.lognet.springboot.grpc.autoconfigure.GRpcServerProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; -import org.springframework.core.MethodIntrospector; import org.springframework.core.Ordered; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.SecurityMetadataSource; @@ -31,18 +27,10 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.util.SimpleMethodInvocation; -import org.springframework.util.ReflectionUtils; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.AbstractMap; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; -import java.util.function.Function; @Slf4j public class SecurityInterceptor extends AbstractSecurityInterceptor implements ServerInterceptor, Ordered { @@ -57,7 +45,9 @@ public class SecurityInterceptor extends AbstractSecurityInterceptor implements private GRpcServerProperties.SecurityProperties.Auth authCfg; private FailureHandlingSupport failureHandlingSupport; - private Map> keyedMethods; + + private GRpcServicesRegistry registry; + static class GrpcMethodInvocation extends SimpleMethodInvocation { final private ServerCall call; @@ -67,8 +57,8 @@ static class GrpcMethodInvocation extends SimpleMethodInvocation { @Setter private Object[] arguments; - public GrpcMethodInvocation(Map.Entry handler, ServerCall call, Metadata headers, ServerCallHandler next) { - super(handler.getKey(), handler.getValue()); + public GrpcMethodInvocation(GRpcServicesRegistry.GrpcServiceMethod serviceMethod, ServerCall call, Metadata headers, ServerCallHandler next) { + super(serviceMethod.getService(), serviceMethod.getMethod()); this.call = call; this.headers = headers; this.next = next; @@ -84,22 +74,7 @@ ServerCall getCall() { } } - @Getter - @EqualsAndHashCode - static class GrpcServiceMethodKey { - - public GrpcServiceMethodKey(MethodDescriptor methodDescriptor) { - this.serviceName = methodDescriptor.getServiceName(); - this.methodName = methodDescriptor.getBareMethodName(); - } - @EqualsAndHashCode.Include - final private String serviceName; - - @EqualsAndHashCode.Include - final private String methodName; - - } public SecurityInterceptor(SecurityMetadataSource securityMetadataSource, AuthenticationSchemeSelector schemeSelector) { @@ -110,28 +85,8 @@ public SecurityInterceptor(SecurityMetadataSource securityMetadataSource, Authen @Autowired public void setGRpcServicesRegistry(GRpcServicesRegistry registry) { + this.registry = registry; - final Map> map = new HashMap<>(); - - Function filterFactory = name -> - method -> method.getName().equalsIgnoreCase(name) ; - - for (BindableService service : registry.getBeanNameToServiceBeanMap().values()) { - for (MethodDescriptor d : service.bindService().getServiceDescriptor().getMethods()) { - Class abstractBaseClass = service.getClass(); - while (!Modifier.isAbstract(abstractBaseClass.getModifiers())){ - abstractBaseClass = abstractBaseClass.getSuperclass(); - } - - final Method method = MethodIntrospector - .selectMethods(abstractBaseClass, filterFactory.apply(d.getBareMethodName())) - .iterator().next(); - map.put(new GrpcServiceMethodKey(d), - new AbstractMap.SimpleImmutableEntry<>(service, method)); - - } - } - keyedMethods = Collections.unmodifiableMap(map); } @Autowired @@ -294,9 +249,9 @@ private Context setupGRpcSecurityContext(ServerCall c context.setAuthentication(authentication); SecurityContextHolder.setContext(context); - final Map.Entry methodHandler = keyedMethods.get(new GrpcServiceMethodKey(call.getMethodDescriptor())); + final GRpcServicesRegistry.GrpcServiceMethod grpcServiceMethod = registry.getGrpServiceMethod(call.getMethodDescriptor()); - final GrpcMethodInvocation methodInvocation = new GrpcMethodInvocation<>(methodHandler, call, headers, next); + final GrpcMethodInvocation methodInvocation = new GrpcMethodInvocation<>(grpcServiceMethod , call, headers, next); final InterceptorStatusToken interceptorStatusToken = beforeInvocation(methodInvocation); return Context.current() From 1fa3df9509f8dda9d8caaaa1ab9e4678e2325e07 Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Thu, 28 Oct 2021 17:02:29 +0300 Subject: [PATCH 09/22] ci --- .github/workflows/gradle.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 6e712cb2..e489e0e0 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -26,3 +26,5 @@ jobs: run: chmod +x gradlew - name: Build with Gradle run: ./gradlew build + - name: Print docker images + run: docker images From 94ce98b3a63885cf62ed9a9bd40af5cd9ef1521e Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Thu, 28 Oct 2021 17:11:51 +0300 Subject: [PATCH 10/22] ci - increase timeout --- .../springboot/grpc/consul/ConsulRegistrationBaseTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java index 9154b3de..4651130d 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java @@ -108,7 +108,7 @@ public void contextLoads() { final List healthServices = Awaitility.await() - .atMost(Duration.ofSeconds(20)) + .atMost(Duration.ofMinutes(1)) .pollInterval(Duration.ofSeconds(3)) .until(() -> consulClient.getHealthServices(serviceId, HealthServicesRequest.newBuilder() .setPassing(true) From 13042900b5150a1c42eb384fc9042e9705d4c0ac Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Sun, 31 Oct 2021 09:20:56 +0200 Subject: [PATCH 11/22] ci with jdk 8 --- .github/workflows/gradle.yml | 2 +- .travis.yml | 2 +- build.gradle | 7 ++++--- grpc-spring-boot-starter/build.gradle | 6 +----- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index e489e0e0..78bfb10f 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -21,7 +21,7 @@ jobs: uses: actions/setup-java@v2 with: distribution: 'zulu' - java-version: "11" + java-version: "8" - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle diff --git a/.travis.yml b/.travis.yml index 67c2d383..0cfbc1bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: java jdk: - - oraclejdk11 + - openjdk8 #sudo: false dist: trusty before_cache: diff --git a/build.gradle b/build.gradle index 08e6d956..1a69a89b 100644 --- a/build.gradle +++ b/build.gradle @@ -48,10 +48,11 @@ subprojects { apply plugin: 'net.ltgt.errorprone' - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + tasks.withType(JavaCompile,{ - options.compilerArgs.addAll(["--release", "8"]) +// options.compilerArgs.addAll(["--release", "8"]) // jdk 11 flag + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 }) diff --git a/grpc-spring-boot-starter/build.gradle b/grpc-spring-boot-starter/build.gradle index e805be7e..b0452419 100644 --- a/grpc-spring-boot-starter/build.gradle +++ b/grpc-spring-boot-starter/build.gradle @@ -253,11 +253,7 @@ publishing { } } -tasks.compileJava { - options.compilerArgs.addAll(["--release", "8"]) - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} + signing { required { // signing is required if this is a release version and the artifacts are to be published From 92c6851d0d6830880ed41a30d41e00964fc1ae5f Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Sun, 31 Oct 2021 09:38:13 +0200 Subject: [PATCH 12/22] ci with jdk 11 - error prone required min jdk 9 --- .github/workflows/gradle.yml | 2 +- .travis.yml | 2 +- build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 78bfb10f..e489e0e0 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -21,7 +21,7 @@ jobs: uses: actions/setup-java@v2 with: distribution: 'zulu' - java-version: "8" + java-version: "11" - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle diff --git a/.travis.yml b/.travis.yml index 0cfbc1bd..67c2d383 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: java jdk: - - openjdk8 + - oraclejdk11 #sudo: false dist: trusty before_cache: diff --git a/build.gradle b/build.gradle index 1a69a89b..061e526c 100644 --- a/build.gradle +++ b/build.gradle @@ -50,7 +50,7 @@ subprojects { tasks.withType(JavaCompile,{ -// options.compilerArgs.addAll(["--release", "8"]) // jdk 11 flag + options.compilerArgs.addAll(["--release", "8"]) // jdk 11 flag sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 }) From ac30ecf480c4e17b555dcd2252d042d849a1e733 Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Sun, 31 Oct 2021 10:09:45 +0200 Subject: [PATCH 13/22] closes #255 --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 60bf4760..459b585c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ grpcVersion=1.41.0 -springBootVersion=2.5.0 -springCloudVersion=2020.0.3 +springBootVersion=2.5.6 +springCloudVersion=2020.0.4 gradleErrorPronePluginVersion=2.0.2 errorProneVersion=2.7.1 lombokVersion=1.18.20 From b13a9f8ea5f3d89d152d23133c5e5d5c2e72e01f Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Mon, 1 Nov 2021 15:51:06 +0200 Subject: [PATCH 14/22] closes #249, closes #250 --- grpc-spring-boot-starter-demo/build.gradle | 13 +++++++ .../grpc/simple/NoConsulDependencyTest.java | 35 +++++++++++++++++++ .../resources/application-test.yml | 6 ++++ .../consul/ConsulDefaultRegistrationTest.java | 35 +++++++++++++++++++ .../consul/ConsulRegistrationBaseTest.java | 15 ++++---- .../application-consul-grpc-config-test.yml | 18 ++++++++++ .../autoconfigure/GRpcServerProperties.java | 4 +++ .../consul/ConsulGrpcAutoConfiguration.java | 35 +++++++++++++++++-- .../consul/GrpcConsulRegistrar.java | 13 ++++--- .../consul/ServiceRegistrationMode.java | 29 +++++++++++++-- 10 files changed, 185 insertions(+), 18 deletions(-) create mode 100644 grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/java/org/lognet/springboot/grpc/simple/NoConsulDependencyTest.java create mode 100644 grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/resources/application-test.yml create mode 100644 grpc-spring-boot-starter-demo/src/test/resources/application-consul-grpc-config-test.yml diff --git a/grpc-spring-boot-starter-demo/build.gradle b/grpc-spring-boot-starter-demo/build.gradle index d95f81f0..053276d9 100644 --- a/grpc-spring-boot-starter-demo/build.gradle +++ b/grpc-spring-boot-starter-demo/build.gradle @@ -22,6 +22,7 @@ facets { kafkaStreamTest customSecurityTest bothPureAndShadedNettyTest + noConsulDependenciesTest } grpcSpringBoot { grpcSpringBootStarterVersion.set((String)null) @@ -42,6 +43,11 @@ configurations.findAll{ cfg -> cfg.exclude group: 'org.springframework.security', module: 'spring-security-oauth2-resource-server' cfg.exclude group: 'org.springframework.security', module: 'spring-security-oauth2-jose' } + if (cfg.name.startsWith("noConsulDependencies")) { + cfg.exclude group: 'org.springframework.cloud', module: 'spring-cloud-starter-consul-discovery' + + } + } @@ -59,6 +65,11 @@ configurations { kafkaStreamTestCompile.extendsFrom( testCompile) kafkaStreamTestRuntime.extendsFrom(testRuntime) + + noConsulDependenciesTestCompile.extendsFrom( testCompile) + noConsulDependenciesTestRuntime.extendsFrom(testRuntime) + + } @@ -106,6 +117,8 @@ dependencies { kafkaStreamTestCompile "com.playtika.testcontainers:embedded-kafka:2.0.9" kafkaStreamTestCompile "org.springframework.cloud:spring-cloud-starter-stream-kafka" + + noConsulDependenciesTestCompile sourceSets.test.output //testCompile "org.testcontainers:junit-jupiter:1.14.3" diff --git a/grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/java/org/lognet/springboot/grpc/simple/NoConsulDependencyTest.java b/grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/java/org/lognet/springboot/grpc/simple/NoConsulDependencyTest.java new file mode 100644 index 00000000..938f96ba --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/java/org/lognet/springboot/grpc/simple/NoConsulDependencyTest.java @@ -0,0 +1,35 @@ +package org.lognet.springboot.grpc.simple; + +import org.hamcrest.Matchers; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.lognet.springboot.grpc.GrpcServerTestBase; +import org.lognet.springboot.grpc.autoconfigure.GRpcServerProperties; +import org.lognet.springboot.grpc.demo.DemoApp; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.ReflectionUtils; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {DemoApp.class}, webEnvironment = NONE) +public class NoConsulDependencyTest extends GrpcServerTestBase { + + @Test + public void noConsulClassesTest() { + + final NoClassDefFoundError error = assertThrows(NoClassDefFoundError.class, () -> { + try { + ReflectionUtils.findMethod(GRpcServerProperties.ConsulProperties.class, "getDiscovery"); + }catch (IllegalStateException illegalStateException){ + throw illegalStateException.getCause(); + } + }); + assertThat(error.getMessage(), Matchers.containsString("org/springframework/cloud/consul/discovery/ConsulDiscoveryProperties")); + } + + +} diff --git a/grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/resources/application-test.yml b/grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/resources/application-test.yml new file mode 100644 index 00000000..a8452a6e --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/resources/application-test.yml @@ -0,0 +1,6 @@ +grpc: + consul: + registration-mode: noop + discovery: + tags: + - dummy \ No newline at end of file diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulDefaultRegistrationTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulDefaultRegistrationTest.java index 144855eb..9bc04ac8 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulDefaultRegistrationTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulDefaultRegistrationTest.java @@ -3,23 +3,58 @@ import com.ecwid.consul.v1.health.model.Check; import com.ecwid.consul.v1.health.model.HealthService; import org.hamcrest.Matchers; +import org.junit.Test; import org.junit.runner.RunWith; +import org.lognet.springboot.grpc.autoconfigure.GRpcServerProperties; import org.lognet.springboot.grpc.demo.DemoApp; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties; +import org.springframework.cloud.consul.discovery.ConsulServiceInstance; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import java.util.List; +import java.util.Map; import static org.hamcrest.MatcherAssert.assertThat; @SpringBootTest(classes = DemoApp.class) @RunWith(SpringRunner.class) +@ActiveProfiles("consul-grpc-config-test") public class ConsulDefaultRegistrationTest extends ConsulRegistrationBaseTest{ + @Test + public void consulPropertiesTest() { + final ConsulDiscoveryProperties cloudConsulProps = applicationContext.getBean(ConsulDiscoveryProperties.class); + assertThat(cloudConsulProps.getTags(),Matchers.contains("a","b")); + assertThat(cloudConsulProps.getInstanceZone(),Matchers.is("zone1")); + assertThat(cloudConsulProps.getInstanceGroup(),Matchers.nullValue(String.class)); + final ConsulDiscoveryProperties grpcConsulProperties = applicationContext.getBean(GRpcServerProperties.class).getConsul().getDiscovery(); + assertThat(grpcConsulProperties.getTags(), Matchers.hasSize(3)); + assertThat(grpcConsulProperties.getTags(), Matchers.hasItem("1")); + + assertThat(grpcConsulProperties.getInstanceZone(),Matchers.is("zone1")); + assertThat(grpcConsulProperties.getInstanceGroup(),Matchers.is("group1")); + + final List instances = discoveryClient.getInstances("grpc-grpc-demo"); + + assertThat(instances,Matchers.hasSize(1)); + assertThat(instances.get(0),Matchers.isA(ConsulServiceInstance.class)); + ConsulServiceInstance consulServiceInstance = (ConsulServiceInstance) instances.get(0); + final Map metadata = consulServiceInstance.getMetadata(); + assertThat(metadata.get("secure"),Matchers.is(Boolean.FALSE.toString())); + assertThat(consulServiceInstance.getTags(),Matchers.containsInAnyOrder("1","grpc=true","customTagName=A")); + + + + + } + @Override void doTest( List healthServices) { diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java index 4651130d..f4ea3c5e 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java @@ -21,8 +21,9 @@ import org.lognet.springboot.grpc.autoconfigure.consul.ServiceRegistrationMode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; -import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.consul.discovery.ConsulDiscoveryClient; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import java.time.Duration; @@ -34,26 +35,25 @@ @ActiveProfiles("consul-test") +@DirtiesContext public abstract class ConsulRegistrationBaseTest { @Autowired - protected DiscoveryClient discoveryClient; + protected ConsulDiscoveryClient discoveryClient; @Autowired protected ConfigurableApplicationContext applicationContext; protected final String serviceId = "grpc-grpc-demo"; + @Autowired protected ConsulClient consulClient; + private ManagedChannel channel; @Before public void setUp() throws Exception { - final String port = applicationContext.getEnvironment().getProperty("embedded.consul.port"); - final String host = applicationContext.getEnvironment().getProperty("embedded.consul.host"); - consulClient = new ConsulClient(host, Integer.parseInt(port)); - List instances = discoveryClient.getInstances(serviceId); @@ -84,9 +84,6 @@ public void tearDown() throws Exception { channel.shutdownNow(); channel.awaitTermination(1, TimeUnit.SECONDS); } - // explicitly close the context to trigger services de-registration - // since we share the same instance of Consul between tests - applicationContext.stop(); } @Test diff --git a/grpc-spring-boot-starter-demo/src/test/resources/application-consul-grpc-config-test.yml b/grpc-spring-boot-starter-demo/src/test/resources/application-consul-grpc-config-test.yml new file mode 100644 index 00000000..b4c8868e --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/test/resources/application-consul-grpc-config-test.yml @@ -0,0 +1,18 @@ +grpc: + consul: + discovery: + tags: + - 1 + - grpc=true + - customTagName=A + instance-group: group1 + +spring: + cloud: + consul: + discovery: + tags: + - a + - b + instance-zone: zone1 + diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/GRpcServerProperties.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/GRpcServerProperties.java index dc55f6d5..5794f452 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/GRpcServerProperties.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/GRpcServerProperties.java @@ -5,6 +5,8 @@ import lombok.Setter; import org.lognet.springboot.grpc.autoconfigure.consul.ServiceRegistrationMode; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties; import org.springframework.context.SmartLifecycle; import org.springframework.core.io.Resource; import org.springframework.util.SocketUtils; @@ -97,6 +99,8 @@ public static class Auth { @Setter public static class ConsulProperties { ServiceRegistrationMode registrationMode = ServiceRegistrationMode.SINGLE_SERVER_WITH_GLOBAL_CHECK; + @NestedConfigurationProperty + ConsulDiscoveryProperties discovery; } @Getter @Setter diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ConsulGrpcAutoConfiguration.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ConsulGrpcAutoConfiguration.java index 643db30e..a6124cdf 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ConsulGrpcAutoConfiguration.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ConsulGrpcAutoConfiguration.java @@ -2,10 +2,18 @@ import org.lognet.springboot.grpc.GRpcServerRunner; import org.lognet.springboot.grpc.autoconfigure.GRpcAutoConfiguration; +import org.lognet.springboot.grpc.autoconfigure.GRpcServerProperties; +import org.lognet.springboot.grpc.autoconfigure.OnGrpcServerEnabled; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindHandlerAdvisor; +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties; import org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistry; import org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistryAutoConfiguration; import org.springframework.context.annotation.Bean; @@ -13,10 +21,33 @@ @Configuration @ConditionalOnClass(ConsulServiceRegistry.class) -@AutoConfigureAfter({ ConsulServiceRegistryAutoConfiguration.class, GRpcAutoConfiguration.class}) +@AutoConfigureAfter({ConsulServiceRegistryAutoConfiguration.class, GRpcAutoConfiguration.class}) @ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true) @ConditionalOnBean({ConsulServiceRegistry.class, GRpcServerRunner.class}) -public class ConsulGrpcAutoConfiguration{ +@OnGrpcServerEnabled +public class ConsulGrpcAutoConfiguration { + + + + + @Bean + public ConfigurationPropertiesBindHandlerAdvisor advisor( ){ + // sets cloud consul discovery bound instance as starting object for grpc consul discovery properties + return b->new AbstractBindHandler(b) { + private final ConfigurationPropertyName grpcConfigName = ConfigurationPropertyName.of("grpc"); + @Override + public Bindable onStart(ConfigurationPropertyName name, Bindable target, BindContext context) { + + if(grpcConfigName.equals(name)){ + + final ConsulDiscoveryProperties result = context.getBinder().bindOrCreate(ConsulDiscoveryProperties.PREFIX, ConsulDiscoveryProperties.class); + final GRpcServerProperties p = (GRpcServerProperties) target.getValue().get(); + p.getConsul().setDiscovery(result); + } + return super.onStart(name, target, context); + } + } ; + } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/GrpcConsulRegistrar.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/GrpcConsulRegistrar.java index f47499ff..01c47e22 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/GrpcConsulRegistrar.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/GrpcConsulRegistrar.java @@ -2,7 +2,6 @@ import org.lognet.springboot.grpc.autoconfigure.GRpcServerProperties; import org.lognet.springboot.grpc.context.GRpcServerInitializedEvent; -import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties; import org.springframework.cloud.consul.serviceregistry.ConsulRegistration; import org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistry; import org.springframework.context.ApplicationContext; @@ -26,12 +25,16 @@ public GrpcConsulRegistrar(ConsulServiceRegistry consulServiceRegistry) { public void onGrpcServerStarted(GRpcServerInitializedEvent event) { ApplicationContext applicationContext = event.getApplicationContext(); - ConsulDiscoveryProperties consulProperties = applicationContext.getBean(ConsulDiscoveryProperties.class); - final ServiceRegistrationStrategy registrationStrategy = applicationContext.getBean(GRpcServerProperties.class).getConsul().getRegistrationMode(); - registrations = registrationStrategy.createServices(event.getServer(),applicationContext) + final GRpcServerProperties.ConsulProperties gRpcConsulProperties = applicationContext + .getBean(GRpcServerProperties.class) + .getConsul(); + + + registrations = gRpcConsulProperties.getRegistrationMode() + .createServices(event.getServer(),applicationContext) .stream() - .map(s->new ConsulRegistration(s, consulProperties)) + .map(s->new ConsulRegistration(s, gRpcConsulProperties.getDiscovery())) .collect(Collectors.toList()); registrations.forEach(consulServiceRegistry::register); diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ServiceRegistrationMode.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ServiceRegistrationMode.java index 4e750b8f..68ddf500 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ServiceRegistrationMode.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ServiceRegistrationMode.java @@ -8,11 +8,15 @@ import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties; import org.springframework.cloud.consul.serviceregistry.ConsulAutoRegistration; import org.springframework.context.ApplicationContext; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -28,7 +32,9 @@ public Collection createServices(Server grpcServer, ApplicationConte @Override public Collection createServices(Server grpcServer, ApplicationContext applicationContext) { GRpcServerProperties gRpcServerProperties = applicationContext.getBean(GRpcServerProperties.class); - ConsulDiscoveryProperties consulProperties = applicationContext.getBean(ConsulDiscoveryProperties.class); + ConsulDiscoveryProperties consulProperties = gRpcServerProperties.getConsul().getDiscovery(); + + NewService grpcService = new NewService(); grpcService.setPort(grpcServer.getPort()); @@ -39,7 +45,8 @@ public Collection createServices(Server grpcServer, ApplicationConte grpcService.setName(ConsulAutoRegistration.normalizeForDns(appName)); grpcService.setId("grpc-" + ConsulAutoRegistration.getInstanceId(consulProperties, applicationContext)); grpcService.setTags(consulProperties.getTags()); - + grpcService.setMeta(getMetadata(consulProperties,gRpcServerProperties)); + grpcService.setEnableTagOverride(consulProperties.getEnableTagOverride()); if (consulProperties.isRegisterHealthCheck()) { final NewService.Check healthCheck = new NewService.Check(); @@ -55,6 +62,24 @@ public Collection createServices(Server grpcServer, ApplicationConte return Collections.singleton(grpcService); } + + private Map getMetadata(ConsulDiscoveryProperties properties,GRpcServerProperties gRpcServerProperties) { + LinkedHashMap metadata = new LinkedHashMap<>(); + if (!CollectionUtils.isEmpty(properties.getMetadata())) { + metadata.putAll(properties.getMetadata()); + } + + if (StringUtils.hasText(properties.getInstanceZone())) { + metadata.put(properties.getDefaultZoneMetadataName(), properties.getInstanceZone()); + } + if (StringUtils.hasText(properties.getInstanceGroup())) { + metadata.put("group", properties.getInstanceGroup()); + } + + metadata.put("secure", Boolean.toString(null!=gRpcServerProperties.getSecurity())); + + return metadata; + } }, SINGLE_SERVER_WITH_CHECK_PER_SERVICE { @Override From e1e657c2425fc2bee20fc15cc564771122e8176d Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Mon, 1 Nov 2021 16:16:34 +0200 Subject: [PATCH 15/22] simplify gradle build config --- .github/workflows/gradle.yml | 4 +-- grpc-spring-boot-starter-demo/build.gradle | 29 ++++++---------------- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index e489e0e0..40c2b1e2 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -25,6 +25,4 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew build - - name: Print docker images - run: docker images + run: ./gradlew build ; docker images diff --git a/grpc-spring-boot-starter-demo/build.gradle b/grpc-spring-boot-starter-demo/build.gradle index 053276d9..3b9071ea 100644 --- a/grpc-spring-boot-starter-demo/build.gradle +++ b/grpc-spring-boot-starter-demo/build.gradle @@ -24,6 +24,7 @@ facets { bothPureAndShadedNettyTest noConsulDependenciesTest } + grpcSpringBoot { grpcSpringBootStarterVersion.set((String)null) } @@ -53,26 +54,15 @@ configurations.findAll{ cfg -> } -configurations { - pureNettyTestCompile.extendsFrom( testCompile) - pureNettyTestRuntime.extendsFrom(testRuntime) - - customSecurityTestCompile.extendsFrom( testCompile) - customSecurityTestRuntime.extendsFrom(testRuntime) - - bothPureAndShadedNettyTestCompile.extendsFrom( testCompile) - bothPureAndShadedNettyTestRuntime.extendsFrom( testRuntime) - - kafkaStreamTestCompile.extendsFrom( testCompile) - kafkaStreamTestRuntime.extendsFrom(testRuntime) - - noConsulDependenciesTestCompile.extendsFrom( testCompile) - noConsulDependenciesTestRuntime.extendsFrom(testRuntime) - +extensions.facets.each{ + if(it.name.endsWith("Test")) { + configurations.getByName("${it.name}Compile").extendsFrom(configurations.testCompile) + configurations.getByName("${it.name}Runtime").extendsFrom(configurations.testRuntime) + dependencies.add("${it.name}Compile", sourceSets.test.output) + } } - dependencies { implementation "org.springframework.boot:spring-boot-starter-actuator" @@ -104,21 +94,16 @@ dependencies { testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'org.mockito:mockito-core:2.23.0' - customSecurityTestCompile sourceSets.test.output - pureNettyTestCompile sourceSets.test.output pureNettyTestCompile "io.grpc:grpc-netty" - bothPureAndShadedNettyTestCompile sourceSets.test.output bothPureAndShadedNettyTestCompile "io.grpc:grpc-netty" - kafkaStreamTestCompile sourceSets.test.output kafkaStreamTestCompile "com.playtika.testcontainers:embedded-kafka:2.0.9" kafkaStreamTestCompile "org.springframework.cloud:spring-cloud-starter-stream-kafka" - noConsulDependenciesTestCompile sourceSets.test.output //testCompile "org.testcontainers:junit-jupiter:1.14.3" From 1525fb29328c36b3eb7305168997c68cd04dce5c Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Tue, 2 Nov 2021 13:05:17 +0200 Subject: [PATCH 16/22] interceptors diagram [skip ci] --- images/interceptors.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 images/interceptors.txt diff --git a/images/interceptors.txt b/images/interceptors.txt new file mode 100644 index 00000000..33a29477 --- /dev/null +++ b/images/interceptors.txt @@ -0,0 +1,18 @@ +@startuml(id=main) +!$color = "#orange" +participant interceptor_1 $color + +interceptor_1 -> interceptor_2 :intercept +interceptor_2 -> interceptor_3 :intercept +interceptor_3 -> interceptor_3 :intercept + +interceptor_1 -> interceptor_2 :onMessage +interceptor_2 -> interceptor_3 :onMessage +interceptor_3 -> interceptor_3 :onMessage +interceptor_3 -> service : actual call +@enduml + +@startuml +!$color = "#red" +!include interceptors.txt!main +@enduml \ No newline at end of file From 94b7800983ee25a270aedc7f63fcd312bcfd5d66 Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Tue, 2 Nov 2021 13:08:15 +0200 Subject: [PATCH 17/22] interceptors diagram [skip ci] --- images/interceptors.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/images/interceptors.txt b/images/interceptors.txt index 33a29477..d310f9db 100644 --- a/images/interceptors.txt +++ b/images/interceptors.txt @@ -13,6 +13,7 @@ interceptor_3 -> service : actual call @enduml @startuml -!$color = "#red" -!include interceptors.txt!main +' '!$color = "#red" +' !include interceptors.txt!main +Bob -> Alice @enduml \ No newline at end of file From f7eb46941afa5d6d05f9dd61ba47cc04f085eaa7 Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Tue, 2 Nov 2021 14:25:10 +0200 Subject: [PATCH 18/22] interceptors diagram [skip ci] --- images/interceptors.txt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/images/interceptors.txt b/images/interceptors.txt index d310f9db..e0fe3db2 100644 --- a/images/interceptors.txt +++ b/images/interceptors.txt @@ -1,5 +1,4 @@ -@startuml(id=main) -!$color = "#orange" +@startdef(id=main) participant interceptor_1 $color interceptor_1 -> interceptor_2 :intercept @@ -10,10 +9,14 @@ interceptor_1 -> interceptor_2 :onMessage interceptor_2 -> interceptor_3 :onMessage interceptor_3 -> interceptor_3 :onMessage interceptor_3 -> service : actual call +@enddef + +@startuml +!$color = "#red" +!includedef main @enduml @startuml -' '!$color = "#red" -' !include interceptors.txt!main -Bob -> Alice +!$color = "#Blue" +!includedef main @enduml \ No newline at end of file From c321231bf4952d652b7ab65110778d844e0b557b Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Wed, 3 Nov 2021 11:19:17 +0200 Subject: [PATCH 19/22] interceptors diagram [skip ci] --- README.adoc | 20 ++---- images/interceptors.txt | 122 +++++++++++++++++++++++++++++++++--- images/interceptors_001.png | Bin 0 -> 18654 bytes images/interceptors_002.png | Bin 0 -> 22134 bytes images/interceptors_003.png | Bin 0 -> 21389 bytes 5 files changed, 117 insertions(+), 25 deletions(-) create mode 100644 images/interceptors_001.png create mode 100644 images/interceptors_002.png create mode 100644 images/interceptors_003.png diff --git a/README.adoc b/README.adoc index 545faf06..74c33eb5 100644 --- a/README.adoc +++ b/README.adoc @@ -281,11 +281,8 @@ Error handling interceptor has the highest precedence. The way grpc interceptor works is that it intercepts the call and returns the server call listener, which in turn can intercept the request message as well, before forwarding it to the actual service call handler : -**** -`interceptor_1(interceptCall)` -> `interceptor_2(interceptCall)` -> `interceptor_3(interceptCall)` -> + -`interceptor_1(On_Message)`-> `interceptor_2(On_Message)`-> `interceptor_3(On_Message)`-> + -`actual service call` -**** +image:./images/interceptors_001.png[] + By setting `grpc.security.auth.fail-fast` property to `false` all downstream interceptors as well as all upstream interceptors (On_Message) will still be executed in case of authentication/authorization failure + @@ -293,20 +290,13 @@ Assuming `interceptor_2` is `securityInterceptor` : * For failed authentication/authorization with `grpc.security.auth.fail-fast=true`(default): + + +image:./images/interceptors_002.png[] -**** -`interceptor_1(interceptCall)` -> `securityInterceptor(interceptCall)` - *Call is Closed* -> ++++++ `interceptor_3(interceptCall)` -> + -`interceptor_1(On_Message)`-> `securityInterceptor(On_Message)`->`interceptor_3(On_Message)`-> + -`actual service call`++++++ -**** * For failed authentication/authorization with `grpc.security.auth.fail-fast=false`: + + -**** -`interceptor_1(interceptCall)` -> `securityInterceptor(interceptCall)` -> `interceptor_3(interceptCall)` -> -`interceptor_1(On_Message)`-> `securityInterceptor(On_Message)` - *Call is Closed* ++++++-> `interceptor_3(On_Message)`-> + -`actual service call`++++++ -**** +image:./images/interceptors_003.png[] + === Distributed tracing support (Spring Cloud Sleuth integration) diff --git a/images/interceptors.txt b/images/interceptors.txt index e0fe3db2..7be6ef16 100644 --- a/images/interceptors.txt +++ b/images/interceptors.txt @@ -1,22 +1,124 @@ +' to generate png files : download the latest jar from https://github.com/plantuml/plantuml/releases and run java -jar plantuml-1.2021.13.jar intercepts.txt + @startdef(id=main) -participant interceptor_1 $color +participant $interceptorName1 as interceptor_1 +participant $interceptorName2 as interceptor_2 +participant $interceptorName3 as interceptor_3 + +[-> interceptor_1 : start call +activate interceptor_1 + +interceptor_1 $arrow1_2 interceptor_2: $intStyle1_2 intercept $intStyle1_2 +activate interceptor_2 + +interceptor_2 $arrow2_3 interceptor_3: $intStyle2_3 intercept $intStyle2_3 +activate interceptor_3 + +interceptor_3 $arrow3_3 interceptor_3: $intStyle3_3 intercept $intStyle3_3 +return $intReturn2 +return $intReturn1 + +deactivate interceptor_3 +deactivate interceptor_2 +[<- interceptor_1 : $intResponseStyle return listener $intResponseStyle +deactivate interceptor_1 + + -interceptor_1 -> interceptor_2 :intercept -interceptor_2 -> interceptor_3 :intercept -interceptor_3 -> interceptor_3 :intercept +[-> interceptor_1 : $reqStyle request $reqStyle +activate interceptor_1 + +interceptor_1 $msgArrow1_2 interceptor_2 :$msgStyle1_2 onMessage $msgStyle1_2 +activate interceptor_2 + +interceptor_2 $msgArrow2_3 interceptor_3 : $msgStyle2_3 onMessage $msgStyle2_3 +activate interceptor_3 + +interceptor_3 $msgArrow3 service : $msgStyle3 actual call $msgStyle3 +return $msgReturn2 +return $msgReturn1 +[<- interceptor_1 : $responseStyle response $responseStyle + +deactivate interceptor_3 +deactivate interceptor_2 +deactivate interceptor_1 -interceptor_1 -> interceptor_2 :onMessage -interceptor_2 -> interceptor_3 :onMessage -interceptor_3 -> interceptor_3 :onMessage -interceptor_3 -> service : actual call @enddef @startuml -!$color = "#red" +!$arrow1_2 = "->" +!$arrow2_3 = "->" +!$arrow3_3 = "->" +!$intStyle1_2 = "" +!$intStyle2_3 = "" +!$intStyle3_3 = "" +!$msgArrow1_2 = "->" +!$msgArrow2_3 = "->" +!$msgArrow3 = "->" +!$msgStyle1_2 = "" +!$msgStyle2_3 = "" +!$msgStyle3 = "" +!$reqStyle = "" +!$responseStyle = "" +!$intResponseStyle = "" +!$intReturn1 = "" +!$intReturn2 = "" +!$msgReturn1 = "" +!$msgReturn2 = "" +!$interceptorName1 = "interceptor_1" +!$interceptorName2 = "interceptor_2" +!$interceptorName3 = "interceptor_3" +!includedef main +@enduml + +@startuml +!$arrow1_2 = "->" +!$arrow2_3 = "x->" +!$arrow3_3 = "x->" +!$intStyle1_2 = "" +!$intStyle2_3 = "---" +!$intStyle3_3 = "---" +!$msgArrow1_2 = "x->" +!$msgArrow2_3 = "x->" +!$msgArrow3 = "x->" +!$msgStyle1_2 = "---" +!$msgStyle2_3 = "---" +!$msgStyle3 = "---" +!$reqStyle = "---" +!$responseStyle = "---" +!$intResponseStyle = "---" +!$intReturn1 = "close call" +!$intReturn2 = "" +!$msgReturn1 = "" +!$msgReturn2 = "" +!$interceptorName1 = "interceptor_1" +!$interceptorName2 = "securityInterceptor" +!$interceptorName3 = "interceptor_3" !includedef main @enduml @startuml -!$color = "#Blue" +!$arrow1_2 = "->" +!$arrow2_3 = "->" +!$arrow3_3 = "->" +!$intStyle1_2= "" +!$intStyle2_3= "" +!$intStyle3_3= "" +!$msgArrow1_2 = "->" +!$msgArrow2_3 = "x->" +!$msgArrow3 = "x->" +!$msgStyle1_2 = "" +!$msgStyle2_3 = "---" +!$msgStyle3 = "---" +!$reqStyle = "" +!$responseStyle = "---" +!$intResponseStyle= "" +!$intReturn1 = "" +!$intReturn2 = "" +!$msgReturn1 = "close call" +!$msgReturn2 = "" +!$interceptorName1 = "interceptor_1" +!$interceptorName2 = "securityInterceptor" +!$interceptorName3 = "interceptor_3" !includedef main @enduml \ No newline at end of file diff --git a/images/interceptors_001.png b/images/interceptors_001.png new file mode 100644 index 0000000000000000000000000000000000000000..dca4f6508453b253f7e26b02eb6f93f735be90b8 GIT binary patch literal 18654 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 literal 0 HcmV?d00001 From d8c6541469075238b46afe3d8184163d01ad55b0 Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Wed, 3 Nov 2021 12:04:31 +0200 Subject: [PATCH 20/22] test for #249 --- .../springboot/grpc/consul/ConsulDefaultRegistrationTest.java | 1 + .../src/test/resources/application-consul-grpc-config-test.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulDefaultRegistrationTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulDefaultRegistrationTest.java index 9bc04ac8..31e0dc15 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulDefaultRegistrationTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulDefaultRegistrationTest.java @@ -48,6 +48,7 @@ public void consulPropertiesTest() { ConsulServiceInstance consulServiceInstance = (ConsulServiceInstance) instances.get(0); final Map metadata = consulServiceInstance.getMetadata(); assertThat(metadata.get("secure"),Matchers.is(Boolean.FALSE.toString())); + assertThat(metadata.get("key1"),Matchers.is("value1")); assertThat(consulServiceInstance.getTags(),Matchers.containsInAnyOrder("1","grpc=true","customTagName=A")); diff --git a/grpc-spring-boot-starter-demo/src/test/resources/application-consul-grpc-config-test.yml b/grpc-spring-boot-starter-demo/src/test/resources/application-consul-grpc-config-test.yml index b4c8868e..67d9deac 100644 --- a/grpc-spring-boot-starter-demo/src/test/resources/application-consul-grpc-config-test.yml +++ b/grpc-spring-boot-starter-demo/src/test/resources/application-consul-grpc-config-test.yml @@ -15,4 +15,6 @@ spring: - a - b instance-zone: zone1 + metadata: + key1: value1 From 066c3a7c1cfb5c210fc434a3c1b2224787b2b0ff Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Wed, 3 Nov 2021 13:46:18 +0200 Subject: [PATCH 21/22] test for #175 --- .../grpc/demo/DemoAppConfiguration.java | 16 +------ .../grpc/auth/PrePostSecurityAuthTest.java | 44 +++++++++++++++++++ .../grpc/auth/UserDetailsAuthTest.java | 17 +++++++ 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/DemoAppConfiguration.java b/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/DemoAppConfiguration.java index 42da990b..2cedfc4a 100644 --- a/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/DemoAppConfiguration.java +++ b/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/DemoAppConfiguration.java @@ -2,11 +2,9 @@ import io.grpc.examples.CalculatorGrpc; import io.grpc.examples.CalculatorOuterClass; -import io.grpc.examples.SecuredCalculatorGrpc; import io.grpc.stub.StreamObserver; import org.lognet.springboot.grpc.GRpcService; import org.springframework.context.annotation.Configuration; -import org.springframework.security.access.annotation.Secured; @Configuration public class DemoAppConfiguration { @@ -21,7 +19,7 @@ public void calculate(CalculatorOuterClass.CalculatorRequest request, StreamObse } - static CalculatorOuterClass.CalculatorResponse calculate(CalculatorOuterClass.CalculatorRequest request){ + public static CalculatorOuterClass.CalculatorResponse calculate(CalculatorOuterClass.CalculatorRequest request){ CalculatorOuterClass.CalculatorResponse.Builder resultBuilder = CalculatorOuterClass.CalculatorResponse.newBuilder(); switch (request.getOperation()){ case ADD: @@ -45,17 +43,5 @@ static CalculatorOuterClass.CalculatorResponse calculate(CalculatorOuterClass.Ca } - @GRpcService(interceptors = NotSpringBeanInterceptor.class) - @Secured({}) - public static class SecuredCalculatorService extends SecuredCalculatorGrpc.SecuredCalculatorImplBase{ - @Override - public void calculate(CalculatorOuterClass.CalculatorRequest request, StreamObserver responseObserver) { - responseObserver.onNext(CalculatorService.calculate(request)); - responseObserver.onCompleted(); - - } - - - } } diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PrePostSecurityAuthTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PrePostSecurityAuthTest.java index 7d22c9b0..52d76923 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PrePostSecurityAuthTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PrePostSecurityAuthTest.java @@ -2,6 +2,8 @@ import io.grpc.Status; import io.grpc.StatusRuntimeException; +import io.grpc.examples.CalculatorOuterClass; +import io.grpc.examples.SecuredCalculatorGrpc; import io.grpc.examples.tasks.Assignment; import io.grpc.examples.tasks.Person; import io.grpc.examples.tasks.TaskServiceGrpc; @@ -10,9 +12,12 @@ import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; +import org.lognet.springboot.grpc.GRpcService; import org.lognet.springboot.grpc.GrpcServerTestBase; import org.lognet.springboot.grpc.demo.DemoApp; +import org.lognet.springboot.grpc.demo.DemoAppConfiguration; import org.lognet.springboot.grpc.demo.ITaskService; +import org.lognet.springboot.grpc.demo.NotSpringBeanInterceptor; import org.lognet.springboot.grpc.security.AuthCallCredentials; import org.lognet.springboot.grpc.security.AuthHeader; import org.lognet.springboot.grpc.security.GrpcSecurity; @@ -22,6 +27,7 @@ import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.userdetails.User; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.test.context.junit4.SpringRunner; @@ -75,6 +81,17 @@ public List get(Duration duration) throws Throwable { @TestConfiguration static class TestCfg extends GrpcSecurityConfigurerAdapter { + @GRpcService(interceptors = NotSpringBeanInterceptor.class) + @PreAuthorize("isAuthenticated()") + public static class SecuredCalculatorService extends SecuredCalculatorGrpc.SecuredCalculatorImplBase{ + @Override + public void calculate(CalculatorOuterClass.CalculatorRequest request, StreamObserver responseObserver) { + responseObserver.onNext(DemoAppConfiguration.CalculatorService.calculate(request)); + responseObserver.onCompleted(); + + + } + } @Override public void configure(GrpcSecurity builder) throws Exception { builder.authorizeRequests() @@ -119,6 +136,33 @@ public void configure(GrpcSecurity builder) throws Exception { @MockBean private ITaskService service; + @Test + public void preAuthAnnotationOnClassTest() { + + + final SecuredCalculatorGrpc.SecuredCalculatorBlockingStub stub = SecuredCalculatorGrpc + .newBlockingStub(selectedChanel); + + final CalculatorOuterClass.CalculatorResponse response = stub + .withCallCredentials(user2Credentials()) + .calculate(CalculatorOuterClass.CalculatorRequest.newBuilder() + .setNumber1(1) + .setNumber2(1) + .setOperation(CalculatorOuterClass.CalculatorRequest.OperationType.ADD) + .build()); + assertThat(response.getResult(),Matchers.is(2d)); + + final StatusRuntimeException statusRuntimeException = Assert.assertThrows(StatusRuntimeException.class, () -> { + stub.withCallCredentials(unAuthUserCredentials()) + .calculate(CalculatorOuterClass.CalculatorRequest.newBuilder() + .setNumber1(1) + .setNumber2(1) + .setOperation(CalculatorOuterClass.CalculatorRequest.OperationType.ADD) + .build()); + }); + assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.UNAUTHENTICATED)); + } + @Test public void unaryPreAuthorizeCallTest() { final TaskServiceGrpc.TaskServiceBlockingStub stub = TaskServiceGrpc.newBlockingStub(getChannel()) diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java index 3060dcb9..b5c96142 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java @@ -10,11 +10,15 @@ import io.grpc.examples.CalculatorOuterClass; import io.grpc.examples.GreeterGrpc; import io.grpc.examples.SecuredCalculatorGrpc; +import io.grpc.stub.StreamObserver; import org.hamcrest.Matchers; import org.junit.Test; import org.junit.runner.RunWith; +import org.lognet.springboot.grpc.GRpcService; import org.lognet.springboot.grpc.GrpcServerTestBase; import org.lognet.springboot.grpc.demo.DemoApp; +import org.lognet.springboot.grpc.demo.DemoAppConfiguration; +import org.lognet.springboot.grpc.demo.NotSpringBeanInterceptor; import org.lognet.springboot.grpc.security.AuthClientInterceptor; import org.lognet.springboot.grpc.security.AuthHeader; import org.lognet.springboot.grpc.security.GrpcSecurity; @@ -24,6 +28,7 @@ import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @@ -45,6 +50,18 @@ public class UserDetailsAuthTest extends GrpcServerTestBase { @TestConfiguration static class TestCfg extends GrpcSecurityConfigurerAdapter { + + @GRpcService(interceptors = NotSpringBeanInterceptor.class) + @Secured({}) + public static class SecuredCalculatorService extends SecuredCalculatorGrpc.SecuredCalculatorImplBase{ + @Override + public void calculate(CalculatorOuterClass.CalculatorRequest request, StreamObserver responseObserver) { + responseObserver.onNext(DemoAppConfiguration.CalculatorService.calculate(request)); + responseObserver.onCompleted(); + + + } + } static final String pwd="strongPassword1"; @Bean From 1ef370969f10a6165b6f10d7e04eb7137150e985 Mon Sep 17 00:00:00 2001 From: Alexander Furer Date: Wed, 3 Nov 2021 13:54:27 +0200 Subject: [PATCH 22/22] release 4.5.9 --- README.adoc | 102 ++++++++++++++++-- ReleaseNotes.md | 16 +++ gradle.properties | 2 +- .../README.adoc | 2 +- grpc-spring-boot-starter/build.gradle | 2 +- 5 files changed, 113 insertions(+), 11 deletions(-) diff --git a/README.adoc b/README.adoc index 74c33eb5..b4e03aae 100644 --- a/README.adoc +++ b/README.adoc @@ -38,7 +38,7 @@ repositories { } dependencies { - compile 'io.github.lognet:grpc-spring-boot-starter:4.5.8' + compile 'io.github.lognet:grpc-spring-boot-starter:4.5.9' } @@ -683,24 +683,24 @@ By following this approach you also decouple the transport layer and business lo === Setup .Dependencies to implement authentiction scheme (to be added to server-side project) -[cols="a,a"] +[cols="1,4"] |=== |Scheme |Dependencies |Basic -| +a| * `org.springframework.security:spring-security-config` |Bearer -| +a| * `org.springframework.security:spring-security-config` * `org.springframework.security:spring-security-oauth2-jose` * `org.springframework.security:spring-security-oauth2-resource-server` |_Custom_ -| +a| * `org.springframework.security:spring-security-config` * `your.custom.lib` @@ -805,6 +805,71 @@ public AuthenticationSchemeSelector myCustomSchemeSelector(){ <> section explains how to pass custom authorization scheme and claim from GRPC client. +=== @PreAuthorize() and @PostAuthorize() support +Starting from version `4.5.9` you can also use standard `@PreAuthorize` and `@PostAuthorize` annotations on grpc service methods and grpc service types. + +.Referencing input/output object in expression +[cols="1,1,2,6"] +|=== +|Call Type |Input object ref |Output object ref | Sample + +|Unary + +(request-response) +|By parameter name +a|`returnObject` +a| +[source,java] +---- +@Override +@PreAuthorize("#person.age<12") +@PostAuthorize("returnObject.description.length()>0") +public void unary(Person person, StreamObserver responseObserver) { + } +---- + +|Input stream, + +single response +a|`#p0` or `#a0` +a|`returnObject` +a| +[source,java] +---- +@Override +@PreAuthorize("#p0.getAge()<12") +@PostAuthorize("returnObject.description.length()>0") +public StreamObserver inStream(StreamObserver responseObserver) { + } +---- + +|Single request, + +output stream +|By parameter name +a|`returnObject` +a| +[source,java] +---- +@Override +@PreAuthorize("#person.age<12") +@PostAuthorize("returnObject.description.length()>0") +public void outStream(Person person, StreamObserver responseObserver) { +} +---- + +|Bidi stream +|`#p0` or `#a0` +a|`returnObject` +a| +[source,java] +---- +@Override +@PreAuthorize("#p0.age<12") +@PostAuthorize("returnObject.description.length()>0") +public StreamObserver bidiStream(StreamObserver responseObserver) { +} +---- +|=== + + === Obtaining Authentication details To obtain `Authentication` object in the implementation of *secured method*, please use below snippet @@ -902,9 +967,30 @@ Starting from version `3.3.0`, the starter will auto-register the running grpc s The registered service name will be prefixed with `grpc-` ,i.e. `grpc-${spring.application.name}` to not interfere with standard registered web-service name if you choose to run both embedded `Grpc` and `Web` servers. + -Setting `spring.cloud.consul.discovery.register-health-check` to true will register GRPC health check service with Consul. +`ConsulDiscoveryProperties` are bound from configuration properties prefixed by `spring.cloud.consul.discovery` and then the values are overwritten by `grpc.consul.discovery` prefixed properties (if set). This allows you to have separate consul discovery configuration for `rest` and `grpc` services if you choose to expose both from your application. + +[source,yml] +---- +spring: + cloud: + consul: + discovery: + metadata: + myKey: myValue <1> + tags: + - myWebTag <2> +grpc: + consul: + discovery: + tags: + - myGrpcTag <3> +---- +<1> Both `rest` and `grpc` services are registered with metadata `myKey=myValue` +<2> Rest services are registered with `myWebTag` +<3> Grpc services are registered with `myGrpcTag` + +Setting `spring.cloud.consul.discovery.register-health-check` (or `grpc.consul.discovery.register-health-check`) to `true` will register GRPC health check service with Consul. -Tags could be set by defining `spring.cloud.consul.discovery.tags` property. There are 4 supported registration modes : @@ -915,7 +1001,7 @@ Please note that default implementation https://github.com/grpc/grpc-java/blob/b In this mode the running grpc server is registered as single service with check per each discovered `grpc` service. . `STANDALONE_SERVICES` + In this mode each discovered grpc service is registered as single service with single check. Each registered service is tagged by its own service name. -. `NOOP` - no grpc services registered. This mode is usefull if you serve both `rest` and `grpc` services in your application, but for some reason, only `rest` services should be registered with Consul. +. `NOOP` - no grpc services registered. This mode is useful if you serve both `rest` and `grpc` services in your application, but for some reason, only `rest` services should be registered with Consul. [source,yml] .You can control the desired mode from application.properties diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 4cad1774..c88a2f36 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,5 +1,6 @@ | Starter Version | gRPC versions |Spring Boot version | -------------------- |:-------------:|:------------------:| +| [4.5.9](#version-459)| 1.41.0 |2.5.6 | | [4.5.8](#version-458)| 1.41.0 |2.5.0 | | [4.5.7](#version-457)| 1.40.1 |2.5.0 | | [4.5.6](#version-456)| 1.40.0 |2.5.0 | @@ -27,6 +28,21 @@ | [4.0.0](#version-400)| 1.32.1 |2.3.3.RELEASE | | [3.5.7](#version-357)| 1.31.1 |1.5.13.RELEASE | +# Version 4.5.9 +## :star: New Features + +- Support separate consul discovery properties for grpc and http services [#250](https://github.com/LogNet/grpc-spring-boot-starter/issues/250) +- Add metadata to consul service discovery [#249](https://github.com/LogNet/grpc-spring-boot-starter/issues/249) +- Spring security SPEL expressions support (`@PreAuthorize` and `@PostAuthorize`) [#175](https://github.com/LogNet/grpc-spring-boot-starter/issues/175) + +## :lady_beetle: Bug Fixes + +- Circular bean dependency since 4.5.8 [#253](https://github.com/LogNet/grpc-spring-boot-starter/issues/253) + +## :hammer: Dependency Upgrades + +- Upgrade spring boot to 2.5.6 [#255](https://github.com/LogNet/grpc-spring-boot-starter/issues/255) + # Version 4.5.8 ## :star: New Features diff --git a/gradle.properties b/gradle.properties index 459b585c..c8bc1331 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ gradleErrorPronePluginVersion=2.0.2 errorProneVersion=2.7.1 lombokVersion=1.18.20 -version=4.5.9-SNAPSHOT +version=4.5.9 group=io.github.lognet description=Spring Boot starter for Google RPC. gitHubUrl=https\://github.com/LogNet/grpc-spring-boot-starter diff --git a/grpc-spring-boot-starter-gradle-plugin/README.adoc b/grpc-spring-boot-starter-gradle-plugin/README.adoc index 3903c519..25323151 100644 --- a/grpc-spring-boot-starter-gradle-plugin/README.adoc +++ b/grpc-spring-boot-starter-gradle-plugin/README.adoc @@ -23,7 +23,7 @@ Bootstraps the project with `com.google.protobuf` gradle plugin (including `grp ---- plugins { id 'java' - id "io.github.lognet.grpc-spring-boot" version '4.5.8' + id "io.github.lognet.grpc-spring-boot" version '4.5.9' } ---- diff --git a/grpc-spring-boot-starter/build.gradle b/grpc-spring-boot-starter/build.gradle index b0452419..8de54cf0 100644 --- a/grpc-spring-boot-starter/build.gradle +++ b/grpc-spring-boot-starter/build.gradle @@ -55,7 +55,7 @@ task generateReleaseNotes(type: JavaExec, group: "documentation") { ) doFirst { download { - src 'https://github.com/spring-io/github-changelog-generator/releases/download/v0.0.6/github-changelog-generator.jar' + src 'https://github.com/spring-io/github-changelog-generator/releases/download/v0.0.7/github-changelog-generator.jar' dest generator onlyIfModified true }