Browse Source

Add Spring gRPC server and client security support

Add auto-configuration to integrate gRPC server applications
with Spring Security. This commit provides both standard
Spring Security support as well as OAuth support.

Closes gh-49047

Co-authored-by: Phillip Webb <phil.webb@broadcom.com>
pull/46608/head
Chris Bono 5 days ago committed by Phillip Webb
parent
commit
1cb8d02ed7
  1. 2
      module/spring-boot-grpc-server/build.gradle
  2. 86
      module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java
  3. 76
      module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfiguration.java
  4. 127
      module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfiguration.java
  5. 23
      module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java
  6. 148
      module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequest.java
  7. 23
      module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/package-info.java
  8. 148
      module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequest.java
  9. 23
      module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/package-info.java
  10. 2
      module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  11. 167
      module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java
  12. 170
      module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java
  13. 152
      module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfigurationTests.java
  14. 114
      module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequestTests.java
  15. 93
      module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequestTests.java
  16. 2
      settings.gradle
  17. 68
      smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle
  18. 60
      smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/HelloWorldService.java
  19. 29
      smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplication.java
  20. 61
      smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SecurityConfiguration.java
  21. 20
      smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/package-info.java
  22. 18
      smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/proto/hello.proto
  23. 21
      smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/resources/application.yaml
  24. 220
      smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/test/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplicationTests.java
  25. 66
      smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle
  26. 68
      smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/HelloWorldService.java
  27. 29
      smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplication.java
  28. 64
      smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SecurityConfiguration.java
  29. 20
      smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/package-info.java
  30. 19
      smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/proto/hello.proto
  31. 192
      smoke-test/spring-boot-smoke-test-grpc-server-secure/src/test/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplicationTests.java

2
module/spring-boot-grpc-server/build.gradle

@ -31,6 +31,8 @@ dependencies { @@ -31,6 +31,8 @@ dependencies {
optional(project(":core:spring-boot-autoconfigure"))
optional(project(":module:spring-boot-health"))
optional(project(":module:spring-boot-micrometer-observation"))
optional(project(":module:spring-boot-security"))
optional(project(":module:spring-boot-security-oauth2-resource-server"))
optional("com.fasterxml.jackson.core:jackson-annotations")
optional("io.projectreactor:reactor-core")
optional("io.grpc:grpc-servlet-jakarta")

86
module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java

@ -0,0 +1,86 @@ @@ -0,0 +1,86 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package org.springframework.boot.grpc.server.autoconfigure.security;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.boot.grpc.server.GrpcServletRegistration;
import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest;
import org.springframework.context.ApplicationContext;
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.ObjectUtils;
/**
* A custom {@link AbstractHttpConfigurer} that disables CSRF protection for gRPC
* requests.
* <p>
* This configurer checks the application context to determine if CSRF protection should
* be disabled for gRPC requests based on the property
* {@code spring.grpc.server.security.csrf.enabled}. By default, CSRF protection is
* disabled unless explicitly enabled in the application properties.
* </p>
*
* @author Dave Syer
* @see AbstractHttpConfigurer
* @see HttpSecurity
*/
class GrpcDisableCsrfHttpConfigurer extends AbstractHttpConfigurer<GrpcDisableCsrfHttpConfigurer, HttpSecurity> {
@Override
public void init(HttpSecurity http) {
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
if (context != null && isCsrfConfigurerPresent(http) && hasBean(context, GrpcServiceDiscoverer.class)
&& hasBean(context, GrpcServletRegistration.class) && isCsrfEnabled(context)) {
http.csrf(this::disable);
}
}
@SuppressWarnings("unchecked")
private boolean isCsrfConfigurerPresent(HttpSecurity http) {
return http.getConfigurer(CsrfConfigurer.class) != null;
}
private boolean hasBean(ApplicationContext context, Class<?> type) {
return !ObjectUtils.isEmpty(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type));
}
private boolean isCsrfEnabled(ApplicationContext context) {
return context.getEnvironment().getProperty("spring.grpc.server.security.csrf.enabled", Boolean.class, true);
}
private void disable(CsrfConfigurer<HttpSecurity> csrf) {
csrf.requireCsrfProtectionMatcher(GrpcCsrfRequestMatcher.INSTANCE);
}
static class GrpcCsrfRequestMatcher implements RequestMatcher {
static GrpcCsrfRequestMatcher INSTANCE = new GrpcCsrfRequestMatcher();
@Override
public boolean matches(HttpServletRequest request) {
return CsrfFilter.DEFAULT_CSRF_MATCHER.matches(request) && !GrpcRequest.toAnyService().matches(request);
}
}
}

76
module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfiguration.java

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package org.springframework.boot.grpc.server.autoconfigure.security;
import io.grpc.BindableService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.grpc.server.GlobalServerInterceptor;
import org.springframework.grpc.server.GrpcServerFactory;
import org.springframework.grpc.server.security.AuthenticationProcessInterceptor;
import org.springframework.grpc.server.security.GrpcSecurity;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import static org.springframework.security.config.Customizer.withDefaults;
/**
* {@link EnableAutoConfiguration Auto-configuration} for gRPC OAuth2 resource server.
*
* @author Dave Syer
* @author Andrey Litvitski
* @author Phillip Webb
* @since 4.1.0
*/
@AutoConfiguration(beforeName = "org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration",
afterName = { "org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration",
"org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration" },
after = { GrpcServerSecurityAutoConfiguration.class, GrpcServerAutoConfiguration.class })
@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true)
@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, ObjectPostProcessor.class })
@ConditionalOnMissingBean(AuthenticationProcessInterceptor.class)
@ConditionalOnBean(GrpcSecurity.class)
public final class GrpcServerOAuth2ResourceServerAutoConfiguration {
@Bean
@ConditionalOnBean(OpaqueTokenIntrospector.class)
@GlobalServerInterceptor
AuthenticationProcessInterceptor opaqueTokenAuthenticationProcessInterceptor(GrpcSecurity grpcSecurity)
throws Exception {
grpcSecurity.authorizeRequests((requests) -> requests.allRequests().authenticated());
grpcSecurity.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults()));
return grpcSecurity.build();
}
@Bean
@ConditionalOnBean(JwtDecoder.class)
@GlobalServerInterceptor
AuthenticationProcessInterceptor jwtAuthenticationProcessInterceptor(GrpcSecurity grpcSecurity) throws Exception {
grpcSecurity.authorizeRequests((requests) -> requests.allRequests().authenticated());
grpcSecurity.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults()));
return grpcSecurity.build();
}
}

127
module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfiguration.java

