Skip to content

Commit

Permalink
Move RESTEasy Classic RBAC security checks to JAX-RS filter
Browse files Browse the repository at this point in the history
(cherry picked from commit 4a82745c000813b4153a1775329776672af95cfa)
  • Loading branch information
michalvavrik authored and gsmet committed Jan 26, 2024
1 parent 660e634 commit 6cd1598
Show file tree
Hide file tree
Showing 4 changed files with 376 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import org.jboss.jandex.ClassInfo;
Expand All @@ -34,14 +33,14 @@
import io.quarkus.resteasy.runtime.JaxRsSecurityConfig;
import io.quarkus.resteasy.runtime.NotFoundExceptionMapper;
import io.quarkus.resteasy.runtime.SecurityContextFilter;
import io.quarkus.resteasy.runtime.StandardSecurityCheckInterceptor;
import io.quarkus.resteasy.runtime.UnauthorizedExceptionMapper;
import io.quarkus.resteasy.runtime.vertx.JsonArrayReader;
import io.quarkus.resteasy.runtime.vertx.JsonArrayWriter;
import io.quarkus.resteasy.runtime.vertx.JsonObjectReader;
import io.quarkus.resteasy.runtime.vertx.JsonObjectWriter;
import io.quarkus.resteasy.server.common.deployment.ResteasyDeploymentBuildItem;
import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem;
import io.quarkus.vertx.http.deployment.EagerSecurityInterceptorBuildItem;
import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem;
import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem;
import io.quarkus.vertx.http.deployment.devmode.RouteDescriptionBuildItem;
Expand Down Expand Up @@ -91,8 +90,7 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
*/
@BuildStep
void setUpSecurity(BuildProducer<ResteasyJaxrsProviderBuildItem> providers,
BuildProducer<AdditionalBeanBuildItem> additionalBeanBuildItem, Capabilities capabilities,
Optional<EagerSecurityInterceptorBuildItem> eagerSecurityInterceptors) {
BuildProducer<AdditionalBeanBuildItem> additionalBeanBuildItem, Capabilities capabilities) {
providers.produce(new ResteasyJaxrsProviderBuildItem(UnauthorizedExceptionMapper.class.getName()));
providers.produce(new ResteasyJaxrsProviderBuildItem(ForbiddenExceptionMapper.class.getName()));
providers.produce(new ResteasyJaxrsProviderBuildItem(AuthenticationFailedExceptionMapper.class.getName()));
Expand All @@ -102,10 +100,16 @@ void setUpSecurity(BuildProducer<ResteasyJaxrsProviderBuildItem> providers,
if (capabilities.isPresent(Capability.SECURITY)) {
providers.produce(new ResteasyJaxrsProviderBuildItem(SecurityContextFilter.class.getName()));
additionalBeanBuildItem.produce(AdditionalBeanBuildItem.unremovableOf(SecurityContextFilter.class));
if (eagerSecurityInterceptors.isPresent()) {
providers.produce(new ResteasyJaxrsProviderBuildItem(EagerSecurityFilter.class.getName()));
additionalBeanBuildItem.produce(AdditionalBeanBuildItem.unremovableOf(EagerSecurityFilter.class));
}
providers.produce(new ResteasyJaxrsProviderBuildItem(EagerSecurityFilter.class.getName()));
additionalBeanBuildItem.produce(AdditionalBeanBuildItem.unremovableOf(EagerSecurityFilter.class));
additionalBeanBuildItem.produce(
AdditionalBeanBuildItem.unremovableOf(StandardSecurityCheckInterceptor.RolesAllowedInterceptor.class));
additionalBeanBuildItem.produce(AdditionalBeanBuildItem
.unremovableOf(StandardSecurityCheckInterceptor.PermissionsAllowedInterceptor.class));
additionalBeanBuildItem.produce(
AdditionalBeanBuildItem.unremovableOf(StandardSecurityCheckInterceptor.PermitAllInterceptor.class));
additionalBeanBuildItem.produce(
AdditionalBeanBuildItem.unremovableOf(StandardSecurityCheckInterceptor.AuthenticatedInterceptor.class));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package io.quarkus.resteasy.test.security;

import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

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.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import io.vertx.core.json.JsonObject;

/**
* Tests that {@link io.quarkus.security.spi.runtime.SecurityCheck}s are executed by Jakarta REST filters.
*/
public class EagerSecurityCheckTest {

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(TestIdentityProvider.class, TestIdentityController.class, JsonResource.class,
AbstractJsonResource.class, JsonSubResource.class));

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

@Test
public void testAuthenticated() {
testPostJson("auth", "admin", true).then().statusCode(400);
testPostJson("auth", null, true).then().statusCode(401);
testPostJson("auth", "admin", false).then().statusCode(200);
testPostJson("auth", null, false).then().statusCode(401);
}

@Test
public void testRolesAllowed() {
testPostJson("roles", "admin", true).then().statusCode(400);
testPostJson("roles", "user", true).then().statusCode(403);
testPostJson("roles", "admin", false).then().statusCode(200);
testPostJson("roles", "user", false).then().statusCode(403);
}

@Test
public void testRolesAllowedOverriddenMethod() {
testPostJson("/roles-overridden", "admin", true).then().statusCode(400);
testPostJson("/roles-overridden", "user", true).then().statusCode(403);
testPostJson("/roles-overridden", "admin", false).then().statusCode(200);
testPostJson("/roles-overridden", "user", false).then().statusCode(403);
}

@Test
public void testDenyAll() {
testPostJson("deny", "admin", true).then().statusCode(403);
testPostJson("deny", null, true).then().statusCode(401);
testPostJson("deny", "admin", false).then().statusCode(403);
testPostJson("deny", null, false).then().statusCode(401);
}

@Test
public void testDenyAllClassLevel() {
testPostJson("/sub-resource/deny-class-level-annotation", "admin", true).then().statusCode(403);
testPostJson("/sub-resource/deny-class-level-annotation", null, true).then().statusCode(401);
testPostJson("/sub-resource/deny-class-level-annotation", "admin", false).then().statusCode(403);
testPostJson("/sub-resource/deny-class-level-annotation", null, false).then().statusCode(401);
}

@Test
public void testPermitAll() {
testPostJson("permit", "admin", true).then().statusCode(400);
testPostJson("permit", null, true).then().statusCode(400);
testPostJson("permit", "admin", false).then().statusCode(200);
testPostJson("permit", null, false).then().statusCode(200);
}

@Test
public void testSubResource() {
testPostJson("/sub-resource/roles", "admin", true).then().statusCode(400);
testPostJson("/sub-resource/roles", "user", true).then().statusCode(403);
testPostJson("/sub-resource/roles", "admin", false).then().statusCode(200);
testPostJson("/sub-resource/roles", "user", false).then().statusCode(403);
}

private static Response testPostJson(String path, String username, boolean invalid) {
var req = RestAssured.given();
if (username != null) {
req = req.auth().preemptive().basic(username, username);
}
return req
.contentType(ContentType.JSON)
.body((invalid ? "}" : "") + "{\"simple\": \"obj\"}").post(path);
}

@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public static class JsonResource extends AbstractJsonResource {

@Authenticated
@Path("/auth")
@POST
public JsonObject auth(JsonObject array) {
return array.put("test", "testval");
}

@RolesAllowed("admin")
@Path("/roles")
@POST
public JsonObject roles(JsonObject array) {
return array.put("test", "testval");
}

@PermitAll
@Path("/permit")
@POST
public JsonObject permit(JsonObject array) {
return array.put("test", "testval");
}

@PermitAll
@Path("/sub-resource")
public JsonSubResource subResource() {
return new JsonSubResource();
}

@RolesAllowed("admin")
@Override
public JsonObject rolesOverridden(JsonObject array) {
return array.put("test", "testval");
}
}

@DenyAll
public static class JsonSubResource {
@RolesAllowed("admin")
@Path("/roles")
@POST
public JsonObject roles(JsonObject array) {
return array.put("test", "testval");
}

@Path("/deny-class-level-annotation")
@POST
public JsonObject denyClassLevelAnnotation(JsonObject array) {
return array.put("test", "testval");
}
}

public static abstract class AbstractJsonResource {
@DenyAll
@Path("/deny")
@POST
public JsonObject deny(JsonObject array) {
return array.put("test", "testval");
}

@Path("/roles-overridden")
@POST
public abstract JsonObject rolesOverridden(JsonObject array);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package io.quarkus.resteasy.runtime;

import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_FAILURE;
import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_SUCCESS;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;

import jakarta.annotation.Priority;
import jakarta.enterprise.event.Event;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
Expand All @@ -14,7 +18,19 @@
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.ext.Provider;

import org.eclipse.microprofile.config.ConfigProvider;

import io.quarkus.arc.Arc;
import io.quarkus.security.UnauthorizedException;
import io.quarkus.security.identity.CurrentIdentityAssociation;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.spi.runtime.AuthorizationController;
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.SecurityCheckStorage;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.quarkus.vertx.http.runtime.security.EagerSecurityInterceptorStorage;
import io.vertx.ext.web.RoutingContext;

Expand All @@ -29,33 +45,106 @@ public void accept(RoutingContext routingContext) {
}
};
private final Map<MethodDescription, Consumer<RoutingContext>> cache = new HashMap<>();
private final EagerSecurityInterceptorStorage interceptorStorage;
private final SecurityEventHelper<AuthorizationSuccessEvent, AuthorizationFailureEvent> eventHelper;

@Context
ResourceInfo resourceInfo;

@Inject
EagerSecurityInterceptorStorage interceptorStorage;
RoutingContext routingContext;

@Inject
RoutingContext routingContext;
SecurityCheckStorage securityCheckStorage;

@Inject
CurrentIdentityAssociation identityAssociation;

@Inject
AuthorizationController authorizationController;

public EagerSecurityFilter() {
var interceptorStorageHandle = Arc.container().instance(EagerSecurityInterceptorStorage.class);
this.interceptorStorage = interceptorStorageHandle.isAvailable() ? interceptorStorageHandle.get() : null;
Event<Object> event = Arc.container().beanManager().getEvent();
this.eventHelper = new SecurityEventHelper<>(event.select(AuthorizationSuccessEvent.class),
event.select(AuthorizationFailureEvent.class), AUTHORIZATION_SUCCESS,
AUTHORIZATION_FAILURE, Arc.container().beanManager(),
ConfigProvider.getConfig().getOptionalValue("quarkus.security.events.enabled", Boolean.class).orElse(false));
}

@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
var description = MethodDescription.ofMethod(resourceInfo.getResourceMethod());
var interceptor = cache.get(description);
if (authorizationController.isAuthorizationEnabled()) {
var description = MethodDescription.ofMethod(resourceInfo.getResourceMethod());
if (interceptorStorage != null) {
applyEagerSecurityInterceptors(description);
}
applySecurityChecks(description);
}
}

private void applySecurityChecks(MethodDescription description) {
SecurityCheck check = securityCheckStorage.getSecurityCheck(description);
if (check != null) {
if (check.isPermitAll()) {
fireEventOnAuthZSuccess(check, null);
} else {
if (check.requiresMethodArguments()) {
if (identityAssociation.getIdentity().isAnonymous()) {
var exception = new UnauthorizedException();
if (eventHelper.fireEventOnFailure()) {
fireEventOnAuthZFailure(exception, check);
}
throw exception;
}
// security check will be performed by CDI interceptor
return;
}
if (eventHelper.fireEventOnFailure()) {
try {
check.apply(identityAssociation.getIdentity(), description, null);
} catch (Exception e) {
fireEventOnAuthZFailure(e, check);
throw e;
}
} else {
check.apply(identityAssociation.getIdentity(), description, null);
}
fireEventOnAuthZSuccess(check, identityAssociation.getIdentity());
}
// prevent repeated security checks
routingContext.put(EagerSecurityFilter.class.getName(), resourceInfo.getResourceMethod());
}
}

private void fireEventOnAuthZFailure(Exception exception, SecurityCheck check) {
eventHelper.fireFailureEvent(new AuthorizationFailureEvent(
identityAssociation.getIdentity(), exception, check.getClass().getName(),
Map.of(RoutingContext.class.getName(), routingContext)));
}

if (interceptor == NULL_SENTINEL) {
return;
} else if (interceptor != null) {
interceptor.accept(routingContext);
private void fireEventOnAuthZSuccess(SecurityCheck check, SecurityIdentity securityIdentity) {
if (eventHelper.fireEventOnSuccess()) {
eventHelper.fireSuccessEvent(new AuthorizationSuccessEvent(securityIdentity,
check.getClass().getName(), Map.of(RoutingContext.class.getName(), routingContext)));
}
}

interceptor = interceptorStorage.getInterceptor(description);
if (interceptor == null) {
cache.put(description, NULL_SENTINEL);
} else {
cache.put(description, interceptor);
interceptor.accept(routingContext);
private void applyEagerSecurityInterceptors(MethodDescription description) {
var interceptor = cache.get(description);
if (interceptor != NULL_SENTINEL) {
if (interceptor != null) {
interceptor.accept(routingContext);
} else {
interceptor = interceptorStorage.getInterceptor(description);
if (interceptor == null) {
cache.put(description, NULL_SENTINEL);
} else {
cache.put(description, interceptor);
interceptor.accept(routingContext);
}
}
}
}
}
Loading

0 comments on commit 6cd1598

Please sign in to comment.