mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
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>
This commit is contained in:
committed by
Phillip Webb
co-authored by
Phillip Webb
parent
1b41cb20c1
commit
1cb8d02ed7
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user