Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't run CDI interceptors on class-level exception mappers #39579

Merged
merged 2 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
}
----

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,8 +2270,9 @@
----

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:

Check warning on line 2275 in docs/src/main/asciidoc/rest.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/rest.adoc", "range": {"start": {"line": 2275, "column": 23}}}, "severity": "WARNING"}

.Exception mapper parameters
|===
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();
}
}
}
Loading