Skip to content

Commit

Permalink
WebSockets Next: security integration
Browse files Browse the repository at this point in the history
- when quarkus-security is present and quarkus.http.auth.proactive=false,
then we force the authentication before the HTTP upgrade so that it's possible
to capture the SecurityIdentity and set it afterwards for all endpoint callbacks
- fixes quarkusio#40312
- also create a new Vertx duplicated context for error handler
invocation
  • Loading branch information
mkouba committed May 10, 2024
1 parent 33db951 commit f8e42e7
Show file tree
Hide file tree
Showing 19 changed files with 636 additions and 90 deletions.
10 changes: 10 additions & 0 deletions extensions/websockets-next/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@
<artifactId>quarkus-test-vertx</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-test-utils</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
import io.quarkus.arc.processor.DotNames;
import io.quarkus.arc.processor.InjectionPointInfo;
import io.quarkus.arc.processor.Types;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.GeneratedClassGizmoAdaptor;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
Expand All @@ -65,6 +67,7 @@
import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.quarkus.vertx.http.runtime.HandlerType;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.quarkus.websockets.next.InboundProcessingMode;
import io.quarkus.websockets.next.WebSocketClientConnection;
import io.quarkus.websockets.next.WebSocketClientException;
Expand All @@ -79,6 +82,7 @@
import io.quarkus.websockets.next.runtime.ConnectionManager;
import io.quarkus.websockets.next.runtime.ContextSupport;
import io.quarkus.websockets.next.runtime.JsonTextMessageCodec;
import io.quarkus.websockets.next.runtime.SecuritySupport;
import io.quarkus.websockets.next.runtime.WebSocketClientRecorder;
import io.quarkus.websockets.next.runtime.WebSocketClientRecorder.ClientEndpoint;
import io.quarkus.websockets.next.runtime.WebSocketConnectionBase;
Expand Down Expand Up @@ -400,12 +404,19 @@ public String apply(String name) {
@Record(RUNTIME_INIT)
@BuildStep
public void registerRoutes(WebSocketServerRecorder recorder, HttpRootPathBuildItem httpRootPath,
List<GeneratedEndpointBuildItem> generatedEndpoints,
List<GeneratedEndpointBuildItem> generatedEndpoints, HttpBuildTimeConfig httpConfig, Capabilities capabilities,
BuildProducer<RouteBuildItem> routes) {
for (GeneratedEndpointBuildItem endpoint : generatedEndpoints.stream().filter(GeneratedEndpointBuildItem::isServer)
.toList()) {
RouteBuildItem.Builder builder = RouteBuildItem.builder()
.route(httpRootPath.relativePath(endpoint.path))
RouteBuildItem.Builder builder = RouteBuildItem.builder();
String relativePath = httpRootPath.relativePath(endpoint.path);
if (capabilities.isPresent(Capability.SECURITY) && !httpConfig.auth.proactive) {
// Add a special handler so that it's possible to capture the SecurityIdentity before the HTTP upgrade
builder.routeFunction(relativePath, recorder.initializeSecurityHandler());
} else {
builder.route(relativePath);
}
builder
.displayOnNotFoundPage("WebSocket Endpoint")
.handlerType(HandlerType.NORMAL)
.handler(recorder.createEndpointHandler(endpoint.generatedClassName, endpoint.endpointId));
Expand Down Expand Up @@ -546,8 +557,8 @@ private void validateOnClose(Callback callback) {
* }
*
* public Echo_WebSocketEndpoint(WebSocketConnection connection, Codecs codecs,
* WebSocketRuntimeConfig config, ContextSupport contextSupport) {
* super(connection, codecs, config, contextSupport);
* WebSocketRuntimeConfig config, ContextSupport contextSupport, SecuritySupport securitySupport) {
* super(connection, codecs, config, contextSupport, securitySupport);
* }
*
* public Uni doOnTextMessage(String message) {
Expand Down Expand Up @@ -617,12 +628,12 @@ static String generateEndpoint(WebSocketEndpointBuildItem endpoint,
.build();

MethodCreator constructor = endpointCreator.getConstructorCreator(WebSocketConnectionBase.class,
Codecs.class, ContextSupport.class);
Codecs.class, ContextSupport.class, SecuritySupport.class);
constructor.invokeSpecialMethod(
MethodDescriptor.ofConstructor(WebSocketEndpointBase.class, WebSocketConnectionBase.class,
Codecs.class, ContextSupport.class),
Codecs.class, ContextSupport.class, SecuritySupport.class),
constructor.getThis(), constructor.getMethodParam(0), constructor.getMethodParam(1),
constructor.getMethodParam(2));
constructor.getMethodParam(2), constructor.getMethodParam(3));
constructor.returnNull();

MethodCreator inboundProcessingMode = endpointCreator.getMethodCreator("inboundProcessingMode",
Expand Down Expand Up @@ -1044,7 +1055,7 @@ private static ResultHandle encodeMessage(ResultHandle endpointThis, BytecodeCre
return uniOnFailureDoOnError(endpointThis, method, callback, uniChain, endpoint, globalErrorHandlers);
}
} else if (callback.isReturnTypeMulti()) {
// return multiText(multi, broadcast, m -> {
// return multiText(multi, m -> {
// try {
// String text = encodeText(m);
// return sendText(buffer,broadcast);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ String decodingError(BinaryDecodeException e) {
Uni<Void> runtimeProblem(RuntimeException e, WebSocketConnection connection) {
assertTrue(Context.isOnEventLoopThread());
assertEquals(connection.id(), this.connection.id());
// The request context from @OnBinaryMessage is reused
assertEquals("ok", requestBean.getState());
// A new request context is used
assertEquals("nok", requestBean.getState());
return connection.sendText(e.getMessage());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ String decodingError(BinaryDecodeException e) {
String runtimeProblem(RuntimeException e, WebSocketConnection connection) {
assertTrue(Context.isOnWorkerThread());
assertEquals(connection.id(), this.connection.id());
// The request context from @OnBinaryMessage is reused
assertEquals("ok", requestBean.getState());
// A new request context is used
assertEquals("nok", requestBean.getState());
return e.getMessage();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.quarkus.websockets.next.test.security;

import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.ApplicationScoped;

@RolesAllowed("admin")
@ApplicationScoped
public class AdminService {

public String ping() {
return "" + 24;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.quarkus.websockets.next.test.security;

import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.Authenticated;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.identity.CurrentIdentityAssociation;
import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.websockets.next.OnError;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.test.utils.WSClient;

public class EagerSecurityTest extends SecurityTestBase {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addAsResource(new StringAsset("quarkus.http.auth.proactive=true\n" +
"quarkus.http.auth.permission.secured.paths=/end\n" +
"quarkus.http.auth.permission.secured.policy=authenticated\n"), "application.properties")
.addClasses(Endpoint.class, WSClient.class, TestIdentityProvider.class, TestIdentityController.class));

@Authenticated
@WebSocket(path = "/end")
public static class Endpoint {

@Inject
CurrentIdentityAssociation currentIdentity;

@OnOpen
String open() {
return "ready";
}

@RolesAllowed("admin")
@OnTextMessage
String echo(String message) {
if (!currentIdentity.getIdentity().hasRole("admin")) {
throw new IllegalStateException();
}
return message;
}

@OnError
String error(ForbiddenException t) {
return "forbidden:" + currentIdentity.getIdentity().getPrincipal().getName();
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.quarkus.websockets.next.test.security;

import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.Authenticated;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.identity.CurrentIdentityAssociation;
import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.websockets.next.OnError;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.test.utils.WSClient;
import io.smallrye.mutiny.Uni;

public class EagerSecurityUniTest extends SecurityTestBase {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addAsResource(new StringAsset("quarkus.http.auth.proactive=true\n" +
"quarkus.http.auth.permission.secured.paths=/end\n" +
"quarkus.http.auth.permission.secured.policy=authenticated\n"), "application.properties")
.addClasses(Endpoint.class, WSClient.class, TestIdentityProvider.class, TestIdentityController.class));

@Authenticated
@WebSocket(path = "/end")
public static class Endpoint {

@Inject
CurrentIdentityAssociation currentIdentity;

@OnOpen
String open() {
return "ready";
}

@RolesAllowed("admin")
@OnTextMessage
Uni<String> echo(String message) {
if (!currentIdentity.getIdentity().hasRole("admin")) {
throw new IllegalStateException();
}
return Uni.createFrom().item(message);
}

@OnError
String error(ForbiddenException t) {
return "forbidden:" + currentIdentity.getIdentity().getPrincipal().getName();
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.quarkus.websockets.next.test.security;

import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.Authenticated;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.identity.CurrentIdentityAssociation;
import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.websockets.next.OnError;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.test.security.EagerSecurityTest.Endpoint;
import io.quarkus.websockets.next.test.utils.WSClient;

public class LazySecurityTest extends SecurityTestBase {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addAsResource(new StringAsset("quarkus.http.auth.proactive=false\n" +
"quarkus.http.auth.permission.secured.paths=/end\n" +
"quarkus.http.auth.permission.secured.policy=authenticated\n"), "application.properties")
.addClasses(Endpoint.class, WSClient.class, TestIdentityProvider.class, TestIdentityController.class));

@Authenticated
@WebSocket(path = "/end")
public static class Endpoint {

@Inject
CurrentIdentityAssociation currentIdentity;

@OnOpen
String open() {
return "ready";
}

@RolesAllowed("admin")
@OnTextMessage
String echo(String message) {
if (!currentIdentity.getIdentity().hasRole("admin")) {
throw new IllegalStateException();
}
return message;
}

@OnError
String error(ForbiddenException t) {
return "forbidden:" + currentIdentity.getIdentity().getPrincipal().getName();
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.quarkus.websockets.next.test.security;

import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.Authenticated;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.identity.CurrentIdentityAssociation;
import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.websockets.next.OnError;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.test.utils.WSClient;
import io.smallrye.mutiny.Uni;

public class LazySecurityUniTest extends SecurityTestBase {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addAsResource(new StringAsset("quarkus.http.auth.proactive=false\n" +
"quarkus.http.auth.permission.secured.paths=/end\n" +
"quarkus.http.auth.permission.secured.policy=authenticated\n"), "application.properties")
.addClasses(Endpoint.class, WSClient.class, TestIdentityProvider.class, TestIdentityController.class));

@Authenticated
@WebSocket(path = "/end")
public static class Endpoint {

@Inject
CurrentIdentityAssociation currentIdentity;

@OnOpen
String open() {
return "ready";
}

@RolesAllowed("admin")
@OnTextMessage
Uni<String> echo(String message) {
if (!currentIdentity.getIdentity().hasRole("admin")) {
throw new IllegalStateException();
}
return Uni.createFrom().item(message);
}

@OnError
String error(ForbiddenException t) {
return "forbidden:" + currentIdentity.getIdentity().getPrincipal().getName();
}

}

}
Loading

0 comments on commit f8e42e7

Please sign in to comment.