Add GrpcSecurity

This commit is contained in:
Dave Syer
2025-01-21 12:12:04 +00:00
parent aa809aaf8a
commit aa8d680c3f
23 changed files with 1528 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
package org.springframework.grpc.sample;
import static org.springframework.security.config.Customizer.withDefaults;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.grpc.server.GlobalServerInterceptor;
import org.springframework.grpc.server.security.GrpcSecurity;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import io.grpc.Metadata;
import io.grpc.ServerInterceptor;
@SpringBootApplication
@EnableMethodSecurity
@Import(AuthenticationConfiguration.class)
public class GrpcServerApplication {
public static final Metadata.Key<String> USER_KEY = Metadata.Key.of("X-USER", Metadata.ASCII_STRING_MARSHALLER);
public static void main(String[] args) {
SpringApplication.run(GrpcServerApplication.class, args);
}
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
return new InMemoryUserDetailsManager(
User.withUsername("user").password("{noop}user").authorities("ROLE_USER").build(),
User.withUsername("admin").password("{noop}admin").authorities("ROLE_ADMIN").build());
}
@Bean
@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();
}
}

View File

@@ -0,0 +1,50 @@
package org.springframework.grpc.sample;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.grpc.sample.proto.HelloReply;
import org.springframework.grpc.sample.proto.HelloRequest;
import org.springframework.grpc.sample.proto.SimpleGrpc;
import org.springframework.stereotype.Service;
import io.grpc.stub.StreamObserver;
@Service
public class GrpcServerService extends SimpleGrpc.SimpleImplBase {
private static Log log = LogFactory.getLog(GrpcServerService.class);
@Override
// @PreAuthorize("hasAuthority('ROLE_USER')")
public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
log.info("Hello " + req.getName());
if (req.getName().startsWith("error")) {
throw new IllegalArgumentException("Bad name: " + req.getName());
}
HelloReply reply = HelloReply.newBuilder().setMessage("Hello ==> " + req.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
@Override
// @PreAuthorize("hasAuthority('ROLE_ADMIN')")
public void streamHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
log.info("Hello " + req.getName());
int count = 0;
while (count < 10) {
HelloReply reply = HelloReply.newBuilder().setMessage("Hello(" + count + ") ==> " + req.getName()).build();
responseObserver.onNext(reply);
count++;
try {
Thread.sleep(1000L);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
responseObserver.onError(e);
return;
}
}
responseObserver.onCompleted();
}
}

View File

@@ -0,0 +1,23 @@
syntax = "proto3";
option java_multiple_files = true;
option java_package = "org.springframework.grpc.sample.proto";
option java_outer_classname = "HelloWorldProto";
// The greeting service definition.
service Simple {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {
}
rpc StreamHello(HelloRequest) returns (stream HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}

View File

@@ -0,0 +1,2 @@
# Ignored unless building in Nix (https://github.com/oracle/graal/issues/8639)
Args = -ENIX_LDFLAGS -ENIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu

View File

@@ -0,0 +1,2 @@
spring.application.name=grpc-server
logging.level.org.springframework.security=debug

View File

@@ -0,0 +1,129 @@
package org.springframework.grpc.sample;
import static org.junit.Assert.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.grpc.client.ChannelBuilderOptions;
import org.springframework.grpc.client.GrpcChannelFactory;
import org.springframework.grpc.client.security.BasicAuthenticationInterceptor;
import org.springframework.grpc.sample.proto.HelloReply;
import org.springframework.grpc.sample.proto.HelloRequest;
import org.springframework.grpc.sample.proto.SimpleGrpc;
import org.springframework.grpc.test.LocalGrpcPort;
import org.springframework.test.annotation.DirtiesContext;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
import io.grpc.MethodDescriptor;
import io.grpc.Status.Code;
import io.grpc.StatusRuntimeException;
@SpringBootTest(properties = { "spring.grpc.server.port=0",
"spring.grpc.client.channels.stub.address=static://0.0.0.0:${local.grpc.port}",
"spring.grpc.client.channels.basic.address=static://0.0.0.0:${local.grpc.port}",
"spring.grpc.client.channels.secure.address=static://0.0.0.0:${local.grpc.port}" })
public class GrpcServerApplicationTests {
public static void main(String[] args) {
new SpringApplicationBuilder(GrpcServerApplication.class, ExtraConfiguration.class)
.run(args);
}
@Autowired
@Qualifier("stub")
private SimpleGrpc.SimpleBlockingStub stub;
@Autowired
@Qualifier("secure")
private SimpleGrpc.SimpleBlockingStub secure;
@Autowired
@Qualifier("basic")
private SimpleGrpc.SimpleBlockingStub basic;
@Test
@DirtiesContext
void contextLoads() {
}
@Test
@DirtiesContext
void unauthenticated() {
StatusRuntimeException exception = assertThrows(StatusRuntimeException.class,
() -> stub.sayHello(HelloRequest.newBuilder().setName("Alien").build()));
assertEquals(Code.UNAUTHENTICATED, exception.getStatus().getCode());
}
@Test
@DirtiesContext
void unauthauthorized() {
StatusRuntimeException exception = assertThrows(StatusRuntimeException.class,
() -> secure.streamHello(HelloRequest.newBuilder().setName("Alien").build()).next());
assertEquals(Code.PERMISSION_DENIED, exception.getStatus().getCode());
}
@Test
@DirtiesContext
void authenticated() {
HelloReply response = secure.sayHello(HelloRequest.newBuilder().setName("Alien").build());
assertEquals("Hello ==> Alien", response.getMessage());
}
@Test
@DirtiesContext
void basic() {
HelloReply response = basic.sayHello(HelloRequest.newBuilder().setName("Alien").build());
assertEquals("Hello ==> Alien", response.getMessage());
}
@TestConfiguration
static class ExtraConfiguration {
@Bean
@Lazy
SimpleGrpc.SimpleBlockingStub secure(GrpcChannelFactory channels) {
return SimpleGrpc.newBlockingStub(channels.createChannel("secure",
ChannelBuilderOptions.defaults().withInterceptors(List.of(new ClientInterceptor() {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method,
CallOptions callOptions, Channel next) {
return new SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
public void start(ClientCall.Listener<RespT> responseListener,
io.grpc.Metadata headers) {
headers.put(GrpcServerApplication.USER_KEY, "user");
super.start(responseListener, headers);
};
};
}
}))));
}
@Bean
@Lazy
SimpleGrpc.SimpleBlockingStub basic(GrpcChannelFactory channels) {
return SimpleGrpc.newBlockingStub(channels.createChannel("basic", ChannelBuilderOptions.defaults()
.withInterceptors(List.of(new BasicAuthenticationInterceptor("user", "user")))));
}
@Bean
@Lazy
SimpleGrpc.SimpleBlockingStub stub(GrpcChannelFactory channels, @LocalGrpcPort int port) {
return SimpleGrpc.newBlockingStub(channels.createChannel("stub"));
}
}
}