Skip to content

Commit 36b8ca6

Browse files
author
Alexander Furer
committed
closes #206, closes #183
1 parent 86c89b5 commit 36b8ca6

35 files changed

+236
-112
lines changed

README.adoc

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -568,30 +568,26 @@ By following this approach you also decouple the transport layer and business lo
568568
GRPC security configuration follows the same principals and APIs as Spring WEB security configuration.
569569

570570
==== Default
571+
GRPC security is enabled by default if you have `org.springframework.security:spring-security-config` dependency in your classpath.
571572

572-
Defining bean with type `GrpcSecurityConfigurerAdapter` annotated with `@EnableGrpcSecurity` is sufficient to secure you GRPC services and/or methods :
573+
This default configuration secures GRPC methods/services annotated with `org.springframework.security.access.annotation.@Secured` annotation. +
574+
Leaving value of the annotation empty (`@Secured({})`) means : `authenticate` only, no authorization will be performed.
573575

574-
[source,java]
575-
----
576+
If `JwtDecoder` bean exists in your context, it will also register `JwtAuthenticationProvider` to handle the validation of authentication claim.
576577

577-
@EnableGrpcSecurity
578-
public class GrpcSecurityConfiguration extends GrpcSecurityConfigurerAdapter {
578+
`BasicAuthSchemeSelector` and `BearerTokenAuthSchemeSelector` are also automatically registered to support authentication with username/password and bearer token.
579579

580-
}
580+
By setting `grpc.security.auth.enabled` to `false`, GRPC security can be turned-off.
581581

582-
----
582+
==== Custom
583583

584-
This default configuration secures GRPC methods/services annotated with `org.springframework.security.access.annotation.@Secured` annotation. +
585-
Leaving value of the annotation empty (`@Secured({})`) means : `authenticate` only, no authorization will be performed. +
586-
If `JwtDecoder` bean exists in your context, it will also register `JwtAuthenticationProvider` to handle the validation of authentication claim.
584+
Customization of GRPC security configuration is done by extending `GrpcSecurityConfigurerAdapter` (Various configuration examples and test scenarios are link:grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth[here].)
587585

588-
==== Custom
589586

590-
Various configuration examples and test scenarios are link:grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth[here].
591587

