From 5e670e455837fe5f35a43f59385046358b979d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Thu, 27 Oct 2022 15:24:01 +0200 Subject: [PATCH] Additional JAX-RS security for endpoints and not all resource methods fixes: #28791 --- .../RestPathAnnotationProcessor.java | 27 ++-- .../deployment/ResteasyBuiltinsProcessor.java | 33 ++-- .../DefaultRolesAllowedJaxRsTest.java | 33 +++- .../test/security/DenyAllJaxRsTest.java | 32 +++- .../ResteasyReactiveCommonProcessor.java | 48 +++--- .../DefaultRolesAllowedJaxRsTest.java | 12 +- .../test/security/DenyAllJaxRsTest.java | 12 +- .../server/test/security/HelloResource.java | 22 +++ ...ditionalDenyingUnannotatedTransformer.java | 14 +- .../AdditionalRolesAllowedTransformer.java | 14 +- .../deployment/SecurityProcessor.java | 124 +++++++++----- .../AdditionalSecuredClassesBuildItem.java | 2 + .../AdditionalSecuredMethodsBuildItem.java | 31 ++++ .../common/processor/EndpointIndexer.java | 151 ++++++++++++------ 14 files changed, 405 insertions(+), 150 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/HelloResource.java create mode 100644 extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredMethodsBuildItem.java diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java index 16cf244beafc9..a187ebafb220c 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java @@ -1,6 +1,5 @@ package io.quarkus.resteasy.deployment; -import java.util.List; import java.util.Optional; import java.util.regex.Pattern; @@ -81,18 +80,15 @@ public void transform(TransformationContext ctx) { MethodInfo methodInfo = target.asMethod(); ClassInfo classInfo = methodInfo.declaringClass(); - AnnotationInstance annotation = methodInfo.annotation(REST_PATH); - if (annotation == null) { - // Check for @Path on class and not method - if (!isRestEndpointMethod(methodInfo.annotations())) { - return; - } + if (!isRestEndpointMethod(methodInfo)) { + return; } // Don't create annotations for rest clients if (classInfo.classAnnotation(REGISTER_REST_CLIENT) != null) { return; } + AnnotationInstance annotation = methodInfo.annotation(REST_PATH); StringBuilder stringBuilder; if (annotation != null) { stringBuilder = new StringBuilder(slashify(annotation.value().asString())); @@ -138,17 +134,18 @@ String slashify(String path) { return '/' + path; } - boolean isRestEndpointMethod(List annotations) { - boolean isRestEndpointMethod = false; + static boolean isRestEndpointMethod(MethodInfo methodInfo) { - for (AnnotationInstance annotation : annotations) { - if (ResteasyDotNames.JAXRS_METHOD_ANNOTATIONS.contains(annotation.name())) { - isRestEndpointMethod = true; - break; + if (!methodInfo.hasAnnotation(REST_PATH)) { + // Check for @Path on class and not method + for (AnnotationInstance annotation : methodInfo.annotations()) { + if (ResteasyDotNames.JAXRS_METHOD_ANNOTATIONS.contains(annotation.name())) { + return true; + } } + return false; } - - return isRestEndpointMethod; + return true; } private boolean notRequired(Capabilities capabilities, diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java index 9e28113a4b7cf..56c8fb54947be 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java @@ -1,6 +1,7 @@ package io.quarkus.resteasy.deployment; import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; +import static io.quarkus.resteasy.deployment.RestPathAnnotationProcessor.isRestEndpointMethod; import static io.quarkus.security.spi.SecurityTransformerUtils.hasSecurityAnnotation; import java.util.ArrayList; @@ -10,6 +11,7 @@ import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; +import org.jboss.jandex.MethodInfo; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.Capabilities; @@ -36,7 +38,7 @@ 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.AdditionalSecuredClassesBuildItem; +import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.quarkus.vertx.http.deployment.devmode.RouteDescriptionBuildItem; @@ -51,30 +53,31 @@ public class ResteasyBuiltinsProcessor { void setUpDenyAllJaxRs(CombinedIndexBuildItem index, JaxRsSecurityConfig config, ResteasyDeploymentBuildItem resteasyDeployment, - BuildProducer additionalSecuredClasses) { - if ((config.denyJaxRs) && (resteasyDeployment != null)) { - final List classes = new ArrayList<>(); + BuildProducer additionalSecuredClasses) { + if (resteasyDeployment != null && (config.denyJaxRs || config.defaultRolesAllowed.isPresent())) { + final List methods = new ArrayList<>(); + // add endpoints List resourceClasses = resteasyDeployment.getDeployment().getScannedResourceClasses(); for (String className : resourceClasses) { ClassInfo classInfo = index.getIndex().getClassByName(DotName.createSimple(className)); if (!hasSecurityAnnotation(classInfo)) { - classes.add(classInfo); + for (MethodInfo methodInfo : classInfo.methods()) { + if (isRestEndpointMethod(methodInfo) && !hasSecurityAnnotation(methodInfo)) { + methods.add(methodInfo); + } + } } } - additionalSecuredClasses.produce(new AdditionalSecuredClassesBuildItem(classes)); - } else if (config.defaultRolesAllowed.isPresent() && resteasyDeployment != null) { - final List classes = new ArrayList<>(); - - List resourceClasses = resteasyDeployment.getDeployment().getScannedResourceClasses(); - for (String className : resourceClasses) { - ClassInfo classInfo = index.getIndex().getClassByName(DotName.createSimple(className)); - if (!hasSecurityAnnotation(classInfo)) { - classes.add(classInfo); + if (!methods.isEmpty()) { + if (config.denyJaxRs) { + additionalSecuredClasses.produce(new AdditionalSecuredMethodsBuildItem(methods)); + } else { + additionalSecuredClasses + .produce(new AdditionalSecuredMethodsBuildItem(methods, config.defaultRolesAllowed)); } } - additionalSecuredClasses.produce(new AdditionalSecuredClassesBuildItem(classes, config.defaultRolesAllowed)); } } diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java index 7caa4e394b281..cc7622615f928 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java @@ -3,6 +3,11 @@ import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.when; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -19,7 +24,7 @@ public class DefaultRolesAllowedJaxRsTest { .addClasses(PermitAllResource.class, UnsecuredResource.class, TestIdentityProvider.class, TestIdentityController.class, - UnsecuredSubResource.class) + UnsecuredSubResource.class, HelloResource.class) .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = admin\n"), "application.properties")); @@ -65,6 +70,17 @@ public void shouldAllowPermitAllClass() { assertStatus(path, 200, 200, 200); } + @Test + public void testNonEndpointMethodAreNotDenied() { + // ensure io.quarkus.resteasy.test.security.DefaultRolesAllowedJaxRsTest.HelloResource.getHello is not secured with RolesAllowedInterceptor + given().auth().preemptive() + .basic("user", "user") + .get("/hello") + .then() + .statusCode(200) + .body(Matchers.equalTo("hello")); + } + private void assertStatus(String path, int adminStatus, int userStatus, int anonStatus) { given().auth().preemptive() .basic("admin", "admin").get(path) @@ -80,4 +96,19 @@ private void assertStatus(String path, int adminStatus, int userStatus, int anon } + @Path("/hello") + public static class HelloResource { + + @RolesAllowed("**") + @GET + public String hello() { + return getHello(); + } + + public String getHello() { + return "hello"; + } + + } + } diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DenyAllJaxRsTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DenyAllJaxRsTest.java index 7c43304218145..5ac0fe79312e6 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DenyAllJaxRsTest.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DenyAllJaxRsTest.java @@ -4,6 +4,11 @@ import static io.restassured.RestAssured.when; import static org.hamcrest.Matchers.emptyString; +import javax.annotation.security.PermitAll; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -23,7 +28,7 @@ public class DenyAllJaxRsTest { .addClasses(PermitAllResource.class, UnsecuredResource.class, TestIdentityProvider.class, TestIdentityController.class, - UnsecuredSubResource.class) + UnsecuredSubResource.class, HelloResource.class) .addAsResource(new StringAsset("quarkus.security.jaxrs.deny-unannotated-endpoints = true\n"), "application.properties")); @@ -82,6 +87,16 @@ public void shouldAllowPermitAllClass() { assertStatus(path, 200, 200); } + @Test + public void testNonEndpointMethodAreNotDenied() { + // ensure io.quarkus.resteasy.test.security.DenyAllJaxRsTest.HelloResource.getHello is not secured with DenyAllInterceptor + given() + .get("/hello") + .then() + .statusCode(200) + .body(Matchers.equalTo("hello")); + } + private void assertStatus(String path, int status, int anonStatus) { given().auth().preemptive() .basic("admin", "admin").get(path) @@ -97,4 +112,19 @@ private void assertStatus(String path, int status, int anonStatus) { } + @Path("/hello") + public static class HelloResource { + + @PermitAll + @GET + public String hello() { + return getHello(); + } + + public String getHello() { + return "hello"; + } + + } + } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java index 64fcc46071310..0c3c5b8ad89e5 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java @@ -1,10 +1,13 @@ package io.quarkus.resteasy.reactive.common.deployment; +import static io.quarkus.security.spi.SecurityTransformerUtils.hasSecurityAnnotation; import static org.jboss.resteasy.reactive.common.model.ResourceInterceptor.FILTER_SOURCE_METHOD_METADATA_KEY; +import static org.jboss.resteasy.reactive.common.processor.EndpointIndexer.collectClassEndpoints; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -18,7 +21,6 @@ import javax.ws.rs.ext.RuntimeDelegate; import org.jboss.jandex.AnnotationTarget; -import org.jboss.jandex.AnnotationTarget.Kind; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.CompositeIndex; import org.jboss.jandex.DotName; @@ -61,8 +63,7 @@ import io.quarkus.resteasy.reactive.spi.MessageBodyWriterOverrideBuildItem; import io.quarkus.resteasy.reactive.spi.ReaderInterceptorBuildItem; import io.quarkus.resteasy.reactive.spi.WriterInterceptorBuildItem; -import io.quarkus.security.spi.AdditionalSecuredClassesBuildItem; -import io.quarkus.security.spi.SecurityTransformerUtils; +import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem; public class ResteasyReactiveCommonProcessor { @@ -72,33 +73,42 @@ public class ResteasyReactiveCommonProcessor { @BuildStep void setUpDenyAllJaxRs( CombinedIndexBuildItem index, - ResteasyReactiveConfig rrConfig, JaxRsSecurityConfig securityConfig, Optional resteasyDeployment, - BuildProducer additionalSecuredClasses) { + BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, + ApplicationResultBuildItem applicationResultBuildItem, + BuildProducer additionalSecuredClasses) { - if (securityConfig.denyJaxRs() && resteasyDeployment.isPresent()) { - List classes = new ArrayList<>(); + if (resteasyDeployment.isPresent() + && (securityConfig.denyJaxRs() || securityConfig.defaultRolesAllowed().isPresent())) { + final List methods = new ArrayList<>(); + Map httpAnnotationToMethod = resteasyDeployment.get().getResult().getHttpAnnotationToMethod(); Set resourceClasses = resteasyDeployment.get().getResult().getScannedResourcePaths().keySet(); + for (DotName className : resourceClasses) { ClassInfo classInfo = index.getIndex().getClassByName(className); - if (!SecurityTransformerUtils.hasSecurityAnnotation(classInfo)) { - classes.add(classInfo); + if (!hasSecurityAnnotation(classInfo)) { + // collect class endpoints + Collection classEndpoints = collectClassEndpoints(classInfo, httpAnnotationToMethod, + beanArchiveIndexBuildItem.getIndex(), applicationResultBuildItem.getResult()); + + // add endpoints + for (MethodInfo classEndpoint : classEndpoints) { + if (!hasSecurityAnnotation(classEndpoint)) { + methods.add(classEndpoint); + } + } } } - additionalSecuredClasses.produce(new AdditionalSecuredClassesBuildItem(classes)); - } else if (securityConfig.defaultRolesAllowed().isPresent() && resteasyDeployment.isPresent()) { - List classes = new ArrayList<>(); - Set resourceClasses = resteasyDeployment.get().getResult().getScannedResourcePaths().keySet(); - for (DotName className : resourceClasses) { - ClassInfo classInfo = index.getIndex().getClassByName(className); - if (!SecurityTransformerUtils.hasSecurityAnnotation(classInfo)) { - classes.add(classInfo); + if (!methods.isEmpty()) { + if (securityConfig.denyJaxRs()) { + additionalSecuredClasses.produce(new AdditionalSecuredMethodsBuildItem(methods)); + } else { + additionalSecuredClasses + .produce(new AdditionalSecuredMethodsBuildItem(methods, securityConfig.defaultRolesAllowed())); } } - additionalSecuredClasses - .produce(new AdditionalSecuredClassesBuildItem(classes, securityConfig.defaultRolesAllowed())); } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedJaxRsTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedJaxRsTest.java index a85df7964ee7e..4590ee9a9d02f 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedJaxRsTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedJaxRsTest.java @@ -3,6 +3,7 @@ import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.when; +import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -19,7 +20,7 @@ public class DefaultRolesAllowedJaxRsTest { .addClasses(PermitAllResource.class, UnsecuredResource.class, TestIdentityProvider.class, TestIdentityController.class, - UnsecuredSubResource.class) + UnsecuredSubResource.class, HelloResource.class) .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed=admin\n"), "application.properties")); @@ -77,6 +78,15 @@ public void shouldAllowPermitAllClass() { assertStatus(path, 200, 200, 200); } + @Test + public void testServerExceptionMapper() { + given() + .get("/hello") + .then() + .statusCode(200) + .body(Matchers.equalTo("unauthorizedExceptionMapper")); + } + private void assertStatus(String path, int adminStatus, int userStatus, int anonStatus) { given().auth().preemptive() .basic("admin", "admin").get(path) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DenyAllJaxRsTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DenyAllJaxRsTest.java index adf4ea53e4fae..2e10fb07015e4 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DenyAllJaxRsTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DenyAllJaxRsTest.java @@ -3,6 +3,7 @@ import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.when; +import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -22,7 +23,7 @@ public class DenyAllJaxRsTest { .addClasses(PermitAllResource.class, UnsecuredResource.class, TestIdentityProvider.class, TestIdentityController.class, - UnsecuredSubResource.class) + UnsecuredSubResource.class, HelloResource.class) .addAsResource(new StringAsset("quarkus.security.jaxrs.deny-unannotated-endpoints = true\n"), "application.properties")); @@ -80,6 +81,15 @@ public void shouldAllowPermitAllClass() { assertStatus(path, 200, 200); } + @Test + public void testServerExceptionMapper() { + given() + .get("/hello") + .then() + .statusCode(200) + .body(Matchers.equalTo("unauthorizedExceptionMapper")); + } + private void assertStatus(String path, int status, int anonStatus) { given().auth().preemptive() .basic("admin", "admin").get(path) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/HelloResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/HelloResource.java new file mode 100644 index 0000000000000..5f470ccf640f7 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/HelloResource.java @@ -0,0 +1,22 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; + +import io.quarkus.security.UnauthorizedException; + +@Path("/hello") +public class HelloResource { + @GET + public String hello() { + return "hello"; + } + + @ServerExceptionMapper + public Response unauthorizedExceptionMapper(UnauthorizedException unauthorizedException) { + return Response.ok("unauthorizedExceptionMapper").build(); + } +} diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/AdditionalDenyingUnannotatedTransformer.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/AdditionalDenyingUnannotatedTransformer.java index a27adc28e081f..48b92f02ef981 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/AdditionalDenyingUnannotatedTransformer.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/AdditionalDenyingUnannotatedTransformer.java @@ -1,5 +1,6 @@ package io.quarkus.security.deployment; +import static io.quarkus.security.deployment.SecurityProcessor.createMethodDescription; import static io.quarkus.security.deployment.SecurityTransformerUtils.DENY_ALL; import java.util.Collection; @@ -9,24 +10,25 @@ import org.jboss.jandex.AnnotationTarget; import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.security.spi.runtime.MethodDescription; public class AdditionalDenyingUnannotatedTransformer implements AnnotationsTransformer { - private final Set classNames; + private final Set methods; - public AdditionalDenyingUnannotatedTransformer(Collection classNames) { - this.classNames = new HashSet<>(classNames); + public AdditionalDenyingUnannotatedTransformer(Collection methods) { + this.methods = new HashSet<>(methods); } @Override public boolean appliesTo(AnnotationTarget.Kind kind) { - return kind == org.jboss.jandex.AnnotationTarget.Kind.CLASS; + return kind == AnnotationTarget.Kind.METHOD; } @Override public void transform(TransformationContext context) { - String className = context.getTarget().asClass().name().toString(); - if (classNames.contains(className)) { + MethodDescription methodDescription = createMethodDescription(context.getTarget().asMethod()); + if (methods.contains(methodDescription)) { context.transform().add(DENY_ALL).done(); } } diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/AdditionalRolesAllowedTransformer.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/AdditionalRolesAllowedTransformer.java index 8362572a780d2..5333526e78dcc 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/AdditionalRolesAllowedTransformer.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/AdditionalRolesAllowedTransformer.java @@ -1,5 +1,6 @@ package io.quarkus.security.deployment; +import static io.quarkus.security.deployment.SecurityProcessor.createMethodDescription; import static io.quarkus.security.deployment.SecurityTransformerUtils.ROLES_ALLOWED; import java.util.Collection; @@ -11,27 +12,28 @@ import org.jboss.jandex.AnnotationValue; import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.security.spi.runtime.MethodDescription; public class AdditionalRolesAllowedTransformer implements AnnotationsTransformer { - private final Set classNames; + private final Set methods; private final AnnotationValue[] rolesAllowed; - public AdditionalRolesAllowedTransformer(Collection classNames, List rolesAllowed) { - this.classNames = new HashSet<>(classNames); + public AdditionalRolesAllowedTransformer(Collection methods, List rolesAllowed) { + this.methods = new HashSet<>(methods); this.rolesAllowed = rolesAllowed.stream().map(s -> AnnotationValue.createStringValue("", s)) .toArray(AnnotationValue[]::new); } @Override public boolean appliesTo(AnnotationTarget.Kind kind) { - return kind == AnnotationTarget.Kind.CLASS; + return kind == AnnotationTarget.Kind.METHOD; } @Override public void transform(TransformationContext context) { - String className = context.getTarget().asClass().name().toString(); - if (classNames.contains(className)) { + MethodDescription method = createMethodDescription(context.getTarget().asMethod()); + if (methods.contains(method)) { context.transform().add(ROLES_ALLOWED, AnnotationValue.createArrayValue("value", rolesAllowed)).done(); } } 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 b1fbdf1640ff4..d690825fbd986 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 @@ -1,6 +1,8 @@ package io.quarkus.security.deployment; import static io.quarkus.gizmo.MethodDescriptor.ofMethod; +import static io.quarkus.security.deployment.DotNames.DENY_ALL; +import static io.quarkus.security.deployment.DotNames.ROLES_ALLOWED; import static io.quarkus.security.runtime.SecurityProviderUtils.findProviderIndex; import java.io.IOException; @@ -77,8 +79,10 @@ 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.AdditionalSecuredMethodsBuildItem; import io.quarkus.security.spi.runtime.AuthorizationController; import io.quarkus.security.spi.runtime.DevModeDisabledAuthorizationController; +import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.security.spi.runtime.SecurityCheck; import io.quarkus.security.spi.runtime.SecurityCheckStorage; @@ -415,6 +419,26 @@ void registerSecurityInterceptors(BuildProducer additionalSecuredClassesBuildItems, + BuildProducer additionalSecuredMethodsBuildItemBuildProducer) { + for (AdditionalSecuredClassesBuildItem additionalSecuredClassesBuildItem : additionalSecuredClassesBuildItems) { + final Collection securedMethods = new ArrayList<>(); + for (ClassInfo additionalSecuredClass : additionalSecuredClassesBuildItem.additionalSecuredClasses) { + for (MethodInfo method : additionalSecuredClass.methods()) { + if (isPublicNonStaticNonConstructor(method)) { + securedMethods.add(method); + } + } + } + additionalSecuredMethodsBuildItemBuildProducer.produce( + new AdditionalSecuredMethodsBuildItem(securedMethods, additionalSecuredClassesBuildItem.rolesAllowed)); + } + } + /* * 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 @@ -422,21 +446,21 @@ void registerSecurityInterceptors(BuildProducer transformers, - List additionalSecuredClasses, + List additionalSecuredMethods, SecurityBuildTimeConfig config) { if (config.denyUnannotated) { transformers.produce(new AnnotationsTransformerBuildItem(new DenyingUnannotatedTransformer())); } - if (!additionalSecuredClasses.isEmpty()) { - for (AdditionalSecuredClassesBuildItem securedClasses : additionalSecuredClasses) { - Set additionalSecured = new HashSet<>(); - for (ClassInfo additionalSecuredClass : securedClasses.additionalSecuredClasses) { - additionalSecured.add(additionalSecuredClass.name().toString()); + if (!additionalSecuredMethods.isEmpty()) { + for (AdditionalSecuredMethodsBuildItem securedMethods : additionalSecuredMethods) { + Collection additionalSecured = new HashSet<>(); + for (MethodInfo additionalSecuredMethod : securedMethods.additionalSecuredMethods) { + additionalSecured.add(createMethodDescription(additionalSecuredMethod)); } - if (securedClasses.rolesAllowed.isPresent()) { + if (securedMethods.rolesAllowed.isPresent()) { transformers.produce( new AnnotationsTransformerBuildItem(new AdditionalRolesAllowedTransformer(additionalSecured, - securedClasses.rolesAllowed.get()))); + securedMethods.rolesAllowed.get()))); } else { transformers.produce( new AnnotationsTransformerBuildItem( @@ -451,23 +475,22 @@ void transformSecurityAnnotations(BuildProducer void gatherSecurityChecks(BuildProducer syntheticBeans, BeanArchiveIndexBuildItem beanArchiveBuildItem, BuildProducer classPredicate, - List additionalSecuredClasses, + List additionalSecuredMethods, SecurityCheckRecorder recorder, List additionalSecurityChecks, SecurityBuildTimeConfig config) { classPredicate.produce(new ApplicationClassPredicateBuildItem(new SecurityCheckStorageAppPredicate())); - final Map additionalSecured = new HashMap<>(); - for (AdditionalSecuredClassesBuildItem securedClasses : additionalSecuredClasses) { - securedClasses.additionalSecuredClasses.forEach(c -> { - if (!additionalSecured.containsKey(c.name())) { - additionalSecured.put(c.name(), new AdditionalSecured(c, securedClasses.rolesAllowed)); - } - }); + final Map additionalSecured = new HashMap<>(); + for (AdditionalSecuredMethodsBuildItem securedMethods : additionalSecuredMethods) { + for (MethodInfo m : securedMethods.additionalSecuredMethods) { + additionalSecured.putIfAbsent(createMethodDescription(m), + new AdditionalSecured(m, securedMethods.rolesAllowed)); + } } IndexView index = beanArchiveBuildItem.getIndex(); Map securityChecks = gatherSecurityAnnotations( - index, additionalSecured, config.denyUnannotated, recorder); + index, additionalSecured.values(), config.denyUnannotated, recorder); for (AdditionalSecurityCheckBuildItem additionalSecurityCheck : additionalSecurityChecks) { securityChecks.put(additionalSecurityCheck.getMethodInfo(), additionalSecurityCheck.getSecurityCheck()); @@ -499,41 +522,48 @@ void gatherSecurityChecks(BuildProducer syntheticBeans, private Map gatherSecurityAnnotations( IndexView index, - Map additionalSecuredClasses, boolean denyUnannotated, SecurityCheckRecorder recorder) { + Collection additionalSecuredMethods, boolean denyUnannotated, SecurityCheckRecorder recorder) { Map methodToInstanceCollector = new HashMap<>(); Map classAnnotations = new HashMap<>(); Map result = new HashMap<>(gatherSecurityAnnotations( - index, DotNames.ROLES_ALLOWED, methodToInstanceCollector, classAnnotations, + index, ROLES_ALLOWED, methodToInstanceCollector, classAnnotations, (instance -> recorder.rolesAllowed(instance.value().asStringArray())))); result.putAll(gatherSecurityAnnotations(index, DotNames.PERMIT_ALL, methodToInstanceCollector, classAnnotations, (instance -> recorder.permitAll()))); result.putAll(gatherSecurityAnnotations(index, DotNames.AUTHENTICATED, methodToInstanceCollector, classAnnotations, (instance -> recorder.authenticated()))); - result.putAll(gatherSecurityAnnotations(index, DotNames.DENY_ALL, methodToInstanceCollector, classAnnotations, + result.putAll(gatherSecurityAnnotations(index, DENY_ALL, methodToInstanceCollector, classAnnotations, (instance -> recorder.denyAll()))); /* - * Handle additional secured classes by adding the denyAll check to all public non-static methods - * that don't have security annotations + * Handle additional secured methods by adding the denyAll/rolesAllowed check to all public non-static methods + * that don't have same security annotations */ - for (Map.Entry additionalSecureClassInfo : additionalSecuredClasses.entrySet()) { - for (MethodInfo methodInfo : additionalSecureClassInfo.getValue().classInfo.methods()) { - if (!isPublicNonStaticNonConstructor(methodInfo)) { - continue; + for (AdditionalSecured additionalSecuredMethod : additionalSecuredMethods) { + if (!isPublicNonStaticNonConstructor(additionalSecuredMethod.methodInfo)) { + continue; + } + AnnotationInstance alreadyExistingInstance = methodToInstanceCollector.get(additionalSecuredMethod.methodInfo); + if (additionalSecuredMethod.rolesAllowed.isPresent()) { + if (alreadyExistingInstance == null) { + result.put(additionalSecuredMethod.methodInfo, recorder + .rolesAllowed(additionalSecuredMethod.rolesAllowed.get().toArray(String[]::new))); + } else if (alreadyHasAnnotation(alreadyExistingInstance, ROLES_ALLOWED)) { + // we should not try to add second @RolesAllowed + throw new IllegalStateException("Method " + additionalSecuredMethod.methodInfo.declaringClass() + "#" + + additionalSecuredMethod.methodInfo.name() + " should not have been added as an additional " + + "secured method as it's already annotated with @RolesAllowed."); } - AnnotationInstance alreadyExistingInstance = methodToInstanceCollector.get(methodInfo); - if ((alreadyExistingInstance == null)) { - if (additionalSecureClassInfo.getValue().rolesAllowed.isPresent()) { - result.put(methodInfo, recorder - .rolesAllowed(additionalSecureClassInfo.getValue().rolesAllowed.get().toArray(String[]::new))); - } else { - result.put(methodInfo, recorder.denyAll()); - } - } else if (alreadyExistingInstance.target().kind() == AnnotationTarget.Kind.CLASS) { - throw new IllegalStateException("Class " + methodInfo.declaringClass() - + " should not have been added as an additional secured class"); + } else { + if (alreadyExistingInstance == null) { + result.put(additionalSecuredMethod.methodInfo, recorder.denyAll()); + } else if (alreadyHasAnnotation(alreadyExistingInstance, DENY_ALL)) { + // we should not try to add second @DenyAll + throw new IllegalStateException("Method " + additionalSecuredMethod.methodInfo.declaringClass() + "#" + + additionalSecuredMethod.methodInfo.name() + " should not have been added as an additional " + + "secured method as it's already annotated with @DenyAll."); } } } @@ -563,6 +593,11 @@ private Map gatherSecurityAnnotations( return result; } + private boolean alreadyHasAnnotation(AnnotationInstance alreadyExistingInstance, DotName annotationName) { + return alreadyExistingInstance.target().kind() == AnnotationTarget.Kind.METHOD + && alreadyExistingInstance.name().equals(annotationName); + } + private boolean isPublicNonStaticNonConstructor(MethodInfo methodInfo) { return Modifier.isPublic(methodInfo.flags()) && !Modifier.isStatic(methodInfo.flags()) && !"".equals(methodInfo.name()); @@ -638,13 +673,22 @@ AdditionalBeanBuildItem authorizationController(LaunchModeBuildItem launchMode) return AdditionalBeanBuildItem.builder().addBeanClass(controllerClass).build(); } + static MethodDescription createMethodDescription(MethodInfo additionalSecuredMethod) { + String[] paramTypes = new String[additionalSecuredMethod.parametersCount()]; + for (int i = 0; i < additionalSecuredMethod.parametersCount(); i++) { + paramTypes[i] = additionalSecuredMethod.parameterTypes().get(i).name().toString(); + } + return new MethodDescription(additionalSecuredMethod.declaringClass().name().toString(), additionalSecuredMethod.name(), + paramTypes); + } + static class AdditionalSecured { - final ClassInfo classInfo; + final MethodInfo methodInfo; final Optional> rolesAllowed; - AdditionalSecured(ClassInfo classInfo, Optional> rolesAllowed) { - this.classInfo = classInfo; + AdditionalSecured(MethodInfo methodInfo, Optional> rolesAllowed) { + this.methodInfo = methodInfo; this.rolesAllowed = rolesAllowed; } } diff --git a/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredClassesBuildItem.java b/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredClassesBuildItem.java index 342012156657d..6da09628ada14 100644 --- a/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredClassesBuildItem.java +++ b/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredClassesBuildItem.java @@ -11,6 +11,8 @@ /** * Contains classes that need to have @DenyAll on all methods that don't have security annotations + * + * @deprecated use {@link AdditionalSecuredMethodsBuildItem} */ public final class AdditionalSecuredClassesBuildItem extends MultiBuildItem { diff --git a/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredMethodsBuildItem.java b/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredMethodsBuildItem.java new file mode 100644 index 0000000000000..9064542782189 --- /dev/null +++ b/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredMethodsBuildItem.java @@ -0,0 +1,31 @@ +package io.quarkus.security.spi; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.jboss.jandex.MethodInfo; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Contains methods that need to have {@link javax.annotation.security.DenyAll} or + * {@link javax.annotation.security.RolesAllowed}. + */ +public final class AdditionalSecuredMethodsBuildItem extends MultiBuildItem { + + public final Collection additionalSecuredMethods; + public final Optional> rolesAllowed; + + public AdditionalSecuredMethodsBuildItem(Collection additionalSecuredMethods) { + this.additionalSecuredMethods = Collections.unmodifiableCollection(additionalSecuredMethods); + rolesAllowed = Optional.empty(); + } + + public AdditionalSecuredMethodsBuildItem(Collection additionalSecuredMethods, + Optional> rolesAllowed) { + this.additionalSecuredMethods = Collections.unmodifiableCollection(additionalSecuredMethods); + this.rolesAllowed = rolesAllowed; + } +} 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 60c3f3f529729..bdc904a98e525 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 @@ -356,6 +356,86 @@ protected abstract METHOD createResourceMethod(MethodInfo info, ClassInfo actual protected List createEndpoints(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, Set seenMethods, Set existingClassNameBindings, Set pathParameters, String resourceClassPath, boolean considerApplication) { + + List ret = new ArrayList<>(); + + List endpoints = collectEndpoints(currentClassInfo, actualEndpointInfo, seenMethods, + existingClassNameBindings, considerApplication, httpAnnotationToMethod, index, applicationScanningResult, + getAnnotationStore()); + Map basicResourceClassInfoMap = new HashMap<>(); + for (FoundEndpoint endpoint : endpoints) { + + // assemble method path + String methodPath = readStringValue(getAnnotationStore().getAnnotation(endpoint.methodInfo, PATH)); + if (endpoint.httpMethod != null) { + // HTTP annotation method + validateHttpAnnotations(endpoint.methodInfo); + if (methodPath != null) { + if (!methodPath.startsWith("/")) { + methodPath = "/" + methodPath; + } + if (methodPath.endsWith("/")) { + methodPath = handleTrailingSlash(methodPath); + } + } else { + methodPath = ""; + } + } else { + // resource locator method + if (methodPath != null) { + if (!methodPath.startsWith("/")) { + methodPath = "/" + methodPath; + } + if (methodPath.endsWith("/")) { + methodPath = methodPath.substring(0, methodPath.length() - 1); + } + } + } + + // create BasicResourceClassInfo and remember it + BasicResourceClassInfo basicResourceClassInfo = basicResourceClassInfoMap.get(endpoint.classInfo.name().toString()); + if (basicResourceClassInfo == null) { + String[] classProduces = extractProducesConsumesValues( + getAnnotationStore().getAnnotation(endpoint.classInfo, PRODUCES)); + String[] classConsumes = extractProducesConsumesValues( + getAnnotationStore().getAnnotation(endpoint.classInfo, CONSUMES)); + basicResourceClassInfo = new BasicResourceClassInfo(resourceClassPath, classProduces, + classConsumes, pathParameters, getStreamAnnotationValue(endpoint.classInfo)); + basicResourceClassInfoMap.put(endpoint.classInfo.name().toString(), basicResourceClassInfo); + } + + ResourceMethod method = createResourceMethod(endpoint.classInfo, actualEndpointInfo, basicResourceClassInfo, + endpoint.classNameBindings, endpoint.httpMethod, endpoint.methodInfo, methodPath); + ret.add(method); + } + + return ret; + } + + /** + * Return endpoints defined directly on classInfo. + * + * @param classInfo resource class + * @return classInfo endpoint method info + */ + public static Collection collectClassEndpoints(ClassInfo classInfo, + Map httpAnnotationToMethod, IndexView index, ApplicationScanningResult applicationScanningResult) { + Collection endpoints = collectEndpoints(classInfo, classInfo, new HashSet<>(), new HashSet<>(), true, + httpAnnotationToMethod, index, applicationScanningResult, new AnnotationStore(null)); + Collection ret = new HashSet<>(); + for (FoundEndpoint endpoint : endpoints) { + if (endpoint.classInfo.equals(classInfo)) { + ret.add(endpoint.methodInfo); + } + } + return ret; + } + + private static List collectEndpoints(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, + Set seenMethods, Set existingClassNameBindings, boolean considerApplication, + Map httpAnnotationToMethod, IndexView index, ApplicationScanningResult applicationScanningResult, + AnnotationStore annotationStore) { + if (considerApplication && applicationScanningResult != null && !applicationScanningResult.keepClass(actualEndpointInfo.name().toString())) { return Collections.emptyList(); @@ -366,15 +446,7 @@ protected List createEndpoints(ClassInfo currentClassInfo, return Collections.emptyList(); } - List ret = new ArrayList<>(); - String[] classProduces = extractProducesConsumesValues(getAnnotationStore().getAnnotation(currentClassInfo, PRODUCES)); - String[] classConsumes = extractProducesConsumesValues(getAnnotationStore().getAnnotation(currentClassInfo, CONSUMES)); - - String classStreamElementType = getStreamAnnotationValue(currentClassInfo); - - BasicResourceClassInfo basicResourceClassInfo = new BasicResourceClassInfo(resourceClassPath, classProduces, - classConsumes, pathParameters, classStreamElementType); - + List ret = new ArrayList<>(); Set classNameBindings = NameBindingUtil.nameBindingNames(index, currentClassInfo); if (classNameBindings.isEmpty()) { classNameBindings = existingClassNameBindings; @@ -383,39 +455,25 @@ protected List createEndpoints(ClassInfo currentClassInfo, for (DotName httpMethod : httpAnnotationToMethod.keySet()) { List methods = currentClassInfo.methods(); for (MethodInfo info : methods) { - AnnotationInstance annotation = getAnnotationStore().getAnnotation(info, httpMethod); + AnnotationInstance annotation = annotationStore.getAnnotation(info, httpMethod); if (annotation != null) { if (!hasProperModifiers(info)) { continue; } - validateHttpAnnotations(info); String descriptor = methodDescriptor(info); if (seenMethods.contains(descriptor)) { continue; } seenMethods.add(descriptor); - String methodPath = readStringValue(getAnnotationStore().getAnnotation(info, PATH)); - if (methodPath != null) { - if (!methodPath.startsWith("/")) { - methodPath = "/" + methodPath; - } - if (methodPath.endsWith("/")) { - methodPath = handleTrailingSlash(methodPath); - } - } else { - methodPath = ""; - } - ResourceMethod method = createResourceMethod(currentClassInfo, actualEndpointInfo, - basicResourceClassInfo, classNameBindings, httpMethod, info, methodPath); - - ret.add(method); + ret.add(new FoundEndpoint(currentClassInfo, classNameBindings, info, httpMethod)); } } } + //now resource locator methods List methods = currentClassInfo.methods(); for (MethodInfo info : methods) { - AnnotationInstance annotation = getAnnotationStore().getAnnotation(info, PATH); + AnnotationInstance annotation = annotationStore.getAnnotation(info, PATH); if (annotation != null) { if (!hasProperModifiers(info)) { continue; @@ -425,18 +483,7 @@ protected List createEndpoints(ClassInfo currentClassInfo, continue; } seenMethods.add(descriptor); - String methodPath = readStringValue(annotation); - if (methodPath != null) { - if (!methodPath.startsWith("/")) { - methodPath = "/" + methodPath; - } - if (methodPath.endsWith("/")) { - methodPath = methodPath.substring(0, methodPath.length() - 1); - } - } - ResourceMethod method = createResourceMethod(currentClassInfo, actualEndpointInfo, - basicResourceClassInfo, classNameBindings, null, info, methodPath); - ret.add(method); + ret.add(new FoundEndpoint(currentClassInfo, classNameBindings, info, null)); } } @@ -444,16 +491,16 @@ protected List createEndpoints(ClassInfo currentClassInfo, if (superClassName != null && !superClassName.equals(OBJECT)) { ClassInfo superClass = index.getClassByName(superClassName); if (superClass != null) { - ret.addAll(createEndpoints(superClass, actualEndpointInfo, seenMethods, classNameBindings, - pathParameters, resourceClassPath, considerApplication)); + ret.addAll(collectEndpoints(superClass, actualEndpointInfo, seenMethods, classNameBindings, considerApplication, + httpAnnotationToMethod, index, applicationScanningResult, annotationStore)); } } List interfaces = currentClassInfo.interfaceNames(); for (DotName i : interfaces) { ClassInfo superClass = index.getClassByName(i); if (superClass != null) { - ret.addAll(createEndpoints(superClass, actualEndpointInfo, seenMethods, classNameBindings, - pathParameters, resourceClassPath, considerApplication)); + ret.addAll(collectEndpoints(superClass, actualEndpointInfo, seenMethods, classNameBindings, considerApplication, + httpAnnotationToMethod, index, applicationScanningResult, annotationStore)); } } return ret; @@ -481,7 +528,7 @@ private void validateHttpAnnotations(MethodInfo info) { } } - private boolean hasProperModifiers(MethodInfo info) { + 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"); @@ -500,7 +547,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 } @@ -1708,4 +1755,18 @@ boolean handleMultipartForReturnType(AdditionalWriters additionalWriters, ClassI public interface MultipartParameterIndexerExtension { void handleMultipartParameter(ClassInfo multipartClassInfo, IndexView index); } + + private static final class FoundEndpoint { + private final ClassInfo classInfo; + private final Set classNameBindings; + private final MethodInfo methodInfo; + private final DotName httpMethod; + + private FoundEndpoint(ClassInfo classInfo, Set classNameBindings, MethodInfo methodInfo, DotName httpMethod) { + this.classInfo = classInfo; + this.classNameBindings = classNameBindings; + this.methodInfo = methodInfo; + this.httpMethod = httpMethod; + } + } }