From 922a661da93fdc3b9a43e6fd2f765299c84043ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sun, 3 Jul 2022 21:20:19 +0200 Subject: [PATCH] Resolve Sec. Identity in Reactive routes when Proactive Auth disabled f ix #23547 for Reactive routes --- .../security-built-in-authentication.adoc | 3 +- extensions/reactive-routes/deployment/pom.xml | 4 + .../AnnotatedRouteHandlerBuildItem.java | 13 ++- .../deployment/ReactiveRoutesProcessor.java | 33 +++++- .../quarkus/vertx/web/LazyAuthRouteTest.java | 100 ++++++++++++++++++ .../vertx/web/runtime/VertxWebRecorder.java | 40 ++++++- 6 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/LazyAuthRouteTest.java diff --git a/docs/src/main/asciidoc/security-built-in-authentication.adoc b/docs/src/main/asciidoc/security-built-in-authentication.adoc index e3c53cd6c539b..2ff446af1c858 100644 --- a/docs/src/main/asciidoc/security-built-in-authentication.adoc +++ b/docs/src/main/asciidoc/security-built-in-authentication.adoc @@ -124,7 +124,8 @@ when authentication is complete and the identity is available. NOTE: It's still possible to access the `SecurityIdentity` synchronously with `public SecurityIdentity getIdentity()` in the xref:resteasy-reactive.adoc[RESTEasy Reactive] from endpoints annotated with `@RolesAllowed`, `@Authenticated`, -or with respective configuration authorization checks as authentication has already happened. +or with respective configuration authorization checks as authentication has already happened. The same is also valid +for the xref:reactive-routes.adoc[Reactive routes] if a route response is synchronous. === How to customize authentication exception responses diff --git a/extensions/reactive-routes/deployment/pom.xml b/extensions/reactive-routes/deployment/pom.xml index 4177dcd50cf4f..a77aee8c987f9 100644 --- a/extensions/reactive-routes/deployment/pom.xml +++ b/extensions/reactive-routes/deployment/pom.xml @@ -64,6 +64,10 @@ rest-assured test + + io.quarkus + quarkus-security-test-utils + diff --git a/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/AnnotatedRouteHandlerBuildItem.java b/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/AnnotatedRouteHandlerBuildItem.java index d600111d7a87c..4c053a44aa778 100644 --- a/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/AnnotatedRouteHandlerBuildItem.java +++ b/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/AnnotatedRouteHandlerBuildItem.java @@ -17,14 +17,18 @@ public final class AnnotatedRouteHandlerBuildItem extends MultiBuildItem { private final MethodInfo method; private final boolean blocking; private final HttpCompression compression; + /** + * If true, always attempt to authenticate user right before the body handler is run + */ + private final boolean alwaysAuthenticateRoute; public AnnotatedRouteHandlerBuildItem(BeanInfo bean, MethodInfo method, List routes, AnnotationInstance routeBase) { - this(bean, method, routes, routeBase, false, HttpCompression.UNDEFINED); + this(bean, method, routes, routeBase, false, HttpCompression.UNDEFINED, false); } public AnnotatedRouteHandlerBuildItem(BeanInfo bean, MethodInfo method, List routes, - AnnotationInstance routeBase, boolean blocking, HttpCompression compression) { + AnnotationInstance routeBase, boolean blocking, HttpCompression compression, boolean alwaysAuthenticateRoute) { super(); this.bean = bean; this.routes = routes; @@ -32,6 +36,7 @@ public AnnotatedRouteHandlerBuildItem(BeanInfo bean, MethodInfo method, List PARAM_INJECTORS = initParamInjectors(); @@ -162,7 +169,8 @@ void validateBeanDeployment( TransformedAnnotationsBuildItem transformedAnnotations, BuildProducer routeHandlerBusinessMethods, BuildProducer routeFilterBusinessMethods, - BuildProducer errors) { + BuildProducer errors, + HttpBuildTimeConfig httpBuildTimeConfig) { // Collect all business methods annotated with @Route and @RouteFilter AnnotationStore annotationStore = validationPhase.getContext().get(BuildExtension.Key.ANNOTATION_STORE); @@ -211,9 +219,28 @@ void validateBeanDeployment( compression = HttpCompression.OFF; } } + + // Authenticate user if the proactive authentication is disabled and the route is secured with + // an RBAC annotation that requires authentication as io.quarkus.security.runtime.interceptor.SecurityConstrainer + // access the SecurityIdentity in a synchronous manner + final boolean blocking = annotationStore.hasAnnotation(method, DotNames.BLOCKING); + final boolean alwaysAuthenticateRoute; + if (!httpBuildTimeConfig.auth.proactive && !blocking) { + final DotName returnTypeName = method.returnType().name(); + // method either returns 'something' in a synchronous manner or void (in which case we can't tell) + final boolean possiblySynchronousResponse = !returnTypeName.equals(DotNames.UNI) + && !returnTypeName.equals(DotNames.MULTI) && !returnTypeName.equals(DotNames.COMPLETION_STAGE); + final boolean hasRbacAnnotationThatRequiresAuth = annotationStore.hasAnnotation(method, ROLES_ALLOWED) + || annotationStore.hasAnnotation(method, AUTHENTICATED) + || annotationStore.hasAnnotation(method, DENY_ALL); + alwaysAuthenticateRoute = possiblySynchronousResponse && hasRbacAnnotationThatRequiresAuth; + } else { + alwaysAuthenticateRoute = false; + } + routeHandlerBusinessMethods .produce(new AnnotatedRouteHandlerBuildItem(bean, method, routes, routeBaseAnnotation, - annotationStore.hasAnnotation(method, DotNames.BLOCKING), compression)); + blocking, compression, alwaysAuthenticateRoute)); } // AnnotationInstance filterAnnotation = annotationStore.getAnnotation(method, @@ -419,7 +446,7 @@ public boolean test(String name) { RouteMatcher matcher = new RouteMatcher(path, regex, produces, consumes, methods, order); matchers.put(matcher, businessMethod.getMethod()); Function routeFunction = recorder.createRouteFunction(matcher, - bodyHandler.getHandler()); + bodyHandler.getHandler(), businessMethod.shouldAlwaysAuthenticateRoute()); //TODO This needs to be refactored to use routeFunction() taking a Consumer instead RouteBuildItem.Builder builder = RouteBuildItem.builder() diff --git a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/LazyAuthRouteTest.java b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/LazyAuthRouteTest.java new file mode 100644 index 0000000000000..1683099f02f24 --- /dev/null +++ b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/LazyAuthRouteTest.java @@ -0,0 +1,100 @@ +package io.quarkus.vertx.web; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.RolesAllowed; +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.runtime.SecurityIdentityAssociation; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.ext.web.RoutingContext; + +public class LazyAuthRouteTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset("quarkus.http.auth.proactive=false\n"), "application.properties") + .addClasses(TestIdentityProvider.class, TestIdentityController.class, HelloWorldBean.class)); + + @BeforeAll + public static void setupUsers() { + TestIdentityController.resetRoles() + .add("admin", "admin", "admin") + .add("user", "user", "user"); + } + + @Test + public void testAuthZInPlace() { + given().auth().basic("user", "user").when().get("/hello-ra").then().statusCode(403); + } + + @Test + public void testRolesAllowedVoidMethod() { + given().auth().basic("admin", "admin").when().get("/hello-ra").then().statusCode(200).body(is("Hello admin")); + } + + @Test + public void testRolesAllowedDirectResponse() { + given().auth().basic("admin", "admin").when().get("/hello-ra-direct").then().statusCode(200).body(is("Hello admin")); + } + + @Test + public void testAuthenticated() { + given().auth().basic("user", "user").when().get("/hello-auth").then().statusCode(200); + } + + @Test + public void testDenyAll() { + given().auth().basic("user", "user").when().get("/hello-deny").then().statusCode(403); + } + + public static final class HelloWorldBean { + + @Inject + SecurityIdentityAssociation securityIdentityAssociation; + + @Authenticated + @Route(path = "/hello-auth", methods = Route.HttpMethod.GET) + public void greetingsAuth(RoutingContext rc) { + respond(rc); + } + + @RolesAllowed("admin") + @Route(path = "/hello-ra", methods = Route.HttpMethod.GET) + public void greetingsRA(RoutingContext rc) { + respond(rc); + } + + @RolesAllowed("admin") + @Route(path = "/hello-ra-direct", methods = Route.HttpMethod.GET) + public String greetingsRADirect() { + return hello(); + } + + @DenyAll + @Route(path = "/hello-deny", methods = Route.HttpMethod.GET) + public String greetingsDeny() { + return hello(); + } + + private void respond(RoutingContext rc) { + rc.response().end(hello()); + } + + private String hello() { + return "Hello " + securityIdentityAssociation.getIdentity().getPrincipal().getName(); + } + } + +} diff --git a/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java index 1f6f658403d62..b6544b08c7b2f 100644 --- a/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java +++ b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java @@ -3,13 +3,19 @@ import java.lang.reflect.InvocationTargetException; import java.util.List; import java.util.Set; +import java.util.function.BiConsumer; import java.util.function.Function; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpCompression; import io.quarkus.vertx.http.runtime.HttpConfiguration; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.UniSubscriber; +import io.smallrye.mutiny.subscription.UniSubscription; import io.vertx.core.Handler; import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.Router; @@ -56,7 +62,7 @@ public Handler compressRouteHandler(Handler rout } public Function createRouteFunction(RouteMatcher matcher, - Handler bodyHandler) { + Handler bodyHandler, boolean alwaysAuthenticateRoute) { return new Function() { @Override public io.vertx.ext.web.Route apply(Router router) { @@ -86,6 +92,38 @@ public io.vertx.ext.web.Route apply(Router router) { route.consumes(consumes); } } + if (alwaysAuthenticateRoute) { + route = route.handler(routingContext -> { + // check auth haven't happened further up the handler chain + if (routingContext.user() == null) { + // authenticate -> on deferred identity (Uni's) termination user is set to the routing context, + // so SecurityIdentity will be accessible in a synchronous manner + routingContext.> get(QuarkusHttpUser.DEFERRED_IDENTITY_KEY) + .subscribe().withSubscriber(new UniSubscriber() { + @Override + public void onSubscribe(UniSubscription subscription) { + } + + @Override + public void onItem(Object item) { + if (routingContext.response().ended()) { + return; + } + routingContext.next(); + } + + @Override + public void onFailure(Throwable failure) { + BiConsumer handler = routingContext + .get(QuarkusHttpUser.AUTH_FAILURE_HANDLER); + if (handler != null) { + handler.accept(routingContext, failure); + } + } + }); + } + }); + } if (bodyHandler != null) { route.handler(bodyHandler); }