592588
[source,java]
593589
----
594-
@EnableGrpcSecurity
590+
@Configuration
595591
public class GrpcSecurityConfiguration extends GrpcSecurityConfigurerAdapter {
596592
@Autowired
597593
private JwtDecoder jwtDecoder;
@@ -616,7 +612,7 @@ One is possible to plug in your own bespoke authentication provider by implement
616612

617613
[source,java]
618614
----
619-
@EnableGrpcSecurity
615+
@Configuration
620616
public class GrpcSecurityConfiguration extends GrpcSecurityConfigurerAdapter {
621617
@Override
622618
public void configure(GrpcSecurity builder) throws Exception {

grpc-spring-boot-starter-demo/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ configurations {
5151
dependencies {
5252

5353
implementation "org.springframework.boot:spring-boot-starter-actuator"
54+
implementation "io.micrometer:micrometer-registry-prometheus"
55+
5456
implementation 'org.springframework.boot:spring-boot-starter-web'
5557

5658
implementation "org.springframework.security:spring-security-config"

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import org.springframework.security.core.Authentication;
1212
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
1313

14+
import java.util.Optional;
15+
1416
@Slf4j
1517
@GRpcService(interceptors = { LogInterceptor.class })
1618
@Secured("SCOPE_profile")
@@ -29,7 +31,10 @@ public void sayAuthHello(Empty request, StreamObserver<GreeterOuterClass.HelloRe
2931

3032
@Override
3133
public void sayAuthHello2(Empty request, StreamObserver<GreeterOuterClass.HelloReply> responseObserver) {
32-
reply(GrpcSecurity.AUTHENTICATION_CONTEXT_KEY.get().getName(),responseObserver);
34+
String userName = Optional.ofNullable(GrpcSecurity.AUTHENTICATION_CONTEXT_KEY.get())
35+
.map(Authentication::getName)
36+
.orElse("anonymous");
37+
reply(userName,responseObserver);
3338
}
3439

3540
private void reply(String userName,StreamObserver<GreeterOuterClass.HelloReply> responseObserver){

grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/CustomInterceptorsOrderTest.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,7 @@ protected void afterGreeting() {
113113
}
114114

115115
@TestConfiguration
116-
static class TestCfg {
117-
118-
@EnableGrpcSecurity
119-
public class DemoGrpcSecurityConfig extends GrpcSecurityConfigurerAdapter {
116+
static class TestCfg extends GrpcSecurityConfigurerAdapter {
120117

121118

122119
static final String pwd = "strongPassword1";
@@ -159,7 +156,7 @@ public boolean supports(Class<?> authentication) {
159156
})
160157
.userDetailsService(new InMemoryUserDetailsManager(user()));
161158
}
162-
}
159+
163160

164161
@Bean
165162
@GRpcGlobalInterceptor
@@ -188,7 +185,7 @@ protected Channel getChannel() {
188185

189186

190187
final AuthClientInterceptor interceptor = new AuthClientInterceptor(AuthHeader.builder()
191-
.basic(user.getUsername(), TestCfg.DemoGrpcSecurityConfig.pwd.getBytes())
188+
.basic(user.getUsername(), TestCfg.pwd.getBytes())
192189
);
193190
return ClientInterceptors.intercept(super.getChannel(), interceptor);
194191
}

grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/DefaultGrpcPortTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
import org.lognet.springboot.grpc.context.LocalRunningGrpcPort;
99
import org.lognet.springboot.grpc.demo.DemoApp;
1010
import org.springframework.boot.test.context.SpringBootTest;
11+
import org.springframework.test.context.ActiveProfiles;
1112
import org.springframework.test.context.junit4.SpringRunner;
1213

1314
import static org.hamcrest.MatcherAssert.assertThat;
1415
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE;
1516

1617
@RunWith(SpringRunner.class)
1718
@SpringBootTest(classes = {DemoApp.class}, webEnvironment = NONE)
19+
@ActiveProfiles("disable-security")
1820
public class DefaultGrpcPortTest extends GrpcServerTestBase {
1921
@LocalRunningGrpcPort
2022
int runningPort;

grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/DemoAppTest.java

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.lognet.springboot.grpc;
22

3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.node.ObjectNode;
35
import io.grpc.ServerInterceptor;
46
import io.grpc.examples.CalculatorGrpc;
57
import io.grpc.examples.CalculatorOuterClass;
@@ -13,6 +15,10 @@
1315
import io.grpc.reflection.v1alpha.ServerReflectionResponse;
1416
import io.grpc.reflection.v1alpha.ServiceResponse;
1517
import io.grpc.stub.StreamObserver;
18+
import io.micrometer.core.instrument.Timer;
19+
import io.micrometer.prometheus.PrometheusConfig;
20+
import org.awaitility.Awaitility;
21+
import org.hamcrest.Matchers;
1622
import org.junit.Assert;
1723
import org.junit.Rule;
1824
import org.junit.Test;
@@ -21,44 +27,67 @@
2127
import org.mockito.Mockito;
2228
import org.springframework.beans.factory.annotation.Autowired;
2329
import org.springframework.beans.factory.annotation.Qualifier;
30+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
2431
import org.springframework.boot.test.context.SpringBootTest;
2532
import org.springframework.boot.test.system.OutputCaptureRule;
2633
import org.springframework.boot.test.web.client.TestRestTemplate;
2734
import org.springframework.http.HttpStatus;
2835
import org.springframework.http.ResponseEntity;
36+
import org.springframework.test.context.ActiveProfiles;
2937
import org.springframework.test.context.junit4.SpringRunner;
38+
import org.springframework.test.web.servlet.MockMvc;
3039

40+
import java.time.Duration;
3141
import java.util.ArrayList;
3242
import java.util.List;
43+
import java.util.Objects;
44+
import java.util.Optional;
45+
import java.util.Spliterator;
46+
import java.util.Spliterators;
47+
import java.util.concurrent.Callable;
3348
import java.util.concurrent.CountDownLatch;
3449
import java.util.concurrent.ExecutionException;
3550
import java.util.concurrent.TimeUnit;
51+
import java.util.stream.Stream;
52+
import java.util.stream.StreamSupport;
3653

3754
import static org.hamcrest.CoreMatchers.containsString;
3855
import static org.hamcrest.CoreMatchers.not;
56+
import static org.hamcrest.MatcherAssert.assertThat;
57+
import static org.hamcrest.Matchers.greaterThan;
58+
import static org.hamcrest.Matchers.notNullValue;
3959
import static org.junit.Assert.*;
4060
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
61+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
62+
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
63+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
64+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
4165

4266
/**
4367
* Created by alexf on 28-Jan-16.
4468
*/
4569
@RunWith(SpringRunner.class)
4670
@SpringBootTest(classes = {DemoApp.class, TestConfig.class}, webEnvironment = RANDOM_PORT
4771
, properties = {"grpc.enableReflection=true",
48-
"grpc.port=0",
4972
"grpc.shutdownGrace=-1"
5073
})
51-
public class DemoAppTest extends GrpcServerTestBase{
74+
@ActiveProfiles({"disable-security", "measure"})
75+
76+
public class DemoAppTest extends GrpcServerTestBase {
77+
78+
@Autowired
79+
private PrometheusConfig prometheusConfig;
5280

5381
@Autowired
5482
private TestRestTemplate restTemplate;
5583

5684
@Rule
5785
public OutputCaptureRule outputCapture = new OutputCaptureRule();
5886

87+
5988
@Autowired
6089
@Qualifier("globalInterceptor")
61-
private ServerInterceptor globalInterceptor;
90+
private ServerInterceptor globalInterceptor;
6291

6392

6493
@Test
@@ -79,7 +108,7 @@ public void interceptorsTest() throws ExecutionException, InterruptedException {
79108
.get().getResult();
80109

81110
// global interceptor should be invoked once on each service
82-
Mockito.verify(globalInterceptor,Mockito.times(2)).interceptCall(Mockito.any(),Mockito.any(),Mockito.any());
111+
Mockito.verify(globalInterceptor, Mockito.times(2)).interceptCall(Mockito.any(), Mockito.any(), Mockito.any());
83112

84113

85114
// log interceptor should be invoked only on GreeterService and not CalculatorService
@@ -90,15 +119,15 @@ public void interceptorsTest() throws ExecutionException, InterruptedException {
90119
outputCapture.expect(containsString("I'm not Spring bean interceptor and still being invoked..."));
91120
}
92121

93-
@Test
122+
@Test
94123
public void actuatorTest() throws ExecutionException, InterruptedException {
95-
ResponseEntity<String> response = restTemplate.getForEntity("/env", String.class);
124+
ResponseEntity<String> response = restTemplate.getForEntity("/actuator/env", String.class);
96125
assertEquals(HttpStatus.OK, response.getStatusCode());
97126
}
98127

99128

100129
@Test
101-
public void testDefaultConfigurer(){
130+
public void testDefaultConfigurer() {
102131
Assert.assertEquals("Default configurer should be picked up",
103132
context.getBean(GRpcServerBuilderConfigurer.class).getClass(),
104133
GRpcServerBuilderConfigurer.class);
@@ -142,4 +171,33 @@ public void testHealthCheck() throws ExecutionException, InterruptedException {
142171
assertNotNull(servingStatus);
143172
assertEquals(servingStatus, HealthCheckResponse.ServingStatus.SERVING);
144173
}
174+
175+
@Override
176+
protected void afterGreeting() throws Exception {
177+
178+
179+
ResponseEntity<ObjectNode> metricsResponse = restTemplate.getForEntity("/actuator/metrics", ObjectNode.class);
180+
assertEquals(HttpStatus.OK, metricsResponse.getStatusCode());
181+
final String metricName = "grpc.server.calls";
182+
final Optional<String> containsGrpcServerCallsMetric = StreamSupport.stream(Spliterators.spliteratorUnknownSize(metricsResponse.getBody().withArray("names")
183+
.elements(), Spliterator.NONNULL), false)
184+
.map(JsonNode::asText)
185+
.filter(metricName::equals)
186+
.findFirst();
187+
assertThat("Should contain " + metricName,containsGrpcServerCallsMetric.isPresent());
188+
189+
190+
Callable<Long> getPrometheusMetrics = () -> {
191+
ResponseEntity<String> response = restTemplate.getForEntity("/actuator/prometheus", String.class);
192+
assertEquals(HttpStatus.OK, response.getStatusCode());
193+
return Stream.of(response.getBody().split(System.lineSeparator()))
194+
.filter(s -> s.contains(metricName.replace('.','_')))
195+
.count();
196+
};
197+
198+
Awaitility
199+
.waitAtMost(Duration.ofMillis(prometheusConfig.step().toMillis() * 2))
200+
.until(getPrometheusMetrics,Matchers.greaterThan(0L));
201+
202+
}
145203
}

grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/DisabledGrpcServerTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.junit.runner.RunWith;
55
import org.lognet.springboot.grpc.demo.DemoApp;
66
import org.springframework.boot.test.context.SpringBootTest;
7+
import org.springframework.test.context.ActiveProfiles;
78
import org.springframework.test.context.junit4.SpringRunner;
89

910
import static org.junit.Assert.assertNotNull;
@@ -17,6 +18,7 @@
1718
@SpringBootTest(classes = {DemoApp.class },webEnvironment = NONE
1819
,properties = {"grpc.enabled=false","grpc.inProcessServerName=testServer","grpc.shutdownGrace=-1"}
1920
)
21+
@ActiveProfiles("disable-security")
2022
public class DisabledGrpcServerTest extends GrpcServerTestBase {
2123

2224

grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/EnvVarGrpcPortTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
import org.lognet.springboot.grpc.context.LocalRunningGrpcPort;
1010
import org.lognet.springboot.grpc.demo.DemoApp;
1111
import org.springframework.boot.test.context.SpringBootTest;
12+
import org.springframework.test.context.ActiveProfiles;
1213
import org.springframework.test.context.junit4.SpringRunner;
1314
import org.springframework.util.SocketUtils;
1415

1516
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE;
1617

1718
@RunWith(SpringRunner.class)
1819
@SpringBootTest(classes = {DemoApp.class}, webEnvironment = NONE)
20+
@ActiveProfiles("disable-security")
1921
public class EnvVarGrpcPortTest extends GrpcServerTestBase {
2022

2123

grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcBuggySecuritySettingsTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
@RunWith(SpringRunnerWithGlobalExpectedExceptionInspected.class)
1818
@SpringBootTest(classes = {DemoApp.class}, webEnvironment = NONE)
19-
@ActiveProfiles("buggy-security")
19+
@ActiveProfiles({"buggy-transport-security","disable-security"})
2020
@ExpectedStartupExceptionWithInspector(GrpcBuggySecuritySettingsTest.ExceptionInspector.class)
2121
public class GrpcBuggySecuritySettingsTest extends GrpcServerTestBase {
2222
@Test

grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcMeterTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.micrometer.core.instrument.Tag;
1010
import io.micrometer.core.instrument.Timer;
1111
import io.micrometer.core.instrument.simple.SimpleConfig;
12+
import io.micrometer.prometheus.PrometheusConfig;
1213
import org.awaitility.Awaitility;
1314
import org.junit.Before;
1415
import org.junit.runner.RunWith;
@@ -83,7 +84,7 @@ public Iterable<Tag> getTags(Status status, MethodDescriptor<?, ?> methodDescrip
8384
private int port;
8485

8586
@Autowired
86-
private SimpleConfig registryConfig;
87+
private PrometheusConfig registryConfig;
8788

8889
@Before
8990
public void setUp() {

0 commit comments

Comments
 (0)