@ -0,0 +1,127 @@ @@ -0,0 +1,127 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package org.springframework.boot.grpc.server.autoconfigure.security;
import io.grpc.BindableService;
import io.grpc.internal.GrpcUtil;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.grpc.server.GrpcServletRegistration;
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration;
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerExecutorProvider;
import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.ExceptionHandlerConfiguration;
import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.GrpcNativeSecurityConfigurerConfiguration;
import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.GrpcServletSecurityConfigurerConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.grpc.server.GlobalServerInterceptor;
import org.springframework.grpc.server.GrpcServerFactory;
import org.springframework.grpc.server.security.CoroutineSecurityContextInterceptor;
import org.springframework.grpc.server.security.GrpcSecurity;
import org.springframework.grpc.server.security.SecurityContextServerInterceptor;
import org.springframework.grpc.server.security.SecurityGrpcExceptionHandler;
import org.springframework.security.concurrent.DelegatingSecurityContextExecutor;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
import org.springframework.security.web.SecurityFilterChain;
/**
* {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side security.
*
* @author Dave Syer
* @author Chris Bono
* @author Andrey Litvitski
* @author Phillip Webb
* @since 4.1.0
*/
@AutoConfiguration(after = GrpcServerAutoConfiguration.class,
afterName = "org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration")
@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true)
@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, ObjectPostProcessor.class })
@Import({ ExceptionHandlerConfiguration.class, GrpcNativeSecurityConfigurerConfiguration.class,
GrpcServletSecurityConfigurerConfiguration.class })
public final class GrpcServerSecurityAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@Import(AuthenticationConfiguration.class)
static class ExceptionHandlerConfiguration {
@Bean
SecurityGrpcExceptionHandler accessExceptionHandler() {
return new SecurityGrpcExceptionHandler();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(GrpcServerFactory.class)
@EnableGlobalAuthentication
static class GrpcNativeSecurityConfigurerConfiguration {
@Bean
GrpcSecurity grpcSecurity(ApplicationContext context, ObjectPostProcessor<Object> objectPostProcessor,
AuthenticationConfiguration authenticationConfiguration) {
AuthenticationManagerBuilder authenticationManagerBuilder = authenticationConfiguration
.authenticationManagerBuilder(objectPostProcessor, context);
authenticationManagerBuilder
.parentAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
return new GrpcSecurity(objectPostProcessor, authenticationManagerBuilder, context);
}
}
@ConditionalOnBean({ GrpcServletRegistration.class, SecurityFilterChain.class })
@Configuration(proxyBeanMethods = false)
static class GrpcServletSecurityConfigurerConfiguration {
@Bean
@GlobalServerInterceptor
SecurityContextServerInterceptor securityContextInterceptor() {
return new SecurityContextServerInterceptor();
}
@Bean
@ConditionalOnMissingBean
GrpcServerExecutorProvider grpcServerExecutorProvider() {
return () -> new DelegatingSecurityContextExecutor(GrpcUtil.SHARED_CHANNEL_EXECUTOR.create());
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "io.grpc.kotlin.CoroutineContextServerInterceptor")
static class GrpcClientCoroutineStubConfiguration {
@Bean
@GlobalServerInterceptor
@ConditionalOnMissingBean
CoroutineSecurityContextInterceptor coroutineSecurityContextInterceptor() {
return new CoroutineSecurityContextInterceptor();
}
}
}

23
module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
/**
* Auto-configuration for gRPC server security.
*/
@NullMarked
package org.springframework.boot.grpc.server.autoconfigure.security;
import org.jspecify.annotations.NullMarked;

148
module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequest.java

@ -0,0 +1,148 @@ @@ -0,0 +1,148 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package org.springframework.boot.grpc.server.autoconfigure.security.web.reactive;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;
import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest.GrpcServletRequestMatcher;
import org.springframework.boot.security.web.reactive.ApplicationContextServerWebExchangeMatcher;
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
/**
* Factory that can be used to create a {@link ServerWebExchangeMatcher} to match against
* gRPC service locations.
*
* @author Dave Syer
* @author Phillip Webb
* @since 4.1.0
*/
public final class GrpcRequest {
private GrpcRequest() {
}
/**
* Returns a matcher that includes all gRPC services. The
* {@link GrpcReactiveRequestMatcher#excluding(String...) excluding} method can be
* used to remove specific services by name if required. For example:
*
* <pre class="code">
* GrpcReactiveRequest.toAnyService().excluding("my-service")
* </pre>
* @return the configured {@link ServerWebExchangeMatcher}
*/
public static GrpcReactiveRequestMatcher toAnyService() {
return new GrpcReactiveRequestMatcher(Collections.emptySet());
}
/**
* The matcher used to match against service locations.
*/
public static final class GrpcReactiveRequestMatcher
extends ApplicationContextServerWebExchangeMatcher<GrpcServiceDiscoverer> {
private static final ServerWebExchangeMatcher EMPTY_MATCHER = (exchange) -> MatchResult.notMatch();
private final Set<String> excludes;
private volatile @Nullable ServerWebExchangeMatcher delegate;
private GrpcReactiveRequestMatcher(Set<String> excludes) {
super(GrpcServiceDiscoverer.class);
this.excludes = excludes;
}
/**
* Return a new {@link GrpcServletRequestMatcher} based on this one but excluding
* the specified services.
* @param services additional services to exclude
* @return a new {@link GrpcServletRequestMatcher}
*/
public GrpcReactiveRequestMatcher excluding(String... services) {
return excluding(Set.of(services));
}
/**
* Return a new {@link GrpcServletRequestMatcher} based on this one but excluding
* the specified services.
* @param services additional service names to exclude
* @return a new {@link GrpcServletRequestMatcher}
*/
public GrpcReactiveRequestMatcher excluding(Collection<String> services) {
Assert.notNull(services, "'services' must not be null");
Set<String> excludes = new LinkedHashSet<>(this.excludes);
excludes.addAll(services);
return new GrpcReactiveRequestMatcher(excludes);
}
@Override
protected void initialized(Supplier<GrpcServiceDiscoverer> context) {
this.delegate = createDelegate(context.get());
}
private ServerWebExchangeMatcher createDelegate(GrpcServiceDiscoverer serviceDiscoverer) {
List<ServerWebExchangeMatcher> delegateMatchers = getDelegateMatchers(serviceDiscoverer);
return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrServerWebExchangeMatcher(delegateMatchers)
: EMPTY_MATCHER;
}
private List<ServerWebExchangeMatcher> getDelegateMatchers(GrpcServiceDiscoverer serviceDiscoverer) {
return getPatterns(serviceDiscoverer).map(this::getDelegateMatcher).toList();
}
private Stream<String> getPatterns(GrpcServiceDiscoverer serviceDiscoverer) {
return serviceDiscoverer.listServiceNames().stream().filter(this::isExcluded).map(this::getPath);
}
private boolean isExcluded(String service) {
return !this.excludes.stream().anyMatch((candidate) -> candidate.equals(service));
}
private String getPath(String service) {
return "/" + service + "/**";
}
private ServerWebExchangeMatcher getDelegateMatcher(String path) {
Assert.hasText(path, "'path' must not be empty");
return new PathPatternParserServerWebExchangeMatcher(path);
}
@Override
protected Mono<MatchResult> matches(ServerWebExchange exchange, Supplier<GrpcServiceDiscoverer> context) {
Assert.state(this.delegate != null, "'delegate' must not be null");
return this.delegate.matches(exchange);
}
}
}

23
module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/package-info.java

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
/**
* Auto-configuration for gRPC web security when using a servlet stack.
*/
@NullMarked
package org.springframework.boot.grpc.server.autoconfigure.security.web.reactive;
import org.jspecify.annotations.NullMarked;

148
module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequest.java

@ -0,0 +1,148 @@ @@ -0,0 +1,148 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package org.springframework.boot.grpc.server.autoconfigure.security.web.servlet;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Stream;
import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.security.web.servlet.ApplicationContextRequestMatcher;
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Factory that can be used to create a {@link RequestMatcher} to match against gRPC
* service locations.
*
* @author Dave Syer
* @author Phillip Webb
* @since 4.1.0
*/
public final class GrpcRequest {
private static final RequestMatcher EMPTY_MATCHER = (request) -> false;
private static final GrpcServletRequestMatcher TO_ANY_SERVICE = new GrpcServletRequestMatcher(
Collections.emptySet());
private GrpcRequest() {
}
/**
* Returns a matcher that includes all gRPC services. The
* {@link GrpcServletRequestMatcher#excluding(String...) excluding} method can be used
* to remove specific services by name if required. For example:
*
* <pre class="code">
* GrpcServletRequest.toAnyService().excluding("my-service")
* </pre>
* @return the configured {@link RequestMatcher}
*/
public static GrpcServletRequestMatcher toAnyService() {
return TO_ANY_SERVICE;
}
/**
* The matcher used to match against service locations.
*/
public static final class GrpcServletRequestMatcher
extends ApplicationContextRequestMatcher<GrpcServiceDiscoverer> {
private final Set<String> excludes;
private volatile @Nullable RequestMatcher delegate;
private GrpcServletRequestMatcher(Set<String> exclusions) {
super(GrpcServiceDiscoverer.class);
this.excludes = exclusions;
}
/**
* Return a new {@link GrpcServletRequestMatcher} based on this one but excluding
* the specified services.
* @param services additional services to exclude
* @return a new {@link GrpcServletRequestMatcher}
*/
public GrpcServletRequestMatcher excluding(String... services) {
return excluding(Set.of(services));
}
/**
* Return a new {@link GrpcServletRequestMatcher} based on this one but excluding
* the specified services.
* @param services additional service names to exclude
* @return a new {@link GrpcServletRequestMatcher}
*/
public GrpcServletRequestMatcher excluding(Collection<String> services) {
Assert.notNull(services, "'services' must not be null");
Set<String> excludes = new LinkedHashSet<>(this.excludes);
excludes.addAll(services);
return new GrpcServletRequestMatcher(excludes);
}
@Override
protected void initialized(Supplier<GrpcServiceDiscoverer> context) {
this.delegate = createDelegate(context.get());
}
private @Nullable RequestMatcher createDelegate(GrpcServiceDiscoverer grpcServiceDiscoverer) {
List<RequestMatcher> delegateMatchers = getDelegateMatchers(grpcServiceDiscoverer);
return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrRequestMatcher(delegateMatchers)
: EMPTY_MATCHER;
}
private List<RequestMatcher> getDelegateMatchers(GrpcServiceDiscoverer serviceDiscoverer) {
return getPatterns(serviceDiscoverer).map(this::getDelegateMatcher).toList();
}
private Stream<String> getPatterns(GrpcServiceDiscoverer serviceDiscoverer) {
return serviceDiscoverer.listServiceNames().stream().filter(this::isExcluded).map(this::getPath);
}
private boolean isExcluded(String service) {
return !this.excludes.stream().anyMatch((candidate) -> candidate.equals(service));
}
private String getPath(String service) {
return "/" + service + "/**";
}
private RequestMatcher getDelegateMatcher(String path) {
Assert.hasText(path, "'path' must not be empty");
return PathPatternRequestMatcher.withDefaults().matcher(path);
}
@Override
protected boolean matches(HttpServletRequest request, Supplier<GrpcServiceDiscoverer> context) {
Assert.state(this.delegate != null, "'delegate' must not be null");
return this.delegate.matches(request);
}
}
}

23
module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/package-info.java

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
/**
* Auto-configuration for gRPC web security when using a reactive stack.
*/
@NullMarked
package org.springframework.boot.grpc.server.autoconfigure.security.web.servlet;
import org.jspecify.annotations.NullMarked;

2
module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@ -3,3 +3,5 @@ org.springframework.boot.grpc.server.autoconfigure.GrpcServerObservationAutoConf @@ -3,3 +3,5 @@ org.springframework.boot.grpc.server.autoconfigure.GrpcServerObservationAutoConf
org.springframework.boot.grpc.server.autoconfigure.GrpcServerServicesAutoConfiguration
org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration
org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthSchedulerAutoConfiguration
org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerOAuth2ResourceServerAutoConfiguration
org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration

167
module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java

@ -0,0 +1,167 @@ @@ -0,0 +1,167 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package org.springframework.boot.grpc.server.autoconfigure.security;
import java.util.HashMap;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.grpc.server.GrpcServletRegistration;
import org.springframework.boot.grpc.server.autoconfigure.security.GrpcDisableCsrfHttpConfigurer.GrpcCsrfRequestMatcher;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.web.util.matcher.RequestMatcher;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
/**
* Tests for {@link GrpcDisableCsrfHttpConfigurer}.
*
* @author Phillip Webb
*/
class GrpcDisableCsrfHttpConfigurerTests {
private GrpcDisableCsrfHttpConfigurer configurer = new GrpcDisableCsrfHttpConfigurer();
@Test
void initDisablesCsrf() {
ObjectPostProcessor<Object> objectPostProcessor = ObjectPostProcessor.identity();
AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>());
StaticApplicationContext applicationContext = addApplicationContext(http);
addServiceDiscover(applicationContext);
addGrpcServletRegistration(applicationContext);
CsrfConfigurer<?> csrf = addCsrf(http);
this.configurer.init(http);
ArgumentCaptor<RequestMatcher> matcher = ArgumentCaptor.captor();
then(csrf).should().requireCsrfProtectionMatcher(matcher.capture());
assertThat(matcher.getValue()).isSameAs(GrpcCsrfRequestMatcher.INSTANCE);
}
@Test
void initWhenNoApplicationContextDoesNothing() {
ObjectPostProcessor<Object> objectPostProcessor = ObjectPostProcessor.identity();
AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>());
CsrfConfigurer<?> csrf = addCsrf(http);
this.configurer.init(http);
then(csrf).should(never()).requireCsrfProtectionMatcher(any());
}
@Test
@SuppressWarnings("unchecked")
void initWhenNoCsrfConfigurerDoesNothing() {
ObjectPostProcessor<Object> objectPostProcessor = ObjectPostProcessor.identity();
AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>());
StaticApplicationContext applicationContext = addApplicationContext(http);
addServiceDiscover(applicationContext);
addGrpcServletRegistration(applicationContext);
this.configurer.init(http);
CsrfConfigurer<?> csrfConfigurer = http.getConfigurer(CsrfConfigurer.class);
assertThat(csrfConfigurer).isNull();
}
@Test
void initWhenNoGrpcServiceDiscovererBeanDoesNothing() {
ObjectPostProcessor<Object> objectPostProcessor = ObjectPostProcessor.identity();
AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>());
StaticApplicationContext applicationContext = addApplicationContext(http);
addGrpcServletRegistration(applicationContext);
CsrfConfigurer<?> csrf = addCsrf(http);
this.configurer.init(http);
then(csrf).should(never()).requireCsrfProtectionMatcher(any());
}
@Test
void initWhenNoGrpcServletRegistrationBeanDoesNothing() {
ObjectPostProcessor<Object> objectPostProcessor = ObjectPostProcessor.identity();
AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>());
StaticApplicationContext applicationContext = addApplicationContext(http);
addServiceDiscover(applicationContext);
CsrfConfigurer<?> csrf = addCsrf(http);
this.configurer.init(http);
then(csrf).should(never()).requireCsrfProtectionMatcher(any());
}
@Test
void initWhenEnabledPropertyFalseDoesNothing() {
ObjectPostProcessor<Object> objectPostProcessor = ObjectPostProcessor.identity();
AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>());
StaticApplicationContext applicationContext = addApplicationContext(http);
TestPropertyValues.of("spring.grpc.server.security.csrf.enabled=false").applyTo(applicationContext);
addServiceDiscover(applicationContext);
addGrpcServletRegistration(applicationContext);
CsrfConfigurer<?> csrf = addCsrf(http);
this.configurer.init(http);
then(csrf).should(never()).requireCsrfProtectionMatcher(any());
}
@Test
void initWhenEnabledPropertyTrueDisablesCsrf() {
ObjectPostProcessor<Object> objectPostProcessor = ObjectPostProcessor.identity();
AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>());
StaticApplicationContext applicationContext = addApplicationContext(http);
TestPropertyValues.of("spring.grpc.server.security.csrf.enabled=true").applyTo(applicationContext);
addServiceDiscover(applicationContext);
addGrpcServletRegistration(applicationContext);
CsrfConfigurer<?> csrf = addCsrf(http);
this.configurer.init(http);
ArgumentCaptor<RequestMatcher> matcher = ArgumentCaptor.captor();
then(csrf).should().requireCsrfProtectionMatcher(matcher.capture());
assertThat(matcher.getValue()).isSameAs(GrpcCsrfRequestMatcher.INSTANCE);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private CsrfConfigurer<?> addCsrf(HttpSecurity http) {
CsrfConfigurer<?> csrf = mock();
http.with((CsrfConfigurer) csrf);
return csrf;
}
private StaticApplicationContext addApplicationContext(HttpSecurity http) {
StaticApplicationContext applicationContext = new StaticApplicationContext();
http.setSharedObject(ApplicationContext.class, applicationContext);
return applicationContext;
}
private void addServiceDiscover(StaticApplicationContext applicationContext) {
GrpcServiceDiscoverer serviceDiscoverer = mock();
applicationContext.registerBean(GrpcServiceDiscoverer.class, serviceDiscoverer);
}
private void addGrpcServletRegistration(StaticApplicationContext applicationContext) {
GrpcServletRegistration servletRegistration = mock();
applicationContext.registerBean(GrpcServletRegistration.class, servletRegistration);
}
}

