Skip to content

Commit 287851b

Browse files
author
Alexander Furer
committed
docs
1 parent de2d98b commit 287851b

File tree

8 files changed

+258
-25
lines changed

8 files changed

+258
-25
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ gradle-app.setting
88
bin/
99
!/.gitignore
1010
!/.travis.yml
11-
out/
11+
out/
12+
/grpc-spring-boot-starter/changelog

README.adoc

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,17 @@ The annotation on java config factory method is also supported :
219219
}
220220
----
221221

222+
The particular service also has the opportunity to disable the global interceptors :
223+
224+
[source,java]
225+
----
226+
@GRpcService(applyGlobalInterceptors = false)
227+
public class GreeterService extends GreeterGrpc.GreeterImplBase{
228+
// ommited
229+
}
230+
----
231+
==== Interceptors ordering
232+
222233
Global interceptors can be ordered using Spring's `@Ordered` or `@Priority` annotations.
223234
Following Spring's ordering semantics, lower order values have higher priority and will be executed first in the interceptor chain.
224235

@@ -237,15 +248,33 @@ public class B implements ServerInterceptor{
237248
}
238249
----
239250

240-
The particular service also has the opportunity to disable the global interceptors :
251+
The starter uses built-in interceptors to implement Spring `Security`, `Validation` and `Metrics` integration.
252+
Their order can also be controlled by below properties :
253+
254+
* `grpc.security.auth.interceptor-order` ( defaults to `Ordered.HIGHEST_PRECEDENCE`)
255+
* `grpc.validation.interceptor-order` ( defaults to `Ordered.HIGHEST_PRECEDENCE+10`)
256+
* `grpc.metrics.interceptor-order` ( defaults to `Ordered.HIGHEST_PRECEDENCE+20`)
257+
258+
This gives you the ability to setup the desired order of built-in and your custom interceptors.
259+
260+
*Keep on reading !!! There is more*
261+
262+
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 :
263+
264+
`interceptor_1(interceptCall)` -> `interceptor_2(interceptCall)` -> `interceptor_3(interceptCall)` -> `interceptor_1(On_Message)`-> `interceptor_2(On_Message)`-> `interceptor_3(On_Message)`-> `actual service call`
265+
266+
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 +
267+
268+
So, for failed authentication/authorization with +
269+
`grpc.security.auth.fail-fast=true`(default): +
270+
271+
`interceptor_1(interceptCall)` -> `securityInterceptor(interceptCall)` - *Call is Closed* [.line-through]#-> `interceptor_3(interceptCall)` -> `interceptor_1(On_Message)`-> `securityInterceptor(On_Message)`-> `interceptor_3(On_Message)`-> `actual service call`#
272+
273+
And for failed authentication/authorization with +
274+
`grpc.security.auth.fail-fast=false`: +
275+
276+
`interceptor_1(interceptCall)` -> `securityInterceptor(interceptCall)` -> `interceptor_3(interceptCall)` -> `interceptor_1(On_Message)`-> `securityInterceptor(On_Message)` - *Call is Closed*-> [.line-through]#`interceptor_3(On_Message)`-> `actual service call`#
241277

242-
[source,java]
243-
----
244-
@GRpcService(applyGlobalInterceptors = false)
245-
public class GreeterService extends GreeterGrpc.GreeterImplBase{
246-
// ommited
247-
}
248-
----
249278
=== Distributed tracing support (Spring Cloud Sleuth integration)
250279

251280
This started is *natively* supported by `spring-cloud-sleuth` project. +
@@ -264,8 +293,20 @@ the starter will collect gRPC server metrics , broken down by
264293
After configuring the exporter of your https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-metrics[choice],
265294
you should see the `timer` named `grpc.server.calls`.
266295

296+
==== Custom tags support
297+
298+
By defining `GRpcMetricsTagsContributor` bean in your application context, you can add custom tags to the `grpc.server.calls` timer. +
299+
You can also use `RequestAwareGRpcMetricsTagsContributor` bean to tag *unary* calls. +
300+
Demo is https://github.com/LogNet/grpc-spring-boot-starter/blob/master/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcMeterTest.java[here]
267301

302+
[TIP]
303+
Keep the dispersion low not to blow up the cardinality of the metric.
268304

