Skip to content

Commit

Permalink
Example of method security & exception translation
Browse files Browse the repository at this point in the history
  • Loading branch information
Pavel Grushetzky committed Oct 24, 2017
1 parent 7dfd3e8 commit 15ab3c0
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 1 deletion.
1 change: 1 addition & 0 deletions grpc-spring-boot-starter-demo/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies {


compile project(':grpc-spring-boot-starter')
compile 'org.springframework.boot:spring-boot-starter-security'

testCompile 'org.springframework.boot:spring-boot-starter-aop'
testCompile('org.springframework.boot:spring-boot-starter-test')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.lognet.springboot.grpc.demo;

import io.grpc.*;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;

public class AuthExceptionTranslator implements ServerInterceptor {
public static final String DEFAULT_DESCRIPTION = "Authentication/Authorization failed";

@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {

ServerCall.Listener<ReqT> listener = null;
try {
// streaming calls may error out in startCall
listener = next.startCall(call, headers);
} catch (AuthenticationException | AccessDeniedException aex) {
closeCallAndRethrow(call, aex);
}

ForwardingServerCallListener.SimpleForwardingServerCallListener exceptionTranslationListener = new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(listener) {
@Override
public void onHalfClose() {
try {
// unary calls may error out here
super.onHalfClose();
} catch (AuthenticationException | AccessDeniedException aex) {
closeCallAndRethrow(call, aex);
}
}
};

return exceptionTranslationListener;
}

private <ReqT, RespT> void closeCallAndRethrow(ServerCall<ReqT, RespT> call, RuntimeException aex) {
call.close(Status.PERMISSION_DENIED.withCause(aex).withDescription(DEFAULT_DESCRIPTION), new Metadata());
throw aex;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.lognet.springboot.grpc.demo;

import io.grpc.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.Collections;

public class GrantRoleInterceptor implements ServerInterceptor {
private final String role;

public GrantRoleInterceptor(String role) {
this.role = role;
}

@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
final Authentication authentication = new UsernamePasswordAuthenticationToken("user", "user",
Collections.singletonList(new SimpleGrantedAuthority(role)));

ServerCall.Listener<ReqT> listener = next.startCall(call, headers);
ForwardingServerCallListener.SimpleForwardingServerCallListener authEnabledListener = new ForwardingServerCallListener.SimpleForwardingServerCallListener(listener) {
@Override
public void onHalfClose() {
SecurityContextHolder.getContext().setAuthentication(authentication);
try {
super.onHalfClose();
} finally {
SecurityContextHolder.getContext().setAuthentication(null);
}
}

};

return authEnabledListener;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
import io.grpc.examples.GreeterGrpc;
import io.grpc.examples.GreeterOuterClass;
import io.grpc.stub.StreamObserver;
import org.springframework.security.access.annotation.Secured;

@Slf4j
@GRpcService(interceptors = { LogInterceptor.class })
public class GreeterService extends GreeterGrpc.GreeterImplBase {
@Secured({ "ROLE_ADMIN" })
@Override
public void sayHello(GreeterOuterClass.HelloRequest request, StreamObserver<GreeterOuterClass.HelloReply> responseObserver) {
String message = "Hello " + request.getName();
Expand All @@ -21,4 +24,4 @@ public void sayHello(GreeterOuterClass.HelloRequest request, StreamObserver<Gree
responseObserver.onCompleted();
log.info("Returning " +message);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package org.lognet.springboot.grpc;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.examples.GreeterGrpc;
import io.grpc.examples.GreeterOuterClass;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.lognet.springboot.grpc.demo.AuthExceptionTranslator;
import org.lognet.springboot.grpc.demo.DemoApp;
import org.lognet.springboot.grpc.demo.GrantRoleInterceptor;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.test.context.junit4.SpringRunner;

import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {DemoApp.class, GreeterWithAuthTest.Config.class},
webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = {"grpc.port=8081",
"spring.aop.proxy-target-class=true"
})
public class GreeterWithAuthTest {
private ManagedChannel channel;
private GreeterGrpc.GreeterBlockingStub client;
@Rule
public ExpectedException exception = ExpectedException.none();

@Before
public void setupChannel() {
channel = ManagedChannelBuilder.forAddress("localhost", 8081)
.usePlaintext(true).build();
client = GreeterGrpc.newBlockingStub(channel);
}

@After
public void shutdownChannel() {
channel.shutdownNow();
}

@Test
public void securityExceptionPropagatesToClient() {
// given
exception.expect(StatusRuntimeException.class);
exception.expect(hasProperty("status", hasProperty("code", is(Status.Code.PERMISSION_DENIED))));
exception.expectMessage(AuthExceptionTranslator.DEFAULT_DESCRIPTION);

// when
GreeterOuterClass.HelloRequest request = GreeterOuterClass.HelloRequest.newBuilder().setName("world").build();
GreeterOuterClass.HelloReply helloReply = client.sayHello(request);

// then expected exception occurs
}

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
static class Config {
@Bean
@GRpcGlobalInterceptor
public AuthExceptionTranslator authExceptionTranslator() {
return new AuthExceptionTranslator();
}

@Bean
@GRpcGlobalInterceptor
public GrantRoleInterceptor grantRole() {
return new GrantRoleInterceptor("NON_ADMIN");
}

}
}

1 comment on commit 15ab3c0

@ST-DDT
Copy link

@ST-DDT ST-DDT commented on 15ab3c0 Nov 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: This example is vulnerable to concurrency issues!

See also: LogNet#61 (comment)

Please sign in to comment.