From 0665fa160682614cc75ab4ff60dcea39da25c1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Tue, 12 Jul 2022 00:52:39 +0200 Subject: [PATCH] RESTEasy Reactive - prevent repeating of standard security checks fixes: #26536 --- .../quarkus/arc/deployment/ArcProcessor.java | 6 + ...thodInterceptorBindingFilterBuildItem.java | 36 +++++ .../MethodInterceptorBindingFilterTest.java | 124 ++++++++++++++++++ .../deployment/ResteasyReactiveProcessor.java | 99 +++++++++++++- .../security/RolesAllowedJaxRsTestCase.java | 20 ++- .../deployment/SecurityProcessor.java | 39 +++++- ...AssumedStandardSecurityCheckBuildItem.java | 17 +++ .../quarkus/arc/processor/BeanDeployment.java | 8 ++ .../io/quarkus/arc/processor/BeanInfo.java | 2 +- .../quarkus/arc/processor/BeanProcessor.java | 14 ++ .../arc/processor/InterceptorResolver.java | 25 ++++ .../common/processor/EndpointIndexer.java | 72 +++++++++- 12 files changed, 449 insertions(+), 13 deletions(-) create mode 100644 extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/MethodInterceptorBindingFilterBuildItem.java create mode 100644 extensions/arc/deployment/src/test/java/io/quarkus/arc/test/interceptor/MethodInterceptorBindingFilterTest.java create mode 100644 extensions/security/spi/src/main/java/io/quarkus/security/spi/AssumedStandardSecurityCheckBuildItem.java diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java index aaff8b53ef91a..a27ab7ace5a11 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java @@ -173,6 +173,7 @@ public ContextRegistrationPhaseBuildItem initialize( List resourceAnnotations, List additionalBeanDefiningAnnotations, List suppressConditionGenerators, + List methodInterceptorBindingFilterBuildItems, Optional testClassPredicate, Capabilities capabilities, CustomScopeAnnotationsBuildItem customScopes, @@ -274,6 +275,11 @@ public void transform(TransformationContext transformationContext) { for (InterceptorBindingRegistrarBuildItem registrar : interceptorBindingRegistrars) { builder.addInterceptorBindingRegistrar(registrar.getInterceptorBindingRegistrar()); } + // register method interceptor binding filters + for (MethodInterceptorBindingFilterBuildItem filterBuildItem : methodInterceptorBindingFilterBuildItems) { + builder.addMethodInterceptorFilter(MethodDescriptor.of(filterBuildItem.interceptedMethod), + filterBuildItem.shouldRemoveBinding); + } // register additional qualifiers for (QualifierRegistrarBuildItem registrar : qualifierRegistrars) { builder.addQualifierRegistrar(registrar.getQualifierRegistrar()); diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/MethodInterceptorBindingFilterBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/MethodInterceptorBindingFilterBuildItem.java new file mode 100644 index 0000000000000..649bd7cec6aba --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/MethodInterceptorBindingFilterBuildItem.java @@ -0,0 +1,36 @@ +package io.quarkus.arc.deployment; + +import java.util.Objects; +import java.util.function.Predicate; + +import org.jboss.jandex.MethodInfo; + +import io.quarkus.arc.processor.InterceptorInfo; +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Makes it possible to break binding between interceptors and concrete method. An interceptor won't + * intercept the {@code interceptedMethod} anymore if {@code shouldRemoveBinding} is true, but the rest of interceptors + * bindings are honoured. + * + * Only a single filter (build item) per method is supported, if there are multiple filters registered for the method, + * the last one is used. + */ +public final class MethodInterceptorBindingFilterBuildItem extends MultiBuildItem { + + /** + * If evaluated as true, {@code interceptedMethod} won't be intercepted by the interceptor + */ + final Predicate shouldRemoveBinding; + + /** + * Intercepted method, only non-static methods are supported. + */ + final MethodInfo interceptedMethod; + + public MethodInterceptorBindingFilterBuildItem(Predicate shouldRemoveBinding, + MethodInfo interceptedMethod) { + this.shouldRemoveBinding = Objects.requireNonNull(shouldRemoveBinding); + this.interceptedMethod = Objects.requireNonNull(interceptedMethod); + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/interceptor/MethodInterceptorBindingFilterTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/interceptor/MethodInterceptorBindingFilterTest.java new file mode 100644 index 0000000000000..2c71bac4dfded --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/interceptor/MethodInterceptorBindingFilterTest.java @@ -0,0 +1,124 @@ +package io.quarkus.arc.test.interceptor; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.List; +import java.util.function.Predicate; + +import javax.annotation.Priority; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +import org.jboss.jandex.DotName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; +import io.quarkus.arc.deployment.InterceptorBindingRegistrarBuildItem; +import io.quarkus.arc.deployment.MethodInterceptorBindingFilterBuildItem; +import io.quarkus.arc.processor.InterceptorBindingRegistrar; +import io.quarkus.arc.processor.InterceptorInfo; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; + +public class MethodInterceptorBindingFilterTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(NotAnInterceptorBinding.class, PingPongBean.class, PingInterceptor.class, + PungInterceptor.class)) + .addBuildChainCustomizer(b -> { + b.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + context.produce(new InterceptorBindingRegistrarBuildItem(new InterceptorBindingRegistrar() { + @Override + public List getAdditionalBindings() { + return List.of(InterceptorBinding.of(NotAnInterceptorBinding.class)); + } + })); + } + }).produces(InterceptorBindingRegistrarBuildItem.class).build(); + }) + .addBuildChainCustomizer(b -> { + b.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + var pingMethodInfo = context + .consume(BeanArchiveIndexBuildItem.class) + .getIndex() + .getClassByName(DotName.createSimple(PingPongBean.class.getName())) + .methods() + .stream() + .filter(mi -> mi.name().contains("ping")) + .findFirst() + .orElseThrow(); + Predicate removePungInterceptorBinding = interceptorInfo -> interceptorInfo + .getTarget() + .map(t -> t.asClass().name().equals(DotName.createSimple(PungInterceptor.class.getName()))) + .orElse(false); + context.produce( + new MethodInterceptorBindingFilterBuildItem(removePungInterceptorBinding, pingMethodInfo)); + } + }).consumes(BeanArchiveIndexBuildItem.class).produces(MethodInterceptorBindingFilterBuildItem.class).build(); + }); + + @Inject + PingPongBean bean; + + @Test + public void testInterceptor() { + assertEquals("PING PONG", bean.ping()); + } + + @Singleton + static class PingPongBean { + + @NotAnInterceptorBinding + public String ping() { + return "PONG"; + } + + } + + @Priority(1) + @Interceptor + @NotAnInterceptorBinding + static class PingInterceptor { + + @AroundInvoke + Object aroundInvoke(InvocationContext ctx) throws Exception { + return "PING " + ctx.proceed(); + } + + } + + @Priority(2) + @Interceptor + @NotAnInterceptorBinding + static class PungInterceptor { + + @AroundInvoke + Object aroundInvoke(InvocationContext ctx) throws Exception { + return "PUNG " + ctx.proceed(); + } + + } + + @Target({ TYPE, METHOD }) + @Retention(RUNTIME) + @interface NotAnInterceptorBinding { + + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 51538490fc80c..f2d28c0404238 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -3,7 +3,9 @@ import static io.quarkus.resteasy.reactive.common.deployment.QuarkusResteasyReactiveDotNames.HTTP_SERVER_REQUEST; import static io.quarkus.resteasy.reactive.common.deployment.QuarkusResteasyReactiveDotNames.HTTP_SERVER_RESPONSE; import static io.quarkus.resteasy.reactive.common.deployment.QuarkusResteasyReactiveDotNames.ROUTING_CONTEXT; +import static io.quarkus.resteasy.reactive.server.deployment.SecurityTransformerUtils.SECURITY_BINDINGS; import static java.util.stream.Collectors.toList; +import static org.jboss.resteasy.reactive.common.processor.EndpointIndexer.findEndpoints; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.DATE_FORMAT; import java.io.File; @@ -65,6 +67,7 @@ import org.jboss.resteasy.reactive.common.processor.TargetJavaVersion; import org.jboss.resteasy.reactive.common.processor.scanning.ApplicationScanningResult; import org.jboss.resteasy.reactive.common.processor.scanning.ResourceScanningResult; +import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationStore; import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationsTransformer; import org.jboss.resteasy.reactive.common.types.AllWriteableMarker; import org.jboss.resteasy.reactive.common.util.Encode; @@ -168,6 +171,8 @@ import io.quarkus.security.AuthenticationCompletionException; import io.quarkus.security.AuthenticationRedirectException; import io.quarkus.security.ForbiddenException; +import io.quarkus.security.spi.AssumedStandardSecurityCheckBuildItem; +import io.quarkus.security.spi.runtime.SecurityCheckStorage; import io.quarkus.vertx.http.deployment.RouteBuildItem; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.VertxHttpRecorder; @@ -323,7 +328,15 @@ void registerCustomExceptionMappers(BuildProducer resourceScanningResultBuildItem, + List assumedStandardSecurityCheckBuildItems, BuildProducer unremoveableBeans) { + + if (!assumedStandardSecurityCheckBuildItems.isEmpty()) { + // Necessary as the Quarkus Security doesn't need the SecurityCheckStorage bean in cases + // when only security checks are for endpoints and these are handled exclusively by EagerSecurityHandler + unremoveableBeans.produce(UnremovableBeanBuildItem.beanTypes(SecurityCheckStorage.class)); + } + if (!resourceScanningResultBuildItem.isPresent()) { return; } @@ -1038,11 +1051,8 @@ MethodScannerBuildItem integrateEagerSecurity(Capabilities capabilities, Combine return null; } - final boolean denyJaxRs = ConfigProvider.getConfig() - .getOptionalValue("quarkus.security.jaxrs.deny-unannotated-endpoints", Boolean.class).orElse(false); - final boolean hasDefaultJaxRsRolesAllowed = ConfigProvider.getConfig() - .getOptionalValues("quarkus.security.jaxrs.default-roles-allowed", String.class).map(l -> !l.isEmpty()) - .orElse(false); + final boolean denyJaxRs = getDenyJaxRs(); + final boolean hasDefaultJaxRsRolesAllowed = getHasDefaultJaxRsRolesAllowed(); var index = indexBuildItem.getComputingIndex(); return new MethodScannerBuildItem(new MethodScanner() { @Override @@ -1061,6 +1071,85 @@ public List scan(MethodInfo method, ClassInfo actualEndp }); } + private boolean getHasDefaultJaxRsRolesAllowed() { + return ConfigProvider.getConfig() + .getOptionalValues("quarkus.security.jaxrs.default-roles-allowed", String.class) + .map(l -> !l.isEmpty()) + .orElse(false); + } + + private boolean getDenyJaxRs() { + return ConfigProvider.getConfig() + .getOptionalValue("quarkus.security.jaxrs.deny-unannotated-endpoints", Boolean.class) + .orElse(false); + } + + /** + * Prevent repeating {@link io.quarkus.security.spi.runtime.SecurityCheck}s in the Quarkus Security on methods + * checked by {@link EagerSecurityHandler}. + */ + @BuildStep + void preventRepeatingSecurityChecks(Capabilities capabilities, + Optional resourceScanningResultBuildItem, + List annotationTransformerBuildItems, + BuildProducer assumedSecurityAnnotationCheckBuildItemBuildProducer, + ApplicationResultBuildItem applicationResultBuildItem, BeanArchiveIndexBuildItem beanArchiveIndexBuildItem) { + + if (!capabilities.isPresent(Capability.SECURITY) || resourceScanningResultBuildItem.isEmpty()) { + return; + } + + final boolean denyJaxRs = getDenyJaxRs(); + final boolean hasDefaultJaxRsRolesAllowed = getHasDefaultJaxRsRolesAllowed(); + final IndexView index = beanArchiveIndexBuildItem.getIndex(); + final AnnotationStore annotationStore; + if (!annotationTransformerBuildItems.isEmpty()) { + List annotationsTransformers = new ArrayList<>(annotationTransformerBuildItems.size()); + for (AnnotationsTransformerBuildItem bi : annotationTransformerBuildItems) { + annotationsTransformers.add(bi.getAnnotationsTransformer()); + } + annotationStore = new AnnotationStore(annotationsTransformers); + } else { + annotationStore = new AnnotationStore(null); + } + + // collect all endpoints + final Set endpoints = findEndpoints(resourceScanningResultBuildItem.get().getResult(), annotationStore, + index, applicationResultBuildItem.getResult()); + + // tell the Quarkus Security we're going to handle standard security checks for the endpoints ourselves + assumeEndpointSecurityCheck: for (MethodInfo endpoint : endpoints) { + + if (hasDefaultJaxRsRolesAllowed || denyJaxRs) { + assumedSecurityAnnotationCheckBuildItemBuildProducer.produce( + new AssumedStandardSecurityCheckBuildItem(endpoint)); + continue; + } + + // find security annotation declared on the method + for (AnnotationInstance annotation : endpoint.annotations()) { + if (SECURITY_BINDINGS.contains(annotation.name())) { + assumedSecurityAnnotationCheckBuildItemBuildProducer.produce( + new AssumedStandardSecurityCheckBuildItem(endpoint)); + continue assumeEndpointSecurityCheck; + } + } + + // find security annotation declared on the class + ClassInfo c = endpoint.declaringClass(); + while (c.superName() != null) { + for (AnnotationInstance annotation : c.classAnnotations()) { + if (SECURITY_BINDINGS.contains(annotation.name())) { + assumedSecurityAnnotationCheckBuildItemBuildProducer.produce( + new AssumedStandardSecurityCheckBuildItem(endpoint)); + continue assumeEndpointSecurityCheck; + } + } + c = index.getClassByName(c.superName()); + } + } + } + /** * This results in adding {@link AllWriteableMarker} to user provided {@link MessageBodyWriter} classes * that handle every class diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedJaxRsTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedJaxRsTestCase.java index c4f23ed4397f0..a24edcc511633 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedJaxRsTestCase.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedJaxRsTestCase.java @@ -8,6 +8,11 @@ import java.lang.reflect.Type; import java.util.Arrays; +import javax.annotation.Priority; +import javax.annotation.security.RolesAllowed; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; @@ -19,6 +24,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.security.runtime.interceptor.SecurityHandler; import io.quarkus.security.test.utils.TestIdentityController; import io.quarkus.security.test.utils.TestIdentityProvider; import io.quarkus.test.QuarkusUnitTest; @@ -32,7 +38,7 @@ public class RolesAllowedJaxRsTestCase { SerializationEntity.class, SerializationRolesResource.class, TestIdentityProvider.class, TestIdentityController.class, - UnsecuredSubResource.class)); + UnsecuredSubResource.class, AssureNoDuplicateSecurityChecksInterceptor.class)); @BeforeAll public static void setupUsers() { @@ -103,4 +109,16 @@ public SerializationEntity readFrom(Class type, Type generi return entity; } } + + @Interceptor + @RolesAllowed("") + @Priority(Interceptor.Priority.PLATFORM_AFTER) + public static class AssureNoDuplicateSecurityChecksInterceptor { + + @AroundInvoke + public Object intercept(InvocationContext ic) throws Exception { + Assertions.assertNull(ic.getContextData().get(SecurityHandler.class.getName())); + return ic.proceed(); + } + } } diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java index f6a8223714d9f..156ccc81f8a67 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java @@ -35,7 +35,9 @@ import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; import io.quarkus.arc.deployment.InterceptorBindingRegistrarBuildItem; +import io.quarkus.arc.deployment.MethodInterceptorBindingFilterBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.arc.processor.InterceptorInfo; import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; @@ -77,6 +79,7 @@ import io.quarkus.security.runtime.interceptor.SecurityConstrainer; import io.quarkus.security.runtime.interceptor.SecurityHandler; import io.quarkus.security.spi.AdditionalSecuredClassesBuildItem; +import io.quarkus.security.spi.AssumedStandardSecurityCheckBuildItem; import io.quarkus.security.spi.runtime.AuthorizationController; import io.quarkus.security.spi.runtime.DevModeDisabledAuthorizationController; import io.quarkus.security.spi.runtime.SecurityCheck; @@ -86,6 +89,9 @@ public class SecurityProcessor { private static final Logger log = Logger.getLogger(SecurityProcessor.class); + private static final Class[] STANDARD_SECURITY_INTERCEPTORS = { AuthenticatedInterceptor.class, DenyAllInterceptor.class, + PermitAllInterceptor.class, RolesAllowedInterceptor.class }; + SecurityConfig security; /** @@ -409,12 +415,39 @@ private List registerProvider(String providerName, void registerSecurityInterceptors(BuildProducer registrars, BuildProducer beans) { registrars.produce(new InterceptorBindingRegistrarBuildItem(new SecurityAnnotationsRegistrar())); - Class[] interceptors = { AuthenticatedInterceptor.class, DenyAllInterceptor.class, PermitAllInterceptor.class, - RolesAllowedInterceptor.class }; - beans.produce(new AdditionalBeanBuildItem(interceptors)); + beans.produce(new AdditionalBeanBuildItem(STANDARD_SECURITY_INTERCEPTORS)); beans.produce(new AdditionalBeanBuildItem(SecurityHandler.class, SecurityConstrainer.class)); } + @BuildStep + void deregisterSecurityInterceptions(List assumedSecurityCheckBuildItems, + BuildProducer filterBuildItemBuildProducer, + BeanArchiveIndexBuildItem beanArchiveIndexBuildItem) { + + // ignore binding between standard security interceptors and the method if the security checks are performed + // elsewhere; the interceptor will still intercept and check all other bound methods + if (!assumedSecurityCheckBuildItems.isEmpty()) { + final IndexView index = beanArchiveIndexBuildItem.getIndex(); + Set interceptorNames = new HashSet<>(); + for (Class clazz : STANDARD_SECURITY_INTERCEPTORS) { + interceptorNames.add(DotName.createSimple(clazz.getName())); + } + final Predicate shouldRemoveBinding = new Predicate() { + @Override + public boolean test(InterceptorInfo interceptorInfo) { + if (interceptorInfo.getTarget().filter(i -> i.kind() == AnnotationTarget.Kind.CLASS).isPresent()) { + return interceptorNames.contains(interceptorInfo.getTarget().get().asClass().name()); + } + return false; + } + }; + for (AssumedStandardSecurityCheckBuildItem buildItem : assumedSecurityCheckBuildItems) { + filterBuildItemBuildProducer.produce( + new MethodInterceptorBindingFilterBuildItem(shouldRemoveBinding, buildItem.method)); + } + } + } + /* * The annotation store is not meant to be generally supported for security annotation. * It is only used here in order to be able to register the DenyAllInterceptor for diff --git a/extensions/security/spi/src/main/java/io/quarkus/security/spi/AssumedStandardSecurityCheckBuildItem.java b/extensions/security/spi/src/main/java/io/quarkus/security/spi/AssumedStandardSecurityCheckBuildItem.java new file mode 100644 index 0000000000000..af7b2ed5adab6 --- /dev/null +++ b/extensions/security/spi/src/main/java/io/quarkus/security/spi/AssumedStandardSecurityCheckBuildItem.java @@ -0,0 +1,17 @@ +package io.quarkus.security.spi; + +import org.jboss.jandex.MethodInfo; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * The Quarkus Security won't perform any standard security checks when the {@code method} is invoked. + */ +public final class AssumedStandardSecurityCheckBuildItem extends MultiBuildItem { + + public final MethodInfo method; + + public AssumedStandardSecurityCheckBuildItem(MethodInfo method) { + this.method = method; + } +} diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java index 4f8e8911da138..d047ad04a6c7d 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java @@ -9,6 +9,7 @@ import io.quarkus.arc.processor.BuildExtension.BuildContext; import io.quarkus.arc.processor.BuildExtension.Key; import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; @@ -111,6 +112,8 @@ public class BeanDeployment { private final List> excludeTypes; + private final Map> methodInterceptorFilterMap; + BeanDeployment(BuildContextImpl buildContext, BeanProcessor.Builder builder) { this.buildContext = buildContext; Set beanDefiningAnnotations = new HashSet<>(); @@ -132,6 +135,7 @@ public class BeanDeployment { this.unusedExclusions = removeUnusedBeans ? new ArrayList<>(builder.removalExclusions) : null; this.removedBeans = removeUnusedBeans ? new CopyOnWriteArraySet<>() : Collections.emptySet(); this.customContexts = new ConcurrentHashMap<>(); + this.methodInterceptorFilterMap = Map.copyOf(builder.methodInterceptorFilterMap); this.excludeTypes = builder.excludeTypes != null ? new ArrayList<>(builder.excludeTypes) : Collections.emptyList(); @@ -546,6 +550,10 @@ boolean isInheritedQualifier(DotName name) { return (getQualifier(name).classAnnotation(DotNames.INHERITED) != null); } + Map> getMethodInterceptorFilterMap() { + return methodInterceptorFilterMap; + } + /** * Extracts qualifiers from given annotation instance. * This returns a collection because in case of repeating qualifiers there can be multiple. diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java index 502024565537c..47e77e22cb791 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java @@ -580,7 +580,7 @@ private Map initInterceptedMethods(List for (Entry> entry : candidates.entrySet()) { List interceptors = beanDeployment.getInterceptorResolver() - .resolve(InterceptionType.AROUND_INVOKE, entry.getValue()); + .filterAndResolve(InterceptionType.AROUND_INVOKE, entry.getValue(), entry.getKey()); if (!interceptors.isEmpty()) { interceptedMethods.put(entry.getKey().method, new InterceptionInfo(interceptors, entry.getValue())); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java index fcdd73e3360e5..8a9ffee01107a 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java @@ -8,6 +8,7 @@ import io.quarkus.arc.processor.ResourceOutput.Resource; import io.quarkus.arc.processor.ResourceOutput.Resource.SpecialType; import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -303,6 +304,10 @@ public static class Builder { final List contextRegistrars; final List qualifierRegistrars; final List interceptorBindingRegistrars; + /** + * Intercepted method -> interceptor + */ + final Map> methodInterceptorFilterMap; final List beanDeploymentValidators; final List>> suppressConditionGenerators; @@ -336,6 +341,7 @@ public Builder() { interceptorBindingRegistrars = new ArrayList<>(); beanDeploymentValidators = new ArrayList<>(); suppressConditionGenerators = new ArrayList<>(); + methodInterceptorFilterMap = new HashMap<>(); removeUnusedBeans = false; removalExclusions = new ArrayList<>(); @@ -404,6 +410,14 @@ public Builder addInterceptorBindingRegistrar(InterceptorBindingRegistrar bindin return this; } + public Builder addMethodInterceptorFilter(MethodDescriptor interceptedMethod, + Predicate shouldRemoveBinding) { + if (shouldRemoveBinding != null) { + this.methodInterceptorFilterMap.put(interceptedMethod, shouldRemoveBinding); + } + return this; + } + public Builder setOutput(ResourceOutput output) { this.output = output; return this; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorResolver.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorResolver.java index 8ed42fe4c3b9a..8177e9be00b30 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorResolver.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorResolver.java @@ -1,10 +1,12 @@ package io.quarkus.arc.processor; +import io.quarkus.gizmo.MethodDescriptor; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.function.Predicate; import javax.enterprise.inject.spi.InterceptionType; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; @@ -18,6 +20,29 @@ public final class InterceptorResolver { this.beanDeployment = beanDeployment; } + /** + * Resolve interceptors for a set of interceptor bindings and a type of interception, but remove interceptors + * matching method interceptor filters. The filter allows you to ignore just one binding between an interceptor + * and a method, but keep interceptor binding for all other methods. + * + * @param methodKey intercepted method + * @param interceptionType + * @param bindings + * @return the list of interceptors for a set of interceptor bindings and a type of interception + */ + public List filterAndResolve(InterceptionType interceptionType, Set bindings, + Methods.MethodKey methodKey) { + final List interceptors = resolve(interceptionType, bindings); + if (!interceptors.isEmpty() && !beanDeployment.getMethodInterceptorFilterMap().isEmpty()) { + final Predicate shouldRemoveBinding = beanDeployment.getMethodInterceptorFilterMap() + .get(MethodDescriptor.of(methodKey.method)); + if (shouldRemoveBinding != null) { + interceptors.removeIf(shouldRemoveBinding); + } + } + return interceptors; + } + /** * * @param interceptionType diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index df55a77fe4f08..c97bf11c798dc 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -120,6 +120,7 @@ import org.jboss.resteasy.reactive.common.model.ResourceMethod; import org.jboss.resteasy.reactive.common.processor.TargetJavaVersion.Status; import org.jboss.resteasy.reactive.common.processor.scanning.ApplicationScanningResult; +import org.jboss.resteasy.reactive.common.processor.scanning.ResourceScanningResult; import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationStore; import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationsTransformer; import org.jboss.resteasy.reactive.common.util.URLUtils; @@ -254,6 +255,7 @@ protected EndpointIndexer(Builder builder) { } public Optional createEndpoints(ClassInfo classInfo, boolean considerApplication) { + // keep logic behind endpoint selection equivalent to org.jboss.resteasy.reactive.common.processor.EndpointIndexer.findEndpoints if (considerApplication && !applicationScanningResult.keepClass(classInfo.name().toString())) { return Optional.empty(); } @@ -357,7 +359,7 @@ protected List createEndpoints(ClassInfo currentClassInfo, } // $$CDIWrapper suffix is used to generate CDI beans from interfaces, we don't want to create endpoints for them: - if (currentClassInfo.name().toString().endsWith(CDI_WRAPPER_SUFFIX)) { + if (hasCDIWrapperSuffix(currentClassInfo)) { return Collections.emptyList(); } @@ -451,6 +453,10 @@ protected List createEndpoints(ClassInfo currentClassInfo, return ret; } + private static boolean hasCDIWrapperSuffix(ClassInfo currentClassInfo) { + return currentClassInfo.name().toString().endsWith(CDI_WRAPPER_SUFFIX); + } + private void validateHttpAnnotations(MethodInfo info) { List annotationInstances = info.annotations(); Set allMethodAnnotations = new HashSet<>(annotationInstances.size()); @@ -469,7 +475,67 @@ private void validateHttpAnnotations(MethodInfo info) { } } - private boolean hasProperModifiers(MethodInfo info) { + public static Set findEndpoints(ResourceScanningResult scanningResult, AnnotationStore annotationStore, + IndexView index, ApplicationScanningResult applicationScanningResult) { + // keep logic behind endpoint selection equivalent to org.jboss.resteasy.reactive.common.processor.EndpointIndexer.createEndpoints(org.jboss.jandex.ClassInfo, boolean) + final Set endpoints = new HashSet<>(); + for (Map.Entry i : scanningResult.getScannedResources().entrySet()) { + if (!applicationScanningResult.keepClass(i.getKey().toString())) { + continue; + } + findEndpoints(i.getValue(), endpoints, annotationStore, new HashSet<>(), + scanningResult.getHttpAnnotationToMethod().keySet(), index); + } + for (ClassInfo currentClassInfo : scanningResult.getPossibleSubResources().values()) { + findEndpoints(currentClassInfo, endpoints, annotationStore, new HashSet<>(), + scanningResult.getHttpAnnotationToMethod().keySet(), index); + } + return endpoints; + } + + private static void findEndpoints(ClassInfo currentClassInfo, Set endpoints, + AnnotationStore annotationStore, Set seenMethods, Set httpAnnotationToMethod, IndexView index) { + if (hasCDIWrapperSuffix(currentClassInfo)) { + return; + } + for (MethodInfo info : currentClassInfo.methods()) { + if (hasHttpAnnotation(info, httpAnnotationToMethod, annotationStore) || annotationStore.hasAnnotation(info, PATH)) { + if (!hasProperModifiers(info)) { + continue; + } + if (!seenMethods.add(methodDescriptor(info))) { + continue; + } + endpoints.add(info); + } + } + final DotName superClassName = currentClassInfo.superName(); + if (superClassName != null && !superClassName.equals(OBJECT)) { + final ClassInfo superClass = index.getClassByName(superClassName); + if (superClass != null) { + findEndpoints(superClass, endpoints, annotationStore, seenMethods, httpAnnotationToMethod, index); + } + } + final List interfaces = currentClassInfo.interfaceNames(); + for (DotName i : interfaces) { + final ClassInfo superClass = index.getClassByName(i); + if (superClass != null) { + findEndpoints(superClass, endpoints, annotationStore, seenMethods, httpAnnotationToMethod, index); + } + } + } + + private static boolean hasHttpAnnotation(MethodInfo info, Set httpAnnotationToMethod, + AnnotationStore annotationStore) { + for (DotName httpMethod : httpAnnotationToMethod) { + if (annotationStore.hasAnnotation(info, httpMethod)) { + return true; + } + } + return false; + } + + private static boolean hasProperModifiers(MethodInfo info) { if (isSynthetic(info.flags())) { log.debug("Method '" + info.name() + " of Resource class '" + info.declaringClass().name() + "' is a synthetic method and will therefore be ignored"); @@ -488,7 +554,7 @@ private boolean hasProperModifiers(MethodInfo info) { return true; } - private boolean isSynthetic(int mod) { + private static boolean isSynthetic(int mod) { return (mod & 0x1000) != 0; //0x1000 == SYNTHETIC }