305+
`RequestAwareGRpcMetricsTagsContributor` can be still executed for failed authentication if `metric` interceptor has higher precedence than `security` interceptor and `grpc.security.auth.fail-fast` set to `false`. +
306+
This case is covered by link:grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/MetricWithSecurityTest.java[this] test. +
307+
308+
[TIP]
309+
Make sure to read <<Interceptors ordering>> chapter.
269310

270311
=== Spring Boot Validation support
271312

@@ -399,6 +440,12 @@ Note also custom cross-field link:grpc-spring-boot-starter-demo/src/main/java/or
399440
</bean>
400441
----
401442

443+
As described in <<Interceptors ordering>> chapter, you can give `validation` interceptor the higher precedence than `security` interceptor and set `grpc.security.auth.fail-fast` property to `false`. +
444+
In this scenario, if call is both unauthenticated and invalid, the client will get `Status.INVALID_ARGUMENT` instead of `Status.PERMISSION_DENIED/Status.UNAUTHENTICATED` response status.
445+
446+
By adding `GRpcErrorHandler` bean to your application, you get a chance to send your custom response headers. The error handler will be called with `Status.INVALID_ARGUMENT` and incoming request message that is failed.
447+
448+
402449
== Spring Security Integration
403450

404451
=== Setup
@@ -527,6 +574,14 @@ final Authentication auth = GrpcSecurity.AUTHENTICATION_CONTEXT_KEY.get();
527574
----
528575

529576

577+
=== Custom authentication failure handling
578+
579+
By adding `GRpcErrorHandler` bean to your application, you get a chance to provide your custom response headers. The error handler will be called with `Status.PERMISSION_DENIED/Status.UNAUTHENTICATED` (and incoming request message , if you set `grpc.security.auth.fail-fast` property to `false`). +
580+
The demo is link:grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtRoleTest.java[here]
581+
582+
583+
584+
530585

