Add authorize requests filters

This commit is contained in:
Dave Syer
2025-01-22 12:16:16 +00:00
parent aa8d680c3f
commit ff40f5b323
15 changed files with 282 additions and 74 deletions

View File

@@ -1,7 +1,5 @@
package org.springframework.grpc.sample;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
@@ -16,7 +14,6 @@ import org.springframework.grpc.sample.proto.HelloReply;
import org.springframework.grpc.sample.proto.HelloRequest;
import org.springframework.grpc.sample.proto.ReactorSimpleGrpc;
import org.springframework.grpc.sample.proto.ReactorSimpleGrpc.ReactorSimpleStub;
import org.springframework.grpc.sample.proto.SimpleGrpc;
import org.springframework.grpc.test.AutoConfigureInProcessTransport;
import org.springframework.grpc.test.LocalGrpcPort;
import org.springframework.test.annotation.DirtiesContext;

View File

@@ -1,13 +1,13 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.0'
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.6'
id 'org.graalvm.buildtools.native' version '0.10.3'
id 'com.google.protobuf' version '0.9.4'
}
group = 'com.example'
version = '0.3.0-SNAPSHOT'
version = '0.4.0-SNAPSHOT'
java {
toolchain {
@@ -24,7 +24,7 @@ repositories {
dependencyManagement {
imports {
mavenBom 'org.springframework.grpc:spring-grpc-dependencies:0.3.0-SNAPSHOT'
mavenBom 'org.springframework.grpc:spring-grpc-dependencies:0.4.0-SNAPSHOT'
}
}

View File

@@ -6,12 +6,12 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<version>3.4.1</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>org.springframework.grpc</groupId>
<artifactId>grpc-secure-sample</artifactId>
<version>0.3.0-SNAPSHOT</version>
<version>0.4.0-SNAPSHOT</version>
<name>Spring gRPC Server Sample</name>
<description>Demo project for Spring gRPC</description>
<url />
@@ -38,7 +38,7 @@
<dependency>
<groupId>org.springframework.grpc</groupId>
<artifactId>spring-grpc-dependencies</artifactId>
<version>0.3.0-SNAPSHOT</version>
<version>0.4.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>

View File

@@ -40,21 +40,23 @@ public class GrpcServerApplication {
@GlobalServerInterceptor
public ServerInterceptor securityInterceptor(GrpcSecurity security) throws Exception {
return security
.authorizeRequests(requests -> requests
.methods("Simple/StreamHello").hasAuthority("ROLE_ADMIN")
.methods("Simple/SayHello").hasAuthority("ROLE_USER")
.allRequests().permitAll())
.httpBasic(withDefaults())
.preauth(withDefaults())
.authenticationExtractor((headers, attributes) -> {
String user = headers.get(USER_KEY);
if (user != null) {
return new PreAuthenticatedAuthenticationToken(user, "N/A",
AuthorityUtils.createAuthorityList("ROLE_" + user.toUpperCase()));
}
return null;
})
.build();
.authorizeRequests(requests -> requests.methods("Simple/StreamHello")
.hasAuthority("ROLE_ADMIN")
.methods("Simple/SayHello")
.hasAuthority("ROLE_USER")
.allRequests()
.permitAll())
.httpBasic(withDefaults())
.preauth(withDefaults())
.authenticationExtractor((headers, attributes) -> {
String user = headers.get(USER_KEY);
if (user != null) {
return new PreAuthenticatedAuthenticationToken(user, "N/A",
AuthorityUtils.createAuthorityList("ROLE_" + user.toUpperCase()));
}
return null;
})
.build();
}

View File

@@ -38,8 +38,7 @@ import io.grpc.StatusRuntimeException;
public class GrpcServerApplicationTests {
public static void main(String[] args) {
new SpringApplicationBuilder(GrpcServerApplication.class, ExtraConfiguration.class)
.run(args);
new SpringApplicationBuilder(GrpcServerApplication.class, ExtraConfiguration.class).run(args);
}
@Autowired
@@ -115,7 +114,7 @@ public class GrpcServerApplicationTests {
@Lazy
SimpleGrpc.SimpleBlockingStub basic(GrpcChannelFactory channels) {
return SimpleGrpc.newBlockingStub(channels.createChannel("basic", ChannelBuilderOptions.defaults()
.withInterceptors(List.of(new BasicAuthenticationInterceptor("user", "user")))));
.withInterceptors(List.of(new BasicAuthenticationInterceptor("user", "user")))));
}
@Bean

View File

@@ -17,6 +17,7 @@
<modules>
<module>grpc-server</module>
<module>grpc-secure</module>
<module>grpc-reactive</module>
<module>grpc-server-netty-shaded</module>
<module>grpc-tomcat</module>

View File

@@ -32,6 +32,11 @@
<artifactId>spring-security-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>

View File

@@ -19,6 +19,7 @@ import org.springframework.core.Ordered;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
@@ -33,12 +34,9 @@ import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
/**
* An interceptor that extracts the authentication credentials from the gRPC
* request
* headers and metadata, authenticates the user, and sets the authentication in
* the
* SecurityContext. This interceptor should be registered with the gRPC server
* to handle
* An interceptor that extracts the authentication credentials from the gRPC request
* headers and metadata, authenticates the user, and sets the authentication in the
* SecurityContext. This interceptor should be registered with the gRPC server to handle
* authentication.
*
* @author Dave Syer
@@ -49,7 +47,7 @@ public class AuthenticationServerInterceptor implements ServerInterceptor, Order
private final GrpcAuthenticationExtractor extractor;
private final AuthorizationManager<CallContext> authorizationManager;
private AuthorizationManager<CallContext> authorizationManager;
@Override
public int getOrder() {
@@ -68,42 +66,51 @@ public class AuthenticationServerInterceptor implements ServerInterceptor, Order
ServerCallHandler<ReqT, RespT> next) {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication user = this.extractor.extract(headers, call.getAttributes());
Authentication authenticated;
if (user != null) {
authenticated = this.authenticationManager.authenticate(user);
} else {
authenticated = new AnonymousAuthenticationToken("anonymous", "anonymous",
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
user = this.authenticationManager.authenticate(user);
securityContext.setAuthentication(user);
}
securityContext.setAuthentication(authenticated);
CallContext context = new CallContext(headers, call.getAttributes(), call.getMethodDescriptor());
if (this.authorizationManager != null && authenticated != null) {
return new AuthenticationListener<ReqT>(next.startCall(call, headers), this.authorizationManager, context,
authenticated);
if (this.authorizationManager != null) {
if (user == null) {
user = new AnonymousAuthenticationToken("anonymous", "anonymous",
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
}
return new AuthenticatedListener<ReqT>(next.startCall(call, headers), this.authorizationManager,
new CallContext(headers, call.getAttributes(), call.getMethodDescriptor()), user);
}
return next.startCall(call, headers);
return new AuthenticatedListener<ReqT>(next.startCall(call, headers), null,
new CallContext(headers, call.getAttributes(), call.getMethodDescriptor()), user);
}
static class AuthenticationListener<ReqT> extends ForwardingServerCallListener<ReqT> {
static class AuthenticatedListener<ReqT> extends ForwardingServerCallListener<ReqT> {
private final Listener<ReqT> delegate;
private final AuthorizationManager<CallContext> authorizationManager;
private final CallContext context;
private final Authentication authentication;
AuthenticationListener(io.grpc.ServerCall.Listener<ReqT> delegate,
AuthorizationManager<CallContext> authorizationManager, CallContext context,
Authentication authenticated) {
private final AuthorizationManager<CallContext> authorizationManager;
AuthenticatedListener(io.grpc.ServerCall.Listener<ReqT> delegate,
AuthorizationManager<CallContext> authorizationManager, CallContext context, Authentication user) {
this.delegate = delegate;
this.authorizationManager = authorizationManager;
this.context = context;
this.authentication = authenticated;
this.authentication = user;
}
@Override
public void onReady() {
if (!this.authorizationManager.authorize(() -> authentication, this.context).isGranted()) {
throw new AccessDeniedException("not allowed");
if (this.authentication == null || !this.authentication.isAuthenticated()
|| this.authentication instanceof AnonymousAuthenticationToken) {
throw new BadCredentialsException("not authenticated");
}
else {
if (!this.authorizationManager.authorize(() -> this.authentication, this.context).isGranted()) {
throw new AccessDeniedException("not allowed");
}
}
super.onReady();
}

View File

@@ -1,3 +1,18 @@
/*
* Copyright 2024-2024 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.grpc.server.security;
import io.grpc.Attributes;
@@ -5,4 +20,4 @@ import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
public record CallContext(Metadata headers, Attributes attributes, MethodDescriptor<?, ?> method) {
}
}

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.grpc.server.security;
public interface CallMatcher {
interface CallMatcher {
CallMatcher ALL = (context) -> true;

View File

@@ -15,22 +15,187 @@
*/
package org.springframework.grpc.server.security;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ObservationAuthenticationManager;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.util.Assert;
import io.grpc.Attributes;
import io.grpc.Metadata;
import io.micrometer.observation.ObservationRegistry;
/**
* Defines constants and utilities for working with gRPC security.
* The <code>GrpcSecurity</code> class is responsible for configuring the security
* settings for a gRPC server. It provides methods to configure authentication providers,
* user details service, and authentication extractors.
*
* The class also defines some static constants, such as the
* <code>AUTHORIZATION_KEY</code> can be used in your security configuration.
*
* The class also provides various methods to configure different authentication
* mechanisms, such as pre-authentication, HTTP basic authentication, and custom
* authentication extractors.
*
* @author Dave Syer
*/
public final class GrpcSecurity {
public final class GrpcSecurity
extends AbstractConfiguredSecurityBuilder<AuthenticationServerInterceptor, GrpcSecurity> {
/**
* Constant for the Authorization header.
* A constant key used for storing and retrieving the "Authorization" header from gRPC
* metadata. This key is used to handle authorization information in gRPC requests.
*
* <p>
* The key is defined with the name "Authorization" and uses the ASCII string
* marshaller for encoding and decoding the header value.
* </p>
*/
public static final Metadata.Key<String> AUTHORIZATION_KEY = Metadata.Key.of("Authorization",
Metadata.ASCII_STRING_MARSHALLER);
private GrpcSecurity() {
/**
* The order value for the context filter in the gRPC security framework. This
* constant defines the position of the context filter in the filter chain. A lower
* value indicates higher precedence.
*/
public static final int CONTEXT_FILTER_ORDER = 0;
private AuthenticationManager authenticationManager;
private List<GrpcAuthenticationExtractor> authenticationExtractors = new ArrayList<>();
private AuthorizationManager<CallContext> authorizationManager;
public GrpcSecurity(ObjectPostProcessor<Object> objectPostProcessor,
AuthenticationManagerBuilder authenticationBuilder, ApplicationContext context) {
super(objectPostProcessor);
setSharedObject(AuthenticationManagerBuilder.class, authenticationBuilder);
setSharedObject(ApplicationContext.class, context);
}
private ObservationRegistry getObservationRegistry() {
ApplicationContext context = getContext();
String[] names = context.getBeanNamesForType(ObservationRegistry.class);
if (names.length == 1) {
return (ObservationRegistry) context.getBean(names[0]);
}
return ObservationRegistry.NOOP;
}
private ApplicationContext getContext() {
return getSharedObject(ApplicationContext.class);
}
@Override
protected AuthenticationServerInterceptor performBuild() throws Exception {
if (this.authenticationManager != null) {
setSharedObject(AuthenticationManager.class, this.authenticationManager);
}
else {
ObservationRegistry registry = getObservationRegistry();
AuthenticationManager manager = getAuthenticationRegistry().build();
if (!registry.isNoop() && manager != null) {
setSharedObject(AuthenticationManager.class, new ObservationAuthenticationManager(registry, manager));
}
else {
setSharedObject(AuthenticationManager.class, manager);
}
}
this.authenticationExtractors.sort(AnnotationAwareOrderComparator.INSTANCE);
return new AuthenticationServerInterceptor(getSharedObject(AuthenticationManager.class),
new CompositeAuthenticationExtractor(this.authenticationExtractors), this.authorizationManager);
}
public GrpcSecurity authenticationProvider(AuthenticationProvider authenticationProvider) {
getAuthenticationRegistry().authenticationProvider(authenticationProvider);
return this;
}
public GrpcSecurity userDetailsService(UserDetailsService userDetailsService) throws Exception {
getAuthenticationRegistry().userDetailsService(userDetailsService);
return this;
}
public GrpcSecurity preauth(Customizer<PreAuthConfigurer<GrpcSecurity>> customizer) throws Exception {
customizer.customize(getOrApply(new PreAuthConfigurer<>(getAuthenticationRegistry(), getContext())));
authenticationExtractor(new SslContextPreAuthenticationExtractor());
return this;
}
public GrpcSecurity httpBasic(Customizer<HttpBasicConfigurer<GrpcSecurity>> customizer) throws Exception {
customizer.customize(getOrApply(new HttpBasicConfigurer<>(getAuthenticationRegistry(), getContext())));
authenticationExtractor(new HttpBasicAuthenticationExtractor());
return this;
}
public GrpcSecurity authorizeRequests(Customizer<RequestMapperConfigurer> customizer) throws Exception {
customizer.customize(getOrApply(new RequestMapperConfigurer(getContext())));
return this;
}
@SuppressWarnings({ "unchecked", "removal" })
private <C extends SecurityConfigurerAdapter<AuthenticationServerInterceptor, GrpcSecurity>> C getOrApply(
C configurer) throws Exception {
C existingConfig = (C) getConfigurer(configurer.getClass());
if (existingConfig != null) {
return existingConfig;
}
return apply(configurer);
}
public GrpcSecurity authenticationManager(AuthenticationManager authenticationManager) {
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
this.authenticationManager = authenticationManager;
return this;
}
public GrpcSecurity authenticationExtractor(GrpcAuthenticationExtractor authenticationExtractor) {
Assert.notNull(authenticationExtractor, "authenticationExtractor cannot be null");
this.authenticationExtractors.add(authenticationExtractor);
return this;
}
public GrpcSecurity authorizationManager(AuthorizationManager<CallContext> authorizationManager) {
this.authorizationManager = authorizationManager;
return this;
}
private AuthenticationManagerBuilder getAuthenticationRegistry() {
return getSharedObject(AuthenticationManagerBuilder.class);
}
private static class CompositeAuthenticationExtractor implements GrpcAuthenticationExtractor {
private final List<GrpcAuthenticationExtractor> extractors;
CompositeAuthenticationExtractor(List<GrpcAuthenticationExtractor> extractors) {
this.extractors = extractors;
}
@Override
public Authentication extract(Metadata headers, Attributes attributes) {
for (GrpcAuthenticationExtractor extractor : this.extractors) {
Authentication authentication = extractor.extract(headers, attributes);
if (authentication != null) {
return authentication;
}
}
return null;
}
}
}

View File

@@ -32,8 +32,7 @@ import org.springframework.util.Assert;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.function.SingletonSupplier;
public class RequestMapperConfigurer
extends SecurityConfigurerAdapter<AuthenticationServerInterceptor, GrpcSecurity> {
public class RequestMapperConfigurer extends SecurityConfigurerAdapter<AuthenticationServerInterceptor, GrpcSecurity> {
private List<AuthorizedCall> authorizedCalls = new ArrayList<>();
@@ -41,8 +40,7 @@ public class RequestMapperConfigurer
public RequestMapperConfigurer(ApplicationContext context) {
this.roleHierarchy = SingletonSupplier.of(() -> (context.getBeanNamesForType(RoleHierarchy.class).length > 0)
? context.getBean(RoleHierarchy.class)
: new NullRoleHierarchy());
? context.getBean(RoleHierarchy.class) : new NullRoleHierarchy());
}
@Override
@@ -66,14 +64,15 @@ public class RequestMapperConfigurer
private String[] patterns;
public MethodCallMatcher(String... patterns) {
MethodCallMatcher(String... patterns) {
this.patterns = patterns;
}
@Override
public boolean matches(CallContext context) {
return PatternMatchUtils.simpleMatch(patterns, context.method().getFullMethodName());
return PatternMatchUtils.simpleMatch(this.patterns, context.method().getFullMethodName());
}
}
public class AuthorizedCall {
@@ -114,14 +113,11 @@ public class RequestMapperConfigurer
public RequestMapperConfigurer access(AuthorizationManager<Object> manager) {
Assert.notNull(manager, "manager cannot be null");
this.authorizationManager = (this.not)
? AuthorizationManagers.not(manager)
: manager;
this.authorizationManager = (this.not) ? AuthorizationManagers.not(manager) : manager;
return RequestMapperConfigurer.this;
}
private AuthorityAuthorizationManager<Object> withRoleHierarchy(
AuthorityAuthorizationManager<Object> manager) {
private AuthorityAuthorizationManager<Object> withRoleHierarchy(AuthorityAuthorizationManager<Object> manager) {
manager.setRoleHierarchy(RequestMapperConfigurer.this.roleHierarchy.get());
return manager;
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.grpc.server.security;
import org.springframework.core.Ordered;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -25,7 +26,12 @@ import io.grpc.ServerCall.Listener;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
public class SecurityContextServerInterceptor implements ServerInterceptor {
public class SecurityContextServerInterceptor implements ServerInterceptor, Ordered {
@Override
public int getOrder() {
return GrpcSecurity.CONTEXT_FILTER_ORDER;
}
@Override
public <ReqT, RespT> Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers,

View File

@@ -122,6 +122,6 @@
<scope>test</scope>
</dependency>
</dependencies>
</dependencies>
</project>

View File

@@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@@ -27,10 +28,12 @@ import org.springframework.grpc.autoconfigure.server.exception.GrpcExceptionHand
import org.springframework.grpc.server.GlobalServerInterceptor;
import org.springframework.grpc.server.ServerBuilderCustomizer;
import org.springframework.grpc.server.exception.GrpcExceptionHandler;
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.web.SecurityFilterChain;
import io.grpc.ServerBuilder;
@@ -50,10 +53,22 @@ public class GrpcSecurityAutoConfiguration {
}
@ConditionalOnBean(ObjectPostProcessor.class)
@Configuration(proxyBeanMethods = false)
@Conditional(GrpcServerFactoryAutoConfiguration.OnNativeGrpcServerCondition.class)
static class GrpcNativeSecurityConfigurerAutoConfiguration {
@Bean
public GrpcSecurity grpcSecurity(ObjectPostProcessor<Object> objectPostProcessor,
AuthenticationManagerBuilder authenticationManagerBuilder, ApplicationContext context) {
return new GrpcSecurity(objectPostProcessor, authenticationManagerBuilder, context);
}
}
@ConditionalOnBean(SecurityFilterChain.class)
@Configuration(proxyBeanMethods = false)
@Conditional(GrpcServerFactoryAutoConfiguration.OnGrpcServletCondition.class)
static class GrpcSecurityConfigurerAutoConfiguration {
static class GrpcServletSecurityConfigurerAutoConfiguration {
@Bean
@GlobalServerInterceptor