170
module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java

@ -0,0 +1,170 @@ @@ -0,0 +1,170 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package org.springframework.boot.grpc.server.autoconfigure.security;
import io.grpc.BindableService;
import io.grpc.ServerServiceDefinition;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.context.annotation.UserConfigurations;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration;
import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration;
import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider;
import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.context.servlet.AnnotationConfigServletWebApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.server.GlobalServerInterceptor;
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
import org.springframework.grpc.server.security.AuthenticationProcessInterceptor;
import org.springframework.grpc.server.security.GrpcSecurity;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GrpcServerOAuth2ResourceServerAutoConfiguration}.
*
* @author Chris Bono
*/
class GrpcServerOAuth2ResourceServerAutoConfigurationTests {
private static final AutoConfigurations autoConfigurations = AutoConfigurations
.of(OAuth2ResourceServerAutoConfiguration.class, GrpcServerOAuth2ResourceServerAutoConfiguration.class);
private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(autoConfigurations)
.withUserConfiguration(GrpcSecurityConfiguration.class)
.with(this::serviceBean)
.withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock);
@Test
void jwtConfiguredWhenIssuerIsProvided() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000")
.run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class));
}
@Test
void jwtConfiguredWhenJwkSetIsProvided() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000")
.run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class));
}
@Test
void customInterceptorWhenJwkSetIsProvided() {
this.contextRunner.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.withConfiguration(UserConfigurations.of(CustomInterceptorConfiguration.class))
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000")
.run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class));
}
@Test
void notConfiguredWhenIssuerNotProvided() {
this.contextRunner
.run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class));
}
@Test
void notConfiguredInWebApplication() {
new WebApplicationContextRunner().withConfiguration(autoConfigurations)
.withConfiguration(AutoConfigurations.of(ServletWebSecurityAutoConfiguration.class,
OAuth2ResourceServerAutoConfiguration.class))
.with(this::serviceBean)
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000")
.run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class));
}
@Test
void notConfiguredInWebApplicationWithNoBindableService() {
new WebApplicationContextRunner().withConfiguration(autoConfigurations)
.withConfiguration(AutoConfigurations.of(ServletWebSecurityAutoConfiguration.class,
OAuth2ResourceServerAutoConfiguration.class))
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000")
.run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class));
}
private <R extends AbstractApplicationContextRunner<R, C, A>, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider<C>> R serviceBean(
R contextRunner) {
BindableService service = mock();
ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build();
given(service.bindService()).willReturn(serviceDefinition);
return contextRunner.withBean(BindableService.class, () -> service);
}
static class FailingApplicationFailedEventContext extends AnnotationConfigServletWebApplicationContext {
@Override
public void refresh() {
try {
super.refresh();
}
catch (Throwable ex) {
publishEvent(new ApplicationFailedEvent(new SpringApplication(this), new String[0], this, ex));
throw ex;
}
}
}
@Configuration(proxyBeanMethods = false)
static class CustomInterceptorConfiguration {
@Bean
@GlobalServerInterceptor
AuthenticationProcessInterceptor jwtSecurityFilterChain(GrpcSecurity grpc) throws Exception {
return grpc.authorizeRequests((requests) -> requests.allRequests().authenticated())
.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()))
.build();
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
static class GrpcSecurityConfiguration {
@Bean
GrpcSecurity grpcSecurity(ApplicationContext context, ObjectPostProcessor<Object> objectPostProcessor,
AuthenticationConfiguration authenticationConfiguration) {
AuthenticationManagerBuilder authenticationManagerBuilder = authenticationConfiguration
.authenticationManagerBuilder(objectPostProcessor, context);
authenticationManagerBuilder
.parentAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
return new GrpcSecurity(objectPostProcessor, authenticationManagerBuilder, context);
}
}
}

152
module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfigurationTests.java

@ -0,0 +1,152 @@ @@ -0,0 +1,152 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package org.springframework.boot.grpc.server.autoconfigure.security;
import io.grpc.BindableService;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.grpc.server.GrpcServletRegistration;
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerExecutorProvider;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.server.GrpcServerFactory;
import org.springframework.grpc.server.exception.GrpcExceptionHandler;
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
import org.springframework.grpc.server.security.AuthenticationProcessInterceptor;
import org.springframework.grpc.server.security.GrpcSecurity;
import org.springframework.grpc.server.security.SecurityContextServerInterceptor;
import org.springframework.grpc.server.security.SecurityGrpcExceptionHandler;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GrpcServerSecurityAutoConfiguration}.
*
* @author Chris Bono
*/
class GrpcServerSecurityAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GrpcServerSecurityAutoConfiguration.class))
.withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock);
@Test
void whenSpringSecurityNotOnClasspathAutoConfigurationIsSkipped() {
this.contextRunner.withClassLoader(new FilteredClassLoader(ObjectPostProcessor.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class));
}
@Test
void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() {
this.contextRunner.withClassLoader(new FilteredClassLoader(BindableService.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class));
}
@Test
void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() {
this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerFactory.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class));
}
@Test
void whenSpringGrpcAndSpringSecurityPresentAndUsingGrpcServletCreatesGrpcSecurity() {
new WebApplicationContextRunner()
.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.withConfiguration(AutoConfigurations.of(GrpcServerSecurityAutoConfiguration.class))
.withUserConfiguration(ServletConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(SecurityContextServerInterceptor.class);
assertThat(context).hasSingleBean(GrpcServerExecutorProvider.class);
});
}
@Test
void whenSpringGrpcAndSpringSecurityPresentAndUsingGrpcNativeCreatesGrpcSecurity() {
new ApplicationContextRunner()
.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.withConfiguration(AutoConfigurations.of(GrpcServerSecurityAutoConfiguration.class))
.withUserConfiguration(NativeConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(GrpcSecurity.class));
}
@Test
void whenServerEnabledPropertySetFalseThenAutoConfigurationIsSkipped() {
this.contextRunner.withPropertyValues("spring.grpc.server.enabled=false")
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class));
}
@Test
void whenServerEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() {
this.contextRunner
.run((context) -> assertThat(context).hasSingleBean(GrpcServerSecurityAutoConfiguration.class));
}
@Test
void whenServerEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() {
this.contextRunner.withPropertyValues("spring.grpc.server.enabled=true")
.run((context) -> assertThat(context).hasSingleBean(GrpcServerSecurityAutoConfiguration.class));
}
@Test
void grpcSecurityAutoConfiguredAsExpected() {
this.contextRunner.run((context) -> {
assertThat(context).getBean(GrpcExceptionHandler.class).isInstanceOf(SecurityGrpcExceptionHandler.class);
assertThat(context).getBean(AuthenticationProcessInterceptor.class).isNull();
});
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
static class ServletConfiguration {
@Bean
GrpcServletRegistration grpcServletRegistration() {
return new GrpcServletRegistration(mock(), mock());
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) {
return http.authorizeHttpRequests((requests) -> requests.anyRequest().permitAll()).build();
}
}
@Configuration(proxyBeanMethods = false)
@EnableMethodSecurity
static class NativeConfiguration {
@Bean
GrpcServerFactory grpcServerFactory() {
return mock();
}
}
}