531586
=== Client side configuration support
532587

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package org.lognet.springboot.grpc;
2+
3+
4+
import io.grpc.Status;
5+
import io.grpc.StatusRuntimeException;
6+
import io.grpc.examples.GreeterGrpc;
7+
import io.grpc.examples.GreeterOuterClass;
8+
import io.micrometer.core.instrument.MeterRegistry;
9+
import io.micrometer.core.instrument.Timer;
10+
import io.micrometer.core.instrument.simple.SimpleConfig;
11+
import org.awaitility.Awaitility;
12+
import org.hamcrest.Matchers;
13+
import org.junit.runner.RunWith;
14+
import org.lognet.springboot.grpc.demo.DemoApp;
15+
import org.lognet.springboot.grpc.security.AuthCallCredentials;
16+
import org.lognet.springboot.grpc.security.AuthHeader;
17+
import org.lognet.springboot.grpc.security.EnableGrpcSecurity;
18+
import org.lognet.springboot.grpc.security.GrpcSecurity;
19+
import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter;
20+
import org.springframework.beans.factory.annotation.Autowired;
21+
import org.springframework.boot.test.context.SpringBootTest;
22+
import org.springframework.boot.test.context.TestConfiguration;
23+
import org.springframework.context.annotation.Import;
24+
import org.springframework.security.authentication.AuthenticationProvider;
25+
import org.springframework.security.authentication.BadCredentialsException;
26+
import org.springframework.security.core.Authentication;
27+
import org.springframework.security.core.AuthenticationException;
28+
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
29+
import org.springframework.test.context.ActiveProfiles;
30+
import org.springframework.test.context.junit4.SpringRunner;
31+
32+
import java.time.Duration;
33+
import java.util.concurrent.ExecutionException;
34+
import java.util.concurrent.TimeUnit;
35+
36+
import static org.hamcrest.MatcherAssert.assertThat;
37+
import static org.hamcrest.Matchers.greaterThan;
38+
import static org.hamcrest.Matchers.is;
39+
import static org.hamcrest.Matchers.notNullValue;
40+
import static org.junit.Assert.assertThrows;
41+
42+
43+
@SpringBootTest(classes = DemoApp.class
44+
,properties = {
45+
"grpc.security.auth.fail-fast=false", // give metric interceptor a chance to record failed authentication
46+
"grpc.security.auth.interceptor-order=4",
47+
"grpc.metrics.interceptor-order=2",
48+
}
49+
)
50+
@RunWith(SpringRunner.class)
51+
@Import({MetricWithSecurityTest.TestCfg.class})
52+
@ActiveProfiles("measure")
53+
public class MetricWithSecurityTest extends GrpcServerTestBase {
54+
55+
56+
@Autowired
57+
private MeterRegistry registry;
58+
59+
60+
61+
@Autowired
62+
private SimpleConfig registryConfig;
63+
64+
65+
// @Test
66+
public void validationShouldInvokedBeforeAuthTest() {
67+
final GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(super.getChannel());
68+
StatusRuntimeException e = assertThrows(StatusRuntimeException.class, () -> {
69+
stub.helloPersonValidResponse(GreeterOuterClass.Person.newBuilder()
70+
.setAge(49)// valid
71+
.clearName()//invalid
72+
.build());
73+
});
74+
75+
assertThat(e.getStatus().getCode(), Matchers.is(Status.Code.INVALID_ARGUMENT));
76+
77+
78+
}
79+
80+
@Override
81+
public void simpleGreeting() throws ExecutionException, InterruptedException {
82+
AuthCallCredentials callCredentials = new AuthCallCredentials(
83+
AuthHeader.builder().basic("user","pwd".getBytes())
84+
);
85+
86+
final GreeterGrpc.GreeterBlockingStub greeterFutureStub = GreeterGrpc.newBlockingStub(selectedChanel);
87+
StatusRuntimeException e = assertThrows(StatusRuntimeException.class, () -> {
88+
89+
greeterFutureStub
90+
.withCallCredentials(callCredentials)
91+
.sayHello(GreeterOuterClass.HelloRequest.newBuilder().setName(name).build());
92+
});
93+
assertThat(e.getStatus().getCode(),Matchers.is(Status.Code.UNAUTHENTICATED));
94+
95+
final Timer timer = registry.find("grpc.server.calls").timer();
96+
assertThat(timer,notNullValue(Timer.class));
97+
98+
Awaitility
99+
.waitAtMost(Duration.ofMillis(registryConfig.step().toMillis() * 2))
100+
.until(timer::count,greaterThan(0L));
101+
102+
assertThat(timer.max(TimeUnit.MILLISECONDS),greaterThan(0d));
103+
assertThat(timer.mean(TimeUnit.MILLISECONDS),greaterThan(0d));
104+
assertThat(timer.totalTime(TimeUnit.MILLISECONDS),greaterThan(0d));
105+
106+
107+
108+
final String resultTag = timer.getId().getTag("result");
109+
assertThat(resultTag,notNullValue());
110+
assertThat(resultTag,is(Status.UNAUTHENTICATED.getCode().name()));
111+
}
112+
113+
@TestConfiguration
114+
@EnableGrpcSecurity
115+
static class TestCfg extends GrpcSecurityConfigurerAdapter {
116+
@Override
117+
public void configure(GrpcSecurity builder) throws Exception {
118+
119+
builder.authorizeRequests()
120+
.anyMethod().authenticated()
121+
.and()
122+
.authenticationProvider(new AuthenticationProvider() {
123+
@Override
124+
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
125+
throw new BadCredentialsException("");
126+
}
127+
128+
@Override
129+
public boolean supports(Class<?> authentication) {
130+
return true;
131+
}
132+
})
133+
.userDetailsService(new InMemoryUserDetailsManager());
134+
}
135+
136+
137+
138+
}
139+
140+
141+
142+
143+
144+
}

