diff --git a/docs/src/main/asciidoc/security-customization.adoc b/docs/src/main/asciidoc/security-customization.adoc index f6f60fbdf42929..bb7003f0e094ac 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 6a285c54ac91aa..34927cfa5e55ab 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 7d50c1e8ec0e2f..67274535ed8432 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 1ff428bba91601..e58a3215f95348 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 1060318bdff7ab..11328145f5956e 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 70124e336727fb..44894de995c85b 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 2a651111126f0d..e3a62f8ca20bcc 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/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index 6345c47d664584..4af1e8d70f5f9a 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -233,6 +233,11 @@ public void handle(RoutingContext event) { if (authenticator == null) { authenticator = CDI.current().select(HttpAuthenticator.class).get(); } + + // 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 + HttpSecurityUtils.setRoutingContextAttribute(event); + //we put the authenticator into the routing context so it can be used by other systems event.put(HttpAuthenticator.class.getName(), authenticator); if (patchMatchingPolicyEnabled == null) { 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 8d5fe1f8a57fa0..f84b614600585f 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,10 +1,18 @@ 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 org.jboss.logging.Logger; + 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 { public final static String ROUTING_CONTEXT_ATTRIBUTE = "quarkus.http.routing.context"; + private static final Logger LOG = Logger.getLogger(HttpSecurityUtils.class); private HttpSecurityUtils() { @@ -18,4 +26,33 @@ 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 (context.getLocal(RoutingContext.class.getName()) == null) { + if (isSafeToUseContext(context)) { + context.putLocal(RoutingContext.class.getName(), event); + } else { + LOG.debug(""" + RoutingContext not added to Vert.x context as it is not safe to use the context local data. + It won't be possible to access RoutingContext with 'HttpSecurityUtils.getRoutingContextAttribute()'. + """); + } + } + } + + /** + * @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); + } }