diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 6e712cb2..40c2b1e2 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -25,4 +25,4 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew build + run: ./gradlew build ; docker images diff --git a/README.adoc b/README.adoc index 2dc20b6a..b4e03aae 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.9' } @@ -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. @@ -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) @@ -693,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` @@ -815,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 @@ -912,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 : @@ -925,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/build.gradle b/build.gradle index 08e6d956..061e526c 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/gradle.properties b/gradle.properties index 2c6b967b..c8bc1331 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,11 @@ 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 -version=4.5.8 +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-demo/build.gradle b/grpc-spring-boot-starter-demo/build.gradle index a2767a5a..3b9071ea 100644 --- a/grpc-spring-boot-starter-demo/build.gradle +++ b/grpc-spring-boot-starter-demo/build.gradle @@ -22,7 +22,9 @@ facets { kafkaStreamTest customSecurityTest bothPureAndShadedNettyTest + noConsulDependenciesTest } + grpcSpringBoot { grpcSpringBootStarterVersion.set((String)null) } @@ -42,26 +44,25 @@ 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' + } -} -configurations { - pureNettyTestCompile.extendsFrom( testCompile) - pureNettyTestRuntime.extendsFrom(testRuntime) +} - customSecurityTestCompile.extendsFrom( testCompile) - customSecurityTestRuntime.extendsFrom(testRuntime) - bothPureAndShadedNettyTestCompile.extendsFrom( testCompile) - bothPureAndShadedNettyTestRuntime.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) - kafkaStreamTestCompile.extendsFrom( testCompile) - kafkaStreamTestRuntime.extendsFrom(testRuntime) + dependencies.add("${it.name}Compile", sourceSets.test.output) + } } - dependencies { implementation "org.springframework.boot:spring-boot-starter-actuator" @@ -82,30 +83,27 @@ 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 - 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" + //testCompile "org.testcontainers:junit-jupiter:1.14.3" 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/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/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/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/CustomInterceptorsOrderTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/CustomInterceptorsOrderTest.java index d10f1c71..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 @@ -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; @@ -35,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; @@ -57,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 { @@ -114,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/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..52d76923 --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PrePostSecurityAuthTest.java @@ -0,0 +1,434 @@ +package org.lognet.springboot.grpc.auth; + +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; +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.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; +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.access.prepost.PreAuthorize; +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 { + @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() + .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 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()) + .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..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 @@ -9,19 +9,18 @@ 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 io.grpc.stub.StreamObserver; 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.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.EnableGrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurity; import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter; import org.springframework.beans.factory.annotation.Autowired; @@ -29,25 +28,21 @@ 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.access.annotation.Secured; 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; 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; -@SpringBootTest(classes = DemoApp.class) +@SpringBootTest(classes = DemoApp.class,webEnvironment = SpringBootTest.WebEnvironment.NONE) @RunWith(SpringRunner.class) @Import({UserDetailsAuthTest.TestCfg.class}) public class UserDetailsAuthTest extends GrpcServerTestBase { @@ -55,17 +50,25 @@ public class UserDetailsAuthTest extends GrpcServerTestBase { @TestConfiguration static class TestCfg extends GrpcSecurityConfigurerAdapter { - static final String pwd="strongPassword1"; - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); + + @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 - 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(); } @@ -110,13 +113,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/ConsulDefaultRegistrationTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulDefaultRegistrationTest.java index 144855eb..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 @@ -3,23 +3,59 @@ 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(metadata.get("key1"),Matchers.is("value1")); + 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 9ef6de55..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 @@ -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,22 +15,19 @@ 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; 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 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; @@ -40,45 +35,26 @@ @ActiveProfiles("consul-test") +@DirtiesContext 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; + 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 { - consulClient = new ConsulClient("localhost", Integer.parseInt(System.getProperty("spring.cloud.consul.port"))); - - List instances = discoveryClient.getInstances(serviceId); final ServiceRegistrationMode registrationMode = applicationContext.getBean(GRpcServerProperties.class) @@ -108,7 +84,6 @@ public void tearDown() throws Exception { channel.shutdownNow(); channel.awaitTermination(1, TimeUnit.SECONDS); } - applicationContext.stop(); } @Test @@ -130,7 +105,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) 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-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..67d9deac --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/test/resources/application-consul-grpc-config-test.yml @@ -0,0 +1,20 @@ +grpc: + consul: + discovery: + tags: + - 1 + - grpc=true + - customTagName=A + instance-group: group1 + +spring: + cloud: + consul: + discovery: + tags: + - a + - b + instance-zone: zone1 + metadata: + key1: value1 + 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-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-gradle-plugin/README.adoc b/grpc-spring-boot-starter-gradle-plugin/README.adoc index cfc1f005..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.7' + 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 e805be7e..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 } @@ -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 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/GRpcServicesRegistry.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/GRpcServicesRegistry.java index f1782f7a..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,80 +1,165 @@ 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 { +public class GRpcServicesRegistry implements InitializingBean, ApplicationContextAware { + @Getter + @Builder + public static class GrpcServiceMethod { + private BindableService service; + private Method method; + + } + private ApplicationContext applicationContext; + private Supplier> beanNameToServiceBean; - private Map beanNameToServiceBean; + private Supplier> serviceNameToServiceBean; - private Map serviceNameToServiceBean; + private Supplier> grpcGlobalInterceptors; + + private Supplier, GrpcServiceMethod>> descriptorToServiceMethod; + + private Supplier< Map>> methodToDescriptor ; - private Collection 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(); + } + + 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) { + 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()))); } @Override public void afterPropertiesSet() throws Exception { + descriptorToServiceMethod = SingletonSupplier.of(this::descriptorToServiceMethod); + + methodToDescriptor = SingletonSupplier.of(()-> + descriptorToServiceMethod.get() + .entrySet() + .stream() + .collect(Collectors.toMap(e->e.getValue().getMethod(), Map.Entry::getKey)) + ); + beanNameToServiceBean = SingletonSupplier.of(() -> + getBeanNamesByTypeWithAnnotation(GRpcService.class, BindableService.class) + ); + + + serviceNameToServiceBean = SingletonSupplier.of(() -> + beanNameToServiceBean + .get() + .values() + .stream() + .collect(Collectors.toMap(s -> s.bindService().getServiceDescriptor().getName(), Function.identity())) + ); + + grpcGlobalInterceptors = SingletonSupplier.of(() -> + getBeanNamesByTypeWithAnnotation(GRpcGlobalInterceptor.class, ServerInterceptor.class) + .values() + ); + } + + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + private Map, GrpcServiceMethod> descriptorToServiceMethod (){ + final Map, GrpcServiceMethod> map = new HashMap<>(); - beanNameToServiceBean = getBeanNamesByTypeWithAnnotation(GRpcService.class, BindableService.class); + Function filterFactory = name -> + method -> method.getName().equalsIgnoreCase(name) ; - serviceNameToServiceBean = beanNameToServiceBean - .values() - .stream() - .collect(Collectors.toMap(s->s.bindService().getServiceDescriptor().getName(),Function.identity())); + 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())); - grpcGlobalInterceptors = getBeanNamesByTypeWithAnnotation(GRpcGlobalInterceptor.class, ServerInterceptor.class) - .values(); - } - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; + 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/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/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 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..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 @@ -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_ABSTAIN: super.vote(authentication, method, attributes); + } + } + )); + accessDecisionManager.setAllowIfAllAbstainDecisions(true); + + 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..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,29 +1,37 @@ package org.lognet.springboot.grpc.security; import io.grpc.MethodDescriptor; +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.List; import java.util.Map; import java.util.stream.Collectors; -public class GrpcSecurityMetadataSource implements SecurityMetadataSource { - private Map, List> methodsMap; +public class GrpcSecurityMetadataSource implements MethodSecurityMetadataSource { + private Map, List> methodDescriptorAttributes; + private GRpcServicesRegistry registry; + + + public GrpcSecurityMetadataSource(GRpcServicesRegistry registry , Map, List> methodDescriptorAttributes) { + this.methodDescriptorAttributes = methodDescriptorAttributes; + this.registry = registry; + - public GrpcSecurityMetadataSource(Map, List> methodsMap) { - this.methodsMap = methodsMap; } @Override public Collection getAttributes(Object object) throws IllegalArgumentException { - return methodsMap.get(object); + final MethodDescriptor methodDescriptor = SecurityInterceptor.GrpcMethodInvocation.class.cast(object).getCall().getMethodDescriptor(); + return methodDescriptorAttributes.get(methodDescriptor); } @Override public Collection getAllConfigAttributes() { - return methodsMap + return methodDescriptorAttributes .values() .stream() .flatMap(Collection::stream) @@ -32,6 +40,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 = registry.getMethodDescriptor(method); + return methodDescriptorAttributes.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..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 @@ -5,24 +5,28 @@ 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.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.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 java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -32,8 +36,9 @@ 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; @@ -41,13 +46,49 @@ public class SecurityInterceptor extends AbstractSecurityInterceptor implements private FailureHandlingSupport failureHandlingSupport; + private GRpcServicesRegistry registry; - public SecurityInterceptor(GrpcSecurityMetadataSource securedMethods,AuthenticationSchemeSelector schemeSelector) { - this.securedMethods = securedMethods; + + static class GrpcMethodInvocation extends SimpleMethodInvocation { + final private ServerCall call; + final private Metadata headers; + final private ServerCallHandler next; + @Getter + @Setter + private Object[] arguments; + + 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; + } + + @Override + public Object proceed() { + return next.startCall(call, headers); + } + + ServerCall getCall() { + return call; + } + } + + + + + public SecurityInterceptor(SecurityMetadataSource securityMetadataSource, AuthenticationSchemeSelector schemeSelector) { + this.securityMetadataSource = securityMetadataSource; this.schemeSelector = schemeSelector; } + @Autowired + public void setGRpcServicesRegistry(GRpcServicesRegistry registry) { + this.registry = registry; + + } + @Autowired public void setFailureHandlingSupport(@Lazy FailureHandlingSupport failureHandlingSupport) { this.failureHandlingSupport = failureHandlingSupport; @@ -64,12 +105,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 +141,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 +152,6 @@ public ServerCall.Listener interceptCall( } finally { SecurityContextHolder.getContext().setAuthentication(null); } - - } private ServerCallHandler authenticationPropagatingHandler(ServerCallHandler next) { @@ -121,7 +160,38 @@ 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: + throw new AuthenticationException("Unsupported call type "+call.getMethodDescriptor().getType()) {}; + } + + beforeInvocation(METHOD_INVOCATION.get()); + super.onMessage(message); + } catch (AccessDeniedException | AuthenticationException e) { + failureHandlingSupport.closeCall(e,call,headers); + } catch (Exception e) { + failureHandlingSupport.closeCall( new AuthenticationException("", e) {},call, headers); + } finally { + METHOD_INVOCATION.get().setArguments(null); + } + + + } + ); } @Override @@ -169,7 +239,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 +249,15 @@ private Context setupGRpcSecurityContext(ServerCall call, CharSequence aut context.setAuthentication(authentication); SecurityContextHolder.setContext(context); - final InterceptorStatusToken interceptorStatusToken = beforeInvocation(call.getMethodDescriptor()); + final GRpcServicesRegistry.GrpcServiceMethod grpcServiceMethod = registry.getGrpServiceMethod(call.getMethodDescriptor()); + + final GrpcMethodInvocation methodInvocation = new GrpcMethodInvocation<>(grpcServiceMethod , 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 +266,8 @@ private ServerCall.Listener fail(ServerCallHandler() { - }; } else { - return new MessageBlockingServerCallListener(next.startCall(call, headers)) { @Override public void onMessage(ReqT message) { diff --git a/images/interceptors.txt b/images/interceptors.txt new file mode 100644 index 00000000..7be6ef16 --- /dev/null +++ b/images/interceptors.txt @@ -0,0 +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 $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 : $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 + +@enddef + +@startuml +!$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 +!$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 00000000..dca4f650 Binary files /dev/null and b/images/interceptors_001.png differ diff --git a/images/interceptors_002.png b/images/interceptors_002.png new file mode 100644 index 00000000..cc347b6f Binary files /dev/null and b/images/interceptors_002.png differ diff --git a/images/interceptors_003.png b/images/interceptors_003.png new file mode 100644 index 00000000..5dbcc927 Binary files /dev/null and b/images/interceptors_003.png differ