grpc-spring-boot-starter/build.gradle

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ buildscript {
88
classpath "io.franzbecker:gradle-lombok:4.0.0"
99
}
1010
}
11-
11+
plugins {
12+
id "de.undercouch.download" version "4.1.1"
13+
}
1214
apply plugin: 'java'
1315
apply plugin: 'org.springframework.boot'
1416
apply plugin: 'io.spring.dependency-management'
@@ -23,6 +25,29 @@ dependencyManagement {
2325
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
2426
}
2527
}
28+
task generateReleaseNotes(type: JavaExec, group: "documentation") {
29+
def m = java.util.regex.Pattern.compile("(\\d+\\.\\d+\\.\\d+).*").matcher(version.toString())
30+
def milestoneLabel = ""
31+
if(m.find()) {
32+
milestoneLabel = m.group(1)
33+
}
34+
35+
def generator = file("changelog/github-changelog-generator.jar")
36+
classpath(generator.absolutePath)
37+
args (
38+
milestoneLabel
39+
,"changelog/changelog.md"
40+
, "--changelog.repository=LogNet/grpc-spring-boot-starter"
41+
)
42+
doFirst {
43+
download {
44+
src 'https://github.com/spring-io/github-changelog-generator/releases/download/v0.0.6/github-changelog-generator.jar'
45+
dest generator
46+
onlyIfModified true
47+
}
48+
}
49+
}
50+
2651
task delombok(type: io.franzbecker.gradle.lombok.task.DelombokTask) {
2752
def outputDir = file("$buildDir/delombok")
2853
outputs.dir(outputDir)

grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/FailureHandlingServerInterceptor.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import io.grpc.ServerCall;
55
import io.grpc.ServerInterceptor;
66
import io.grpc.Status;
7+
import io.grpc.StatusRuntimeException;
78

89
public interface FailureHandlingServerInterceptor extends ServerInterceptor {
9-
default void closeCall(Object o, GRpcErrorHandler errorHandler, ServerCall<?, ?> call, Metadata headers, final Status status, Exception exception){
10+
default StatusRuntimeException closeCall(Object o, GRpcErrorHandler errorHandler, ServerCall<?, ?> call, Metadata headers, final Status status, Exception exception){
1011

1112
final Metadata responseHeaders = new Metadata();
1213
Status statusToSend;
@@ -17,5 +18,7 @@ default void closeCall(Object o, GRpcErrorHandler errorHandler, ServerCall<?,
1718
}
1819

1920
call.close(statusToSend, responseHeaders);
21+
return statusToSend.asRuntimeException(responseHeaders);
22+
2023
}
2124
}

grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/metrics/GRpcMetricsAutoConfiguration.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.Collection;
3636
import java.util.List;
3737
import java.util.Optional;
38+
import java.util.concurrent.atomic.AtomicBoolean;
3839
import java.util.stream.Collectors;
3940
import java.util.stream.Stream;
4041
import java.util.stream.StreamSupport;
@@ -71,6 +72,7 @@ static class MonitoringServerCall<ReqT, RespT> extends ForwardingServerCall.Simp
7172

7273
private Collection<GRpcMetricsTagsContributor> tagsContributors;
7374
private List<Tag> additionalTags;
75+
private AtomicBoolean closed = new AtomicBoolean(false);
7476

7577

7678

@@ -84,14 +86,17 @@ protected MonitoringServerCall(ServerCall<ReqT, RespT> delegate, MeterRegistry r
8486
@Override
8587
public void close(Status status, Metadata trailers) {
8688

87-
final Timer.Builder timerBuilder = Timer.builder("grpc.server.calls");
88-
tagsContributors.forEach(c->
89-
timerBuilder.tags(c.getTags(status,getMethodDescriptor(),getAttributes()))
90-
);
91-
Optional.ofNullable(additionalTags)
92-
.ifPresent(timerBuilder::tags);
89+
if(closed.compareAndSet(false,true)){ //close is called twice , first time with actual status
90+
final Timer.Builder timerBuilder = Timer.builder("grpc.server.calls");
91+
tagsContributors.forEach(c->
92+
timerBuilder.tags(c.getTags(status,getMethodDescriptor(),getAttributes()))
93+
);
94+
Optional.ofNullable(additionalTags)
95+
.ifPresent(timerBuilder::tags);
96+
97+
start.stop(timerBuilder.register(registry));
98+
}
9399

94-
start.stop(timerBuilder.register(registry));
95100

96101
super.close(status, trailers);
97102
}

grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,15 @@ public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
110110
private <RespT, ReqT> ServerCall.Listener<ReqT> fail(ServerCallHandler<ReqT, RespT> next, ServerCall<ReqT, RespT> call, Metadata headers,final Status status, Exception exception) {
111111

112112
if (authCfg.isFailFast()) {
113-
closeCall(null,errorHandler,call,headers,status,exception);
114-
return new ServerCall.Listener<ReqT>() {
115-
// noop
116-
};
113+
throw closeCall(null,errorHandler,call,headers,status,exception);
114+
117115
} else {
118116

119117
return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(next.startCall(call,headers)) {
120118
@Override
121119
public void onMessage(ReqT message) {
122-
closeCall(message,errorHandler,call,headers,status,exception);
120+
throw closeCall(message, errorHandler, call, headers, status, exception);
121+
123122

124123
}
125124
};

0 commit comments

Comments
 (0)