Skip to content

Commit

Permalink
Merge pull request #45848 from michalvavrik/reproducer/permission-che…
Browse files Browse the repository at this point in the history
…cker-multi

Quarkus REST: Run security checks that require method arguments in a non-blocking manner before secured methods are invoked
  • Loading branch information
geoand authored Jan 24, 2025
2 parents 1746564 + 64b81af commit 221d76e
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 129 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,9 @@
import io.quarkus.resteasy.reactive.server.runtime.exceptionmappers.ForbiddenExceptionMapper;
import io.quarkus.resteasy.reactive.server.runtime.exceptionmappers.UnauthorizedExceptionMapper;
import io.quarkus.resteasy.reactive.server.runtime.security.EagerSecurityContext;
import io.quarkus.resteasy.reactive.server.runtime.security.EagerSecurityHandler;
import io.quarkus.resteasy.reactive.server.runtime.security.EagerSecurityHandler.AuthZPolicyCustomizer;
import io.quarkus.resteasy.reactive.server.runtime.security.EagerSecurityHandler.HttpPermissionsAndSecurityChecksCustomizer;
import io.quarkus.resteasy.reactive.server.runtime.security.EagerSecurityHandler.HttpPermissionsOnlyCustomizer;
import io.quarkus.resteasy.reactive.server.runtime.security.EagerSecurityInterceptorHandler;
import io.quarkus.resteasy.reactive.server.runtime.security.SecurityContextOverrideHandler;
import io.quarkus.resteasy.reactive.server.spi.AllowNotRestParametersBuildItem;
Expand Down Expand Up @@ -1666,7 +1668,8 @@ private static List<HandlerChainCustomizer> createEagerSecCustomizerWithIntercep
var requiresSecurityCheck = interceptedMethods.get(method);
final HandlerChainCustomizer eagerSecCustomizer;
if (requiresSecurityCheck && !applyAuthorizationPolicy) {
eagerSecCustomizer = EagerSecurityHandler.Customizer.newInstance(false);
// standard security annotation and possibly authorization using configuration
eagerSecCustomizer = new HttpPermissionsAndSecurityChecksCustomizer();
} else {
eagerSecCustomizer = newEagerSecurityHandlerCustomizerInstance(originalMethod, endpointImpl,
withDefaultSecurityCheck, applyAuthorizationPolicy, permsAllowedMetaAnnotationItem);
Expand All @@ -1678,13 +1681,16 @@ private static HandlerChainCustomizer newEagerSecurityHandlerCustomizerInstance(
boolean withDefaultSecurityCheck, boolean applyAuthorizationPolicy,
PermissionsAllowedMetaAnnotationBuildItem permsAllowedMetaAnnotationItem) {
if (applyAuthorizationPolicy) {
return EagerSecurityHandler.Customizer.newInstanceWithAuthorizationPolicy();
// @AuthorizationPolicy and possibly authorization using configuration
return new AuthZPolicyCustomizer();
}
if (withDefaultSecurityCheck
|| consumesStandardSecurityAnnotations(method, endpointImpl, permsAllowedMetaAnnotationItem)) {
return EagerSecurityHandler.Customizer.newInstance(false);
// standard security annotation and possibly authorization using configuration
return new HttpPermissionsAndSecurityChecksCustomizer();
}
return EagerSecurityHandler.Customizer.newInstance(true);
// authorization using configuration that applies to JAX-RS only
return new HttpPermissionsOnlyCustomizer();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package io.quarkus.resteasy.reactive.server.test.security;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;

import org.hamcrest.Matchers;
import org.jboss.resteasy.reactive.RestMulti;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.PermissionChecker;
import io.quarkus.security.PermissionsAllowed;
import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;
import io.smallrye.mutiny.Multi;

public abstract class AbstractPermissionCheckerRestMultiTest {

@RegisterExtension
static QuarkusUnitTest TEST = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar.addClasses(TestResource.class, TestIdentityController.class,
TestIdentityProvider.class));

@BeforeAll
public static void setupUsers() {
TestIdentityController.resetRoles().add("user", "user");
}

@Test
public void testReturnRestMultiAsMulti() {
RestAssured
.given()
.auth().preemptive().basic("user", "user")
.queryParam("user", "Georgios")
.get("/test/public-multi")
.then()
.statusCode(201)
.header("header1", "value header 1");
RestAssured
.given()
.auth().preemptive().basic("user", "user")
.queryParam("user", "Georgios")
.get("/test/secured-multi")
.then()
.statusCode(201)
.header("header1", "value header 1");
RestAssured
.given()
.auth().preemptive().basic("user", "user")
.queryParam("user", "Sergey")
.get("/test/secured-multi")
.then()
.statusCode(403)
.header("header1", Matchers.nullValue());
}

@Test
public void testReturnRestMulti() {
RestAssured
.given()
.auth().preemptive().basic("user", "user")
.queryParam("user", "Georgios")
.get("/test/secured-rest-multi")
.then()
.statusCode(201)
.header("header1", "value header 1");
RestAssured
.given()
.auth().preemptive().basic("user", "user")
.queryParam("user", "Sergey")
.get("/test/secured-rest-multi")
.then()
.statusCode(403)
.header("header1", Matchers.nullValue());
}

@Path("/test")
public static class TestResource {

@GET
@Path("public-multi")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Multi<Byte> publicMethod(@QueryParam("user") String user) {
return RestMulti.fromMultiData(Multi.createFrom().<Byte> empty())
.status(201)
.header("header1", "value header 1")
.build();
}

@PermissionsAllowed("secured")
@GET
@Path("secured-multi")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Multi<Byte> securedMethod(@QueryParam("user") String user) {
return RestMulti.fromMultiData(Multi.createFrom().<Byte> empty())
.status(201)
.header("header1", "value header 1")
.build();
}

@PermissionsAllowed("secured")
@GET
@Path("secured-rest-multi")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public RestMulti<Byte> securedMethodRestMulti(@QueryParam("user") String user) {
return RestMulti.fromMultiData(Multi.createFrom().<Byte> empty())
.status(201)
.header("header1", "value header 1")
.build();
}

@PermissionChecker(value = "secured")
public boolean canCallSecured(String user) {
return "Georgios".equals(user);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.quarkus.resteasy.reactive.server.test.security;

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

import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;

public class LazyAuthPermissionCheckerRestMultiTest extends AbstractPermissionCheckerRestMultiTest {

@RegisterExtension
static QuarkusUnitTest TEST = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addAsResource(new StringAsset("""
quarkus.http.auth.proactive=false
"""), "application.properties")
.addClasses(TestResource.class, TestIdentityController.class, TestIdentityProvider.class));

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.resteasy.reactive.server.test.security;

import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;

public class ProactiveAuthPermissionCheckerRestMultiTest extends AbstractPermissionCheckerRestMultiTest {

@RegisterExtension
static QuarkusUnitTest TEST = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar.addClasses(TestResource.class, TestIdentityController.class,
TestIdentityProvider.class));

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package io.quarkus.resteasy.reactive.server.runtime.security;

import static io.quarkus.resteasy.reactive.server.runtime.StandardSecurityCheckInterceptor.STANDARD_SECURITY_CHECK_INTERCEPTOR;
import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_FAILURE;
import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_SUCCESS;

import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

Expand All @@ -26,6 +28,7 @@
import io.quarkus.security.spi.runtime.AuthorizationFailureEvent;
import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent;
import io.quarkus.security.spi.runtime.MethodDescription;
import io.quarkus.security.spi.runtime.SecurityCheck;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.quarkus.vertx.http.runtime.security.AbstractPathMatchingHttpSecurityPolicy;
Expand Down Expand Up @@ -82,7 +85,7 @@ Uni<SecurityIdentity> getDeferredIdentity() {
return Uni.createFrom().deferred(new Supplier<Uni<? extends SecurityIdentity>>() {
@Override
public Uni<SecurityIdentity> get() {
return EagerSecurityContext.instance.identityAssociation.get().getDeferredIdentity();
return identityAssociation.get().getDeferredIdentity();
}
});
}
Expand Down Expand Up @@ -159,4 +162,53 @@ public SecurityIdentity apply(SecurityCheckWithIdentity checkWithIdentity) {
}
});
}

Uni<?> runSecurityCheck(SecurityCheck check, MethodDescription invokedMethodDesc,
ResteasyReactiveRequestContext requestContext, SecurityIdentity securityIdentity) {
preventRepeatedSecurityChecks(requestContext, invokedMethodDesc);
var checkResult = check.nonBlockingApply(securityIdentity, invokedMethodDesc,
requestContext.getParameters());
if (eventHelper.fireEventOnFailure()) {
checkResult = checkResult
.onFailure()
.invoke(new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) {
eventHelper
.fireFailureEvent(new AuthorizationFailureEvent(
securityIdentity, throwable, check.getClass().getName(),
createEventPropsWithRoutingCtx(requestContext), invokedMethodDesc));
}
});
}
if (eventHelper.fireEventOnSuccess()) {
checkResult = checkResult
.invoke(new Runnable() {
@Override
public void run() {
eventHelper.fireSuccessEvent(
new AuthorizationSuccessEvent(securityIdentity,
check.getClass().getName(),
createEventPropsWithRoutingCtx(requestContext), invokedMethodDesc));
}
});
}
return checkResult;
}

static void preventRepeatedSecurityChecks(ResteasyReactiveRequestContext requestContext,
MethodDescription methodDescription) {
// propagate information that security check has been performed on this method to the SecurityHandler
// via io.quarkus.resteasy.reactive.server.runtime.StandardSecurityCheckInterceptor
requestContext.setProperty(STANDARD_SECURITY_CHECK_INTERCEPTOR, methodDescription);
}

static Map<String, Object> createEventPropsWithRoutingCtx(ResteasyReactiveRequestContext requestContext) {
final RoutingContext routingContext = requestContext.unwrap(RoutingContext.class);
if (routingContext == null) {
return Map.of();
} else {
return Map.of(RoutingContext.class.getName(), routingContext);
}
}
}
Loading

0 comments on commit 221d76e

Please sign in to comment.