Skip to content

Commit

Permalink
Merge pull request quarkusio#39579 from geoand/security-serverexcepti…
Browse files Browse the repository at this point in the history
…onmapper

Don't run CDI interceptors on class-level exception mappers
  • Loading branch information
geoand authored Mar 20, 2024
2 parents cf0c0ce + 71a139f commit 6f7244a
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 2 deletions.
13 changes: 11 additions & 2 deletions docs/src/main/asciidoc/rest.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2242,8 +2242,16 @@ public class Endpoint {
}
----

NOTE: exception mappers defined in REST endpoint classes will only be called if the
exception is thrown in the same class. If you want to define global exception mappers,
[TIP]
====
By default, methods annotated with `@ServerExceptionMapper` do **not** run CDI interceptors that apply to the other methods of the class (like ones needed for implementing security method level security).
Users however can opt into interceptors by adding the corresponding annotations to the method.
====

[NOTE]
====
Εxception mappers defined in REST endpoint classes will only be called if the exception is thrown in the same class. If you want to define global exception mappers,
simply define them outside a REST endpoint class:
[source,java]
Expand All @@ -2262,6 +2270,7 @@ class ExceptionMappers {
----
You can also declare link:{jaxrsspec}#exceptionmapper[exception mappers in the Jakarta REST way].
====

Your exception mapper may declare any of the following parameter types:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import org.jboss.jandex.DotName;
import org.jboss.resteasy.reactive.multipart.FileUpload;

import io.quarkus.arc.NoClassInterceptors;

final class DotNames {

static final String POPULATE_METHOD_NAME = "populate";
Expand All @@ -20,6 +22,8 @@ final class DotNames {
static final DotName PATH_NAME = DotName.createSimple(Path.class.getName());
static final DotName FILE_NAME = DotName.createSimple(File.class.getName());

static final DotName NO_CLASS_INTERCEPTORS = DotName.createSimple(NoClassInterceptors.class);

private DotNames() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames;
import org.jboss.resteasy.reactive.common.processor.scanning.ResourceScanningResult;
import org.jboss.resteasy.reactive.server.injection.ContextProducers;
import org.jboss.resteasy.reactive.server.processor.util.ResteasyReactiveServerDotNames;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
import io.quarkus.arc.deployment.AutoInjectAnnotationBuildItem;
import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.arc.processor.Annotations;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.BuiltinScope;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.deployment.annotations.BuildProducer;
Expand Down Expand Up @@ -85,6 +90,39 @@ void unremovableContextMethodParams(Optional<ResourceScanningResultBuildItem> re
}
}

@BuildStep
void perClassExceptionMapperSupport(Optional<ResourceScanningResultBuildItem> resourceScanningResultBuildItem,
BuildProducer<AnnotationsTransformerBuildItem> producer) {
if (resourceScanningResultBuildItem.isEmpty()) {
return;
}
List<MethodInfo> methodExceptionMapper = resourceScanningResultBuildItem.get().getResult()
.getClassLevelExceptionMappers();
if (methodExceptionMapper.isEmpty()) {
return;
}
producer.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() {

@Override
public boolean appliesTo(AnnotationTarget.Kind kind) {
return kind == AnnotationTarget.Kind.METHOD;
}

@Override
public void transform(TransformationContext ctx) {
final Collection<AnnotationInstance> annotations = ctx.getAnnotations();
AnnotationInstance serverExceptionMapper = Annotations.find(annotations,
ResteasyReactiveDotNames.SERVER_EXCEPTION_MAPPER);
if (serverExceptionMapper == null) {
return;
}
// we want to make sure that class level exception mappers do not run
// interceptors bound using class-level interceptor bindings (like security)
ctx.transform().add(DotNames.NO_CLASS_INTERCEPTORS).done();
}
}));
}

@BuildStep
void subResourcesAsBeans(ResourceScanningResultBuildItem setupEndpointsResult,
List<SubResourcesAsBeansBuildItem> subResourcesAsBeans,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.quarkus.resteasy.reactive.server.test.security;

import static io.restassured.RestAssured.when;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;

import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.UnauthorizedException;
import io.quarkus.test.QuarkusUnitTest;

public class CustomClassLevelExceptionMapperTest {

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(HelloResource.class));

@Test
public void shouldDenyUnannotated() {
when().get("hello")
.then()
.statusCode(999);
}

@Path("hello")
@RolesAllowed("test")
public static final class HelloResource {

@GET
public String hello() {
return "hello world";
}

@ServerExceptionMapper(UnauthorizedException.class)
public Response forbidden() {
return Response.status(999).build();
}
}
}

0 comments on commit 6f7244a

Please sign in to comment.