Skip to content

Commit

Permalink
Add RoutingContext to Vert.x duplicated context local data
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Dec 18, 2023
1 parent 4470678 commit 424e6f1
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 0 deletions.
37 changes: 37 additions & 0 deletions docs/src/main/asciidoc/security-customization.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,43 @@ class SecurityIdentitySupplier implements Supplier<SecurityIdentity> {
}
----

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<SecurityIdentity> 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]]

Check warning on line 342 in docs/src/main/asciidoc/security-customization.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Custom Jakarta REST SecurityContext'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Custom Jakarta REST SecurityContext'.", "location": {"path": "docs/src/main/asciidoc/security-customization.adoc", "range": {"start": {"line": 342, "column": 20}}}, "severity": "INFO"}
== Custom Jakarta REST SecurityContext

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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 {

Expand All @@ -59,6 +75,14 @@ public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRe
Supplier<SecurityIdentity> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -65,14 +79,71 @@ 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 {

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<Boolean>() {
@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<SecurityIdentity> 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<Permission, Uni<Boolean>>() {
@Override
public Uni<Boolean> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {

Expand All @@ -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);
}
}

0 comments on commit 424e6f1

Please sign in to comment.