Skip to content

Commit f0d1b9e

Browse files
committed
fixes #355
1 parent 8dd9d47 commit f0d1b9e

File tree

4 files changed

+113
-59
lines changed

4 files changed

+113
-59
lines changed

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

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

3-
import io.grpc.CallOptions;
4-
import io.grpc.Context;
53
import io.grpc.Status;
64
import io.grpc.examples.reactor.ReactiveHelloRequest;
75
import io.grpc.examples.reactor.ReactiveHelloResponse;
@@ -10,9 +8,8 @@
108
import org.lognet.springboot.grpc.GRpcService;
119
import org.lognet.springboot.grpc.recovery.GRpcExceptionHandler;
1210
import org.lognet.springboot.grpc.recovery.GRpcExceptionScope;
13-
import org.springframework.beans.factory.annotation.Autowired;
1411
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
15-
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
12+
import org.springframework.security.access.annotation.Secured;
1613
import org.springframework.transaction.annotation.Transactional;
1714
import reactor.core.publisher.Flux;
1815
import reactor.core.publisher.Mono;
@@ -32,6 +29,7 @@ public ReactiveGreeterGrpcService(ReactiveGreeterService reactiveGreeterService)
3229
}
3330

3431
@Override
32+
@Secured({})
3533
public Mono<ReactiveHelloResponse> greet(Mono<ReactiveHelloRequest> request) {
3634
return reactiveGreeterService.greet(request);
3735

grpc-spring-boot-starter-demo/src/reactiveTest/java/org/lognet/springboot/grpc/reactive/ReactiveDemoTest.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
import org.hamcrest.collection.IsIn;
1212
import org.junit.Test;
1313
import org.junit.runner.RunWith;
14-
import org.lognet.springboot.grpc.GrpcServerTestBase;
14+
import org.lognet.springboot.grpc.auth.JwtAuthBaseTest;
1515
import org.lognet.springboot.grpc.demo.DemoApp;
16+
import org.lognet.springboot.grpc.security.GrpcSecurity;
17+
import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter;
1618
import org.springframework.boot.test.context.SpringBootTest;
19+
import org.springframework.boot.test.context.TestConfiguration;
1720
import org.springframework.test.annotation.DirtiesContext;
1821
import org.springframework.test.context.ActiveProfiles;
1922
import org.springframework.test.context.junit4.SpringRunner;
@@ -32,9 +35,22 @@
3235
@Slf4j
3336
@RunWith(SpringRunner.class)
3437
@SpringBootTest(classes = DemoApp.class, webEnvironment = NONE)
35-
@ActiveProfiles({"disable-security","r2dbc-test"})
38+
@ActiveProfiles({"keycloack-test", "r2dbc-test"})
3639
@DirtiesContext
37-
public class ReactiveDemoTest extends GrpcServerTestBase {
40+
public class ReactiveDemoTest extends JwtAuthBaseTest {
41+
42+
@TestConfiguration
43+
static class TestCfg {
44+
private static class DemoGrpcSecurityAdapter extends GrpcSecurityConfigurerAdapter {
45+
@Override
46+
public void configure(GrpcSecurity builder) throws Exception {
47+
builder.authorizeRequests()
48+
.withSecuredAnnotation();
49+
50+
}
51+
}
52+
}
53+
3854
@Test
3955
public void grpcGreetTest() {
4056
String shrek = "Shrek";

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

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
1111
import org.springframework.context.ApplicationContext;
1212
import org.springframework.context.ApplicationContextAware;
1313
import org.springframework.core.MethodIntrospector;
14+
import org.springframework.core.MethodParameter;
1415
import org.springframework.util.ReflectionUtils;
1516
import org.springframework.util.function.SingletonSupplier;
1617

1718
import java.lang.annotation.Annotation;
1819
import java.lang.reflect.Method;
1920
import java.lang.reflect.Modifier;
21+
import java.lang.reflect.ParameterizedType;
2022
import java.util.*;
2123
import java.util.function.Function;
24+
import java.util.function.Predicate;
2225
import java.util.function.Supplier;
2326
import java.util.stream.Collectors;
2427

@@ -41,9 +44,7 @@ public static class GrpcServiceMethod {
4144

4245
private Supplier<Map<MethodDescriptor<?, ?>, GrpcServiceMethod>> descriptorToServiceMethod;
4346

44-
private Supplier< Map<Method,MethodDescriptor<?,?>>> methodToDescriptor ;
45-
46-
47+
private Supplier<Map<Method, MethodDescriptor<?, ?>>> methodToDescriptor;
4748

4849

4950
/**
@@ -65,11 +66,11 @@ Collection<ServerInterceptor> getGlobalInterceptors() {
6566
return grpcGlobalInterceptors.get();
6667
}
6768

68-
public GrpcServiceMethod getGrpServiceMethod(MethodDescriptor<?,?> descriptor) {
69+
public GrpcServiceMethod getGrpServiceMethod(MethodDescriptor<?, ?> descriptor) {
6970
return descriptorToServiceMethod.get().get(descriptor);
7071
}
7172

72-
public MethodDescriptor<?,?> getMethodDescriptor( Method method) {
73+
public MethodDescriptor<?, ?> getMethodDescriptor(Method method) {
7374
return methodToDescriptor.get().get(method);
7475
}
7576

@@ -88,11 +89,11 @@ public void afterPropertiesSet() throws Exception {
8889

8990
descriptorToServiceMethod = SingletonSupplier.of(this::descriptorToServiceMethod);
9091

91-
methodToDescriptor = SingletonSupplier.of(()->
92-
descriptorToServiceMethod.get()
93-
.entrySet()
94-
.stream()
95-
.collect(Collectors.toMap(e->e.getValue().getMethod(), Map.Entry::getKey))
92+
methodToDescriptor = SingletonSupplier.of(() ->
93+
descriptorToServiceMethod.get()
94+
.entrySet()
95+
.stream()
96+
.collect(Collectors.toMap(e -> e.getValue().getMethod(), Map.Entry::getKey))
9697
);
9798
beanNameToServiceBean = SingletonSupplier.of(() ->
9899
getBeanNamesByTypeWithAnnotation(GRpcService.class, BindableService.class)
@@ -119,45 +120,74 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
119120
this.applicationContext = applicationContext;
120121
}
121122

122-
private Map<MethodDescriptor<?, ?>, GrpcServiceMethod> descriptorToServiceMethod (){
123+
private Map<MethodDescriptor<?, ?>, GrpcServiceMethod> descriptorToServiceMethod() {
123124
final Map<MethodDescriptor<?, ?>, GrpcServiceMethod> map = new HashMap<>();
124125

125126
Function<String, ReflectionUtils.MethodFilter> filterFactory = name ->
126127
method ->
127-
method.getName().equalsIgnoreCase(name.replaceAll("_",""));
128+
method.getName().equalsIgnoreCase(name.replaceAll("_", ""));
129+
128130

131+
Predicate<Method> firstArgIsMono = m -> "reactor.core.publisher.Mono".equals(m.getParameterTypes()[0].getName());
132+
Predicate<Method> singleArg = m -> 1 == m.getParameterCount();
129133

130134
for (BindableService service : getBeanNameToServiceBeanMap().values()) {
131135
final ServerServiceDefinition serviceDefinition = service.bindService();
132136
for (MethodDescriptor<?, ?> d : serviceDefinition.getServiceDescriptor().getMethods()) {
133137
Class<?> abstractBaseClass = service.getClass();
134-
while (!Modifier.isAbstract(abstractBaseClass.getModifiers())){
138+
while (!Modifier.isAbstract(abstractBaseClass.getModifiers())) {
135139
abstractBaseClass = abstractBaseClass.getSuperclass();
136140
}
137141

138142
final Set<Method> methods = MethodIntrospector
139143
.selectMethods(abstractBaseClass, filterFactory.apply(d.getBareMethodName()));
140144

141145

142-
switch (methods.size()){
146+
switch (methods.size()) {
143147
case 0:
144-
throw new IllegalStateException("Method " +d.getBareMethodName()+ "not found in service "+ serviceDefinition.getServiceDescriptor().getName());
148+
throw new IllegalStateException("Method " + d.getBareMethodName() + "not found in service " + serviceDefinition.getServiceDescriptor().getName());
145149
case 1:
146150
map.put(d, GrpcServiceMethod.builder()
147151
.service(service)
148152
.method(methods.iterator().next())
149153
.build());
150154
break;
151155
default:
152-
throw new IllegalStateException("Ambiguous method " +d.getBareMethodName()+ " in service "+ serviceDefinition.getServiceDescriptor().getName());
153-
}
154-
156+
if (2 == methods.size()) {
157+
158+
Optional<Method> methodWithMono = methods.stream() // grpcMethod(Mono<Payload> arg)
159+
.filter(singleArg.and(firstArgIsMono))
160+
.findFirst();
161+
162+
Optional<Method> methodPure = methods.stream() // grpcMethod(Payload arg)
163+
.filter(singleArg.and(firstArgIsMono.negate()))
164+
.findFirst();
165+
166+
Class<?> finalAbstractBaseClass = abstractBaseClass;
167+
Boolean typesAreEqual = methodWithMono
168+
.map(m -> ((ParameterizedType) new MethodParameter(m, 0)
169+
.withContainingClass(finalAbstractBaseClass)
170+
.getGenericParameterType())
171+
.getActualTypeArguments()[0]
172+
).map(t -> t.equals(methodPure.map(m -> m.getParameterTypes()[0]).orElse(null)))
173+
.orElse(false);
174+
175+
if (typesAreEqual) {
176+
map.put(d, GrpcServiceMethod.builder()
177+
.service(service)
178+
.method(methodWithMono.get())
179+
.build());
180+
break;
181+
}
182+
}
183+
throw new IllegalStateException("Ambiguous method " + d.getBareMethodName() + " in service " + serviceDefinition.getServiceDescriptor().getName());
155184

156185

186+
}
157187

158188

159189
}
160190
}
161-
return Collections.unmodifiableMap(map);
191+
return Collections.unmodifiableMap(map);
162192
}
163193
}

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

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
package org.lognet.springboot.grpc.security;
22

3-
import io.grpc.BindableService;
4-
import io.grpc.MethodDescriptor;
5-
import io.grpc.ServerInterceptor;
6-
import io.grpc.ServerMethodDefinition;
7-
import io.grpc.ServerServiceDefinition;
8-
import io.grpc.ServiceDescriptor;
3+
import io.grpc.*;
94
import org.lognet.springboot.grpc.GRpcServicesRegistry;
5+
import org.springframework.beans.factory.BeanCreationException;
106
import org.springframework.core.annotation.AnnotationUtils;
117
import org.springframework.security.access.ConfigAttribute;
128
import org.springframework.security.access.SecurityConfig;
@@ -15,11 +11,7 @@
1511
import org.springframework.util.LinkedMultiValueMap;
1612
import org.springframework.util.MultiValueMap;
1713

18-
import java.util.Arrays;
19-
import java.util.Collection;
20-
import java.util.Collections;
21-
import java.util.List;
22-
import java.util.Optional;
14+
import java.util.*;
2315
import java.util.function.Predicate;
2416
import java.util.stream.Collectors;
2517
import java.util.stream.Stream;
@@ -40,7 +32,7 @@ public Registry getRegistry() {
4032
@Override
4133
public void configure(GrpcSecurity builder) throws Exception {
4234
registry.processSecuredAnnotation();
43-
builder.setSharedObject(GrpcSecurityMetadataSource.class, new GrpcSecurityMetadataSource(registry.servicesRegistry,registry.securedMethods));
35+
builder.setSharedObject(GrpcSecurityMetadataSource.class, new GrpcSecurityMetadataSource(registry.servicesRegistry, registry.securedMethods));
4436
}
4537

4638

@@ -99,18 +91,18 @@ public GrpcSecurity withoutSecuredAnnotation() {
9991
}
10092

10193
public AuthorizedMethod anyMethod() {
102-
return anyMethodExcluding(s->false);
94+
return anyMethodExcluding(s -> false);
10395
}
10496

10597
public AuthorizedMethod anyMethodExcluding(MethodDescriptor<?, ?>... methodDescriptor) {
106-
List<MethodDescriptor<?,?>> excludedMethods = Arrays.asList(methodDescriptor);
98+
List<MethodDescriptor<?, ?>> excludedMethods = Arrays.asList(methodDescriptor);
10799
return anyMethodExcluding(excludedMethods::contains);
108100

109101
}
110102

111103

112104
public AuthorizedMethod anyMethodExcluding(Predicate<MethodDescriptor<?, ?>> excludePredicate) {
113-
MethodDescriptor<?,?>[] allMethods = servicesRegistry.getBeanNameToServiceBeanMap()
105+
MethodDescriptor<?, ?>[] allMethods = servicesRegistry.getBeanNameToServiceBeanMap()
114106
.values()
115107
.stream()
116108
.map(BindableService::bindService)
@@ -124,12 +116,14 @@ public AuthorizedMethod anyMethodExcluding(Predicate<MethodDescriptor<?, ?>> exc
124116

125117

126118
public AuthorizedMethod anyService() {
127-
return anyServiceExcluding(s-> false);
119+
return anyServiceExcluding(s -> false);
128120
}
121+
129122
public AuthorizedMethod anyServiceExcluding(ServiceDescriptor... serviceDescriptor) {
130123
List<ServiceDescriptor> excludedServices = Arrays.asList(serviceDescriptor);
131124
return anyServiceExcluding(excludedServices::contains);
132125
}
126+
133127
public AuthorizedMethod anyServiceExcluding(Predicate<ServiceDescriptor> excludePredicate) {
134128

135129
ServiceDescriptor[] allServices = servicesRegistry.getBeanNameToServiceBeanMap()
@@ -144,11 +138,13 @@ public AuthorizedMethod anyServiceExcluding(Predicate<ServiceDescriptor> exclude
144138

145139
/**
146140
* Same as {@code withSecuredAnnotation(true)}
141+
*
147142
* @return GrpcSecurity configuration
148143
*/
149144
public GrpcSecurity withSecuredAnnotation() {
150145
return withSecuredAnnotation(true);
151146
}
147+
152148
public GrpcSecurity withSecuredAnnotation(boolean withSecuredAnnotation) {
153149
this.withSecuredAnnotation = withSecuredAnnotation;
154150
return and();
@@ -163,29 +159,43 @@ private void processSecuredAnnotation() {
163159
// service level security
164160
{
165161
Optional.ofNullable(AnnotationUtils.findAnnotation(service.getClass(), Secured.class))
166-
.ifPresent(secured -> {
167-
if (secured.value().length == 0) {
168-
new AuthorizedMethod(serverServiceDefinition.getServiceDescriptor()).authenticated();
169-
} else {
170-
new AuthorizedMethod(serverServiceDefinition.getServiceDescriptor()).hasAnyAuthority(secured.value());
171-
}
172-
});
173-
174-
}
175-
// method level security
176-
for (ServerMethodDefinition<?, ?> methodDefinition : serverServiceDefinition.getMethods()) {
177-
Stream.of(service.getClass().getMethods()) // get method from methodDefinition
178-
.filter(m -> m.getName().equalsIgnoreCase(methodDefinition.getMethodDescriptor().getBareMethodName()))
179-
.findFirst()
180-
.flatMap(m -> Optional.ofNullable(AnnotationUtils.findAnnotation(m, Secured.class)))
181162
.ifPresent(secured -> {
182163
if (secured.value().length == 0) {
183-
new AuthorizedMethod(methodDefinition.getMethodDescriptor()).authenticated();
164+
new AuthorizedMethod(serverServiceDefinition.getServiceDescriptor()).authenticated();
184165
} else {
185-
new AuthorizedMethod(methodDefinition.getMethodDescriptor()).hasAnyAuthority(secured.value());
166+
new AuthorizedMethod(serverServiceDefinition.getServiceDescriptor()).hasAnyAuthority(secured.value());
186167
}
187168
});
188169

170+
}
171+
// method level security
172+
for (ServerMethodDefinition<?, ?> methodDefinition : serverServiceDefinition.getMethods()) {
173+
174+
List<Secured> secureds = Stream.of(service.getClass().getMethods()) // get method from methodDefinition
175+
.filter(m -> m.getName().equalsIgnoreCase(methodDefinition.getMethodDescriptor().getBareMethodName()))
176+
.map(m -> AnnotationUtils.findAnnotation(m, Secured.class))
177+
.filter(Objects::nonNull)
178+
.toList();
179+
if (secureds.isEmpty()) {
180+
continue;
181+
}
182+
if (1 == secureds.size()) {
183+
Secured secured = secureds.get(0);
184+
if (secured.value().length == 0) {
185+
new AuthorizedMethod(methodDefinition.getMethodDescriptor()).authenticated();
186+
} else {
187+
new AuthorizedMethod(methodDefinition.getMethodDescriptor()).hasAnyAuthority(secured.value());
188+
}
189+
} else {
190+
String errorMessage = String.format("Ambiguous 'Secured' method '%s' in service '%s'." +
191+
"When securing reactive method, the @Secured annotation should be added to the method getting 'Mono<Request>' and not with pure 'Request' argument.",
192+
methodDefinition.getMethodDescriptor().getBareMethodName(),
193+
service.getClass().getName()
194+
);
195+
throw new BeanCreationException(errorMessage);
196+
}
197+
198+
189199
}
190200
}
191201
}

0 commit comments

Comments
 (0)