114
module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequestTests.java

@ -0,0 +1,114 @@ @@ -0,0 +1,114 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package org.springframework.boot.grpc.server.autoconfigure.security.web.reactive;
import io.grpc.BindableService;
import io.grpc.ServerServiceDefinition;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.grpc.server.autoconfigure.security.web.reactive.GrpcRequest.GrpcReactiveRequestMatcher;
import org.springframework.boot.web.context.reactive.GenericReactiveWebApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer;
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
import org.springframework.web.server.session.DefaultWebSessionManager;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GrpcRequest}.
*
* @author Dave Syer
* @author Phillip Webb
*/
class GrpcRequestTests {
private GenericReactiveWebApplicationContext context = new GenericReactiveWebApplicationContext();
@BeforeEach
void setup() {
MockService service1 = mock();
given(service1.bindService()).willReturn(ServerServiceDefinition.builder("my-service").build());
MockService service2 = mock();
given(service2.bindService()).willReturn(ServerServiceDefinition.builder("my-other-service").build());
this.context.registerBean("s1", BindableService.class, () -> service1);
this.context.registerBean("s2", BindableService.class, () -> service2);
this.context.registerBean(GrpcServiceDiscoverer.class, () -> new DefaultGrpcServiceDiscoverer(this.context));
this.context.refresh();
}
@Test
void whenToAnyService() {
GrpcReactiveRequestMatcher matcher = GrpcRequest.toAnyService();
assertThat(isMatch(matcher, "/my-service/Method")).isTrue();
assertThat(isMatch(matcher, "/my-service/Other")).isTrue();
assertThat(isMatch(matcher, "/my-other-service/Other")).isTrue();
assertThat(isMatch(matcher, "/notaservice")).isFalse();
}
@Test
void whenToAnyServiceWithExclude() {
GrpcReactiveRequestMatcher matcher = GrpcRequest.toAnyService().excluding("my-other-service");
assertThat(isMatch(matcher, "/my-service/Method")).isTrue();
assertThat(isMatch(matcher, "/my-service/Other")).isTrue();
assertThat(isMatch(matcher, "/my-other-service/Other")).isFalse();
assertThat(isMatch(matcher, "/notaservice")).isFalse();
}
private boolean isMatch(GrpcReactiveRequestMatcher matcher, String path) {
MockExchange request = mockRequest(path);
MatchResult result = matcher.matches(request).block();
return (result != null) && result.isMatch();
}
private MockExchange mockRequest(String path) {
MockServerHttpRequest servletContext = MockServerHttpRequest.get(path).build();
MockExchange request = new MockExchange(servletContext, this.context);
return request;
}
interface MockService extends BindableService {
}
static class MockExchange extends DefaultServerWebExchange {
private ApplicationContext context;
MockExchange(MockServerHttpRequest request, ApplicationContext context) {
super(request, new MockServerHttpResponse(), new DefaultWebSessionManager(), ServerCodecConfigurer.create(),
new AcceptHeaderLocaleContextResolver());
this.context = context;
}
@Override
public ApplicationContext getApplicationContext() {
return this.context;
}
}
}

93
module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequestTests.java

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package org.springframework.boot.grpc.server.autoconfigure.security.web.servlet;
import io.grpc.BindableService;
import io.grpc.ServerServiceDefinition;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest.GrpcServletRequestMatcher;
import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer;
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Test for {@link GrpcRequest}.
*
* @author Dave Syer
* @author Phillip Webb
*/
class GrpcRequestTests {
private GenericWebApplicationContext context = new GenericWebApplicationContext();
@BeforeEach
void setup() {
MockService service1 = mock();
given(service1.bindService()).willReturn(ServerServiceDefinition.builder("my-service").build());
MockService service2 = mock();
given(service2.bindService()).willReturn(ServerServiceDefinition.builder("my-other-service").build());
this.context.registerBean("s1", BindableService.class, () -> service1);
this.context.registerBean("s2", BindableService.class, () -> service2);
this.context.registerBean(GrpcServiceDiscoverer.class, () -> new DefaultGrpcServiceDiscoverer(this.context));
this.context.refresh();
}
@Test
void whenToAnyService() {
GrpcServletRequestMatcher matcher = GrpcRequest.toAnyService();
assertThat(isMatch(matcher, "/my-service/Method")).isTrue();
assertThat(isMatch(matcher, "/my-service/Other")).isTrue();
assertThat(isMatch(matcher, "/my-other-service/Other")).isTrue();
assertThat(isMatch(matcher, "/notaservice")).isFalse();
}
@Test
void whenToAnyServiceWithExclude() {
GrpcServletRequestMatcher matcher = GrpcRequest.toAnyService().excluding("my-other-service");
assertThat(isMatch(matcher, "/my-service/Method")).isTrue();
assertThat(isMatch(matcher, "/my-service/Other")).isTrue();
assertThat(isMatch(matcher, "/my-other-service/Other")).isFalse();
assertThat(isMatch(matcher, "/notaservice")).isFalse();
}
private boolean isMatch(GrpcServletRequestMatcher matcher, String path) {
MockHttpServletRequest request = mockRequest(path);
return matcher.matches(request);
}
private MockHttpServletRequest mockRequest(String path) {
MockServletContext servletContext = new MockServletContext();
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
MockHttpServletRequest request = new MockHttpServletRequest(servletContext);
request.setRequestURI(path);
return request;
}
interface MockService extends BindableService {
}
}

2
settings.gradle

@ -431,7 +431,9 @@ include ":smoke-test:spring-boot-smoke-test-grpc-client" @@ -431,7 +431,9 @@ include ":smoke-test:spring-boot-smoke-test-grpc-client"
include ":smoke-test:spring-boot-smoke-test-grpc-client-test"
include ":smoke-test:spring-boot-smoke-test-grpc-server"
include ":smoke-test:spring-boot-smoke-test-grpc-server-netty-shaded"
include ":smoke-test:spring-boot-smoke-test-grpc-server-oauth"
include ":smoke-test:spring-boot-smoke-test-grpc-server-servlet"
include ":smoke-test:spring-boot-smoke-test-grpc-server-secure"
include ":smoke-test:spring-boot-smoke-test-grpc-server-test"
include ":smoke-test:spring-boot-smoke-test-hateoas"
include ":smoke-test:spring-boot-smoke-test-hibernate"

