From 293070ea9d329bb73b6e9f5bdc7488451e1a744d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sun, 17 Dec 2023 16:39:41 +0100 Subject: [PATCH] Add RoutingContext to Vert.x duplicated context local data --- .../main/asciidoc/security-customization.adoc | 37 ++++++++++ .../test/security/RolesAllowedResource.java | 7 ++ .../SecurityIdentityAugmentorTest.java | 24 +++++++ .../AbstractPermissionsAllowedTestCase.java | 7 ++ .../security/PermissionsAllowedResource.java | 8 +++ .../PermissionsIdentityAugmentor.java | 10 +++ .../security/MtlsRequestBasicAuthTest.java | 71 +++++++++++++++++++ .../vertx/http/runtime/RouteConstants.java | 5 ++ .../vertx/http/runtime/VertxHttpRecorder.java | 11 +++ .../runtime/security/HttpSecurityUtils.java | 27 +++++++ 10 files changed, 207 insertions(+) diff --git a/docs/src/main/asciidoc/security-customization.adoc b/docs/src/main/asciidoc/security-customization.adoc index da22ad2849803..45b42e16c6322 100644 --- a/docs/src/main/asciidoc/security-customization.adoc +++ b/docs/src/main/asciidoc/security-customization.adoc @@ -302,6 +302,43 @@ class SecurityIdentitySupplier implements Supplier { } ---- +The CDI request context activation shown in the example above wouldn't help you to access the `RoutingContext` when proactive authentication is enabled. +The following example illustrates how you can access the `RoutingContext` from the `SecurityIdentityAugmentor`: + +[source,java] +---- +package org.acme.security; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + @Override + public Uni augment(SecurityIdentity securityIdentity, AuthenticationRequestContext authenticationRequestContext) { + var builder = QuarkusSecurityIdentity.builder(securityIdentity); + + final RoutingContext routingContext; + if (!securityIdentity.isAnonymous()) { + routingContext = HttpSecurityUtils.getRoutingContextAttribute(); <1> + } else { + routingContext = securityIdentity.getAttribute(RoutingContext.class.getName()); <2> + } + + if (routingContext != null) { + // here you augment SecurityIdentity based on RoutingContext + } + return Uni.createFrom().item(builder.build()); + } +} +---- +<1> Quarkus puts the `RoutingContext` to Vert.x duplicated context local data so that it is available during authentication and authorization. +<2> Some authentication mechanisms like the OIDC authentication mechanism add `RoutingContext` to the `SecurityIdentity` attributes. + [[jaxrs-security-context]] == Custom Jakarta REST SecurityContext diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedResource.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedResource.java index 6a285c54ac91a..34927cfa5e55a 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedResource.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedResource.java @@ -37,4 +37,11 @@ public String admin() { public String getSecurityIdentity() { return currentIdentityAssociation.getIdentity().getPrincipal().getName(); } + + @Path("/admin/security-identity/routing-context") + @RolesAllowed("root") + @GET + public String getSecurityIdentityPrincipal() { + return currentIdentityAssociation.getIdentity().getPrincipal().getName(); + } } diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/SecurityIdentityAugmentorTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/SecurityIdentityAugmentorTest.java index 7d50c1e8ec0e2..67274535ed843 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/SecurityIdentityAugmentorTest.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/SecurityIdentityAugmentorTest.java @@ -21,8 +21,10 @@ import io.quarkus.security.test.utils.TestIdentityController; import io.quarkus.security.test.utils.TestIdentityProvider; import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; import io.restassured.RestAssured; import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; public class SecurityIdentityAugmentorTest { @@ -44,6 +46,20 @@ public void testSecurityIdentityAugmentor() { .body(Matchers.is("admin")); } + @Test + public void testAccessToRoutingContext() { + RestAssured.given() + .auth().basic("admin", "admin") + .get("/roles/admin/security-identity/routing-context") + .then().statusCode(403); + RestAssured.given() + .auth().basic("admin", "admin") + .header("extra-role", "root") + .get("/roles/admin/security-identity/routing-context") + .then().statusCode(200) + .body(Matchers.is("admin")); + } + @ApplicationScoped public static class CustomAugmentor implements SecurityIdentityAugmentor { @@ -59,6 +75,14 @@ public Uni augment(SecurityIdentity identity, AuthenticationRe Supplier build(SecurityIdentity identity) { QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); builder.addRole("admin"); + RoutingContext event = HttpSecurityUtils.getRoutingContextAttribute(); + if (event == null) { + throw new IllegalStateException( + "RoutingContext is expected to be present in Vert.x duplicated context local data"); + } + if ("root".equals(event.request().getHeader("extra-role"))) { + builder.addRole("root"); + } return builder::build; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractPermissionsAllowedTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractPermissionsAllowedTestCase.java index 1ff428bba9160..e58a3215f9534 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractPermissionsAllowedTestCase.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractPermissionsAllowedTestCase.java @@ -74,6 +74,13 @@ public void testStringPermissionOneOfPermissionsAndActionsNonBlocking() { RestAssured.given().auth().basic("viewer", "viewer").get("/permissions-non-blocking/admin").then().statusCode(403); } + @Test + public void testConditionalPermissionBasedOnRoutingContext() { + RestAssured.given().auth().basic("viewer", "viewer").put("permissions/edit").then().statusCode(403); + RestAssured.given().auth().basic("viewer", "viewer").header("sudo", "edit").put("permissions/edit").then() + .statusCode(200).body(Matchers.is("edit")); + } + @Test public void testBlockingAccessToIdentityOnIOThread() { // invokes GET /permissions/security-identity endpoint that requires one permission: get-identity diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/PermissionsAllowedResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/PermissionsAllowedResource.java index 1060318bdff7a..11328145f5956 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/PermissionsAllowedResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/PermissionsAllowedResource.java @@ -3,6 +3,7 @@ import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.QueryParam; @@ -34,6 +35,13 @@ public String admin() { return "admin"; } + @Path("/edit") + @PermissionsAllowed("edit") + @PUT + public String edit() { + return "edit"; + } + @NonBlocking @Path("/admin/security-identity") @PermissionsAllowed("get-identity") diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/PermissionsIdentityAugmentor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/PermissionsIdentityAugmentor.java index 70124e336727f..44894de995c85 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/PermissionsIdentityAugmentor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/PermissionsIdentityAugmentor.java @@ -12,7 +12,9 @@ import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; @ApplicationScoped public class PermissionsIdentityAugmentor implements SecurityIdentityAugmentor { @@ -42,6 +44,14 @@ SecurityIdentity build(SecurityIdentity identity) { builder.addPermissionChecker(new PermissionCheckBuilder().addPermission("read", "resource-viewer").build()); break; } + RoutingContext event = HttpSecurityUtils.getRoutingContextAttribute(); + if (event == null) { + throw new IllegalStateException( + "RoutingContext is expected to be present in Vert.x duplicated context local data"); + } + if ("edit".equals(event.request().getHeader("sudo"))) { + builder.addPermissionChecker(new PermissionCheckBuilder().addPermission("edit").build()); + } return builder.build(); } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/MtlsRequestBasicAuthTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/MtlsRequestBasicAuthTest.java index 2a651111126f0..e3a62f8ca20bc 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/MtlsRequestBasicAuthTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/MtlsRequestBasicAuthTest.java @@ -4,6 +4,9 @@ import java.io.File; import java.net.URL; +import java.security.Permission; +import java.util.function.Consumer; +import java.util.function.Function; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; @@ -12,19 +15,30 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.security.StringPermission; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.quarkus.security.test.utils.TestIdentityController; import io.quarkus.security.test.utils.TestIdentityProvider; import io.quarkus.test.QuarkusUnitTest; import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.restassured.RestAssured; +import io.smallrye.mutiny.Uni; import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; public class MtlsRequestBasicAuthTest { @TestHTTPResource(value = "/mtls", ssl = true) URL url; + @TestHTTPResource(value = "/mtls-augmentor", ssl = true) + URL augmentorUrl; + @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar @@ -65,6 +79,19 @@ public void testNoClientCertBasicAuth() { .get(url).then().statusCode(200).body(is("admin")); } + @Test + public void testSecurityIdentityAugmentor() { + RestAssured.given() + .keyStore(new File("src/test/resources/conf/mtls/client-keystore.jks"), "password") + .trustStore(new File("src/test/resources/conf/mtls/client-truststore.jks"), "password") + .get(augmentorUrl).then().statusCode(401); + RestAssured.given() + .header("add-perm", "true") + .keyStore(new File("src/test/resources/conf/mtls/client-keystore.jks"), "password") + .trustStore(new File("src/test/resources/conf/mtls/client-truststore.jks"), "password") + .get(augmentorUrl).then().statusCode(200); + } + @ApplicationScoped static class MyBean { @@ -72,7 +99,51 @@ public void register(@Observes Router router) { router.get("/mtls").handler(rc -> { rc.response().end(QuarkusHttpUser.class.cast(rc.user()).getSecurityIdentity().getPrincipal().getName()); }); + router.get("/mtls-augmentor").handler(rc -> { + if (rc.user() instanceof QuarkusHttpUser quarkusHttpUser) { + quarkusHttpUser.getSecurityIdentity().checkPermission(new StringPermission("use-mTLS")) + .subscribe().with(new Consumer() { + @Override + public void accept(Boolean accessGranted) { + if (accessGranted) { + rc.end(); + } else { + rc.fail(401); + } + } + }); + } else { + rc.fail(500); + } + }); } } + + @ApplicationScoped + static class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + + @Override + public Uni augment(SecurityIdentity securityIdentity, + AuthenticationRequestContext authenticationRequestContext) { + if (!securityIdentity.isAnonymous() + && "CN=client,OU=cert,O=quarkus,L=city,ST=state,C=AU".equals(securityIdentity.getPrincipal().getName())) { + return Uni.createFrom().item(QuarkusSecurityIdentity.builder(securityIdentity) + .addPermissionChecker(new Function>() { + @Override + public Uni apply(Permission required) { + RoutingContext event = HttpSecurityUtils.getRoutingContextAttribute(); + final boolean pass; + if (event != null) { + pass = Boolean.parseBoolean(event.request().headers().get("add-perm")); + } else { + pass = false; + } + return Uni.createFrom().item(pass); + } + }).build()); + } + return Uni.createFrom().item(securityIdentity); + } + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/RouteConstants.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/RouteConstants.java index 6d00a3afa9b07..163491e014383 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/RouteConstants.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/RouteConstants.java @@ -17,6 +17,11 @@ private RouteConstants() { * configuration. */ public static final int ROUTE_ORDER_RECORD_START_TIME = Integer.MIN_VALUE; + /** + * Order value ({@value #ROUTE_ORDER_ROUTING_CONTEXT}) for the handler adding + * the {@link io.vertx.ext.web.RoutingContext} to duplicated context local data. + */ + public static final int ROUTE_ORDER_ROUTING_CONTEXT = Integer.MIN_VALUE; /** * Order value ({@value #ROUTE_ORDER_HOT_REPLACEMENT}) for the hot-replacement body handler. */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index 5978fa7d2e819..f8edc558f58ed 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -60,6 +60,7 @@ import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration; import io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers; import io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; import io.smallrye.common.vertx.VertxContext; import io.vertx.core.*; import io.vertx.core.http.*; @@ -489,6 +490,16 @@ public void handle(RoutingContext event) { } }); } + httpRouteRouter.route().order(RouteConstants.ROUTE_ORDER_ROUTING_CONTEXT).handler(new Handler() { + @Override + public void handle(RoutingContext context) { + // primarily we put RoutingContext to duplicated context local data so that users don't have to activate + // CDI request context during authentication and authorization when proactive auth is enabled, however + // it is done here so that others can profit from it as well + HttpSecurityUtils.setRoutingContextAttribute(context); + context.next(); + } + }); if (launchMode == LaunchMode.DEVELOPMENT && liveReloadConfig.password.isPresent() && hotReplacementContext.getDevModeType() == DevModeType.REMOTE_SERVER_SIDE) { root = remoteSyncHandler = new RemoteSyncHandler(liveReloadConfig.password.get(), root, hotReplacementContext); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityUtils.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityUtils.java index 8d5fe1f8a57fa..508bde8601b11 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityUtils.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityUtils.java @@ -1,6 +1,11 @@ package io.quarkus.vertx.http.runtime.security; +import static io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.isExplicitlyMarkedAsUnsafe; +import static io.smallrye.common.vertx.VertxContext.isDuplicatedContext; + import io.quarkus.security.identity.request.AuthenticationRequest; +import io.vertx.core.Context; +import io.vertx.core.Vertx; import io.vertx.ext.web.RoutingContext; public final class HttpSecurityUtils { @@ -18,4 +23,26 @@ public static AuthenticationRequest setRoutingContextAttribute(AuthenticationReq public static RoutingContext getRoutingContextAttribute(AuthenticationRequest request) { return request.getAttribute(ROUTING_CONTEXT_ATTRIBUTE); } + + /** + * Add {@link RoutingContext} to Vert.x duplicated context local data. + */ + public static void setRoutingContextAttribute(RoutingContext event) { + final Context context = Vertx.currentContext(); + if (isSafeToUseContext(context)) { + context.putLocal(RoutingContext.class.getName(), event); + } + } + + /** + * @return RoutingContext if present in Vert.x duplicated context local data + */ + public static RoutingContext getRoutingContextAttribute() { + final Context context = Vertx.currentContext(); + return isSafeToUseContext(context) ? context.getLocal(RoutingContext.class.getName()) : null; + } + + private static boolean isSafeToUseContext(io.vertx.core.Context context) { + return context != null && isDuplicatedContext(context) && !isExplicitlyMarkedAsUnsafe(context); + } }