Add GrpcSecurity
This commit is contained in:
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
23
samples/grpc-secure/src/main/proto/hello.proto
Normal file
23
samples/grpc-secure/src/main/proto/hello.proto
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
spring.application.name=grpc-server
|
||||
logging.level.org.springframework.security=debug
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user