68
smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle

@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
plugins {
id "java"
id "com.google.protobuf" version "${protobufGradlePluginVersion}"
id "org.springframework.boot.docker-test"
}
description = "Spring Boot gRPC server oauth smoke test"
dependencies {
implementation(project(":starter:spring-boot-starter-oauth2-authorization-server"))
implementation(project(":starter:spring-boot-starter-oauth2-resource-server"))
implementation(project(":starter:spring-boot-starter-grpc-server"))
testImplementation(project(":starter:spring-boot-starter-grpc-client"))
testImplementation(project(":starter:spring-boot-starter-grpc-test"))
testImplementation(project(":starter:spring-boot-starter-oauth2-client"))
testImplementation(project(":starter:spring-boot-starter-test"))
}
def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom")
def grpcJava = dependenciesBom.getLibrary("Grpc Java")
def protobufJava = dependenciesBom.getLibrary("Protobuf Java")
tasks.named("compileTestJava") {
options.nullability.checking = "tests"
}
nullability {
requireExplicitNullMarking = false
}
configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all {
extendsFrom(configurations.dependencyManagement)
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protobufJava.version}"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}"
}
}
generateProtoTasks {
all()*.plugins {
grpc {
option '@generated=omit'
}
}
}
}

60
smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/HelloWorldService.java

@ -0,0 +1,60 @@ @@ -0,0 +1,60 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package smoketest.grpcserveroauth;
import io.grpc.stub.StreamObserver;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import smoketest.grpcserveroauth.proto.HelloReply;
import smoketest.grpcserveroauth.proto.HelloRequest;
import smoketest.grpcserveroauth.proto.HelloWorldGrpc;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
@Service
public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase {
private static Log logger = LogFactory.getLog(HelloWorldService.class);
@Override
public void sayHelloProfileScope(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
sayHello("sayHelloProfileScope", request, responseObserver);
}
@Override
public void sayHelloEmailScope(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
sayHello("sayHelloEmailScope", request, responseObserver);
}
@Override
public void sayHelloAuthenticated(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
sayHello("sayHelloAuthenticated", request, responseObserver);
}
public void sayHello(String methodName, HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String name = request.getName();
logger.info(methodName + " " + name);
Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name);
Assert.state(!name.startsWith("internal"), "Internal error");
String message = "%s '%s'".formatted(methodName, name);
HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}

29
smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplication.java

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package smoketest.grpcserveroauth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleGrpcServerOAuthApplication {
public static void main(String[] args) {
SpringApplication.run(SampleGrpcServerOAuthApplication.class, args);
}
}

61
smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SecurityConfiguration.java

@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package smoketest.grpcserveroauth;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.server.GlobalServerInterceptor;
import org.springframework.grpc.server.security.AuthenticationProcessInterceptor;
import org.springframework.grpc.server.security.GrpcSecurity;
import org.springframework.grpc.server.security.RequestMapperConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration(proxyBeanMethods = false)
public class SecurityConfiguration {
@Bean
@GlobalServerInterceptor
AuthenticationProcessInterceptor securityInterceptor(GrpcSecurity grpcSecurity) throws Exception {
return grpcSecurity.authorizeRequests(this::authorizeGrpcRequests)
.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults()))
.build();
}
private void authorizeGrpcRequests(RequestMapperConfigurer requests) {
requests.methods("HelloWorld/SayHelloProfileScope").hasAuthority("SCOPE_profile");
requests.methods("HelloWorld/SayHelloEmailScope").hasAuthority("SCOPE_email");
requests.methods("HelloWorld/SayHelloAuthenticated").authenticated();
requests.methods("grpc.*/*").permitAll();
requests.allRequests().denyAll();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
http.oauth2AuthorizationServer((authorizationServer) -> {
http.securityMatcher(authorizationServer.getEndpointsMatcher());
authorizationServer.oidc(withDefaults());
});
http.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults()));
http.authorizeHttpRequests((requests) -> requests.anyRequest().permitAll());
http.csrf((csrf) -> csrf.disable());
return http.build();
}
}

20
smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/package-info.java

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
@NullMarked
package smoketest.grpcserveroauth;
import org.jspecify.annotations.NullMarked;

