Browse Source
Add auto-configuration to integrate gRPC server applications with Spring Security. This commit provides both standard Spring Security support as well as OAuth support. Closes gh-49047 Co-authored-by: Phillip Webb <phil.webb@broadcom.com>pull/46608/head
31 changed files with 2311 additions and 0 deletions
@ -0,0 +1,86 @@
@@ -0,0 +1,86 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security; |
||||
|
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils; |
||||
import org.springframework.boot.grpc.server.GrpcServletRegistration; |
||||
import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer; |
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; |
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; |
||||
import org.springframework.security.web.csrf.CsrfFilter; |
||||
import org.springframework.security.web.util.matcher.RequestMatcher; |
||||
import org.springframework.util.ObjectUtils; |
||||
|
||||
/** |
||||
* A custom {@link AbstractHttpConfigurer} that disables CSRF protection for gRPC |
||||
* requests. |
||||
* <p> |
||||
* This configurer checks the application context to determine if CSRF protection should |
||||
* be disabled for gRPC requests based on the property |
||||
* {@code spring.grpc.server.security.csrf.enabled}. By default, CSRF protection is |
||||
* disabled unless explicitly enabled in the application properties. |
||||
* </p> |
||||
* |
||||
* @author Dave Syer |
||||
* @see AbstractHttpConfigurer |
||||
* @see HttpSecurity |
||||
*/ |
||||
class GrpcDisableCsrfHttpConfigurer extends AbstractHttpConfigurer<GrpcDisableCsrfHttpConfigurer, HttpSecurity> { |
||||
|
||||
@Override |
||||
public void init(HttpSecurity http) { |
||||
ApplicationContext context = http.getSharedObject(ApplicationContext.class); |
||||
if (context != null && isCsrfConfigurerPresent(http) && hasBean(context, GrpcServiceDiscoverer.class) |
||||
&& hasBean(context, GrpcServletRegistration.class) && isCsrfEnabled(context)) { |
||||
http.csrf(this::disable); |
||||
} |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
private boolean isCsrfConfigurerPresent(HttpSecurity http) { |
||||
return http.getConfigurer(CsrfConfigurer.class) != null; |
||||
} |
||||
|
||||
private boolean hasBean(ApplicationContext context, Class<?> type) { |
||||
return !ObjectUtils.isEmpty(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type)); |
||||
} |
||||
|
||||
private boolean isCsrfEnabled(ApplicationContext context) { |
||||
return context.getEnvironment().getProperty("spring.grpc.server.security.csrf.enabled", Boolean.class, true); |
||||
} |
||||
|
||||
private void disable(CsrfConfigurer<HttpSecurity> csrf) { |
||||
csrf.requireCsrfProtectionMatcher(GrpcCsrfRequestMatcher.INSTANCE); |
||||
} |
||||
|
||||
static class GrpcCsrfRequestMatcher implements RequestMatcher { |
||||
|
||||
static GrpcCsrfRequestMatcher INSTANCE = new GrpcCsrfRequestMatcher(); |
||||
|
||||
@Override |
||||
public boolean matches(HttpServletRequest request) { |
||||
return CsrfFilter.DEFAULT_CSRF_MATCHER.matches(request) && !GrpcRequest.toAnyService().matches(request); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security; |
||||
|
||||
import io.grpc.BindableService; |
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration; |
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
||||
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.grpc.server.GlobalServerInterceptor; |
||||
import org.springframework.grpc.server.GrpcServerFactory; |
||||
import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; |
||||
import org.springframework.grpc.server.security.GrpcSecurity; |
||||
import org.springframework.security.config.ObjectPostProcessor; |
||||
import org.springframework.security.oauth2.jwt.JwtDecoder; |
||||
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; |
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults; |
||||
|
||||
/** |
||||
* {@link EnableAutoConfiguration Auto-configuration} for gRPC OAuth2 resource server. |
||||
* |
||||
* @author Dave Syer |
||||
* @author Andrey Litvitski |
||||
* @author Phillip Webb |
||||
* @since 4.1.0 |
||||
*/ |
||||
@AutoConfiguration(beforeName = "org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration", |
||||
afterName = { "org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration", |
||||
"org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration" }, |
||||
after = { GrpcServerSecurityAutoConfiguration.class, GrpcServerAutoConfiguration.class }) |
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true) |
||||
@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, ObjectPostProcessor.class }) |
||||
@ConditionalOnMissingBean(AuthenticationProcessInterceptor.class) |
||||
@ConditionalOnBean(GrpcSecurity.class) |
||||
public final class GrpcServerOAuth2ResourceServerAutoConfiguration { |
||||
|
||||
@Bean |
||||
@ConditionalOnBean(OpaqueTokenIntrospector.class) |
||||
@GlobalServerInterceptor |
||||
AuthenticationProcessInterceptor opaqueTokenAuthenticationProcessInterceptor(GrpcSecurity grpcSecurity) |
||||
throws Exception { |
||||
grpcSecurity.authorizeRequests((requests) -> requests.allRequests().authenticated()); |
||||
grpcSecurity.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults())); |
||||
return grpcSecurity.build(); |
||||
} |
||||
|
||||
@Bean |
||||
@ConditionalOnBean(JwtDecoder.class) |
||||
@GlobalServerInterceptor |
||||
AuthenticationProcessInterceptor jwtAuthenticationProcessInterceptor(GrpcSecurity grpcSecurity) throws Exception { |
||||
grpcSecurity.authorizeRequests((requests) -> requests.allRequests().authenticated()); |
||||
grpcSecurity.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults())); |
||||
return grpcSecurity.build(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,127 @@
@@ -0,0 +1,127 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security; |
||||
|
||||
import io.grpc.BindableService; |
||||
import io.grpc.internal.GrpcUtil; |
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration; |
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
||||
import org.springframework.boot.grpc.server.GrpcServletRegistration; |
||||
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration; |
||||
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerExecutorProvider; |
||||
import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.ExceptionHandlerConfiguration; |
||||
import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.GrpcNativeSecurityConfigurerConfiguration; |
||||
import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.GrpcServletSecurityConfigurerConfiguration; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.context.annotation.Import; |
||||
import org.springframework.grpc.server.GlobalServerInterceptor; |
||||
import org.springframework.grpc.server.GrpcServerFactory; |
||||
import org.springframework.grpc.server.security.CoroutineSecurityContextInterceptor; |
||||
import org.springframework.grpc.server.security.GrpcSecurity; |
||||
import org.springframework.grpc.server.security.SecurityContextServerInterceptor; |
||||
import org.springframework.grpc.server.security.SecurityGrpcExceptionHandler; |
||||
import org.springframework.security.concurrent.DelegatingSecurityContextExecutor; |
||||
import org.springframework.security.config.ObjectPostProcessor; |
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; |
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; |
||||
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; |
||||
import org.springframework.security.web.SecurityFilterChain; |
||||
|
||||
/** |
||||
* {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side security. |
||||
* |
||||
* @author Dave Syer |
||||
* @author Chris Bono |
||||
* @author Andrey Litvitski |
||||
* @author Phillip Webb |
||||
* @since 4.1.0 |
||||
*/ |
||||
@AutoConfiguration(after = GrpcServerAutoConfiguration.class, |
||||
afterName = "org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration") |
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true) |
||||
@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, ObjectPostProcessor.class }) |
||||
@Import({ ExceptionHandlerConfiguration.class, GrpcNativeSecurityConfigurerConfiguration.class, |
||||
GrpcServletSecurityConfigurerConfiguration.class }) |
||||
public final class GrpcServerSecurityAutoConfiguration { |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
@Import(AuthenticationConfiguration.class) |
||||
static class ExceptionHandlerConfiguration { |
||||
|
||||
@Bean |
||||
SecurityGrpcExceptionHandler accessExceptionHandler() { |
||||
return new SecurityGrpcExceptionHandler(); |
||||
} |
||||
|
||||
} |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
@ConditionalOnBean(GrpcServerFactory.class) |
||||
@EnableGlobalAuthentication |
||||
static class GrpcNativeSecurityConfigurerConfiguration { |
||||
|
||||
@Bean |
||||
GrpcSecurity grpcSecurity(ApplicationContext context, ObjectPostProcessor<Object> objectPostProcessor, |
||||
AuthenticationConfiguration authenticationConfiguration) { |
||||
AuthenticationManagerBuilder authenticationManagerBuilder = authenticationConfiguration |
||||
.authenticationManagerBuilder(objectPostProcessor, context); |
||||
authenticationManagerBuilder |
||||
.parentAuthenticationManager(authenticationConfiguration.getAuthenticationManager()); |
||||
return new GrpcSecurity(objectPostProcessor, authenticationManagerBuilder, context); |
||||
} |
||||
|
||||
} |
||||
|
||||
@ConditionalOnBean({ GrpcServletRegistration.class, SecurityFilterChain.class }) |
||||
@Configuration(proxyBeanMethods = false) |
||||
static class GrpcServletSecurityConfigurerConfiguration { |
||||
|
||||
@Bean |
||||
@GlobalServerInterceptor |
||||
SecurityContextServerInterceptor securityContextInterceptor() { |
||||
return new SecurityContextServerInterceptor(); |
||||
} |
||||
|
||||
@Bean |
||||
@ConditionalOnMissingBean |
||||
GrpcServerExecutorProvider grpcServerExecutorProvider() { |
||||
return () -> new DelegatingSecurityContextExecutor(GrpcUtil.SHARED_CHANNEL_EXECUTOR.create()); |
||||
} |
||||
|
||||
} |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
@ConditionalOnClass(name = "io.grpc.kotlin.CoroutineContextServerInterceptor") |
||||
static class GrpcClientCoroutineStubConfiguration { |
||||
|
||||
@Bean |
||||
@GlobalServerInterceptor |
||||
@ConditionalOnMissingBean |
||||
CoroutineSecurityContextInterceptor coroutineSecurityContextInterceptor() { |
||||
return new CoroutineSecurityContextInterceptor(); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
/** |
||||
* Auto-configuration for gRPC server security. |
||||
*/ |
||||
@NullMarked |
||||
package org.springframework.boot.grpc.server.autoconfigure.security; |
||||
|
||||
import org.jspecify.annotations.NullMarked; |
||||
@ -0,0 +1,148 @@
@@ -0,0 +1,148 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security.web.reactive; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.LinkedHashSet; |
||||
import java.util.List; |
||||
import java.util.Set; |
||||
import java.util.function.Supplier; |
||||
import java.util.stream.Stream; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest.GrpcServletRequestMatcher; |
||||
import org.springframework.boot.security.web.reactive.ApplicationContextServerWebExchangeMatcher; |
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer; |
||||
import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher; |
||||
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; |
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.CollectionUtils; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
/** |
||||
* Factory that can be used to create a {@link ServerWebExchangeMatcher} to match against |
||||
* gRPC service locations. |
||||
* |
||||
* @author Dave Syer |
||||
* @author Phillip Webb |
||||
* @since 4.1.0 |
||||
*/ |
||||
public final class GrpcRequest { |
||||
|
||||
private GrpcRequest() { |
||||
} |
||||
|
||||
/** |
||||
* Returns a matcher that includes all gRPC services. The |
||||
* {@link GrpcReactiveRequestMatcher#excluding(String...) excluding} method can be |
||||
* used to remove specific services by name if required. For example: |
||||
* |
||||
* <pre class="code"> |
||||
* GrpcReactiveRequest.toAnyService().excluding("my-service") |
||||
* </pre> |
||||
* @return the configured {@link ServerWebExchangeMatcher} |
||||
*/ |
||||
public static GrpcReactiveRequestMatcher toAnyService() { |
||||
return new GrpcReactiveRequestMatcher(Collections.emptySet()); |
||||
} |
||||
|
||||
/** |
||||
* The matcher used to match against service locations. |
||||
*/ |
||||
public static final class GrpcReactiveRequestMatcher |
||||
extends ApplicationContextServerWebExchangeMatcher<GrpcServiceDiscoverer> { |
||||
|
||||
private static final ServerWebExchangeMatcher EMPTY_MATCHER = (exchange) -> MatchResult.notMatch(); |
||||
|
||||
private final Set<String> excludes; |
||||
|
||||
private volatile @Nullable ServerWebExchangeMatcher delegate; |
||||
|
||||
private GrpcReactiveRequestMatcher(Set<String> excludes) { |
||||
super(GrpcServiceDiscoverer.class); |
||||
this.excludes = excludes; |
||||
} |
||||
|
||||
/** |
||||
* Return a new {@link GrpcServletRequestMatcher} based on this one but excluding |
||||
* the specified services. |
||||
* @param services additional services to exclude |
||||
* @return a new {@link GrpcServletRequestMatcher} |
||||
*/ |
||||
public GrpcReactiveRequestMatcher excluding(String... services) { |
||||
return excluding(Set.of(services)); |
||||
} |
||||
|
||||
/** |
||||
* Return a new {@link GrpcServletRequestMatcher} based on this one but excluding |
||||
* the specified services. |
||||
* @param services additional service names to exclude |
||||
* @return a new {@link GrpcServletRequestMatcher} |
||||
*/ |
||||
public GrpcReactiveRequestMatcher excluding(Collection<String> services) { |
||||
Assert.notNull(services, "'services' must not be null"); |
||||
Set<String> excludes = new LinkedHashSet<>(this.excludes); |
||||
excludes.addAll(services); |
||||
return new GrpcReactiveRequestMatcher(excludes); |
||||
} |
||||
|
||||
@Override |
||||
protected void initialized(Supplier<GrpcServiceDiscoverer> context) { |
||||
this.delegate = createDelegate(context.get()); |
||||
|
||||
} |
||||
|
||||
private ServerWebExchangeMatcher createDelegate(GrpcServiceDiscoverer serviceDiscoverer) { |
||||
List<ServerWebExchangeMatcher> delegateMatchers = getDelegateMatchers(serviceDiscoverer); |
||||
return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrServerWebExchangeMatcher(delegateMatchers) |
||||
: EMPTY_MATCHER; |
||||
} |
||||
|
||||
private List<ServerWebExchangeMatcher> getDelegateMatchers(GrpcServiceDiscoverer serviceDiscoverer) { |
||||
return getPatterns(serviceDiscoverer).map(this::getDelegateMatcher).toList(); |
||||
} |
||||
|
||||
private Stream<String> getPatterns(GrpcServiceDiscoverer serviceDiscoverer) { |
||||
return serviceDiscoverer.listServiceNames().stream().filter(this::isExcluded).map(this::getPath); |
||||
} |
||||
|
||||
private boolean isExcluded(String service) { |
||||
return !this.excludes.stream().anyMatch((candidate) -> candidate.equals(service)); |
||||
} |
||||
|
||||
private String getPath(String service) { |
||||
return "/" + service + "/**"; |
||||
} |
||||
|
||||
private ServerWebExchangeMatcher getDelegateMatcher(String path) { |
||||
Assert.hasText(path, "'path' must not be empty"); |
||||
return new PathPatternParserServerWebExchangeMatcher(path); |
||||
} |
||||
|
||||
@Override |
||||
protected Mono<MatchResult> matches(ServerWebExchange exchange, Supplier<GrpcServiceDiscoverer> context) { |
||||
Assert.state(this.delegate != null, "'delegate' must not be null"); |
||||
return this.delegate.matches(exchange); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
/** |
||||
* Auto-configuration for gRPC web security when using a servlet stack. |
||||
*/ |
||||
@NullMarked |
||||
package org.springframework.boot.grpc.server.autoconfigure.security.web.reactive; |
||||
|
||||
import org.jspecify.annotations.NullMarked; |
||||
@ -0,0 +1,148 @@
@@ -0,0 +1,148 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security.web.servlet; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.LinkedHashSet; |
||||
import java.util.List; |
||||
import java.util.Set; |
||||
import java.util.function.Supplier; |
||||
import java.util.stream.Stream; |
||||
|
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import org.jspecify.annotations.Nullable; |
||||
|
||||
import org.springframework.boot.security.web.servlet.ApplicationContextRequestMatcher; |
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer; |
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; |
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher; |
||||
import org.springframework.security.web.util.matcher.RequestMatcher; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.CollectionUtils; |
||||
|
||||
/** |
||||
* Factory that can be used to create a {@link RequestMatcher} to match against gRPC |
||||
* service locations. |
||||
* |
||||
* @author Dave Syer |
||||
* @author Phillip Webb |
||||
* @since 4.1.0 |
||||
*/ |
||||
public final class GrpcRequest { |
||||
|
||||
private static final RequestMatcher EMPTY_MATCHER = (request) -> false; |
||||
|
||||
private static final GrpcServletRequestMatcher TO_ANY_SERVICE = new GrpcServletRequestMatcher( |
||||
Collections.emptySet()); |
||||
|
||||
private GrpcRequest() { |
||||
} |
||||
|
||||
/** |
||||
* Returns a matcher that includes all gRPC services. The |
||||
* {@link GrpcServletRequestMatcher#excluding(String...) excluding} method can be used |
||||
* to remove specific services by name if required. For example: |
||||
* |
||||
* <pre class="code"> |
||||
* GrpcServletRequest.toAnyService().excluding("my-service") |
||||
* </pre> |
||||
* @return the configured {@link RequestMatcher} |
||||
*/ |
||||
public static GrpcServletRequestMatcher toAnyService() { |
||||
return TO_ANY_SERVICE; |
||||
} |
||||
|
||||
/** |
||||
* The matcher used to match against service locations. |
||||
*/ |
||||
public static final class GrpcServletRequestMatcher |
||||
extends ApplicationContextRequestMatcher<GrpcServiceDiscoverer> { |
||||
|
||||
private final Set<String> excludes; |
||||
|
||||
private volatile @Nullable RequestMatcher delegate; |
||||
|
||||
private GrpcServletRequestMatcher(Set<String> exclusions) { |
||||
super(GrpcServiceDiscoverer.class); |
||||
this.excludes = exclusions; |
||||
} |
||||
|
||||
/** |
||||
* Return a new {@link GrpcServletRequestMatcher} based on this one but excluding |
||||
* the specified services. |
||||
* @param services additional services to exclude |
||||
* @return a new {@link GrpcServletRequestMatcher} |
||||
*/ |
||||
public GrpcServletRequestMatcher excluding(String... services) { |
||||
return excluding(Set.of(services)); |
||||
} |
||||
|
||||
/** |
||||
* Return a new {@link GrpcServletRequestMatcher} based on this one but excluding |
||||
* the specified services. |
||||
* @param services additional service names to exclude |
||||
* @return a new {@link GrpcServletRequestMatcher} |
||||
*/ |
||||
public GrpcServletRequestMatcher excluding(Collection<String> services) { |
||||
Assert.notNull(services, "'services' must not be null"); |
||||
Set<String> excludes = new LinkedHashSet<>(this.excludes); |
||||
excludes.addAll(services); |
||||
return new GrpcServletRequestMatcher(excludes); |
||||
} |
||||
|
||||
@Override |
||||
protected void initialized(Supplier<GrpcServiceDiscoverer> context) { |
||||
this.delegate = createDelegate(context.get()); |
||||
} |
||||
|
||||
private @Nullable RequestMatcher createDelegate(GrpcServiceDiscoverer grpcServiceDiscoverer) { |
||||
List<RequestMatcher> delegateMatchers = getDelegateMatchers(grpcServiceDiscoverer); |
||||
return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrRequestMatcher(delegateMatchers) |
||||
: EMPTY_MATCHER; |
||||
} |
||||
|
||||
private List<RequestMatcher> getDelegateMatchers(GrpcServiceDiscoverer serviceDiscoverer) { |
||||
return getPatterns(serviceDiscoverer).map(this::getDelegateMatcher).toList(); |
||||
} |
||||
|
||||
private Stream<String> getPatterns(GrpcServiceDiscoverer serviceDiscoverer) { |
||||
return serviceDiscoverer.listServiceNames().stream().filter(this::isExcluded).map(this::getPath); |
||||
} |
||||
|
||||
private boolean isExcluded(String service) { |
||||
return !this.excludes.stream().anyMatch((candidate) -> candidate.equals(service)); |
||||
} |
||||
|
||||
private String getPath(String service) { |
||||
return "/" + service + "/**"; |
||||
} |
||||
|
||||
private RequestMatcher getDelegateMatcher(String path) { |
||||
Assert.hasText(path, "'path' must not be empty"); |
||||
return PathPatternRequestMatcher.withDefaults().matcher(path); |
||||
} |
||||
|
||||
@Override |
||||
protected boolean matches(HttpServletRequest request, Supplier<GrpcServiceDiscoverer> context) { |
||||
Assert.state(this.delegate != null, "'delegate' must not be null"); |
||||
return this.delegate.matches(request); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
/** |
||||
* Auto-configuration for gRPC web security when using a reactive stack. |
||||
*/ |
||||
@NullMarked |
||||
package org.springframework.boot.grpc.server.autoconfigure.security.web.servlet; |
||||
|
||||
import org.jspecify.annotations.NullMarked; |
||||
@ -0,0 +1,167 @@
@@ -0,0 +1,167 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security; |
||||
|
||||
import java.util.HashMap; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.ArgumentCaptor; |
||||
|
||||
import org.springframework.boot.grpc.server.GrpcServletRegistration; |
||||
import org.springframework.boot.grpc.server.autoconfigure.security.GrpcDisableCsrfHttpConfigurer.GrpcCsrfRequestMatcher; |
||||
import org.springframework.boot.test.util.TestPropertyValues; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.context.support.StaticApplicationContext; |
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer; |
||||
import org.springframework.security.config.ObjectPostProcessor; |
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; |
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; |
||||
import org.springframework.security.web.util.matcher.RequestMatcher; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.BDDMockito.then; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.never; |
||||
|
||||
/** |
||||
* Tests for {@link GrpcDisableCsrfHttpConfigurer}. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class GrpcDisableCsrfHttpConfigurerTests { |
||||
|
||||
private GrpcDisableCsrfHttpConfigurer configurer = new GrpcDisableCsrfHttpConfigurer(); |
||||
|
||||
@Test |
||||
void initDisablesCsrf() { |
||||
ObjectPostProcessor<Object> objectPostProcessor = ObjectPostProcessor.identity(); |
||||
AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); |
||||
HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); |
||||
StaticApplicationContext applicationContext = addApplicationContext(http); |
||||
addServiceDiscover(applicationContext); |
||||
addGrpcServletRegistration(applicationContext); |
||||
CsrfConfigurer<?> csrf = addCsrf(http); |
||||
this.configurer.init(http); |
||||
ArgumentCaptor<RequestMatcher> matcher = ArgumentCaptor.captor(); |
||||
then(csrf).should().requireCsrfProtectionMatcher(matcher.capture()); |
||||
assertThat(matcher.getValue()).isSameAs(GrpcCsrfRequestMatcher.INSTANCE); |
||||
} |
||||
|
||||
@Test |
||||
void initWhenNoApplicationContextDoesNothing() { |
||||
ObjectPostProcessor<Object> objectPostProcessor = ObjectPostProcessor.identity(); |
||||
AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); |
||||
HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); |
||||
CsrfConfigurer<?> csrf = addCsrf(http); |
||||
this.configurer.init(http); |
||||
then(csrf).should(never()).requireCsrfProtectionMatcher(any()); |
||||
} |
||||
|
||||
@Test |
||||
@SuppressWarnings("unchecked") |
||||
void initWhenNoCsrfConfigurerDoesNothing() { |
||||
ObjectPostProcessor<Object> objectPostProcessor = ObjectPostProcessor.identity(); |
||||
AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); |
||||
HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); |
||||
StaticApplicationContext applicationContext = addApplicationContext(http); |
||||
addServiceDiscover(applicationContext); |
||||
addGrpcServletRegistration(applicationContext); |
||||
this.configurer.init(http); |
||||
CsrfConfigurer<?> csrfConfigurer = http.getConfigurer(CsrfConfigurer.class); |
||||
assertThat(csrfConfigurer).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
void initWhenNoGrpcServiceDiscovererBeanDoesNothing() { |
||||
ObjectPostProcessor<Object> objectPostProcessor = ObjectPostProcessor.identity(); |
||||
AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); |
||||
HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); |
||||
StaticApplicationContext applicationContext = addApplicationContext(http); |
||||
addGrpcServletRegistration(applicationContext); |
||||
CsrfConfigurer<?> csrf = addCsrf(http); |
||||
this.configurer.init(http); |
||||
then(csrf).should(never()).requireCsrfProtectionMatcher(any()); |
||||
} |
||||
|
||||
@Test |
||||
void initWhenNoGrpcServletRegistrationBeanDoesNothing() { |
||||
ObjectPostProcessor<Object> objectPostProcessor = ObjectPostProcessor.identity(); |
||||
AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); |
||||
HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); |
||||
StaticApplicationContext applicationContext = addApplicationContext(http); |
||||
addServiceDiscover(applicationContext); |
||||
CsrfConfigurer<?> csrf = addCsrf(http); |
||||
this.configurer.init(http); |
||||
then(csrf).should(never()).requireCsrfProtectionMatcher(any()); |
||||
} |
||||
|
||||
@Test |
||||
void initWhenEnabledPropertyFalseDoesNothing() { |
||||
ObjectPostProcessor<Object> objectPostProcessor = ObjectPostProcessor.identity(); |
||||
AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); |
||||
HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); |
||||
StaticApplicationContext applicationContext = addApplicationContext(http); |
||||
TestPropertyValues.of("spring.grpc.server.security.csrf.enabled=false").applyTo(applicationContext); |
||||
addServiceDiscover(applicationContext); |
||||
addGrpcServletRegistration(applicationContext); |
||||
CsrfConfigurer<?> csrf = addCsrf(http); |
||||
this.configurer.init(http); |
||||
then(csrf).should(never()).requireCsrfProtectionMatcher(any()); |
||||
} |
||||
|
||||
@Test |
||||
void initWhenEnabledPropertyTrueDisablesCsrf() { |
||||
ObjectPostProcessor<Object> objectPostProcessor = ObjectPostProcessor.identity(); |
||||
AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); |
||||
HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>()); |
||||
StaticApplicationContext applicationContext = addApplicationContext(http); |
||||
TestPropertyValues.of("spring.grpc.server.security.csrf.enabled=true").applyTo(applicationContext); |
||||
addServiceDiscover(applicationContext); |
||||
addGrpcServletRegistration(applicationContext); |
||||
CsrfConfigurer<?> csrf = addCsrf(http); |
||||
this.configurer.init(http); |
||||
ArgumentCaptor<RequestMatcher> matcher = ArgumentCaptor.captor(); |
||||
then(csrf).should().requireCsrfProtectionMatcher(matcher.capture()); |
||||
assertThat(matcher.getValue()).isSameAs(GrpcCsrfRequestMatcher.INSTANCE); |
||||
} |
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||
private CsrfConfigurer<?> addCsrf(HttpSecurity http) { |
||||
CsrfConfigurer<?> csrf = mock(); |
||||
http.with((CsrfConfigurer) csrf); |
||||
return csrf; |
||||
} |
||||
|
||||
private StaticApplicationContext addApplicationContext(HttpSecurity http) { |
||||
StaticApplicationContext applicationContext = new StaticApplicationContext(); |
||||
http.setSharedObject(ApplicationContext.class, applicationContext); |
||||
return applicationContext; |
||||
} |
||||
|
||||
private void addServiceDiscover(StaticApplicationContext applicationContext) { |
||||
GrpcServiceDiscoverer serviceDiscoverer = mock(); |
||||
applicationContext.registerBean(GrpcServiceDiscoverer.class, serviceDiscoverer); |
||||
} |
||||
|
||||
private void addGrpcServletRegistration(StaticApplicationContext applicationContext) { |
||||
GrpcServletRegistration servletRegistration = mock(); |
||||
applicationContext.registerBean(GrpcServletRegistration.class, servletRegistration); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,170 @@
@@ -0,0 +1,170 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security; |
||||
|
||||
import io.grpc.BindableService; |
||||
import io.grpc.ServerServiceDefinition; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.Mockito; |
||||
|
||||
import org.springframework.boot.SpringApplication; |
||||
import org.springframework.boot.autoconfigure.AutoConfigurations; |
||||
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; |
||||
import org.springframework.boot.context.annotation.UserConfigurations; |
||||
import org.springframework.boot.context.event.ApplicationFailedEvent; |
||||
import org.springframework.boot.logging.LogLevel; |
||||
import org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration; |
||||
import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration; |
||||
import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider; |
||||
import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner; |
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner; |
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner; |
||||
import org.springframework.boot.web.context.servlet.AnnotationConfigServletWebApplicationContext; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.context.ConfigurableApplicationContext; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.grpc.server.GlobalServerInterceptor; |
||||
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; |
||||
import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; |
||||
import org.springframework.grpc.server.security.GrpcSecurity; |
||||
import org.springframework.security.config.Customizer; |
||||
import org.springframework.security.config.ObjectPostProcessor; |
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; |
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; |
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.BDDMockito.given; |
||||
import static org.mockito.Mockito.mock; |
||||
|
||||
/** |
||||
* Tests for {@link GrpcServerOAuth2ResourceServerAutoConfiguration}. |
||||
* |
||||
* @author Chris Bono |
||||
*/ |
||||
class GrpcServerOAuth2ResourceServerAutoConfigurationTests { |
||||
|
||||
private static final AutoConfigurations autoConfigurations = AutoConfigurations |
||||
.of(OAuth2ResourceServerAutoConfiguration.class, GrpcServerOAuth2ResourceServerAutoConfiguration.class); |
||||
|
||||
private ApplicationContextRunner contextRunner = new ApplicationContextRunner() |
||||
.withConfiguration(autoConfigurations) |
||||
.withUserConfiguration(GrpcSecurityConfiguration.class) |
||||
.with(this::serviceBean) |
||||
.withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock); |
||||
|
||||
@Test |
||||
void jwtConfiguredWhenIssuerIsProvided() { |
||||
this.contextRunner |
||||
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000") |
||||
.run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class)); |
||||
} |
||||
|
||||
@Test |
||||
void jwtConfiguredWhenJwkSetIsProvided() { |
||||
this.contextRunner |
||||
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000") |
||||
.run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class)); |
||||
} |
||||
|
||||
@Test |
||||
void customInterceptorWhenJwkSetIsProvided() { |
||||
this.contextRunner.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) |
||||
.withConfiguration(UserConfigurations.of(CustomInterceptorConfiguration.class)) |
||||
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000") |
||||
.run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class)); |
||||
} |
||||
|
||||
@Test |
||||
void notConfiguredWhenIssuerNotProvided() { |
||||
this.contextRunner |
||||
.run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class)); |
||||
} |
||||
|
||||
@Test |
||||
void notConfiguredInWebApplication() { |
||||
new WebApplicationContextRunner().withConfiguration(autoConfigurations) |
||||
.withConfiguration(AutoConfigurations.of(ServletWebSecurityAutoConfiguration.class, |
||||
OAuth2ResourceServerAutoConfiguration.class)) |
||||
.with(this::serviceBean) |
||||
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000") |
||||
.run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class)); |
||||
} |
||||
|
||||
@Test |
||||
void notConfiguredInWebApplicationWithNoBindableService() { |
||||
new WebApplicationContextRunner().withConfiguration(autoConfigurations) |
||||
.withConfiguration(AutoConfigurations.of(ServletWebSecurityAutoConfiguration.class, |
||||
OAuth2ResourceServerAutoConfiguration.class)) |
||||
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000") |
||||
.run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class)); |
||||
} |
||||
|
||||
private <R extends AbstractApplicationContextRunner<R, C, A>, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider<C>> R serviceBean( |
||||
R contextRunner) { |
||||
BindableService service = mock(); |
||||
ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); |
||||
given(service.bindService()).willReturn(serviceDefinition); |
||||
return contextRunner.withBean(BindableService.class, () -> service); |
||||
} |
||||
|
||||
static class FailingApplicationFailedEventContext extends AnnotationConfigServletWebApplicationContext { |
||||
|
||||
@Override |
||||
public void refresh() { |
||||
try { |
||||
super.refresh(); |
||||
} |
||||
catch (Throwable ex) { |
||||
publishEvent(new ApplicationFailedEvent(new SpringApplication(this), new String[0], this, ex)); |
||||
throw ex; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
static class CustomInterceptorConfiguration { |
||||
|
||||
@Bean |
||||
@GlobalServerInterceptor |
||||
AuthenticationProcessInterceptor jwtSecurityFilterChain(GrpcSecurity grpc) throws Exception { |
||||
return grpc.authorizeRequests((requests) -> requests.allRequests().authenticated()) |
||||
.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults())) |
||||
.build(); |
||||
} |
||||
|
||||
} |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
@EnableWebSecurity |
||||
static class GrpcSecurityConfiguration { |
||||
|
||||
@Bean |
||||
GrpcSecurity grpcSecurity(ApplicationContext context, ObjectPostProcessor<Object> objectPostProcessor, |
||||
AuthenticationConfiguration authenticationConfiguration) { |
||||
AuthenticationManagerBuilder authenticationManagerBuilder = authenticationConfiguration |
||||
.authenticationManagerBuilder(objectPostProcessor, context); |
||||
authenticationManagerBuilder |
||||
.parentAuthenticationManager(authenticationConfiguration.getAuthenticationManager()); |
||||
return new GrpcSecurity(objectPostProcessor, authenticationManagerBuilder, context); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,152 @@
@@ -0,0 +1,152 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security; |
||||
|
||||
import io.grpc.BindableService; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.Mockito; |
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations; |
||||
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; |
||||
import org.springframework.boot.grpc.server.GrpcServletRegistration; |
||||
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerExecutorProvider; |
||||
import org.springframework.boot.logging.LogLevel; |
||||
import org.springframework.boot.test.context.FilteredClassLoader; |
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner; |
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.grpc.server.GrpcServerFactory; |
||||
import org.springframework.grpc.server.exception.GrpcExceptionHandler; |
||||
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; |
||||
import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; |
||||
import org.springframework.grpc.server.security.GrpcSecurity; |
||||
import org.springframework.grpc.server.security.SecurityContextServerInterceptor; |
||||
import org.springframework.grpc.server.security.SecurityGrpcExceptionHandler; |
||||
import org.springframework.security.config.ObjectPostProcessor; |
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; |
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||
import org.springframework.security.web.SecurityFilterChain; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.Mockito.mock; |
||||
|
||||
/** |
||||
* Tests for {@link GrpcServerSecurityAutoConfiguration}. |
||||
* |
||||
* @author Chris Bono |
||||
*/ |
||||
class GrpcServerSecurityAutoConfigurationTests { |
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() |
||||
.withConfiguration(AutoConfigurations.of(GrpcServerSecurityAutoConfiguration.class)) |
||||
.withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock); |
||||
|
||||
@Test |
||||
void whenSpringSecurityNotOnClasspathAutoConfigurationIsSkipped() { |
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(ObjectPostProcessor.class)) |
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class)); |
||||
} |
||||
|
||||
@Test |
||||
void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { |
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(BindableService.class)) |
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class)); |
||||
} |
||||
|
||||
@Test |
||||
void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() { |
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)) |
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class)); |
||||
} |
||||
|
||||
@Test |
||||
void whenSpringGrpcAndSpringSecurityPresentAndUsingGrpcServletCreatesGrpcSecurity() { |
||||
new WebApplicationContextRunner() |
||||
.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) |
||||
.withConfiguration(AutoConfigurations.of(GrpcServerSecurityAutoConfiguration.class)) |
||||
.withUserConfiguration(ServletConfiguration.class) |
||||
.run((context) -> { |
||||
assertThat(context).hasSingleBean(SecurityContextServerInterceptor.class); |
||||
assertThat(context).hasSingleBean(GrpcServerExecutorProvider.class); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void whenSpringGrpcAndSpringSecurityPresentAndUsingGrpcNativeCreatesGrpcSecurity() { |
||||
new ApplicationContextRunner() |
||||
.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) |
||||
.withConfiguration(AutoConfigurations.of(GrpcServerSecurityAutoConfiguration.class)) |
||||
.withUserConfiguration(NativeConfiguration.class) |
||||
.run((context) -> assertThat(context).hasSingleBean(GrpcSecurity.class)); |
||||
} |
||||
|
||||
@Test |
||||
void whenServerEnabledPropertySetFalseThenAutoConfigurationIsSkipped() { |
||||
this.contextRunner.withPropertyValues("spring.grpc.server.enabled=false") |
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class)); |
||||
} |
||||
|
||||
@Test |
||||
void whenServerEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() { |
||||
this.contextRunner |
||||
.run((context) -> assertThat(context).hasSingleBean(GrpcServerSecurityAutoConfiguration.class)); |
||||
} |
||||
|
||||
@Test |
||||
void whenServerEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() { |
||||
this.contextRunner.withPropertyValues("spring.grpc.server.enabled=true") |
||||
.run((context) -> assertThat(context).hasSingleBean(GrpcServerSecurityAutoConfiguration.class)); |
||||
} |
||||
|
||||
@Test |
||||
void grpcSecurityAutoConfiguredAsExpected() { |
||||
this.contextRunner.run((context) -> { |
||||
assertThat(context).getBean(GrpcExceptionHandler.class).isInstanceOf(SecurityGrpcExceptionHandler.class); |
||||
assertThat(context).getBean(AuthenticationProcessInterceptor.class).isNull(); |
||||
}); |
||||
} |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
@EnableWebSecurity |
||||
static class ServletConfiguration { |
||||
|
||||
@Bean |
||||
GrpcServletRegistration grpcServletRegistration() { |
||||
return new GrpcServletRegistration(mock(), mock()); |
||||
} |
||||
|
||||
@Bean |
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) { |
||||
return http.authorizeHttpRequests((requests) -> requests.anyRequest().permitAll()).build(); |
||||
} |
||||
|
||||
} |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
@EnableMethodSecurity |
||||
static class NativeConfiguration { |
||||
|
||||
@Bean |
||||
GrpcServerFactory grpcServerFactory() { |
||||
return mock(); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,114 @@
@@ -0,0 +1,114 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security.web.reactive; |
||||
|
||||
import io.grpc.BindableService; |
||||
import io.grpc.ServerServiceDefinition; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.boot.grpc.server.autoconfigure.security.web.reactive.GrpcRequest.GrpcReactiveRequestMatcher; |
||||
import org.springframework.boot.web.context.reactive.GenericReactiveWebApplicationContext; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer; |
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer; |
||||
import org.springframework.http.codec.ServerCodecConfigurer; |
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest; |
||||
import org.springframework.mock.http.server.reactive.MockServerHttpResponse; |
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult; |
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange; |
||||
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; |
||||
import org.springframework.web.server.session.DefaultWebSessionManager; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.BDDMockito.given; |
||||
import static org.mockito.Mockito.mock; |
||||
|
||||
/** |
||||
* Tests for {@link GrpcRequest}. |
||||
* |
||||
* @author Dave Syer |
||||
* @author Phillip Webb |
||||
*/ |
||||
class GrpcRequestTests { |
||||
|
||||
private GenericReactiveWebApplicationContext context = new GenericReactiveWebApplicationContext(); |
||||
|
||||
@BeforeEach |
||||
void setup() { |
||||
MockService service1 = mock(); |
||||
given(service1.bindService()).willReturn(ServerServiceDefinition.builder("my-service").build()); |
||||
MockService service2 = mock(); |
||||
given(service2.bindService()).willReturn(ServerServiceDefinition.builder("my-other-service").build()); |
||||
this.context.registerBean("s1", BindableService.class, () -> service1); |
||||
this.context.registerBean("s2", BindableService.class, () -> service2); |
||||
this.context.registerBean(GrpcServiceDiscoverer.class, () -> new DefaultGrpcServiceDiscoverer(this.context)); |
||||
this.context.refresh(); |
||||
} |
||||
|
||||
@Test |
||||
void whenToAnyService() { |
||||
GrpcReactiveRequestMatcher matcher = GrpcRequest.toAnyService(); |
||||
assertThat(isMatch(matcher, "/my-service/Method")).isTrue(); |
||||
assertThat(isMatch(matcher, "/my-service/Other")).isTrue(); |
||||
assertThat(isMatch(matcher, "/my-other-service/Other")).isTrue(); |
||||
assertThat(isMatch(matcher, "/notaservice")).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void whenToAnyServiceWithExclude() { |
||||
GrpcReactiveRequestMatcher matcher = GrpcRequest.toAnyService().excluding("my-other-service"); |
||||
assertThat(isMatch(matcher, "/my-service/Method")).isTrue(); |
||||
assertThat(isMatch(matcher, "/my-service/Other")).isTrue(); |
||||
assertThat(isMatch(matcher, "/my-other-service/Other")).isFalse(); |
||||
assertThat(isMatch(matcher, "/notaservice")).isFalse(); |
||||
} |
||||
|
||||
private boolean isMatch(GrpcReactiveRequestMatcher matcher, String path) { |
||||
MockExchange request = mockRequest(path); |
||||
MatchResult result = matcher.matches(request).block(); |
||||
return (result != null) && result.isMatch(); |
||||
} |
||||
|
||||
private MockExchange mockRequest(String path) { |
||||
MockServerHttpRequest servletContext = MockServerHttpRequest.get(path).build(); |
||||
MockExchange request = new MockExchange(servletContext, this.context); |
||||
return request; |
||||
} |
||||
|
||||
interface MockService extends BindableService { |
||||
|
||||
} |
||||
|
||||
static class MockExchange extends DefaultServerWebExchange { |
||||
|
||||
private ApplicationContext context; |
||||
|
||||
MockExchange(MockServerHttpRequest request, ApplicationContext context) { |
||||
super(request, new MockServerHttpResponse(), new DefaultWebSessionManager(), ServerCodecConfigurer.create(), |
||||
new AcceptHeaderLocaleContextResolver()); |
||||
this.context = context; |
||||
} |
||||
|
||||
@Override |
||||
public ApplicationContext getApplicationContext() { |
||||
return this.context; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security.web.servlet; |
||||
|
||||
import io.grpc.BindableService; |
||||
import io.grpc.ServerServiceDefinition; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest.GrpcServletRequestMatcher; |
||||
import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer; |
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer; |
||||
import org.springframework.mock.web.MockHttpServletRequest; |
||||
import org.springframework.mock.web.MockServletContext; |
||||
import org.springframework.web.context.WebApplicationContext; |
||||
import org.springframework.web.context.support.GenericWebApplicationContext; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.BDDMockito.given; |
||||
import static org.mockito.Mockito.mock; |
||||
|
||||
/** |
||||
* Test for {@link GrpcRequest}. |
||||
* |
||||
* @author Dave Syer |
||||
* @author Phillip Webb |
||||
*/ |
||||
class GrpcRequestTests { |
||||
|
||||
private GenericWebApplicationContext context = new GenericWebApplicationContext(); |
||||
|
||||
@BeforeEach |
||||
void setup() { |
||||
MockService service1 = mock(); |
||||
given(service1.bindService()).willReturn(ServerServiceDefinition.builder("my-service").build()); |
||||
MockService service2 = mock(); |
||||
given(service2.bindService()).willReturn(ServerServiceDefinition.builder("my-other-service").build()); |
||||
this.context.registerBean("s1", BindableService.class, () -> service1); |
||||
this.context.registerBean("s2", BindableService.class, () -> service2); |
||||
this.context.registerBean(GrpcServiceDiscoverer.class, () -> new DefaultGrpcServiceDiscoverer(this.context)); |
||||
this.context.refresh(); |
||||
} |
||||
|
||||
@Test |
||||
void whenToAnyService() { |
||||
GrpcServletRequestMatcher matcher = GrpcRequest.toAnyService(); |
||||
assertThat(isMatch(matcher, "/my-service/Method")).isTrue(); |
||||
assertThat(isMatch(matcher, "/my-service/Other")).isTrue(); |
||||
assertThat(isMatch(matcher, "/my-other-service/Other")).isTrue(); |
||||
assertThat(isMatch(matcher, "/notaservice")).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void whenToAnyServiceWithExclude() { |
||||
GrpcServletRequestMatcher matcher = GrpcRequest.toAnyService().excluding("my-other-service"); |
||||
assertThat(isMatch(matcher, "/my-service/Method")).isTrue(); |
||||
assertThat(isMatch(matcher, "/my-service/Other")).isTrue(); |
||||
assertThat(isMatch(matcher, "/my-other-service/Other")).isFalse(); |
||||
assertThat(isMatch(matcher, "/notaservice")).isFalse(); |
||||
} |
||||
|
||||
private boolean isMatch(GrpcServletRequestMatcher matcher, String path) { |
||||
MockHttpServletRequest request = mockRequest(path); |
||||
return matcher.matches(request); |
||||
} |
||||
|
||||
private MockHttpServletRequest mockRequest(String path) { |
||||
MockServletContext servletContext = new MockServletContext(); |
||||
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); |
||||
MockHttpServletRequest request = new MockHttpServletRequest(servletContext); |
||||
request.setRequestURI(path); |
||||
return request; |
||||
} |
||||
|
||||
interface MockService extends BindableService { |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,68 @@
@@ -0,0 +1,68 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the License); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
plugins { |
||||
id "java" |
||||
id "com.google.protobuf" version "${protobufGradlePluginVersion}" |
||||
id "org.springframework.boot.docker-test" |
||||
} |
||||
|
||||
description = "Spring Boot gRPC server oauth smoke test" |
||||
|
||||
dependencies { |
||||
implementation(project(":starter:spring-boot-starter-oauth2-authorization-server")) |
||||
implementation(project(":starter:spring-boot-starter-oauth2-resource-server")) |
||||
implementation(project(":starter:spring-boot-starter-grpc-server")) |
||||
|
||||
testImplementation(project(":starter:spring-boot-starter-grpc-client")) |
||||
testImplementation(project(":starter:spring-boot-starter-grpc-test")) |
||||
testImplementation(project(":starter:spring-boot-starter-oauth2-client")) |
||||
testImplementation(project(":starter:spring-boot-starter-test")) |
||||
} |
||||
|
||||
def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom") |
||||
def grpcJava = dependenciesBom.getLibrary("Grpc Java") |
||||
def protobufJava = dependenciesBom.getLibrary("Protobuf Java") |
||||
|
||||
tasks.named("compileTestJava") { |
||||
options.nullability.checking = "tests" |
||||
} |
||||
|
||||
nullability { |
||||
requireExplicitNullMarking = false |
||||
} |
||||
|
||||
configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all { |
||||
extendsFrom(configurations.dependencyManagement) |
||||
} |
||||
|
||||
protobuf { |
||||
protoc { |
||||
artifact = "com.google.protobuf:protoc:${protobufJava.version}" |
||||
} |
||||
plugins { |
||||
grpc { |
||||
artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}" |
||||
} |
||||
} |
||||
generateProtoTasks { |
||||
all()*.plugins { |
||||
grpc { |
||||
option '@generated=omit' |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package smoketest.grpcserveroauth; |
||||
|
||||
import io.grpc.stub.StreamObserver; |
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import smoketest.grpcserveroauth.proto.HelloReply; |
||||
import smoketest.grpcserveroauth.proto.HelloRequest; |
||||
import smoketest.grpcserveroauth.proto.HelloWorldGrpc; |
||||
|
||||
import org.springframework.stereotype.Service; |
||||
import org.springframework.util.Assert; |
||||
|
||||
@Service |
||||
public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase { |
||||
|
||||
private static Log logger = LogFactory.getLog(HelloWorldService.class); |
||||
|
||||
@Override |
||||
public void sayHelloProfileScope(HelloRequest request, StreamObserver<HelloReply> responseObserver) { |
||||
sayHello("sayHelloProfileScope", request, responseObserver); |
||||
} |
||||
|
||||
@Override |
||||
public void sayHelloEmailScope(HelloRequest request, StreamObserver<HelloReply> responseObserver) { |
||||
sayHello("sayHelloEmailScope", request, responseObserver); |
||||
} |
||||
|
||||
@Override |
||||
public void sayHelloAuthenticated(HelloRequest request, StreamObserver<HelloReply> responseObserver) { |
||||
sayHello("sayHelloAuthenticated", request, responseObserver); |
||||
} |
||||
|
||||
public void sayHello(String methodName, HelloRequest request, StreamObserver<HelloReply> responseObserver) { |
||||
String name = request.getName(); |
||||
logger.info(methodName + " " + name); |
||||
Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name); |
||||
Assert.state(!name.startsWith("internal"), "Internal error"); |
||||
String message = "%s '%s'".formatted(methodName, name); |
||||
HelloReply reply = HelloReply.newBuilder().setMessage(message).build(); |
||||
responseObserver.onNext(reply); |
||||
responseObserver.onCompleted(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package smoketest.grpcserveroauth; |
||||
|
||||
import org.springframework.boot.SpringApplication; |
||||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
||||
|
||||
@SpringBootApplication |
||||
public class SampleGrpcServerOAuthApplication { |
||||
|
||||
public static void main(String[] args) { |
||||
SpringApplication.run(SampleGrpcServerOAuthApplication.class, args); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package smoketest.grpcserveroauth; |
||||
|
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.grpc.server.GlobalServerInterceptor; |
||||
import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; |
||||
import org.springframework.grpc.server.security.GrpcSecurity; |
||||
import org.springframework.grpc.server.security.RequestMapperConfigurer; |
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||
import org.springframework.security.web.SecurityFilterChain; |
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults; |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
public class SecurityConfiguration { |
||||
|
||||
@Bean |
||||
@GlobalServerInterceptor |
||||
AuthenticationProcessInterceptor securityInterceptor(GrpcSecurity grpcSecurity) throws Exception { |
||||
return grpcSecurity.authorizeRequests(this::authorizeGrpcRequests) |
||||
.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults())) |
||||
.build(); |
||||
} |
||||
|
||||
private void authorizeGrpcRequests(RequestMapperConfigurer requests) { |
||||
requests.methods("HelloWorld/SayHelloProfileScope").hasAuthority("SCOPE_profile"); |
||||
requests.methods("HelloWorld/SayHelloEmailScope").hasAuthority("SCOPE_email"); |
||||
requests.methods("HelloWorld/SayHelloAuthenticated").authenticated(); |
||||
requests.methods("grpc.*/*").permitAll(); |
||||
requests.allRequests().denyAll(); |
||||
} |
||||
|
||||
@Bean |
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) { |
||||
http.oauth2AuthorizationServer((authorizationServer) -> { |
||||
http.securityMatcher(authorizationServer.getEndpointsMatcher()); |
||||
authorizationServer.oidc(withDefaults()); |
||||
}); |
||||
http.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults())); |
||||
http.authorizeHttpRequests((requests) -> requests.anyRequest().permitAll()); |
||||
http.csrf((csrf) -> csrf.disable()); |
||||
return http.build(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
@NullMarked |
||||
package smoketest.grpcserveroauth; |
||||
|
||||
import org.jspecify.annotations.NullMarked; |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
syntax = "proto3"; |
||||
|
||||
option java_package = "smoketest.grpcserveroauth.proto"; |
||||
option java_multiple_files = true; |
||||
|
||||
service HelloWorld { |
||||
rpc SayHelloProfileScope (HelloRequest) returns (HelloReply) {} |
||||
rpc SayHelloEmailScope (HelloRequest) returns (HelloReply) {} |
||||
rpc SayHelloAuthenticated (HelloRequest) returns (HelloReply) {} |
||||
} |
||||
|
||||
message HelloRequest { |
||||
string name = 1; |
||||
} |
||||
|
||||
message HelloReply { |
||||
string message = 1; |
||||
} |
||||
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
logging.level.org.springframework.security: TRACE |
||||
spring: |
||||
security: |
||||
oauth2: |
||||
authorizationserver: |
||||
client: |
||||
oidc-client: |
||||
registration: |
||||
client-id: "spring" |
||||
client-secret: "{noop}secret" |
||||
client-authentication-methods: |
||||
- "client_secret_basic" |
||||
authorization-grant-types: |
||||
- "client_credentials" |
||||
- "refresh_token" |
||||
scopes: |
||||
- "openid" |
||||
- "profile" |
||||
resourceserver: |
||||
jwt: |
||||
jwk-set-uri: http://localhost:8080/oauth2/jwks |
||||
@ -0,0 +1,220 @@
@@ -0,0 +1,220 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package smoketest.grpcserveroauth; |
||||
|
||||
import java.util.function.Consumer; |
||||
import java.util.function.Function; |
||||
import java.util.function.Supplier; |
||||
|
||||
import io.grpc.ManagedChannelBuilder; |
||||
import io.grpc.Status.Code; |
||||
import io.grpc.StatusRuntimeException; |
||||
import io.grpc.reflection.v1.ServerReflectionGrpc.ServerReflectionStub; |
||||
import io.grpc.reflection.v1.ServerReflectionRequest; |
||||
import io.grpc.reflection.v1.ServerReflectionResponse; |
||||
import io.grpc.stub.StreamObserver; |
||||
import org.awaitility.Awaitility; |
||||
import org.jspecify.annotations.Nullable; |
||||
import org.junit.jupiter.api.Test; |
||||
import smoketest.grpcserveroauth.proto.HelloReply; |
||||
import smoketest.grpcserveroauth.proto.HelloRequest; |
||||
import smoketest.grpcserveroauth.proto.HelloWorldGrpc.HelloWorldBlockingStub; |
||||
|
||||
import org.springframework.beans.factory.ObjectProvider; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.beans.factory.annotation.Qualifier; |
||||
import org.springframework.boot.test.context.SpringBootTest; |
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; |
||||
import org.springframework.boot.test.context.TestConfiguration; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; |
||||
import org.springframework.grpc.client.ImportGrpcClients; |
||||
import org.springframework.grpc.client.interceptor.security.BearerTokenAuthenticationInterceptor; |
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; |
||||
import org.springframework.security.oauth2.client.endpoint.RestClientClientCredentialsTokenResponseClient; |
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration; |
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; |
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; |
||||
import org.springframework.security.oauth2.client.registration.SupplierClientRegistrationRepository; |
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType; |
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; |
||||
import org.springframework.security.oauth2.jwt.JwtDecoder; |
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; |
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder; |
||||
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; |
||||
import org.springframework.test.annotation.DirtiesContext; |
||||
import org.springframework.util.function.SingletonSupplier; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, |
||||
properties = { "spring.grpc.server.port=0", |
||||
"spring.grpc.client.channel.default.target=static://localhost:${local.grpc.server.port}", |
||||
"spring.grpc.client.channel.oauth.target=static://localhost:${local.grpc.server.port}" }) |
||||
@DirtiesContext |
||||
class SampleGrpcServerOAuthApplicationTests { |
||||
|
||||
@Autowired |
||||
@Qualifier("unauthenticatedHelloWorldBlockingStub") |
||||
private HelloWorldBlockingStub unuathenticated; |
||||
|
||||
@Autowired |
||||
@Qualifier("oauthHelloWorldBlockingStub") |
||||
private HelloWorldBlockingStub oauth; |
||||
|
||||
@Autowired |
||||
private ServerReflectionStub reflection; |
||||
|
||||
@Test |
||||
void whenUnauthenticatedStub() { |
||||
HelloWorldBlockingStub stub = this.unuathenticated; |
||||
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloProfileScope)) |
||||
.satisfies(statusCode(Code.UNAUTHENTICATED)); |
||||
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloEmailScope)) |
||||
.satisfies(statusCode(Code.UNAUTHENTICATED)); |
||||
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAuthenticated)) |
||||
.satisfies(statusCode(Code.UNAUTHENTICATED)); |
||||
assertCanInvokeReflection(); |
||||
} |
||||
|
||||
@Test |
||||
void whenOAuth() { |
||||
HelloWorldBlockingStub stub = this.oauth; |
||||
assertThat(invoke(stub::sayHelloProfileScope)).isEqualTo("sayHelloProfileScope 'Spring'"); |
||||
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloEmailScope)) |
||||
.satisfies(statusCode(Code.PERMISSION_DENIED)); |
||||
assertThat(invoke(stub::sayHelloAuthenticated)).isEqualTo("sayHelloAuthenticated 'Spring'"); |
||||
assertCanInvokeReflection(); |
||||
} |
||||
|
||||
private String invoke(Function<HelloRequest, HelloReply> method) { |
||||
HelloRequest request = HelloRequest.newBuilder().setName("Spring").build(); |
||||
return method.apply(request).getMessage(); |
||||
} |
||||
|
||||
private void assertCanInvokeReflection() { |
||||
ObservedResponse<ServerReflectionResponse> response = invokeReflection(); |
||||
assertThat(response.getValue()).isNotNull(); |
||||
assertThat(response.getError()).isNull(); |
||||
} |
||||
|
||||
private ObservedResponse<ServerReflectionResponse> invokeReflection() { |
||||
ObservedResponse<ServerReflectionResponse> response = new ObservedResponse<>(); |
||||
StreamObserver<ServerReflectionRequest> request = this.reflection.serverReflectionInfo(response); |
||||
request.onNext(ServerReflectionRequest.newBuilder().setListServices("").build()); |
||||
request.onCompleted(); |
||||
response.await(); |
||||
return response; |
||||
} |
||||
|
||||
private Consumer<StatusRuntimeException> statusCode(Code expected) { |
||||
return (ex) -> assertThat(ex).extracting("status.code").isEqualTo(expected); |
||||
} |
||||
|
||||
@TestConfiguration(proxyBeanMethods = false) |
||||
@ImportGrpcClients(types = ServerReflectionStub.class) |
||||
@ImportGrpcClients(prefix = "unauthenticated", types = HelloWorldBlockingStub.class) |
||||
@ImportGrpcClients(target = "oauth", prefix = "oauth", types = HelloWorldBlockingStub.class) |
||||
static class GrpcClientTestConfiguration { |
||||
|
||||
@Bean |
||||
<B extends ManagedChannelBuilder<B>> GrpcChannelBuilderCustomizer<B> channelSecurityCustomizer( |
||||
ObjectProvider<ClientRegistrationRepository> clientRegistrationRepository) { |
||||
return GrpcChannelBuilderCustomizer.matching("oauth", (builder) -> { |
||||
Supplier<String> tokenSupplier = SingletonSupplier |
||||
.of(() -> token(clientRegistrationRepository.getObject())); |
||||
builder.intercept(new BearerTokenAuthenticationInterceptor(tokenSupplier)); |
||||
}); |
||||
} |
||||
|
||||
private String token(ClientRegistrationRepository clientRegistrationRepository) { |
||||
RestClientClientCredentialsTokenResponseClient client = new RestClientClientCredentialsTokenResponseClient(); |
||||
ClientRegistration registration = clientRegistrationRepository.findByRegistrationId("spring"); |
||||
OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(registration); |
||||
return client.getTokenResponse(request).getAccessToken().getTokenValue(); |
||||
} |
||||
|
||||
@Bean |
||||
ClientRegistrationRepository lazyClientRegistrationRepository(Environment environment) { |
||||
return new SupplierClientRegistrationRepository(() -> getClientRegistrationRepository(environment)); |
||||
} |
||||
|
||||
private InMemoryClientRegistrationRepository getClientRegistrationRepository(Environment environment) { |
||||
return new InMemoryClientRegistrationRepository(ClientRegistration.withRegistrationId("spring") |
||||
.clientId("spring") |
||||
.clientSecret("secret") |
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) |
||||
.scope("profile") |
||||
.tokenUri(environment.resolvePlaceholders("http://localhost:${local.server.port}/oauth2/token")) |
||||
.build()); |
||||
} |
||||
|
||||
@Bean |
||||
SupplierJwtDecoder lazyJwtDecoder(Environment environment) { |
||||
return new SupplierJwtDecoder(() -> getJwtDecoder(environment)); |
||||
} |
||||
|
||||
private JwtDecoder getJwtDecoder(Environment environment) { |
||||
JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder |
||||
.withJwkSetUri(environment.resolvePlaceholders("http://localhost:${local.server.port}/oauth2/jwks")); |
||||
builder.jwsAlgorithms((algorithms) -> algorithms.add(SignatureAlgorithm.from("RS256"))); |
||||
return builder.build(); |
||||
} |
||||
|
||||
} |
||||
|
||||
static class ObservedResponse<T> implements StreamObserver<T> { |
||||
|
||||
private volatile @Nullable T value; |
||||
|
||||
private volatile @Nullable Throwable error; |
||||
|
||||
@Override |
||||
public synchronized void onNext(T value) { |
||||
this.value = value; |
||||
} |
||||
|
||||
@Override |
||||
public synchronized void onError(Throwable error) { |
||||
this.error = error; |
||||
} |
||||
|
||||
@Override |
||||
public void onCompleted() { |
||||
} |
||||
|
||||
void await() { |
||||
Awaitility.await().until(this::hasResponse); |
||||
} |
||||
|
||||
private synchronized boolean hasResponse() { |
||||
return this.value != null || this.error != null; |
||||
} |
||||
|
||||
@Nullable T getValue() { |
||||
return this.value; |
||||
} |
||||
|
||||
@Nullable Throwable getError() { |
||||
return this.error; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the License); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
plugins { |
||||
id "java" |
||||
id "com.google.protobuf" version "${protobufGradlePluginVersion}" |
||||
id "org.springframework.boot.docker-test" |
||||
} |
||||
|
||||
description = "Spring Boot gRPC server secure smoke test" |
||||
|
||||
dependencies { |
||||
implementation(project(":starter:spring-boot-starter-grpc-server")) |
||||
implementation(project(":starter:spring-boot-starter-security")) |
||||
|
||||
testImplementation(project(":starter:spring-boot-starter-grpc-client")) |
||||
testImplementation(project(":starter:spring-boot-starter-grpc-test")) |
||||
testImplementation(project(":starter:spring-boot-starter-test")) |
||||
} |
||||
|
||||
def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom") |
||||
def grpcJava = dependenciesBom.getLibrary("Grpc Java") |
||||
def protobufJava = dependenciesBom.getLibrary("Protobuf Java") |
||||
|
||||
tasks.named("compileTestJava") { |
||||
options.nullability.checking = "tests" |
||||
} |
||||
|
||||
nullability { |
||||
requireExplicitNullMarking = false |
||||
} |
||||
|
||||
configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all { |
||||
extendsFrom(configurations.dependencyManagement) |
||||
} |
||||
|
||||
protobuf { |
||||
protoc { |
||||
artifact = "com.google.protobuf:protoc:${protobufJava.version}" |
||||
} |
||||
plugins { |
||||
grpc { |
||||
artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}" |
||||
} |
||||
} |
||||
generateProtoTasks { |
||||
all()*.plugins { |
||||
grpc { |
||||
option '@generated=omit' |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,68 @@
@@ -0,0 +1,68 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package smoketest.grpcserversecure; |
||||
|
||||
import io.grpc.stub.StreamObserver; |
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import smoketest.grpcserversecure.proto.HelloReply; |
||||
import smoketest.grpcserversecure.proto.HelloRequest; |
||||
import smoketest.grpcserversecure.proto.HelloWorldGrpc; |
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize; |
||||
import org.springframework.stereotype.Service; |
||||
import org.springframework.util.Assert; |
||||
|
||||
@Service |
||||
public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase { |
||||
|
||||
private static Log logger = LogFactory.getLog(HelloWorldService.class); |
||||
|
||||
@Override |
||||
public void sayHelloAdmin(HelloRequest request, StreamObserver<HelloReply> responseObserver) { |
||||
sayHello("sayHelloAdmin", request, responseObserver); |
||||
} |
||||
|
||||
@Override |
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')") |
||||
public void sayHelloAdminAnnotated(HelloRequest request, StreamObserver<HelloReply> responseObserver) { |
||||
sayHello("sayHelloAdminAnnotated", request, responseObserver); |
||||
} |
||||
|
||||
@Override |
||||
public void sayHelloUser(HelloRequest request, StreamObserver<HelloReply> responseObserver) { |
||||
sayHello("sayHelloUser", request, responseObserver); |
||||
} |
||||
|
||||
@Override |
||||
@PreAuthorize("hasAuthority('ROLE_USER')") |
||||
public void sayHelloUserAnnotated(HelloRequest request, StreamObserver<HelloReply> responseObserver) { |
||||
sayHello("sayHelloUserAnnotated", request, responseObserver); |
||||
} |
||||
|
||||
public void sayHello(String methodName, HelloRequest request, StreamObserver<HelloReply> responseObserver) { |
||||
String name = request.getName(); |
||||
logger.info(methodName + " " + name); |
||||
Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name); |
||||
Assert.state(!name.startsWith("internal"), "Internal error"); |
||||
String message = "%s '%s'".formatted(methodName, name); |
||||
HelloReply reply = HelloReply.newBuilder().setMessage(message).build(); |
||||
responseObserver.onNext(reply); |
||||
responseObserver.onCompleted(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package smoketest.grpcserversecure; |
||||
|
||||
import org.springframework.boot.SpringApplication; |
||||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
||||
|
||||
@SpringBootApplication |
||||
public class SampleGrpcServerSecureApplication { |
||||
|
||||
public static void main(String[] args) { |
||||
SpringApplication.run(SampleGrpcServerSecureApplication.class, args); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package smoketest.grpcserversecure; |
||||
|
||||
import io.grpc.ServerInterceptor; |
||||
|
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.grpc.server.GlobalServerInterceptor; |
||||
import org.springframework.grpc.server.security.GrpcSecurity; |
||||
import org.springframework.grpc.server.security.RequestMapperConfigurer; |
||||
import org.springframework.security.core.userdetails.User; |
||||
import org.springframework.security.core.userdetails.UserDetails; |
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager; |
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults; |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
public class SecurityConfiguration { |
||||
|
||||
@Bean |
||||
InMemoryUserDetailsManager inMemoryUserDetailsManager() { |
||||
UserDetails user = user("user", "{noop}userpassword", "ROLE_USER"); |
||||
UserDetails admin = user("admin", "{noop}adminpassword", "ROLE_ADMIN"); |
||||
return new InMemoryUserDetailsManager(user, admin); |
||||
} |
||||
|
||||
private UserDetails user(String username, String password, String authority) { |
||||
return User.withUsername(username).password(password).authorities(authority).build(); |
||||
} |
||||
|
||||
@Bean |
||||
@GlobalServerInterceptor |
||||
ServerInterceptor securityInterceptor(GrpcSecurity security) throws Exception { |
||||
return security.authorizeRequests(this::authorizeRequests) |
||||
.httpBasic(withDefaults()) |
||||
.preauth(withDefaults()) |
||||
.build(); |
||||
} |
||||
|
||||
private void authorizeRequests(RequestMapperConfigurer requests) { |
||||
requests.methods("HelloWorld/SayHelloAdmin").hasAuthority("ROLE_ADMIN"); |
||||
requests.methods("HelloWorld/SayHelloAdminAnnotated").hasAuthority("ROLE_ADMIN"); |
||||
requests.methods("HelloWorld/SayHelloUser").hasAuthority("ROLE_USER"); |
||||
requests.methods("HelloWorld/SayHelloUserAnnotated").hasAuthority("ROLE_USER"); |
||||
requests.methods("grpc.*/*").permitAll(); |
||||
requests.allRequests().denyAll(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
@NullMarked |
||||
package smoketest.grpcserversecure; |
||||
|
||||
import org.jspecify.annotations.NullMarked; |
||||
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
syntax = "proto3"; |
||||
|
||||
option java_package = "smoketest.grpcserversecure.proto"; |
||||
option java_multiple_files = true; |
||||
|
||||
service HelloWorld { |
||||
rpc SayHelloAdmin (HelloRequest) returns (HelloReply) {} |
||||
rpc SayHelloAdminAnnotated (HelloRequest) returns (HelloReply) {} |
||||
rpc SayHelloUser (HelloRequest) returns (HelloReply) {} |
||||
rpc SayHelloUserAnnotated (HelloRequest) returns (HelloReply) {} |
||||
} |
||||
|
||||
message HelloRequest { |
||||
string name = 1; |
||||
} |
||||
|
||||
message HelloReply { |
||||
string message = 1; |
||||
} |
||||
@ -0,0 +1,192 @@
@@ -0,0 +1,192 @@
|
||||
/* |
||||
* Copyright 2012-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package smoketest.grpcserversecure; |
||||
|
||||
import java.util.function.Consumer; |
||||
import java.util.function.Function; |
||||
|
||||
import io.grpc.ManagedChannelBuilder; |
||||
import io.grpc.Status.Code; |
||||
import io.grpc.StatusRuntimeException; |
||||
import io.grpc.reflection.v1.ServerReflectionGrpc.ServerReflectionStub; |
||||
import io.grpc.reflection.v1.ServerReflectionRequest; |
||||
import io.grpc.reflection.v1.ServerReflectionResponse; |
||||
import io.grpc.stub.StreamObserver; |
||||
import org.awaitility.Awaitility; |
||||
import org.jspecify.annotations.Nullable; |
||||
import org.junit.jupiter.api.Test; |
||||
import smoketest.grpcserversecure.proto.HelloReply; |
||||
import smoketest.grpcserversecure.proto.HelloRequest; |
||||
import smoketest.grpcserversecure.proto.HelloWorldGrpc.HelloWorldBlockingStub; |
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.beans.factory.annotation.Qualifier; |
||||
import org.springframework.boot.test.context.SpringBootTest; |
||||
import org.springframework.boot.test.context.TestConfiguration; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; |
||||
import org.springframework.grpc.client.ImportGrpcClients; |
||||
import org.springframework.grpc.client.interceptor.security.BasicAuthenticationInterceptor; |
||||
import org.springframework.test.annotation.DirtiesContext; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||
|
||||
@SpringBootTest(properties = { "spring.grpc.server.port=0", |
||||
"spring.grpc.client.channel.default.target=static://localhost:${local.grpc.server.port}", |
||||
"spring.grpc.client.channel.user.target=static://localhost:${local.grpc.server.port}", |
||||
"spring.grpc.client.channel.admin.target=static://localhost:${local.grpc.server.port}" }) |
||||
@DirtiesContext |
||||
class SampleGrpcServerSecureApplicationTests { |
||||
|
||||
@Autowired |
||||
@Qualifier("unauthenticatedHelloWorldBlockingStub") |
||||
private HelloWorldBlockingStub unuathenticated; |
||||
|
||||
@Autowired |
||||
@Qualifier("userAuthenticatedHelloWorldBlockingStub") |
||||
private HelloWorldBlockingStub userAuthenticated; |
||||
|
||||
@Autowired |
||||
@Qualifier("adminAuthenticatedHelloWorldBlockingStub") |
||||
private HelloWorldBlockingStub adminAuthenticated; |
||||
|
||||
@Autowired |
||||
private ServerReflectionStub reflection; |
||||
|
||||
@Test |
||||
void whenUnauthenticatedStub() { |
||||
HelloWorldBlockingStub stub = this.unuathenticated; |
||||
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUser)) |
||||
.satisfies(statusCode(Code.UNAUTHENTICATED)); |
||||
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUserAnnotated)) |
||||
.satisfies(statusCode(Code.UNAUTHENTICATED)); |
||||
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdmin)) |
||||
.satisfies(statusCode(Code.UNAUTHENTICATED)); |
||||
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdminAnnotated)) |
||||
.satisfies(statusCode(Code.UNAUTHENTICATED)); |
||||
assertCanInvokeReflection(); |
||||
} |
||||
|
||||
@Test |
||||
void whenUserAuthenticatedStub() { |
||||
HelloWorldBlockingStub stub = this.userAuthenticated; |
||||
assertThat(invoke(stub::sayHelloUser)).isEqualTo("sayHelloUser 'Spring'"); |
||||
assertThat(invoke(stub::sayHelloUserAnnotated)).isEqualTo("sayHelloUserAnnotated 'Spring'"); |
||||
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdmin)) |
||||
.satisfies(statusCode(Code.PERMISSION_DENIED)); |
||||
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdminAnnotated)) |
||||
.satisfies(statusCode(Code.PERMISSION_DENIED)); |
||||
assertCanInvokeReflection(); |
||||
} |
||||
|
||||
@Test |
||||
void whenAdminAuthenticatedStub() { |
||||
HelloWorldBlockingStub stub = this.adminAuthenticated; |
||||
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUser)) |
||||
.satisfies(statusCode(Code.PERMISSION_DENIED)); |
||||
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUserAnnotated)) |
||||
.satisfies(statusCode(Code.PERMISSION_DENIED)); |
||||
assertThat(invoke(stub::sayHelloAdmin)).isEqualTo("sayHelloAdmin 'Spring'"); |
||||
assertThat(invoke(stub::sayHelloAdminAnnotated)).isEqualTo("sayHelloAdminAnnotated 'Spring'"); |
||||
assertCanInvokeReflection(); |
||||
} |
||||
|
||||
private String invoke(Function<HelloRequest, HelloReply> method) { |
||||
HelloRequest request = HelloRequest.newBuilder().setName("Spring").build(); |
||||
return method.apply(request).getMessage(); |
||||
} |
||||
|
||||
private void assertCanInvokeReflection() { |
||||
ObservedResponse<ServerReflectionResponse> response = invokeReflection(); |
||||
assertThat(response.getValue()).isNotNull(); |
||||
assertThat(response.getError()).isNull(); |
||||
} |
||||
|
||||
private ObservedResponse<ServerReflectionResponse> invokeReflection() { |
||||
ObservedResponse<ServerReflectionResponse> response = new ObservedResponse<>(); |
||||
StreamObserver<ServerReflectionRequest> request = this.reflection.serverReflectionInfo(response); |
||||
request.onNext(ServerReflectionRequest.newBuilder().setListServices("").build()); |
||||
request.onCompleted(); |
||||
response.await(); |
||||
return response; |
||||
} |
||||
|
||||
private Consumer<StatusRuntimeException> statusCode(Code expected) { |
||||
return (ex) -> assertThat(ex).extracting("status.code").isEqualTo(expected); |
||||
} |
||||
|
||||
@TestConfiguration(proxyBeanMethods = false) |
||||
@ImportGrpcClients(types = ServerReflectionStub.class) |
||||
@ImportGrpcClients(prefix = "unauthenticated", types = HelloWorldBlockingStub.class) |
||||
@ImportGrpcClients(target = "user", prefix = "userAuthenticated", types = HelloWorldBlockingStub.class) |
||||
@ImportGrpcClients(target = "admin", prefix = "adminAuthenticated", types = HelloWorldBlockingStub.class) |
||||
static class GrpcClientTestConfiguration { |
||||
|
||||
@Bean |
||||
<B extends ManagedChannelBuilder<B>> GrpcChannelBuilderCustomizer<B> channelSecurityCustomizer() { |
||||
return (target, builder) -> { |
||||
if ("user".equals(target)) { |
||||
builder.intercept(new BasicAuthenticationInterceptor("user", "userpassword")); |
||||
} |
||||
if ("admin".equals(target)) { |
||||
builder.intercept(new BasicAuthenticationInterceptor("admin", "adminpassword")); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
} |
||||
|
||||
static class ObservedResponse<T> implements StreamObserver<T> { |
||||
|
||||
private volatile @Nullable T value; |
||||
|
||||
private volatile @Nullable Throwable error; |
||||
|
||||
@Override |
||||
public synchronized void onNext(T value) { |
||||
this.value = value; |
||||
} |
||||
|
||||
@Override |
||||
public synchronized void onError(Throwable error) { |
||||
this.error = error; |
||||
} |
||||
|
||||
@Override |
||||
public void onCompleted() { |
||||
} |
||||
|
||||
void await() { |
||||
Awaitility.await().until(this::hasResponse); |
||||
} |
||||
|
||||
private synchronized boolean hasResponse() { |
||||
return this.value != null || this.error != null; |
||||
} |
||||
|
||||
@Nullable T getValue() { |
||||
return this.value; |
||||
} |
||||
|
||||
@Nullable Throwable getError() { |
||||
return this.error; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue