-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20481 from michalszynkiewicz/grpc-security
Quarkus gRPC security interceptor + non-blocking security check
- Loading branch information
Showing
13 changed files
with
604 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
168 changes: 168 additions & 0 deletions
168
extensions/grpc/deployment/src/test/java/io/quarkus/grpc/auth/GrpcAuthTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
package io.quarkus.grpc.auth; | ||
|
||
import static com.example.security.Security.ThreadInfo.newBuilder; | ||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.awaitility.Awaitility.await; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
import java.util.Base64; | ||
import java.util.List; | ||
import java.util.concurrent.CopyOnWriteArrayList; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.concurrent.atomic.AtomicInteger; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
|
||
import javax.annotation.security.RolesAllowed; | ||
import javax.inject.Singleton; | ||
|
||
import org.jboss.shrinkwrap.api.ShrinkWrap; | ||
import org.jboss.shrinkwrap.api.asset.StringAsset; | ||
import org.jboss.shrinkwrap.api.spec.JavaArchive; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.RegisterExtension; | ||
|
||
import com.example.security.SecuredService; | ||
import com.example.security.Security; | ||
|
||
import io.grpc.Metadata; | ||
import io.quarkus.grpc.GrpcClient; | ||
import io.quarkus.grpc.GrpcClientUtils; | ||
import io.quarkus.grpc.GrpcService; | ||
import io.quarkus.security.credential.PasswordCredential; | ||
import io.quarkus.security.identity.request.AuthenticationRequest; | ||
import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; | ||
import io.quarkus.test.QuarkusUnitTest; | ||
import io.smallrye.mutiny.Multi; | ||
import io.smallrye.mutiny.Uni; | ||
import io.vertx.core.Context; | ||
|
||
public class GrpcAuthTest { | ||
|
||
public static final Metadata.Key<String> AUTHORIZATION = Metadata.Key.of("Authorization", | ||
Metadata.ASCII_STRING_MARSHALLER); | ||
|
||
@RegisterExtension | ||
static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( | ||
() -> ShrinkWrap.create(JavaArchive.class) | ||
.addClasses(Service.class, BasicGrpcSecurityMechanism.class) | ||
.addPackage(SecuredService.class.getPackage()) | ||
.add(new StringAsset("quarkus.security.users.embedded.enabled=true\n" + | ||
"quarkus.security.users.embedded.users.john=john\n" + | ||
"quarkus.security.users.embedded.roles.john=employees\n" + | ||
"quarkus.security.users.embedded.users.paul=paul\n" + | ||
"quarkus.security.users.embedded.roles.paul=interns\n" + | ||
"quarkus.security.users.embedded.plain-text=true\n" + | ||
"quarkus.http.auth.basic=true"), "application.properties")); | ||
public static final String JOHN_BASIC_CREDS = "am9objpqb2hu"; | ||
public static final String PAUL_BASIC_CREDS = "cGF1bDpwYXVs"; | ||
|
||
@GrpcClient | ||
SecuredService securityClient; | ||
|
||
@Test | ||
void shouldSecureUniEndpoint() { | ||
Metadata headers = new Metadata(); | ||
headers.put(AUTHORIZATION, "Basic " + JOHN_BASIC_CREDS); | ||
SecuredService client = GrpcClientUtils.attachHeaders(securityClient, headers); | ||
AtomicInteger resultCount = new AtomicInteger(); | ||
client.unaryCall(Security.Container.newBuilder().setText("woo-hoo").build()) | ||
.subscribe().with(e -> resultCount.incrementAndGet()); | ||
|
||
await().atMost(5, TimeUnit.SECONDS) | ||
.until(() -> resultCount.get() == 1); | ||
} | ||
|
||
@Test | ||
void shouldSecureMultiEndpoint() { | ||
Metadata headers = new Metadata(); | ||
headers.put(AUTHORIZATION, "Basic " + PAUL_BASIC_CREDS); | ||
SecuredService client = GrpcClientUtils.attachHeaders(securityClient, headers); | ||
List<Boolean> results = new CopyOnWriteArrayList<>(); | ||
client.streamCall(Multi.createBy().repeating() | ||
.supplier(() -> (Security.Container.newBuilder().setText("woo-hoo").build())).atMost(4)) | ||
.subscribe().with(e -> results.add(e.getIsOnEventLoop())); | ||
|
||
await().atMost(5, TimeUnit.SECONDS) | ||
.until(() -> results.size() == 5); | ||
|
||
assertThat(results.stream().filter(e -> !e)).isEmpty(); | ||
} | ||
|
||
@Test | ||
void shouldFailWithInvalidCredentials() { | ||
Metadata headers = new Metadata(); | ||
headers.put(AUTHORIZATION, "Basic invalid creds"); | ||
SecuredService client = GrpcClientUtils.attachHeaders(securityClient, headers); | ||
|
||
AtomicReference<Throwable> error = new AtomicReference<>(); | ||
|
||
AtomicInteger resultCount = new AtomicInteger(); | ||
client.unaryCall(Security.Container.newBuilder().setText("woo-hoo").build()) | ||
.onFailure().invoke(error::set) | ||
.subscribe().with(e -> resultCount.incrementAndGet()); | ||
|
||
await().atMost(5, TimeUnit.SECONDS) | ||
.until(() -> error.get() != null); | ||
} | ||
|
||
@Test | ||
void shouldFailWithInvalidInsufficientRole() { | ||
Metadata headers = new Metadata(); | ||
headers.put(AUTHORIZATION, PAUL_BASIC_CREDS); | ||
SecuredService client = GrpcClientUtils.attachHeaders(securityClient, headers); | ||
|
||
AtomicReference<Throwable> error = new AtomicReference<>(); | ||
|
||
AtomicInteger resultCount = new AtomicInteger(); | ||
client.unaryCall(Security.Container.newBuilder().setText("woo-hoo").build()) | ||
.onFailure().invoke(error::set) | ||
.subscribe().with(e -> resultCount.incrementAndGet()); | ||
|
||
await().atMost(5, TimeUnit.SECONDS) | ||
.until(() -> error.get() != null); | ||
} | ||
|
||
@GrpcService | ||
public static class Service implements SecuredService { | ||
@Override | ||
@RolesAllowed("employees") | ||
public Uni<Security.ThreadInfo> unaryCall(Security.Container request) { | ||
return Uni.createFrom() | ||
.item(newBuilder().setIsOnEventLoop(Context.isOnEventLoopThread()).build()); | ||
} | ||
|
||
@Override | ||
@RolesAllowed("interns") | ||
public Multi<Security.ThreadInfo> streamCall(Multi<Security.Container> request) { | ||
return Multi.createBy() | ||
.repeating().supplier(() -> newBuilder().setIsOnEventLoop(Context.isOnEventLoopThread()).build()) | ||
.atMost(5); | ||
} | ||
|
||
} | ||
|
||
@Singleton | ||
public static class BasicGrpcSecurityMechanism implements GrpcSecurityMechanism { | ||
@Override | ||
public boolean handles(Metadata metadata) { | ||
String authString = metadata.get(AUTHORIZATION); | ||
return authString != null && authString.startsWith("Basic "); | ||
} | ||
|
||
@Override | ||
public AuthenticationRequest createAuthenticationRequest(Metadata metadata) { | ||
String authString = metadata.get(AUTHORIZATION); | ||
authString = authString.substring("Basic ".length()); | ||
byte[] decode = Base64.getDecoder().decode(authString); | ||
String plainChallenge = new String(decode, StandardCharsets.UTF_8); | ||
int colonPos; | ||
if ((colonPos = plainChallenge.indexOf(':')) > -1) { | ||
String userName = plainChallenge.substring(0, colonPos); | ||
char[] password = plainChallenge.substring(colonPos + 1).toCharArray(); | ||
return new UsernamePasswordAuthenticationRequest(userName, new PasswordCredential(password)); | ||
} else { | ||
return null; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
syntax = "proto3"; | ||
|
||
package security; | ||
option java_package = "com.example.security"; | ||
|
||
service SecuredService { | ||
rpc unaryCall(Container) returns (ThreadInfo); | ||
rpc streamCall(stream Container) returns (stream ThreadInfo); | ||
} | ||
|
||
message ThreadInfo { | ||
bool isOnEventLoop = 1; | ||
} | ||
|
||
message Container { | ||
string text = 1; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
78 changes: 78 additions & 0 deletions
78
extensions/grpc/runtime/src/main/java/io/quarkus/grpc/auth/AuthExceptionHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package io.quarkus.grpc.auth; | ||
|
||
import javax.enterprise.inject.spi.Prioritized; | ||
|
||
import io.grpc.ForwardingServerCallListener; | ||
import io.grpc.Metadata; | ||
import io.grpc.ServerCall; | ||
import io.grpc.Status; | ||
import io.quarkus.security.AuthenticationFailedException; | ||
|
||
/** | ||
* Exception mapper for authentication and authorization exceptions | ||
* | ||
* To alter mapping exceptions, create a subclass of this handler and create an appropriate | ||
* {@link AuthExceptionHandlerProvider} | ||
*/ | ||
public class AuthExceptionHandler<ReqT, RespT> | ||
extends ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT> implements Prioritized { | ||
|
||
private final ServerCall<ReqT, RespT> serverCall; | ||
private final Metadata metadata; | ||
|
||
public AuthExceptionHandler(ServerCall.Listener<ReqT> listener, ServerCall<ReqT, RespT> serverCall, | ||
Metadata metadata) { | ||
super(listener); | ||
this.metadata = metadata; | ||
this.serverCall = serverCall; | ||
} | ||
|
||
@Override | ||
public void onMessage(ReqT message) { | ||
try { | ||
super.onMessage(message); | ||
} catch (RuntimeException e) { | ||
handleException(e, serverCall, metadata); | ||
} | ||
} | ||
|
||
@Override | ||
public void onHalfClose() { | ||
try { | ||
super.onHalfClose(); | ||
} catch (RuntimeException e) { | ||
handleException(e, serverCall, metadata); | ||
} | ||
} | ||
|
||
@Override | ||
public void onReady() { | ||
try { | ||
super.onReady(); | ||
} catch (RuntimeException e) { | ||
handleException(e, serverCall, metadata); | ||
} | ||
} | ||
|
||
/** | ||
* Maps exception to a gRPC error. Override this method to customize the mapping | ||
* | ||
* @param exception exception thrown | ||
* @param serverCall server call to close with error | ||
* @param metadata call metadata | ||
*/ | ||
protected void handleException(RuntimeException exception, ServerCall<ReqT, RespT> serverCall, Metadata metadata) { | ||
if (exception instanceof AuthenticationFailedException) { | ||
serverCall.close(Status.UNAUTHENTICATED.withDescription(exception.getMessage()), metadata); | ||
} else if (exception instanceof SecurityException) { | ||
serverCall.close(Status.PERMISSION_DENIED.withDescription(exception.getMessage()), metadata); | ||
} else { | ||
throw exception; | ||
} | ||
} | ||
|
||
@Override | ||
public int getPriority() { | ||
return 0; | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
extensions/grpc/runtime/src/main/java/io/quarkus/grpc/auth/AuthExceptionHandlerProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package io.quarkus.grpc.auth; | ||
|
||
import javax.enterprise.inject.spi.Prioritized; | ||
|
||
import io.grpc.Metadata; | ||
import io.grpc.ServerCall; | ||
import io.grpc.ServerCall.Listener; | ||
|
||
/** | ||
* Provider for AuthExceptionHandler. | ||
* | ||
* To use a custom AuthExceptionHandler, extend {@link AuthExceptionHandler} and implement | ||
* an {@link AuthExceptionHandlerProvider} with priority greater than the default one. | ||
*/ | ||
public interface AuthExceptionHandlerProvider extends Prioritized { | ||
int DEFAULT_PRIORITY = 0; | ||
|
||
<ReqT, RespT> AuthExceptionHandler<ReqT, RespT> createHandler(Listener<ReqT> listener, | ||
ServerCall<ReqT, RespT> serverCall, Metadata metadata); | ||
} |
21 changes: 21 additions & 0 deletions
21
.../grpc/runtime/src/main/java/io/quarkus/grpc/auth/DefaultAuthExceptionHandlerProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package io.quarkus.grpc.auth; | ||
|
||
import javax.inject.Singleton; | ||
|
||
import io.grpc.Metadata; | ||
import io.grpc.ServerCall; | ||
|
||
@Singleton | ||
public class DefaultAuthExceptionHandlerProvider implements AuthExceptionHandlerProvider { | ||
|
||
@Override | ||
public int getPriority() { | ||
return DEFAULT_PRIORITY; | ||
} | ||
|
||
@Override | ||
public <ReqT, RespT> AuthExceptionHandler<ReqT, RespT> createHandler(ServerCall.Listener<ReqT> listener, | ||
ServerCall<ReqT, RespT> serverCall, Metadata metadata) { | ||
return new AuthExceptionHandler<>(listener, serverCall, metadata); | ||
} | ||
} |
Oops, something went wrong.