Add support for events and anonymous

This commit is contained in:
Dave Syer
2025-01-23 13:03:07 +00:00
parent ff40f5b323
commit 5433d422c0
5 changed files with 91 additions and 10 deletions

View File

@@ -44,8 +44,10 @@ public class GrpcServerApplication {
.hasAuthority("ROLE_ADMIN")
.methods("Simple/SayHello")
.hasAuthority("ROLE_USER")
.methods("grpc.*/*")
.permitAll()
.allRequests()
.permitAll())
.denyAll())
.httpBasic(withDefaults())
.preauth(withDefaults())
.authenticationExtractor((headers, attributes) -> {

View File

@@ -4,7 +4,10 @@ import static org.junit.Assert.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -30,6 +33,10 @@ import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
import io.grpc.MethodDescriptor;
import io.grpc.Status.Code;
import io.grpc.StatusRuntimeException;
import io.grpc.reflection.v1.ServerReflectionGrpc;
import io.grpc.reflection.v1.ServerReflectionRequest;
import io.grpc.reflection.v1.ServerReflectionResponse;
import io.grpc.stub.StreamObserver;
@SpringBootTest(properties = { "spring.grpc.server.port=0",
"spring.grpc.client.channels.stub.address=static://0.0.0.0:${local.grpc.port}",
@@ -45,6 +52,10 @@ public class GrpcServerApplicationTests {
@Qualifier("stub")
private SimpleGrpc.SimpleBlockingStub stub;
@Autowired
@Qualifier("reflect")
private ServerReflectionGrpc.ServerReflectionStub reflect;
@Autowired
@Qualifier("secure")
private SimpleGrpc.SimpleBlockingStub secure;
@@ -66,6 +77,32 @@ public class GrpcServerApplicationTests {
assertEquals(Code.UNAUTHENTICATED, exception.getStatus().getCode());
}
@Test
@DirtiesContext
void anonymous() throws Exception {
AtomicReference<ServerReflectionResponse> response = new AtomicReference<>();
AtomicBoolean error = new AtomicBoolean();
StreamObserver<ServerReflectionResponse> responses = new StreamObserver<ServerReflectionResponse>() {
@Override
public void onNext(ServerReflectionResponse value) {
response.set(value);
}
@Override
public void onError(Throwable t) {
error.set(true);
}
@Override
public void onCompleted() {
}
};
StreamObserver<ServerReflectionRequest> request = reflect.serverReflectionInfo(responses);
request.onNext(ServerReflectionRequest.newBuilder().setListServices("").build());
request.onCompleted();
Awaitility.await().until(() -> response.get() != null || error.get());
}
@Test
@DirtiesContext
void unauthauthorized() {
@@ -114,7 +151,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
@@ -123,6 +160,12 @@ public class GrpcServerApplicationTests {
return SimpleGrpc.newBlockingStub(channels.createChannel("stub"));
}
@Bean
@Lazy
ServerReflectionGrpc.ServerReflectionStub reflect(GrpcChannelFactory channels, @LocalGrpcPort int port) {
return ServerReflectionGrpc.newStub(channels.createChannel("stub"));
}
}
}

View File

@@ -103,12 +103,14 @@ public class AuthenticationServerInterceptor implements ServerInterceptor, Order
@Override
public void onReady() {
if (this.authentication == null || !this.authentication.isAuthenticated()
|| this.authentication instanceof AnonymousAuthenticationToken) {
if (this.authentication == null || !this.authentication.isAuthenticated()) {
throw new BadCredentialsException("not authenticated");
}
else {
if (!this.authorizationManager.authorize(() -> this.authentication, this.context).isGranted()) {
if (this.authentication instanceof AnonymousAuthenticationToken) {
throw new BadCredentialsException("not authenticated");
}
throw new AccessDeniedException("not allowed");
}
}

View File

@@ -20,4 +20,9 @@ import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
public record CallContext(Metadata headers, Attributes attributes, MethodDescriptor<?, ?> method) {
@Override
public final String toString() {
return "CallContext:[" + this.method.getFullMethodName() + "]";
}
}

View File

@@ -22,10 +22,13 @@ import java.util.function.Supplier;
import org.springframework.context.ApplicationContext;
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
import org.springframework.security.authorization.AuthorityAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationManagers;
import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
@@ -38,14 +41,22 @@ public class RequestMapperConfigurer extends SecurityConfigurerAdapter<Authentic
private final Supplier<RoleHierarchy> roleHierarchy;
public RequestMapperConfigurer(ApplicationContext context) {
private final AuthorizationEventPublisher publisher;
public RequestMapperConfigurer(ApplicationContext context) throws Exception {
if (context.getBeanNamesForType(AuthorizationEventPublisher.class).length > 0) {
this.publisher = context.getBean(AuthorizationEventPublisher.class);
} else {
this.publisher = new SpringAuthorizationEventPublisher(context);
}
this.roleHierarchy = SingletonSupplier.of(() -> (context.getBeanNamesForType(RoleHierarchy.class).length > 0)
? context.getBean(RoleHierarchy.class) : new NullRoleHierarchy());
? context.getBean(RoleHierarchy.class)
: new NullRoleHierarchy());
}
@Override
public void configure(GrpcSecurity builder) throws Exception {
builder.authorizationManager(new RequestMapperAuthorizationManager(this.authorizedCalls));
builder.authorizationManager(new RequestMapperAuthorizationManager(this.authorizedCalls, this.publisher));
}
public AuthorizedCall allRequests() {
@@ -111,6 +122,18 @@ public class RequestMapperConfigurer extends SecurityConfigurerAdapter<Authentic
return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities)));
}
public RequestMapperConfigurer authenticated() {
return access(AuthenticatedAuthorizationManager.authenticated());
}
public RequestMapperConfigurer fullyAuthenticated() {
return access(AuthenticatedAuthorizationManager.fullyAuthenticated());
}
public RequestMapperConfigurer anonymous() {
return access(AuthenticatedAuthorizationManager.anonymous());
}
public RequestMapperConfigurer access(AuthorizationManager<Object> manager) {
Assert.notNull(manager, "manager cannot be null");
this.authorizationManager = (this.not) ? AuthorizationManagers.not(manager) : manager;
@@ -127,20 +150,26 @@ public class RequestMapperConfigurer extends SecurityConfigurerAdapter<Authentic
public static class RequestMapperAuthorizationManager implements AuthorizationManager<CallContext> {
private final List<AuthorizedCall> authorizedCalls;
private final AuthorizationEventPublisher publisher;
public RequestMapperAuthorizationManager(List<AuthorizedCall> authorizedCalls) {
public RequestMapperAuthorizationManager(List<AuthorizedCall> authorizedCalls,
AuthorizationEventPublisher publisher) {
this.authorizedCalls = authorizedCalls;
this.publisher = publisher;
}
@SuppressWarnings("deprecation")
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, CallContext context) {
AuthorizationDecision result = new AuthorizationDecision(false);
for (AuthorizedCall authorizedCall : this.authorizedCalls) {
if (authorizedCall.matcher.matches(context)) {
return authorizedCall.authorizationManager.check(authentication, context);
result = authorizedCall.authorizationManager.check(authentication, context);
break;
}
}
return new AuthorizationDecision(false);
this.publisher.publishAuthorizationEvent(authentication, context, result);
return result;
}
}