18
smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/proto/hello.proto

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
syntax = "proto3";
option java_package = "smoketest.grpcserveroauth.proto";
option java_multiple_files = true;
service HelloWorld {
rpc SayHelloProfileScope (HelloRequest) returns (HelloReply) {}
rpc SayHelloEmailScope (HelloRequest) returns (HelloReply) {}
rpc SayHelloAuthenticated (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}

21
smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/resources/application.yaml

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
logging.level.org.springframework.security: TRACE
spring:
security:
oauth2:
authorizationserver:
client:
oidc-client:
registration:
client-id: "spring"
client-secret: "{noop}secret"
client-authentication-methods:
- "client_secret_basic"
authorization-grant-types:
- "client_credentials"
- "refresh_token"
scopes:
- "openid"
- "profile"
resourceserver:
jwt:
jwk-set-uri: http://localhost:8080/oauth2/jwks

220
smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/test/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplicationTests.java

@ -0,0 +1,220 @@ @@ -0,0 +1,220 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package smoketest.grpcserveroauth;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Status.Code;
import io.grpc.StatusRuntimeException;
import io.grpc.reflection.v1.ServerReflectionGrpc.ServerReflectionStub;
import io.grpc.reflection.v1.ServerReflectionRequest;
import io.grpc.reflection.v1.ServerReflectionResponse;
import io.grpc.stub.StreamObserver;
import org.awaitility.Awaitility;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import smoketest.grpcserveroauth.proto.HelloReply;
import smoketest.grpcserveroauth.proto.HelloRequest;
import smoketest.grpcserveroauth.proto.HelloWorldGrpc.HelloWorldBlockingStub;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
import org.springframework.grpc.client.ImportGrpcClients;
import org.springframework.grpc.client.interceptor.security.BearerTokenAuthenticationInterceptor;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.RestClientClientCredentialsTokenResponseClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.SupplierClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder;
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.util.function.SingletonSupplier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = { "spring.grpc.server.port=0",
"spring.grpc.client.channel.default.target=static://localhost:${local.grpc.server.port}",
"spring.grpc.client.channel.oauth.target=static://localhost:${local.grpc.server.port}" })
@DirtiesContext
class SampleGrpcServerOAuthApplicationTests {
@Autowired
@Qualifier("unauthenticatedHelloWorldBlockingStub")
private HelloWorldBlockingStub unuathenticated;
@Autowired
@Qualifier("oauthHelloWorldBlockingStub")
private HelloWorldBlockingStub oauth;
@Autowired
private ServerReflectionStub reflection;
@Test
void whenUnauthenticatedStub() {
HelloWorldBlockingStub stub = this.unuathenticated;
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloProfileScope))
.satisfies(statusCode(Code.UNAUTHENTICATED));
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloEmailScope))
.satisfies(statusCode(Code.UNAUTHENTICATED));
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAuthenticated))
.satisfies(statusCode(Code.UNAUTHENTICATED));
assertCanInvokeReflection();
}
@Test
void whenOAuth() {
HelloWorldBlockingStub stub = this.oauth;
assertThat(invoke(stub::sayHelloProfileScope)).isEqualTo("sayHelloProfileScope 'Spring'");
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloEmailScope))
.satisfies(statusCode(Code.PERMISSION_DENIED));
assertThat(invoke(stub::sayHelloAuthenticated)).isEqualTo("sayHelloAuthenticated 'Spring'");
assertCanInvokeReflection();
}
private String invoke(Function<HelloRequest, HelloReply> method) {
HelloRequest request = HelloRequest.newBuilder().setName("Spring").build();
return method.apply(request).getMessage();
}
private void assertCanInvokeReflection() {
ObservedResponse<ServerReflectionResponse> response = invokeReflection();
assertThat(response.getValue()).isNotNull();
assertThat(response.getError()).isNull();
}
private ObservedResponse<ServerReflectionResponse> invokeReflection() {
ObservedResponse<ServerReflectionResponse> response = new ObservedResponse<>();
StreamObserver<ServerReflectionRequest> request = this.reflection.serverReflectionInfo(response);
request.onNext(ServerReflectionRequest.newBuilder().setListServices("").build());
request.onCompleted();
response.await();
return response;
}
private Consumer<StatusRuntimeException> statusCode(Code expected) {
return (ex) -> assertThat(ex).extracting("status.code").isEqualTo(expected);
}
@TestConfiguration(proxyBeanMethods = false)
@ImportGrpcClients(types = ServerReflectionStub.class)
@ImportGrpcClients(prefix = "unauthenticated", types = HelloWorldBlockingStub.class)
@ImportGrpcClients(target = "oauth", prefix = "oauth", types = HelloWorldBlockingStub.class)
static class GrpcClientTestConfiguration {
@Bean
<B extends ManagedChannelBuilder<B>> GrpcChannelBuilderCustomizer<B> channelSecurityCustomizer(
ObjectProvider<ClientRegistrationRepository> clientRegistrationRepository) {
return GrpcChannelBuilderCustomizer.matching("oauth", (builder) -> {
Supplier<String> tokenSupplier = SingletonSupplier
.of(() -> token(clientRegistrationRepository.getObject()));
builder.intercept(new BearerTokenAuthenticationInterceptor(tokenSupplier));
});
}
private String token(ClientRegistrationRepository clientRegistrationRepository) {
RestClientClientCredentialsTokenResponseClient client = new RestClientClientCredentialsTokenResponseClient();
ClientRegistration registration = clientRegistrationRepository.findByRegistrationId("spring");
OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(registration);
return client.getTokenResponse(request).getAccessToken().getTokenValue();
}
@Bean
ClientRegistrationRepository lazyClientRegistrationRepository(Environment environment) {
return new SupplierClientRegistrationRepository(() -> getClientRegistrationRepository(environment));
}
private InMemoryClientRegistrationRepository getClientRegistrationRepository(Environment environment) {
return new InMemoryClientRegistrationRepository(ClientRegistration.withRegistrationId("spring")
.clientId("spring")
.clientSecret("secret")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("profile")
.tokenUri(environment.resolvePlaceholders("http://localhost:${local.server.port}/oauth2/token"))
.build());
}
@Bean
SupplierJwtDecoder lazyJwtDecoder(Environment environment) {
return new SupplierJwtDecoder(() -> getJwtDecoder(environment));
}
private JwtDecoder getJwtDecoder(Environment environment) {
JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder
.withJwkSetUri(environment.resolvePlaceholders("http://localhost:${local.server.port}/oauth2/jwks"));
builder.jwsAlgorithms((algorithms) -> algorithms.add(SignatureAlgorithm.from("RS256")));
return builder.build();
}
}
static class ObservedResponse<T> implements StreamObserver<T> {
private volatile @Nullable T value;
private volatile @Nullable Throwable error;
@Override
public synchronized void onNext(T value) {
this.value = value;
}
@Override
public synchronized void onError(Throwable error) {
this.error = error;
}
@Override
public void onCompleted() {
}
void await() {
Awaitility.await().until(this::hasResponse);
}
private synchronized boolean hasResponse() {
return this.value != null || this.error != null;
}
@Nullable T getValue() {
return this.value;
}
@Nullable Throwable getError() {
return this.error;
}
}
}

66
smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
plugins {
id "java"
id "com.google.protobuf" version "${protobufGradlePluginVersion}"
id "org.springframework.boot.docker-test"
}
description = "Spring Boot gRPC server secure smoke test"
dependencies {
implementation(project(":starter:spring-boot-starter-grpc-server"))
implementation(project(":starter:spring-boot-starter-security"))
testImplementation(project(":starter:spring-boot-starter-grpc-client"))
testImplementation(project(":starter:spring-boot-starter-grpc-test"))
testImplementation(project(":starter:spring-boot-starter-test"))
}
def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom")
def grpcJava = dependenciesBom.getLibrary("Grpc Java")
def protobufJava = dependenciesBom.getLibrary("Protobuf Java")
tasks.named("compileTestJava") {
options.nullability.checking = "tests"
}
nullability {
requireExplicitNullMarking = false
}
configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all {
extendsFrom(configurations.dependencyManagement)
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protobufJava.version}"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}"
}
}
generateProtoTasks {
all()*.plugins {
grpc {
option '@generated=omit'
}
}
}
}

68
smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/HelloWorldService.java

@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package smoketest.grpcserversecure;
import io.grpc.stub.StreamObserver;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import smoketest.grpcserversecure.proto.HelloReply;
import smoketest.grpcserversecure.proto.HelloRequest;
import smoketest.grpcserversecure.proto.HelloWorldGrpc;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
@Service
public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase {
private static Log logger = LogFactory.getLog(HelloWorldService.class);
@Override
public void sayHelloAdmin(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
sayHello("sayHelloAdmin", request, responseObserver);
}
@Override
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public void sayHelloAdminAnnotated(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
sayHello("sayHelloAdminAnnotated", request, responseObserver);
}
@Override
public void sayHelloUser(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
sayHello("sayHelloUser", request, responseObserver);
}
@Override
@PreAuthorize("hasAuthority('ROLE_USER')")
public void sayHelloUserAnnotated(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
sayHello("sayHelloUserAnnotated", request, responseObserver);
}
public void sayHello(String methodName, HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String name = request.getName();
logger.info(methodName + " " + name);
Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name);
Assert.state(!name.startsWith("internal"), "Internal error");
String message = "%s '%s'".formatted(methodName, name);
HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}

29
smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplication.java

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package smoketest.grpcserversecure;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleGrpcServerSecureApplication {
public static void main(String[] args) {
SpringApplication.run(SampleGrpcServerSecureApplication.class, args);
}
}

64
smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SecurityConfiguration.java

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package smoketest.grpcserversecure;
import io.grpc.ServerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.server.GlobalServerInterceptor;
import org.springframework.grpc.server.security.GrpcSecurity;
import org.springframework.grpc.server.security.RequestMapperConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration(proxyBeanMethods = false)
public class SecurityConfiguration {
@Bean
InMemoryUserDetailsManager inMemoryUserDetailsManager() {
UserDetails user = user("user", "{noop}userpassword", "ROLE_USER");
UserDetails admin = user("admin", "{noop}adminpassword", "ROLE_ADMIN");
return new InMemoryUserDetailsManager(user, admin);
}
private UserDetails user(String username, String password, String authority) {
return User.withUsername(username).password(password).authorities(authority).build();
}
@Bean
@GlobalServerInterceptor
ServerInterceptor securityInterceptor(GrpcSecurity security) throws Exception {
return security.authorizeRequests(this::authorizeRequests)
.httpBasic(withDefaults())
.preauth(withDefaults())
.build();
}
private void authorizeRequests(RequestMapperConfigurer requests) {
requests.methods("HelloWorld/SayHelloAdmin").hasAuthority("ROLE_ADMIN");
requests.methods("HelloWorld/SayHelloAdminAnnotated").hasAuthority("ROLE_ADMIN");
requests.methods("HelloWorld/SayHelloUser").hasAuthority("ROLE_USER");
requests.methods("HelloWorld/SayHelloUserAnnotated").hasAuthority("ROLE_USER");
requests.methods("grpc.*/*").permitAll();
requests.allRequests().denyAll();
}
}

20
smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/package-info.java

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
@NullMarked
package smoketest.grpcserversecure;
import org.jspecify.annotations.NullMarked;

19
smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/proto/hello.proto

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
syntax = "proto3";
option java_package = "smoketest.grpcserversecure.proto";
option java_multiple_files = true;
service HelloWorld {
rpc SayHelloAdmin (HelloRequest) returns (HelloReply) {}
rpc SayHelloAdminAnnotated (HelloRequest) returns (HelloReply) {}
rpc SayHelloUser (HelloRequest) returns (HelloReply) {}
rpc SayHelloUserAnnotated (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}

192
smoke-test/spring-boot-smoke-test-grpc-server-secure/src/test/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplicationTests.java

@ -0,0 +1,192 @@ @@ -0,0 +1,192 @@
/*
* Copyright 2012-present the original author or 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
*
* https://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.
*/
package smoketest.grpcserversecure;
import java.util.function.Consumer;
import java.util.function.Function;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Status.Code;
import io.grpc.StatusRuntimeException;
import io.grpc.reflection.v1.ServerReflectionGrpc.ServerReflectionStub;
import io.grpc.reflection.v1.ServerReflectionRequest;
import io.grpc.reflection.v1.ServerReflectionResponse;
import io.grpc.stub.StreamObserver;
import org.awaitility.Awaitility;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import smoketest.grpcserversecure.proto.HelloReply;
import smoketest.grpcserversecure.proto.HelloRequest;
import smoketest.grpcserversecure.proto.HelloWorldGrpc.HelloWorldBlockingStub;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
import org.springframework.grpc.client.ImportGrpcClients;
import org.springframework.grpc.client.interceptor.security.BasicAuthenticationInterceptor;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@SpringBootTest(properties = { "spring.grpc.server.port=0",
"spring.grpc.client.channel.default.target=static://localhost:${local.grpc.server.port}",
"spring.grpc.client.channel.user.target=static://localhost:${local.grpc.server.port}",
"spring.grpc.client.channel.admin.target=static://localhost:${local.grpc.server.port}" })
@DirtiesContext
class SampleGrpcServerSecureApplicationTests {
@Autowired
@Qualifier("unauthenticatedHelloWorldBlockingStub")
private HelloWorldBlockingStub unuathenticated;
@Autowired
@Qualifier("userAuthenticatedHelloWorldBlockingStub")
private HelloWorldBlockingStub userAuthenticated;
@Autowired
@Qualifier("adminAuthenticatedHelloWorldBlockingStub")
private HelloWorldBlockingStub adminAuthenticated;
@Autowired
private ServerReflectionStub reflection;
@Test
void whenUnauthenticatedStub() {
HelloWorldBlockingStub stub = this.unuathenticated;
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUser))
.satisfies(statusCode(Code.UNAUTHENTICATED));
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUserAnnotated))
.satisfies(statusCode(Code.UNAUTHENTICATED));
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdmin))
.satisfies(statusCode(Code.UNAUTHENTICATED));
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdminAnnotated))
.satisfies(statusCode(Code.UNAUTHENTICATED));
assertCanInvokeReflection();
}
@Test
void whenUserAuthenticatedStub() {
HelloWorldBlockingStub stub = this.userAuthenticated;
assertThat(invoke(stub::sayHelloUser)).isEqualTo("sayHelloUser 'Spring'");
assertThat(invoke(stub::sayHelloUserAnnotated)).isEqualTo("sayHelloUserAnnotated 'Spring'");
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdmin))
.satisfies(statusCode(Code.PERMISSION_DENIED));
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdminAnnotated))
.satisfies(statusCode(Code.PERMISSION_DENIED));
assertCanInvokeReflection();
}
@Test
void whenAdminAuthenticatedStub() {
HelloWorldBlockingStub stub = this.adminAuthenticated;
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUser))
.satisfies(statusCode(Code.PERMISSION_DENIED));
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUserAnnotated))
.satisfies(statusCode(Code.PERMISSION_DENIED));
assertThat(invoke(stub::sayHelloAdmin)).isEqualTo("sayHelloAdmin 'Spring'");
assertThat(invoke(stub::sayHelloAdminAnnotated)).isEqualTo("sayHelloAdminAnnotated 'Spring'");
assertCanInvokeReflection();
}
private String invoke(Function<HelloRequest, HelloReply> method) {
HelloRequest request = HelloRequest.newBuilder().setName("Spring").build();
return method.apply(request).getMessage();
}
private void assertCanInvokeReflection() {
ObservedResponse<ServerReflectionResponse> response = invokeReflection();
assertThat(response.getValue()).isNotNull();
assertThat(response.getError()).isNull();
}
private ObservedResponse<ServerReflectionResponse> invokeReflection() {
ObservedResponse<ServerReflectionResponse> response = new ObservedResponse<>();
StreamObserver<ServerReflectionRequest> request = this.reflection.serverReflectionInfo(response);
request.onNext(ServerReflectionRequest.newBuilder().setListServices("").build());
request.onCompleted();
response.await();
return response;
}
private Consumer<StatusRuntimeException> statusCode(Code expected) {
return (ex) -> assertThat(ex).extracting("status.code").isEqualTo(expected);
}
@TestConfiguration(proxyBeanMethods = false)
@ImportGrpcClients(types = ServerReflectionStub.class)
@ImportGrpcClients(prefix = "unauthenticated", types = HelloWorldBlockingStub.class)
@ImportGrpcClients(target = "user", prefix = "userAuthenticated", types = HelloWorldBlockingStub.class)
@ImportGrpcClients(target = "admin", prefix = "adminAuthenticated", types = HelloWorldBlockingStub.class)
static class GrpcClientTestConfiguration {
@Bean
<B extends ManagedChannelBuilder<B>> GrpcChannelBuilderCustomizer<B> channelSecurityCustomizer() {
return (target, builder) -> {
if ("user".equals(target)) {
builder.intercept(new BasicAuthenticationInterceptor("user", "userpassword"));
}
if ("admin".equals(target)) {
builder.intercept(new BasicAuthenticationInterceptor("admin", "adminpassword"));
}
};
}
}
static class ObservedResponse<T> implements StreamObserver<T> {
private volatile @Nullable T value;
private volatile @Nullable Throwable error;
@Override
public synchronized void onNext(T value) {
this.value = value;
}
@Override
public synchronized void onError(Throwable error) {
this.error = error;
}
@Override
public void onCompleted() {
}
void await() {
Awaitility.await().until(this::hasResponse);
}
private synchronized boolean hasResponse() {
return this.value != null || this.error != null;
}
@Nullable T getValue() {
return this.value;
}
@Nullable Throwable getError() {
return this.error;
}
}
}
Loading…
Cancel
Save