VYPR
Medium severity6.5NVD Advisory· Published Apr 25, 2024· Updated Apr 15, 2026

CVE-2023-5675

CVE-2023-5675

Description

A flaw was found in Quarkus. When a Quarkus RestEasy Classic or Reactive JAX-RS endpoint has its methods declared in the abstract Java class or customized by Quarkus extensions using the annotation processor, the authorization of these methods will not be enforced if it is enabled by either 'quarkus.security.jaxrs.deny-unannotated-endpoints' or 'quarkus.security.jaxrs.default-roles-allowed' properties.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
io.quarkus:quarkus-resteasy-reactive-common-deploymentMaven
< 3.2.10.Final3.2.10.Final
io.quarkus:quarkus-resteasy-reactive-commonMaven
< 3.2.10.Final3.2.10.Final
io.quarkus:quarkus-resteasy-reactive-common-deploymentMaven
>= 3.3.0, < 3.6.93.6.9
io.quarkus:quarkus-resteasy-reactive-commonMaven
>= 3.3.0, < 3.6.93.6.9
io.quarkus:quarkus-resteasy-reactive-common-deploymentMaven
>= 3.7.0, < 3.7.13.7.1
io.quarkus:quarkus-resteasy-reactive-commonMaven
>= 3.7.0, < 3.7.13.7.1

Patches

4
d802748128cd

Fix JAX-RS default security checks for inherited / transformed endpoints

https://github.com/quarkusio/quarkusMichal VavříkJan 23, 2024via ghsa
24 files changed · +339 89
  • extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java+62 4 modified
    @@ -1,17 +1,21 @@
     package io.quarkus.resteasy.deployment;
     
     import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
    +import static io.quarkus.resteasy.deployment.RestPathAnnotationProcessor.getAllClassInterfaces;
     import static io.quarkus.resteasy.deployment.RestPathAnnotationProcessor.isRestEndpointMethod;
     import static io.quarkus.security.spi.SecurityTransformerUtils.hasSecurityAnnotation;
     
     import java.util.ArrayList;
    +import java.util.Collection;
     import java.util.List;
     import java.util.Objects;
    +import java.util.function.Predicate;
     import java.util.stream.Collectors;
     
     import org.jboss.jandex.ClassInfo;
     import org.jboss.jandex.DotName;
     import org.jboss.jandex.MethodInfo;
    +import org.jboss.logging.Logger;
     
     import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
     import io.quarkus.deployment.Capabilities;
    @@ -50,6 +54,7 @@
     public class ResteasyBuiltinsProcessor {
     
         protected static final String META_INF_RESOURCES = "META-INF/resources";
    +    private static final Logger LOG = Logger.getLogger(ResteasyBuiltinsProcessor.class);
     
         @BuildStep
         void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
    @@ -65,10 +70,42 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
                     ClassInfo classInfo = index.getIndex().getClassByName(DotName.createSimple(className));
                     if (classInfo == null)
                         throw new IllegalStateException("Unable to find class info for " + className);
    -                if (!hasSecurityAnnotation(classInfo)) {
    -                    for (MethodInfo methodInfo : classInfo.methods()) {
    -                        if (isRestEndpointMethod(index, methodInfo) && !hasSecurityAnnotation(methodInfo)) {
    -                            methods.add(methodInfo);
    +                // add unannotated class endpoints as well as parent class unannotated endpoints
    +                addAllUnannotatedEndpoints(index, classInfo, methods);
    +
    +                // interface endpoints implemented on resources are already in, now we need to resolve default interface
    +                // methods as there, CDI interceptors won't work, therefore neither will our additional secured methods
    +                Collection<ClassInfo> interfaces = getAllClassInterfaces(index, List.of(classInfo), new ArrayList<>());
    +                if (!interfaces.isEmpty()) {
    +                    final List<MethodInfo> interfaceEndpoints = new ArrayList<>();
    +                    for (ClassInfo anInterface : interfaces) {
    +                        addUnannotatedEndpoints(index, anInterface, interfaceEndpoints);
    +                    }
    +                    // look for implementors as implementors on resource classes are secured by CDI interceptors
    +                    if (!interfaceEndpoints.isEmpty()) {
    +                        interfaceBlock: for (MethodInfo interfaceEndpoint : interfaceEndpoints) {
    +                            if (interfaceEndpoint.isDefault()) {
    +                                for (MethodInfo endpoint : methods) {
    +                                    boolean nameParamsMatch = endpoint.name().equals(interfaceEndpoint.name())
    +                                            && (interfaceEndpoint.parameterTypes().equals(endpoint.parameterTypes()));
    +                                    if (nameParamsMatch) {
    +                                        // whether matched method is declared on class that implements interface endpoint
    +                                        Predicate<DotName> isEndpointInterface = interfaceEndpoint.declaringClass()
    +                                                .name()::equals;
    +                                        if (endpoint.declaringClass().interfaceNames().stream().anyMatch(isEndpointInterface)) {
    +                                            continue interfaceBlock;
    +                                        }
    +                                    }
    +                                }
    +                                String configProperty = config.denyJaxRs ? "quarkus.security.jaxrs.deny-unannotated-endpoints"
    +                                        : "quarkus.security.jaxrs.default-roles-allowed";
    +                                // this is logging only as I'm a bit worried about false positives and breaking things
    +                                // for what is very much edge case
    +                                LOG.warn("Default interface method '" + interfaceEndpoint
    +                                        + "' cannot be secured with the '" + configProperty
    +                                        + "' configuration property. Please implement this method for CDI "
    +                                        + "interceptor binding to work");
    +                            }
                             }
                         }
                     }
    @@ -85,6 +122,27 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
             }
         }
     
    +    private static void addAllUnannotatedEndpoints(CombinedIndexBuildItem index, ClassInfo classInfo,
    +            List<MethodInfo> methods) {
    +        if (classInfo == null) {
    +            return;
    +        }
    +        addUnannotatedEndpoints(index, classInfo, methods);
    +        if (classInfo.superClassType() != null && !classInfo.superClassType().name().equals(DotName.OBJECT_NAME)) {
    +            addAllUnannotatedEndpoints(index, index.getIndex().getClassByName(classInfo.superClassType().name()), methods);
    +        }
    +    }
    +
    +    private static void addUnannotatedEndpoints(CombinedIndexBuildItem index, ClassInfo classInfo, List<MethodInfo> methods) {
    +        if (!hasSecurityAnnotation(classInfo)) {
    +            for (MethodInfo methodInfo : classInfo.methods()) {
    +                if (isRestEndpointMethod(index, methodInfo) && !hasSecurityAnnotation(methodInfo)) {
    +                    methods.add(methodInfo);
    +                }
    +            }
    +        }
    +    }
    +
         /**
          * Install the JAX-RS security provider.
          */
    
  • extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java+1 1 modified
    @@ -182,7 +182,7 @@ static Optional<AnnotationInstance> searchPathAnnotationOnInterfaces(CombinedInd
          * @param resultAcc accumulator for tail-recursion
          * @return Collection of all interfaces und their parents. Never null.
          */
    -    private static Collection<ClassInfo> getAllClassInterfaces(
    +    static Collection<ClassInfo> getAllClassInterfaces(
                 CombinedIndexBuildItem index,
                 Collection<ClassInfo> classInfos,
                 List<ClassInfo> resultAcc) {
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AbstractSecurityEventTest.java+2 1 modified
    @@ -37,7 +37,8 @@ public abstract class AbstractSecurityEventTest {
     
         protected static final Class<?>[] TEST_CLASSES = {
                 RolesAllowedResource.class, TestIdentityProvider.class, TestIdentityController.class,
    -            UnsecuredResource.class, UnsecuredSubResource.class, EventObserver.class
    +            UnsecuredResource.class, UnsecuredSubResource.class, EventObserver.class, UnsecuredResourceInterface.class,
    +            UnsecuredParentResource.class
         };
     
         @Inject
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java+14 2 modified
    @@ -22,8 +22,8 @@ public class DefaultRolesAllowedJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
    +                            TestIdentityController.class, UnsecuredParentResource.class,
                                 UnsecuredSubResource.class, HelloResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = admin\n"),
                                 "application.properties"));
    @@ -41,6 +41,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 200, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedOnParentClass() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedOnInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyDenyAllMethod() {
             String path = "/unsecured/denyAll";
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedStarJaxRsTest.java+2 2 modified
    @@ -17,8 +17,8 @@ public class DefaultRolesAllowedStarJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredParentResource.class,
    +                            TestIdentityController.class, UnsecuredResourceInterface.class,
                                 UnsecuredSubResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = **\n"),
                                 "application.properties"));
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DenyAllJaxRsTest.java+14 2 modified
    @@ -26,8 +26,8 @@ public class DenyAllJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredParentResource.class,
    +                            TestIdentityController.class, UnsecuredResourceInterface.class,
                                 UnsecuredSubResource.class, HelloResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.deny-unannotated-endpoints = true\n"),
                                 "application.properties"));
    @@ -58,6 +58,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedOnParentClass() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedOnInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyDenyAllMethod() {
             String path = "/unsecured/denyAll";
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredParentResource.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public class UnsecuredParentResource {
    +
    +    @Path("/defaultSecurityParent")
    +    @GET
    +    public String defaultSecurityParent() {
    +        return "defaultSecurityParent";
    +    }
    +
    +}
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResourceInterface.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public interface UnsecuredResourceInterface {
    +
    +    @Path("/defaultSecurityInterface")
    +    @GET
    +    default String defaultSecurityInterface() {
    +        return "defaultSecurityInterface";
    +    }
    +
    +}
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResource.java+6 1 modified
    @@ -12,13 +12,18 @@
      * @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com
      */
     @Path("/unsecured")
    -public class UnsecuredResource {
    +public class UnsecuredResource extends UnsecuredParentResource implements UnsecuredResourceInterface {
         @Path("/defaultSecurity")
         @GET
         public String defaultSecurity() {
             return "defaultSecurity";
         }
     
    +    @Override
    +    public String defaultSecurityInterface() {
    +        return UnsecuredResourceInterface.super.defaultSecurityInterface();
    +    }
    +
         @Path("/permitAllPathParam/{index}")
         @GET
         @PermitAll
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java+8 45 modified
    @@ -1,13 +1,9 @@
     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;
    @@ -76,7 +72,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.AdditionalSecuredMethodsBuildItem;
    +import io.quarkus.security.spi.DefaultSecurityCheckBuildItem;
     
     public class ResteasyReactiveCommonProcessor {
     
    @@ -134,46 +130,13 @@ void searchForProviders(Capabilities capabilities,
         }
     
         @BuildStep
    -    void setUpDenyAllJaxRs(
    -            CombinedIndexBuildItem index,
    -            JaxRsSecurityConfig securityConfig,
    -            Optional<ResourceScanningResultBuildItem> resteasyDeployment,
    -            BeanArchiveIndexBuildItem beanArchiveIndexBuildItem,
    -            ApplicationResultBuildItem applicationResultBuildItem,
    -            BuildProducer<AdditionalSecuredMethodsBuildItem> additionalSecuredClasses) {
    -
    -        if (resteasyDeployment.isPresent()
    -                && (securityConfig.denyJaxRs() || securityConfig.defaultRolesAllowed().isPresent())) {
    -            final List<MethodInfo> methods = new ArrayList<>();
    -            Map<DotName, String> httpAnnotationToMethod = resteasyDeployment.get().getResult().getHttpAnnotationToMethod();
    -            Set<DotName> resourceClasses = resteasyDeployment.get().getResult().getScannedResourcePaths().keySet();
    -
    -            for (DotName className : resourceClasses) {
    -                ClassInfo classInfo = index.getIndex().getClassByName(className);
    -                if (classInfo == null)
    -                    throw new IllegalStateException("Unable to find class info for " + className);
    -                if (!hasSecurityAnnotation(classInfo)) {
    -                    // collect class endpoints
    -                    Collection<MethodInfo> classEndpoints = collectClassEndpoints(classInfo, httpAnnotationToMethod,
    -                            beanArchiveIndexBuildItem.getIndex(), applicationResultBuildItem.getResult());
    -
    -                    // add endpoints
    -                    for (MethodInfo classEndpoint : classEndpoints) {
    -                        if (!hasSecurityAnnotation(classEndpoint)) {
    -                            methods.add(classEndpoint);
    -                        }
    -                    }
    -                }
    -            }
    -
    -            if (!methods.isEmpty()) {
    -                if (securityConfig.denyJaxRs()) {
    -                    additionalSecuredClasses.produce(new AdditionalSecuredMethodsBuildItem(methods));
    -                } else {
    -                    additionalSecuredClasses
    -                            .produce(new AdditionalSecuredMethodsBuildItem(methods, securityConfig.defaultRolesAllowed()));
    -                }
    -            }
    +    void setUpDenyAllJaxRs(JaxRsSecurityConfig securityConfig,
    +            BuildProducer<DefaultSecurityCheckBuildItem> defaultSecurityCheckProducer) {
    +        if (securityConfig.denyJaxRs()) {
    +            defaultSecurityCheckProducer.produce(DefaultSecurityCheckBuildItem.denyAll());
    +        } else if (securityConfig.defaultRolesAllowed().isPresent()) {
    +            defaultSecurityCheckProducer
    +                    .produce(DefaultSecurityCheckBuildItem.rolesAllowed(securityConfig.defaultRolesAllowed().get()));
             }
         }
     
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractSecurityEventTest.java+2 1 modified
    @@ -40,7 +40,8 @@ public abstract class AbstractSecurityEventTest {
         protected static final Class<?>[] TEST_CLASSES = {
                 RolesAllowedResource.class, RolesAllowedBlockingResource.class, TestIdentityProvider.class,
                 TestIdentityController.class, UnsecuredResource.class, UnsecuredSubResource.class, RolesAllowedService.class,
    -            RolesAllowedServiceResource.class, EventObserver.class
    +            RolesAllowedServiceResource.class, EventObserver.class, UnsecuredResourceInterface.class,
    +            UnsecuredParentResource.class
         };
     
         @Inject
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedJaxRsTest.java+14 2 modified
    @@ -18,9 +18,9 @@ public class DefaultRolesAllowedJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
                                 TestIdentityController.class,
    -                            UnsecuredSubResource.class, HelloResource.class)
    +                            UnsecuredSubResource.class, HelloResource.class, UnsecuredParentResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed=admin\n"),
                                 "application.properties"));
     
    @@ -37,6 +37,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 200, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedParent() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyDenyAllMethod() {
             String path = "/unsecured/denyAll";
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedStarJaxRsTest.java+2 2 modified
    @@ -17,8 +17,8 @@ public class DefaultRolesAllowedStarJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
    +                            TestIdentityController.class, UnsecuredParentResource.class,
                                 UnsecuredSubResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = **\n"),
                                 "application.properties"));
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DenyAllJaxRsTest.java+80 4 modified
    @@ -3,12 +3,25 @@
     import static io.restassured.RestAssured.given;
     import static io.restassured.RestAssured.when;
     
    +import java.lang.reflect.Modifier;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
     import org.hamcrest.Matchers;
    +import org.jboss.jandex.AnnotationTarget;
    +import org.jboss.jandex.AnnotationValue;
    +import org.jboss.jandex.ClassInfo;
    +import org.jboss.jandex.MethodInfo;
    +import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationsTransformer;
     import org.jboss.shrinkwrap.api.asset.StringAsset;
     import org.junit.jupiter.api.BeforeAll;
     import org.junit.jupiter.api.Test;
     import org.junit.jupiter.api.extension.RegisterExtension;
     
    +import io.quarkus.builder.BuildContext;
    +import io.quarkus.builder.BuildStep;
    +import io.quarkus.resteasy.reactive.server.spi.AnnotationsTransformerBuildItem;
     import io.quarkus.security.test.utils.TestIdentityController;
     import io.quarkus.security.test.utils.TestIdentityProvider;
     import io.quarkus.test.QuarkusUnitTest;
    @@ -21,11 +34,43 @@ public class DenyAllJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    -                            UnsecuredSubResource.class, HelloResource.class)
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
    +                            TestIdentityController.class, SpecialResource.class,
    +                            UnsecuredSubResource.class, HelloResource.class, UnsecuredParentResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.deny-unannotated-endpoints = true\n"),
    -                            "application.properties"));
    +                            "application.properties"))
    +            .addBuildChainCustomizer(builder -> {
    +                builder.addBuildStep(new BuildStep() {
    +                    @Override
    +                    public void execute(BuildContext context) {
    +                        // Here we add an AnnotationsTransformer in order to make sure that the security layer
    +                        // uses the proper set of transformers
    +                        context.produce(
    +                                new AnnotationsTransformerBuildItem(
    +                                        AnnotationsTransformer.builder().appliesTo(AnnotationTarget.Kind.METHOD)
    +                                                .transform(transformerContext -> {
    +                                                    // This transformer auto-adds @GET and @Path if missing, thus emulating Renarde
    +                                                    MethodInfo methodInfo = transformerContext.getTarget().asMethod();
    +                                                    ClassInfo declaringClass = methodInfo.declaringClass();
    +                                                    if (declaringClass.name().toString().equals(SpecialResource.class.getName())
    +                                                            && !methodInfo.isConstructor()
    +                                                            && !Modifier.isStatic(methodInfo.flags())) {
    +                                                        if (methodInfo.declaredAnnotation(GET.class.getName()) == null) {
    +                                                            // auto-add it
    +                                                            transformerContext.transform().add(GET.class).done();
    +                                                        }
    +                                                        if (methodInfo.declaredAnnotation(Path.class.getName()) == null) {
    +                                                            // auto-add it
    +                                                            transformerContext.transform().add(Path.class,
    +                                                                    AnnotationValue.createStringValue("value",
    +                                                                            methodInfo.name()))
    +                                                                    .done();
    +                                                        }
    +                                                    }
    +                                                })));
    +                    }
    +                }).produces(AnnotationsTransformerBuildItem.class).build();
    +            });
     
         @BeforeAll
         public static void setupUsers() {
    @@ -40,6 +85,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedOnParentClass() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedOnInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyUnannotatedNonBlocking() {
             String path = "/unsecured/defaultSecurityNonBlocking";
    @@ -90,6 +147,14 @@ public void testServerExceptionMapper() {
                     .body(Matchers.equalTo("unauthorizedExceptionMapper"));
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedWithAnnotationTransformer() {
    +        String path = "/special/explicit";
    +        assertStatus(path, 403, 401);
    +        path = "/special/implicit";
    +        assertStatus(path, 403, 401);
    +    }
    +
         private void assertStatus(String path, int status, int anonStatus) {
             given().auth().preemptive()
                     .basic("admin", "admin").get(path)
    @@ -105,4 +170,15 @@ private void assertStatus(String path, int status, int anonStatus) {
     
         }
     
    +    @Path("/special")
    +    public static class SpecialResource {
    +        @GET
    +        public String explicit() {
    +            return "explicit";
    +        }
    +
    +        public String implicit() {
    +            return "implicit";
    +        }
    +    }
     }
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/UnsecuredParentResource.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.reactive.server.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public class UnsecuredParentResource {
    +
    +    @Path("/defaultSecurityParent")
    +    @GET
    +    public String defaultSecurityParent() {
    +        return "defaultSecurityParent";
    +    }
    +
    +}
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/UnsecuredResourceInterface.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.reactive.server.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public interface UnsecuredResourceInterface {
    +
    +    @Path("/defaultSecurityInterface")
    +    @GET
    +    default String defaultSecurityInterface() {
    +        return "defaultSecurityInterface";
    +    }
    +
    +}
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/UnsecuredResource.java+1 1 modified
    @@ -12,7 +12,7 @@
      * @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com
      */
     @Path("/unsecured")
    -public class UnsecuredResource {
    +public class UnsecuredResource extends UnsecuredParentResource implements UnsecuredResourceInterface {
         @Path("/defaultSecurity")
         @GET
         public String defaultSecurity() {
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/EagerSecurityHandler.java+14 2 modified
    @@ -66,9 +66,14 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti
             ResteasyReactiveResourceInfo lazyMethod = requestContext.getTarget().getLazyMethod();
             MethodDescription methodDescription = lazyMethodToMethodDescription(lazyMethod);
             if (check == null) {
    -            check = Arc.container().instance(SecurityCheckStorage.class).get().getSecurityCheck(methodDescription);
    +            SecurityCheckStorage storage = Arc.container().instance(SecurityCheckStorage.class).get();
    +            check = storage.getSecurityCheck(methodDescription);
                 if (check == null) {
    -                check = NULL_SENTINEL;
    +                if (storage.getDefaultSecurityCheck() == null || isRequestAlreadyChecked(requestContext)) {
    +                    check = NULL_SENTINEL;
    +                } else {
    +                    check = storage.getDefaultSecurityCheck();
    +                }
                 }
                 this.check = check;
             }
    @@ -193,6 +198,13 @@ private void preventRepeatedSecurityChecks(ResteasyReactiveRequestContext reques
             requestContext.setProperty(STANDARD_SECURITY_CHECK_INTERCEPTOR, methodDescription);
         }
     
    +    private boolean isRequestAlreadyChecked(ResteasyReactiveRequestContext requestContext) {
    +        // when request has already been checked at least once (by another instance of this handler)
    +        // then default security checks, like denied access to all JAX-RS resources by default
    +        // shouldn't be applied; this doesn't mean security checks registered for methods shouldn't be applied
    +        return requestContext.getProperty(STANDARD_SECURITY_CHECK_INTERCEPTOR) != null;
    +    }
    +
         private InjectableInstance<CurrentIdentityAssociation> getCurrentIdentityAssociation() {
             InjectableInstance<CurrentIdentityAssociation> identityAssociation = this.currentIdentityAssociation;
             if (identityAssociation == null) {
    
  • extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java+11 0 modified
    @@ -105,6 +105,7 @@
     import io.quarkus.security.runtime.interceptor.SecurityHandler;
     import io.quarkus.security.spi.AdditionalSecuredClassesBuildItem;
     import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem;
    +import io.quarkus.security.spi.DefaultSecurityCheckBuildItem;
     import io.quarkus.security.spi.RolesAllowedConfigExpResolverBuildItem;
     import io.quarkus.security.spi.runtime.AuthorizationController;
     import io.quarkus.security.spi.runtime.DevModeDisabledAuthorizationController;
    @@ -527,6 +528,7 @@ void gatherSecurityChecks(BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
                 BuildProducer<RunTimeConfigBuilderBuildItem> configBuilderProducer,
                 List<AdditionalSecuredMethodsBuildItem> additionalSecuredMethods,
                 SecurityCheckRecorder recorder,
    +            Optional<DefaultSecurityCheckBuildItem> defaultSecurityCheckBuildItem,
                 BuildProducer<ReflectiveClassBuildItem> reflectiveClassBuildItemBuildProducer,
                 List<AdditionalSecurityCheckBuildItem> additionalSecurityChecks, SecurityBuildTimeConfig config) {
             classPredicate.produce(new ApplicationClassPredicateBuildItem(new SecurityCheckStorageAppPredicate()));
    @@ -559,6 +561,15 @@ void gatherSecurityChecks(BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
                 recorder.addMethod(builder, method.declaringClass().name().toString(), method.name(), params,
                         methodEntry.getValue());
             }
    +
    +        if (defaultSecurityCheckBuildItem.isPresent()) {
    +            var roles = defaultSecurityCheckBuildItem.get().getRolesAllowed();
    +            if (roles == null) {
    +                recorder.registerDefaultSecurityCheck(builder, recorder.denyAll());
    +            } else {
    +                recorder.registerDefaultSecurityCheck(builder, recorder.rolesAllowed(roles.toArray(new String[0])));
    +            }
    +        }
             recorder.create(builder);
     
             syntheticBeans.produce(
    
  • extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/SecurityCheckStorage.java+5 0 modified
    @@ -10,4 +10,9 @@ default SecurityCheck getSecurityCheck(Method method) {
     
         SecurityCheck getSecurityCheck(MethodDescription methodDescription);
     
    +    /**
    +     * {@link SecurityCheck} that should be applied when there is no other check applied on incoming request.
    +     */
    +    SecurityCheck getDefaultSecurityCheck();
    +
     }
    
  • extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityCheckStorageBuilder.java+13 0 modified
    @@ -9,6 +9,7 @@
     
     public class SecurityCheckStorageBuilder {
         private final Map<MethodDescription, SecurityCheck> securityChecks = new HashMap<>();
    +    private SecurityCheck defaultSecurityCheck;
     
         public void registerCheck(String className,
                 String methodName,
    @@ -17,12 +18,24 @@ public void registerCheck(String className,
             securityChecks.put(new MethodDescription(className, methodName, parameterTypes), securityCheck);
         }
     
    +    public void registerDefaultSecurityCheck(SecurityCheck defaultSecurityCheck) {
    +        if (this.defaultSecurityCheck != null) {
    +            throw new IllegalStateException("Default SecurityCheck has already been registered");
    +        }
    +        this.defaultSecurityCheck = defaultSecurityCheck;
    +    }
    +
         public SecurityCheckStorage create() {
             return new SecurityCheckStorage() {
                 @Override
                 public SecurityCheck getSecurityCheck(MethodDescription methodDescription) {
                     return securityChecks.get(methodDescription);
                 }
    +
    +            @Override
    +            public SecurityCheck getDefaultSecurityCheck() {
    +                return defaultSecurityCheck;
    +            }
             };
         }
     }
    
  • extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java+4 0 modified
    @@ -351,4 +351,8 @@ private Class<?> loadClass(String className) {
                 throw new RuntimeException("Unable to load class '" + className + "' for creating permission", e);
             }
         }
    +
    +    public void registerDefaultSecurityCheck(RuntimeValue<SecurityCheckStorageBuilder> builder, SecurityCheck securityCheck) {
    +        builder.getValue().registerDefaultSecurityCheck(securityCheck);
    +    }
     }
    
  • extensions/security/spi/src/main/java/io/quarkus/security/spi/DefaultSecurityCheckBuildItem.java+28 0 added
    @@ -0,0 +1,28 @@
    +package io.quarkus.security.spi;
    +
    +import java.util.List;
    +import java.util.Objects;
    +
    +import io.quarkus.builder.item.SimpleBuildItem;
    +
    +public final class DefaultSecurityCheckBuildItem extends SimpleBuildItem {
    +
    +    public final List<String> rolesAllowed;
    +
    +    private DefaultSecurityCheckBuildItem(List<String> rolesAllowed) {
    +        this.rolesAllowed = rolesAllowed;
    +    }
    +
    +    public static DefaultSecurityCheckBuildItem denyAll() {
    +        return new DefaultSecurityCheckBuildItem(null);
    +    }
    +
    +    public static DefaultSecurityCheckBuildItem rolesAllowed(List<String> rolesAllowed) {
    +        Objects.requireNonNull(rolesAllowed);
    +        return new DefaultSecurityCheckBuildItem(List.copyOf(rolesAllowed));
    +    }
    +
    +    public List<String> getRolesAllowed() {
    +        return rolesAllowed;
    +    }
    +}
    
  • independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java+0 19 modified
    @@ -422,25 +422,6 @@ protected List<ResourceMethod> createEndpoints(ClassInfo currentClassInfo,
             return ret;
         }
     
    -    /**
    -     * Return endpoints defined directly on classInfo.
    -     *
    -     * @param classInfo resource class
    -     * @return classInfo endpoint method info
    -     */
    -    public static Collection<MethodInfo> collectClassEndpoints(ClassInfo classInfo,
    -            Map<DotName, String> httpAnnotationToMethod, IndexView index, ApplicationScanningResult applicationScanningResult) {
    -        Collection<FoundEndpoint> endpoints = collectEndpoints(classInfo, classInfo, new HashSet<>(), new HashSet<>(), true,
    -                httpAnnotationToMethod, index, applicationScanningResult, new AnnotationStore(null));
    -        Collection<MethodInfo> ret = new HashSet<>();
    -        for (FoundEndpoint endpoint : endpoints) {
    -            if (endpoint.classInfo.equals(classInfo)) {
    -                ret.add(endpoint.methodInfo);
    -            }
    -        }
    -        return ret;
    -    }
    -
         private static List<FoundEndpoint> collectEndpoints(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo,
                 Set<String> seenMethods, Set<String> existingClassNameBindings, boolean considerApplication,
                 Map<DotName, String> httpAnnotationToMethod, IndexView index, ApplicationScanningResult applicationScanningResult,
    
bf2ef6c504b9

Fix JAX-RS default security checks for inherited / transformed endpoints

https://github.com/quarkusio/quarkusMichal VavříkJan 23, 2024via ghsa
23 files changed · +337 133
  • extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java+62 4 modified
    @@ -1,17 +1,21 @@
     package io.quarkus.resteasy.deployment;
     
     import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
    +import static io.quarkus.resteasy.deployment.RestPathAnnotationProcessor.getAllClassInterfaces;
     import static io.quarkus.resteasy.deployment.RestPathAnnotationProcessor.isRestEndpointMethod;
     import static io.quarkus.security.spi.SecurityTransformerUtils.hasSecurityAnnotation;
     
     import java.util.ArrayList;
    +import java.util.Collection;
     import java.util.List;
     import java.util.Objects;
    +import java.util.function.Predicate;
     import java.util.stream.Collectors;
     
     import org.jboss.jandex.ClassInfo;
     import org.jboss.jandex.DotName;
     import org.jboss.jandex.MethodInfo;
    +import org.jboss.logging.Logger;
     
     import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
     import io.quarkus.deployment.Capabilities;
    @@ -50,6 +54,7 @@
     public class ResteasyBuiltinsProcessor {
     
         protected static final String META_INF_RESOURCES = "META-INF/resources";
    +    private static final Logger LOG = Logger.getLogger(ResteasyBuiltinsProcessor.class);
     
         @BuildStep
         void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
    @@ -65,10 +70,42 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
                     ClassInfo classInfo = index.getIndex().getClassByName(DotName.createSimple(className));
                     if (classInfo == null)
                         throw new IllegalStateException("Unable to find class info for " + className);
    -                if (!hasSecurityAnnotation(classInfo)) {
    -                    for (MethodInfo methodInfo : classInfo.methods()) {
    -                        if (isRestEndpointMethod(index, methodInfo) && !hasSecurityAnnotation(methodInfo)) {
    -                            methods.add(methodInfo);
    +                // add unannotated class endpoints as well as parent class unannotated endpoints
    +                addAllUnannotatedEndpoints(index, classInfo, methods);
    +
    +                // interface endpoints implemented on resources are already in, now we need to resolve default interface
    +                // methods as there, CDI interceptors won't work, therefore neither will our additional secured methods
    +                Collection<ClassInfo> interfaces = getAllClassInterfaces(index, List.of(classInfo), new ArrayList<>());
    +                if (!interfaces.isEmpty()) {
    +                    final List<MethodInfo> interfaceEndpoints = new ArrayList<>();
    +                    for (ClassInfo anInterface : interfaces) {
    +                        addUnannotatedEndpoints(index, anInterface, interfaceEndpoints);
    +                    }
    +                    // look for implementors as implementors on resource classes are secured by CDI interceptors
    +                    if (!interfaceEndpoints.isEmpty()) {
    +                        interfaceBlock: for (MethodInfo interfaceEndpoint : interfaceEndpoints) {
    +                            if (interfaceEndpoint.isDefault()) {
    +                                for (MethodInfo endpoint : methods) {
    +                                    boolean nameParamsMatch = endpoint.name().equals(interfaceEndpoint.name())
    +                                            && (interfaceEndpoint.parameterTypes().equals(endpoint.parameterTypes()));
    +                                    if (nameParamsMatch) {
    +                                        // whether matched method is declared on class that implements interface endpoint
    +                                        Predicate<DotName> isEndpointInterface = interfaceEndpoint.declaringClass()
    +                                                .name()::equals;
    +                                        if (endpoint.declaringClass().interfaceNames().stream().anyMatch(isEndpointInterface)) {
    +                                            continue interfaceBlock;
    +                                        }
    +                                    }
    +                                }
    +                                String configProperty = config.denyJaxRs ? "quarkus.security.jaxrs.deny-unannotated-endpoints"
    +                                        : "quarkus.security.jaxrs.default-roles-allowed";
    +                                // this is logging only as I'm a bit worried about false positives and breaking things
    +                                // for what is very much edge case
    +                                LOG.warn("Default interface method '" + interfaceEndpoint
    +                                        + "' cannot be secured with the '" + configProperty
    +                                        + "' configuration property. Please implement this method for CDI "
    +                                        + "interceptor binding to work");
    +                            }
                             }
                         }
                     }
    @@ -85,6 +122,27 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
             }
         }
     
    +    private static void addAllUnannotatedEndpoints(CombinedIndexBuildItem index, ClassInfo classInfo,
    +            List<MethodInfo> methods) {
    +        if (classInfo == null) {
    +            return;
    +        }
    +        addUnannotatedEndpoints(index, classInfo, methods);
    +        if (classInfo.superClassType() != null && !classInfo.superClassType().name().equals(DotName.OBJECT_NAME)) {
    +            addAllUnannotatedEndpoints(index, index.getIndex().getClassByName(classInfo.superClassType().name()), methods);
    +        }
    +    }
    +
    +    private static void addUnannotatedEndpoints(CombinedIndexBuildItem index, ClassInfo classInfo, List<MethodInfo> methods) {
    +        if (!hasSecurityAnnotation(classInfo)) {
    +            for (MethodInfo methodInfo : classInfo.methods()) {
    +                if (isRestEndpointMethod(index, methodInfo) && !hasSecurityAnnotation(methodInfo)) {
    +                    methods.add(methodInfo);
    +                }
    +            }
    +        }
    +    }
    +
         /**
          * Install the JAX-RS security provider.
          */
    
  • extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java+1 1 modified
    @@ -182,7 +182,7 @@ static Optional<AnnotationInstance> searchPathAnnotationOnInterfaces(CombinedInd
          * @param resultAcc accumulator for tail-recursion
          * @return Collection of all interfaces und their parents. Never null.
          */
    -    private static Collection<ClassInfo> getAllClassInterfaces(
    +    static Collection<ClassInfo> getAllClassInterfaces(
                 CombinedIndexBuildItem index,
                 Collection<ClassInfo> classInfos,
                 List<ClassInfo> resultAcc) {
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java+14 2 modified
    @@ -22,8 +22,8 @@ public class DefaultRolesAllowedJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
    +                            TestIdentityController.class, UnsecuredParentResource.class,
                                 UnsecuredSubResource.class, HelloResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = admin\n"),
                                 "application.properties"));
    @@ -41,6 +41,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 200, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedOnParentClass() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedOnInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyDenyAllMethod() {
             String path = "/unsecured/denyAll";
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedStarJaxRsTest.java+2 2 modified
    @@ -17,8 +17,8 @@ public class DefaultRolesAllowedStarJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredParentResource.class,
    +                            TestIdentityController.class, UnsecuredResourceInterface.class,
                                 UnsecuredSubResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = **\n"),
                                 "application.properties"));
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DenyAllJaxRsTest.java+14 2 modified
    @@ -26,8 +26,8 @@ public class DenyAllJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredParentResource.class,
    +                            TestIdentityController.class, UnsecuredResourceInterface.class,
                                 UnsecuredSubResource.class, HelloResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.deny-unannotated-endpoints = true\n"),
                                 "application.properties"));
    @@ -58,6 +58,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedOnParentClass() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedOnInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyDenyAllMethod() {
             String path = "/unsecured/denyAll";
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredParentResource.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public class UnsecuredParentResource {
    +
    +    @Path("/defaultSecurityParent")
    +    @GET
    +    public String defaultSecurityParent() {
    +        return "defaultSecurityParent";
    +    }
    +
    +}
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResourceInterface.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public interface UnsecuredResourceInterface {
    +
    +    @Path("/defaultSecurityInterface")
    +    @GET
    +    default String defaultSecurityInterface() {
    +        return "defaultSecurityInterface";
    +    }
    +
    +}
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResource.java+6 1 modified
    @@ -10,13 +10,18 @@
      * @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com
      */
     @Path("/unsecured")
    -public class UnsecuredResource {
    +public class UnsecuredResource extends UnsecuredParentResource implements UnsecuredResourceInterface {
         @Path("/defaultSecurity")
         @GET
         public String defaultSecurity() {
             return "defaultSecurity";
         }
     
    +    @Override
    +    public String defaultSecurityInterface() {
    +        return UnsecuredResourceInterface.super.defaultSecurityInterface();
    +    }
    +
         @Path("/permitAllPathParam/{index}")
         @GET
         @PermitAll
    
  • extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/EagerSecurityFilter.java+2 46 modified
    @@ -1,15 +1,11 @@
     package io.quarkus.resteasy.runtime;
     
    -import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_FAILURE;
    -import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_SUCCESS;
    -
     import java.io.IOException;
     import java.util.HashMap;
     import java.util.Map;
     import java.util.function.Consumer;
     
     import jakarta.annotation.Priority;
    -import jakarta.enterprise.event.Event;
     import jakarta.inject.Inject;
     import jakarta.ws.rs.Priorities;
     import jakarta.ws.rs.container.ContainerRequestContext;
    @@ -18,19 +14,13 @@
     import jakarta.ws.rs.core.Context;
     import jakarta.ws.rs.ext.Provider;
     
    -import org.eclipse.microprofile.config.ConfigProvider;
    -
     import io.quarkus.arc.Arc;
     import io.quarkus.security.UnauthorizedException;
     import io.quarkus.security.identity.CurrentIdentityAssociation;
    -import io.quarkus.security.identity.SecurityIdentity;
     import io.quarkus.security.spi.runtime.AuthorizationController;
    -import io.quarkus.security.spi.runtime.AuthorizationFailureEvent;
    -import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent;
     import io.quarkus.security.spi.runtime.MethodDescription;
     import io.quarkus.security.spi.runtime.SecurityCheck;
     import io.quarkus.security.spi.runtime.SecurityCheckStorage;
    -import io.quarkus.security.spi.runtime.SecurityEventHelper;
     import io.quarkus.vertx.http.runtime.security.EagerSecurityInterceptorStorage;
     import io.vertx.ext.web.RoutingContext;
     
    @@ -46,7 +36,6 @@ public void accept(RoutingContext routingContext) {
         };
         private final Map<MethodDescription, Consumer<RoutingContext>> cache = new HashMap<>();
         private final EagerSecurityInterceptorStorage interceptorStorage;
    -    private final SecurityEventHelper<AuthorizationSuccessEvent, AuthorizationFailureEvent> eventHelper;
     
         @Context
         ResourceInfo resourceInfo;
    @@ -66,11 +55,6 @@ public void accept(RoutingContext routingContext) {
         public EagerSecurityFilter() {
             var interceptorStorageHandle = Arc.container().instance(EagerSecurityInterceptorStorage.class);
             this.interceptorStorage = interceptorStorageHandle.isAvailable() ? interceptorStorageHandle.get() : null;
    -        Event<Object> event = Arc.container().beanManager().getEvent();
    -        this.eventHelper = new SecurityEventHelper<>(event.select(AuthorizationSuccessEvent.class),
    -                event.select(AuthorizationFailureEvent.class), AUTHORIZATION_SUCCESS,
    -                AUTHORIZATION_FAILURE, Arc.container().beanManager(),
    -                ConfigProvider.getConfig().getOptionalValue("quarkus.security.events.enabled", Boolean.class).orElse(false));
         }
     
         @Override
    @@ -87,50 +71,22 @@ public void filter(ContainerRequestContext requestContext) throws IOException {
         private void applySecurityChecks(MethodDescription description) {
             SecurityCheck check = securityCheckStorage.getSecurityCheck(description);
             if (check != null) {
    -            if (check.isPermitAll()) {
    -                fireEventOnAuthZSuccess(check, null);
    -            } else {
    +            if (!check.isPermitAll()) {
                     if (check.requiresMethodArguments()) {
                         if (identityAssociation.getIdentity().isAnonymous()) {
                             var exception = new UnauthorizedException();
    -                        if (eventHelper.fireEventOnFailure()) {
    -                            fireEventOnAuthZFailure(exception, check);
    -                        }
                             throw exception;
                         }
                         // security check will be performed by CDI interceptor
                         return;
                     }
    -                if (eventHelper.fireEventOnFailure()) {
    -                    try {
    -                        check.apply(identityAssociation.getIdentity(), description, null);
    -                    } catch (Exception e) {
    -                        fireEventOnAuthZFailure(e, check);
    -                        throw e;
    -                    }
    -                } else {
    -                    check.apply(identityAssociation.getIdentity(), description, null);
    -                }
    -                fireEventOnAuthZSuccess(check, identityAssociation.getIdentity());
    +                check.apply(identityAssociation.getIdentity(), description, null);
                 }
                 // prevent repeated security checks
                 routingContext.put(EagerSecurityFilter.class.getName(), resourceInfo.getResourceMethod());
             }
         }
     
    -    private void fireEventOnAuthZFailure(Exception exception, SecurityCheck check) {
    -        eventHelper.fireFailureEvent(new AuthorizationFailureEvent(
    -                identityAssociation.getIdentity(), exception, check.getClass().getName(),
    -                Map.of(RoutingContext.class.getName(), routingContext)));
    -    }
    -
    -    private void fireEventOnAuthZSuccess(SecurityCheck check, SecurityIdentity securityIdentity) {
    -        if (eventHelper.fireEventOnSuccess()) {
    -            eventHelper.fireSuccessEvent(new AuthorizationSuccessEvent(securityIdentity,
    -                    check.getClass().getName(), Map.of(RoutingContext.class.getName(), routingContext)));
    -        }
    -    }
    -
         private void applyEagerSecurityInterceptors(MethodDescription description) {
             var interceptor = cache.get(description);
             if (interceptor != NULL_SENTINEL) {
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java+8 45 modified
    @@ -1,13 +1,9 @@
     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;
    @@ -75,7 +71,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.AdditionalSecuredMethodsBuildItem;
    +import io.quarkus.security.spi.DefaultSecurityCheckBuildItem;
     
     public class ResteasyReactiveCommonProcessor {
     
    @@ -129,46 +125,13 @@ void searchForProviders(Capabilities capabilities,
         }
     
         @BuildStep
    -    void setUpDenyAllJaxRs(
    -            CombinedIndexBuildItem index,
    -            JaxRsSecurityConfig securityConfig,
    -            Optional<ResourceScanningResultBuildItem> resteasyDeployment,
    -            BeanArchiveIndexBuildItem beanArchiveIndexBuildItem,
    -            ApplicationResultBuildItem applicationResultBuildItem,
    -            BuildProducer<AdditionalSecuredMethodsBuildItem> additionalSecuredClasses) {
    -
    -        if (resteasyDeployment.isPresent()
    -                && (securityConfig.denyJaxRs() || securityConfig.defaultRolesAllowed().isPresent())) {
    -            final List<MethodInfo> methods = new ArrayList<>();
    -            Map<DotName, String> httpAnnotationToMethod = resteasyDeployment.get().getResult().getHttpAnnotationToMethod();
    -            Set<DotName> resourceClasses = resteasyDeployment.get().getResult().getScannedResourcePaths().keySet();
    -
    -            for (DotName className : resourceClasses) {
    -                ClassInfo classInfo = index.getIndex().getClassByName(className);
    -                if (classInfo == null)
    -                    throw new IllegalStateException("Unable to find class info for " + className);
    -                if (!hasSecurityAnnotation(classInfo)) {
    -                    // collect class endpoints
    -                    Collection<MethodInfo> classEndpoints = collectClassEndpoints(classInfo, httpAnnotationToMethod,
    -                            beanArchiveIndexBuildItem.getIndex(), applicationResultBuildItem.getResult());
    -
    -                    // add endpoints
    -                    for (MethodInfo classEndpoint : classEndpoints) {
    -                        if (!hasSecurityAnnotation(classEndpoint)) {
    -                            methods.add(classEndpoint);
    -                        }
    -                    }
    -                }
    -            }
    -
    -            if (!methods.isEmpty()) {
    -                if (securityConfig.denyJaxRs()) {
    -                    additionalSecuredClasses.produce(new AdditionalSecuredMethodsBuildItem(methods));
    -                } else {
    -                    additionalSecuredClasses
    -                            .produce(new AdditionalSecuredMethodsBuildItem(methods, securityConfig.defaultRolesAllowed()));
    -                }
    -            }
    +    void setUpDenyAllJaxRs(JaxRsSecurityConfig securityConfig,
    +            BuildProducer<DefaultSecurityCheckBuildItem> defaultSecurityCheckProducer) {
    +        if (securityConfig.denyJaxRs()) {
    +            defaultSecurityCheckProducer.produce(DefaultSecurityCheckBuildItem.denyAll());
    +        } else if (securityConfig.defaultRolesAllowed().isPresent()) {
    +            defaultSecurityCheckProducer
    +                    .produce(DefaultSecurityCheckBuildItem.rolesAllowed(securityConfig.defaultRolesAllowed().get()));
             }
         }
     
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedJaxRsTest.java+14 2 modified
    @@ -18,9 +18,9 @@ public class DefaultRolesAllowedJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
                                 TestIdentityController.class,
    -                            UnsecuredSubResource.class, HelloResource.class)
    +                            UnsecuredSubResource.class, HelloResource.class, UnsecuredParentResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed=admin\n"),
                                 "application.properties"));
     
    @@ -37,6 +37,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 200, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedParent() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyDenyAllMethod() {
             String path = "/unsecured/denyAll";
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedStarJaxRsTest.java+2 2 modified
    @@ -17,8 +17,8 @@ public class DefaultRolesAllowedStarJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
    +                            TestIdentityController.class, UnsecuredParentResource.class,
                                 UnsecuredSubResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = **\n"),
                                 "application.properties"));
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DenyAllJaxRsTest.java+80 4 modified
    @@ -3,12 +3,25 @@
     import static io.restassured.RestAssured.given;
     import static io.restassured.RestAssured.when;
     
    +import java.lang.reflect.Modifier;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
     import org.hamcrest.Matchers;
    +import org.jboss.jandex.AnnotationTarget;
    +import org.jboss.jandex.AnnotationValue;
    +import org.jboss.jandex.ClassInfo;
    +import org.jboss.jandex.MethodInfo;
    +import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationsTransformer;
     import org.jboss.shrinkwrap.api.asset.StringAsset;
     import org.junit.jupiter.api.BeforeAll;
     import org.junit.jupiter.api.Test;
     import org.junit.jupiter.api.extension.RegisterExtension;
     
    +import io.quarkus.builder.BuildContext;
    +import io.quarkus.builder.BuildStep;
    +import io.quarkus.resteasy.reactive.server.spi.AnnotationsTransformerBuildItem;
     import io.quarkus.security.test.utils.TestIdentityController;
     import io.quarkus.security.test.utils.TestIdentityProvider;
     import io.quarkus.test.QuarkusUnitTest;
    @@ -21,11 +34,43 @@ public class DenyAllJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    -                            UnsecuredSubResource.class, HelloResource.class)
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
    +                            TestIdentityController.class, SpecialResource.class,
    +                            UnsecuredSubResource.class, HelloResource.class, UnsecuredParentResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.deny-unannotated-endpoints = true\n"),
    -                            "application.properties"));
    +                            "application.properties"))
    +            .addBuildChainCustomizer(builder -> {
    +                builder.addBuildStep(new BuildStep() {
    +                    @Override
    +                    public void execute(BuildContext context) {
    +                        // Here we add an AnnotationsTransformer in order to make sure that the security layer
    +                        // uses the proper set of transformers
    +                        context.produce(
    +                                new AnnotationsTransformerBuildItem(
    +                                        AnnotationsTransformer.builder().appliesTo(AnnotationTarget.Kind.METHOD)
    +                                                .transform(transformerContext -> {
    +                                                    // This transformer auto-adds @GET and @Path if missing, thus emulating Renarde
    +                                                    MethodInfo methodInfo = transformerContext.getTarget().asMethod();
    +                                                    ClassInfo declaringClass = methodInfo.declaringClass();
    +                                                    if (declaringClass.name().toString().equals(SpecialResource.class.getName())
    +                                                            && !methodInfo.isConstructor()
    +                                                            && !Modifier.isStatic(methodInfo.flags())) {
    +                                                        if (methodInfo.declaredAnnotation(GET.class.getName()) == null) {
    +                                                            // auto-add it
    +                                                            transformerContext.transform().add(GET.class).done();
    +                                                        }
    +                                                        if (methodInfo.declaredAnnotation(Path.class.getName()) == null) {
    +                                                            // auto-add it
    +                                                            transformerContext.transform().add(Path.class,
    +                                                                    AnnotationValue.createStringValue("value",
    +                                                                            methodInfo.name()))
    +                                                                    .done();
    +                                                        }
    +                                                    }
    +                                                })));
    +                    }
    +                }).produces(AnnotationsTransformerBuildItem.class).build();
    +            });
     
         @BeforeAll
         public static void setupUsers() {
    @@ -40,6 +85,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedOnParentClass() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedOnInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyUnannotatedNonBlocking() {
             String path = "/unsecured/defaultSecurityNonBlocking";
    @@ -90,6 +147,14 @@ public void testServerExceptionMapper() {
                     .body(Matchers.equalTo("unauthorizedExceptionMapper"));
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedWithAnnotationTransformer() {
    +        String path = "/special/explicit";
    +        assertStatus(path, 403, 401);
    +        path = "/special/implicit";
    +        assertStatus(path, 403, 401);
    +    }
    +
         private void assertStatus(String path, int status, int anonStatus) {
             given().auth().preemptive()
                     .basic("admin", "admin").get(path)
    @@ -105,4 +170,15 @@ private void assertStatus(String path, int status, int anonStatus) {
     
         }
     
    +    @Path("/special")
    +    public static class SpecialResource {
    +        @GET
    +        public String explicit() {
    +            return "explicit";
    +        }
    +
    +        public String implicit() {
    +            return "implicit";
    +        }
    +    }
     }
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/UnsecuredParentResource.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.reactive.server.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public class UnsecuredParentResource {
    +
    +    @Path("/defaultSecurityParent")
    +    @GET
    +    public String defaultSecurityParent() {
    +        return "defaultSecurityParent";
    +    }
    +
    +}
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/UnsecuredResourceInterface.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.reactive.server.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public interface UnsecuredResourceInterface {
    +
    +    @Path("/defaultSecurityInterface")
    +    @GET
    +    default String defaultSecurityInterface() {
    +        return "defaultSecurityInterface";
    +    }
    +
    +}
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/UnsecuredResource.java+1 1 modified
    @@ -11,7 +11,7 @@
      * @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com
      */
     @Path("/unsecured")
    -public class UnsecuredResource {
    +public class UnsecuredResource extends UnsecuredParentResource implements UnsecuredResourceInterface {
         @Path("/defaultSecurity")
         @GET
         public String defaultSecurity() {
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/EagerSecurityHandler.java+14 2 modified
    @@ -59,9 +59,14 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti
             ResteasyReactiveResourceInfo lazyMethod = requestContext.getTarget().getLazyMethod();
             MethodDescription methodDescription = lazyMethodToMethodDescription(lazyMethod);
             if (check == null) {
    -            check = Arc.container().instance(SecurityCheckStorage.class).get().getSecurityCheck(methodDescription);
    +            SecurityCheckStorage storage = Arc.container().instance(SecurityCheckStorage.class).get();
    +            check = storage.getSecurityCheck(methodDescription);
                 if (check == null) {
    -                check = NULL_SENTINEL;
    +                if (storage.getDefaultSecurityCheck() == null || isRequestAlreadyChecked(requestContext)) {
    +                    check = NULL_SENTINEL;
    +                } else {
    +                    check = storage.getDefaultSecurityCheck();
    +                }
                 }
                 this.check = check;
             }
    @@ -142,6 +147,13 @@ private void preventRepeatedSecurityChecks(ResteasyReactiveRequestContext reques
             requestContext.setProperty(STANDARD_SECURITY_CHECK_INTERCEPTOR, methodDescription);
         }
     
    +    private boolean isRequestAlreadyChecked(ResteasyReactiveRequestContext requestContext) {
    +        // when request has already been checked at least once (by another instance of this handler)
    +        // then default security checks, like denied access to all JAX-RS resources by default
    +        // shouldn't be applied; this doesn't mean security checks registered for methods shouldn't be applied
    +        return requestContext.getProperty(STANDARD_SECURITY_CHECK_INTERCEPTOR) != null;
    +    }
    +
         private InjectableInstance<CurrentIdentityAssociation> getCurrentIdentityAssociation() {
             InjectableInstance<CurrentIdentityAssociation> identityAssociation = this.currentIdentityAssociation;
             if (identityAssociation == null) {
    
  • extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java+11 0 modified
    @@ -105,6 +105,7 @@
     import io.quarkus.security.runtime.interceptor.SecurityHandler;
     import io.quarkus.security.spi.AdditionalSecuredClassesBuildItem;
     import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem;
    +import io.quarkus.security.spi.DefaultSecurityCheckBuildItem;
     import io.quarkus.security.spi.RolesAllowedConfigExpResolverBuildItem;
     import io.quarkus.security.spi.runtime.AuthorizationController;
     import io.quarkus.security.spi.runtime.DevModeDisabledAuthorizationController;
    @@ -527,6 +528,7 @@ void gatherSecurityChecks(BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
                 BuildProducer<RunTimeConfigBuilderBuildItem> configBuilderProducer,
                 List<AdditionalSecuredMethodsBuildItem> additionalSecuredMethods,
                 SecurityCheckRecorder recorder,
    +            Optional<DefaultSecurityCheckBuildItem> defaultSecurityCheckBuildItem,
                 BuildProducer<ReflectiveClassBuildItem> reflectiveClassBuildItemBuildProducer,
                 List<AdditionalSecurityCheckBuildItem> additionalSecurityChecks, SecurityBuildTimeConfig config) {
             classPredicate.produce(new ApplicationClassPredicateBuildItem(new SecurityCheckStorageAppPredicate()));
    @@ -559,6 +561,15 @@ void gatherSecurityChecks(BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
                 recorder.addMethod(builder, method.declaringClass().name().toString(), method.name(), params,
                         methodEntry.getValue());
             }
    +
    +        if (defaultSecurityCheckBuildItem.isPresent()) {
    +            var roles = defaultSecurityCheckBuildItem.get().getRolesAllowed();
    +            if (roles == null) {
    +                recorder.registerDefaultSecurityCheck(builder, recorder.denyAll());
    +            } else {
    +                recorder.registerDefaultSecurityCheck(builder, recorder.rolesAllowed(roles.toArray(new String[0])));
    +            }
    +        }
             recorder.create(builder);
     
             syntheticBeans.produce(
    
  • extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/SecurityCheckStorage.java+5 0 modified
    @@ -10,4 +10,9 @@ default SecurityCheck getSecurityCheck(Method method) {
     
         SecurityCheck getSecurityCheck(MethodDescription methodDescription);
     
    +    /**
    +     * {@link SecurityCheck} that should be applied when there is no other check applied on incoming request.
    +     */
    +    SecurityCheck getDefaultSecurityCheck();
    +
     }
    
  • extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityCheckStorageBuilder.java+13 0 modified
    @@ -9,6 +9,7 @@
     
     public class SecurityCheckStorageBuilder {
         private final Map<MethodDescription, SecurityCheck> securityChecks = new HashMap<>();
    +    private SecurityCheck defaultSecurityCheck;
     
         public void registerCheck(String className,
                 String methodName,
    @@ -17,12 +18,24 @@ public void registerCheck(String className,
             securityChecks.put(new MethodDescription(className, methodName, parameterTypes), securityCheck);
         }
     
    +    public void registerDefaultSecurityCheck(SecurityCheck defaultSecurityCheck) {
    +        if (this.defaultSecurityCheck != null) {
    +            throw new IllegalStateException("Default SecurityCheck has already been registered");
    +        }
    +        this.defaultSecurityCheck = defaultSecurityCheck;
    +    }
    +
         public SecurityCheckStorage create() {
             return new SecurityCheckStorage() {
                 @Override
                 public SecurityCheck getSecurityCheck(MethodDescription methodDescription) {
                     return securityChecks.get(methodDescription);
                 }
    +
    +            @Override
    +            public SecurityCheck getDefaultSecurityCheck() {
    +                return defaultSecurityCheck;
    +            }
             };
         }
     }
    
  • extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java+4 0 modified
    @@ -351,4 +351,8 @@ private Class<?> loadClass(String className) {
                 throw new RuntimeException("Unable to load class '" + className + "' for creating permission", e);
             }
         }
    +
    +    public void registerDefaultSecurityCheck(RuntimeValue<SecurityCheckStorageBuilder> builder, SecurityCheck securityCheck) {
    +        builder.getValue().registerDefaultSecurityCheck(securityCheck);
    +    }
     }
    
  • extensions/security/spi/src/main/java/io/quarkus/security/spi/DefaultSecurityCheckBuildItem.java+28 0 added
    @@ -0,0 +1,28 @@
    +package io.quarkus.security.spi;
    +
    +import java.util.List;
    +import java.util.Objects;
    +
    +import io.quarkus.builder.item.SimpleBuildItem;
    +
    +public final class DefaultSecurityCheckBuildItem extends SimpleBuildItem {
    +
    +    public final List<String> rolesAllowed;
    +
    +    private DefaultSecurityCheckBuildItem(List<String> rolesAllowed) {
    +        this.rolesAllowed = rolesAllowed;
    +    }
    +
    +    public static DefaultSecurityCheckBuildItem denyAll() {
    +        return new DefaultSecurityCheckBuildItem(null);
    +    }
    +
    +    public static DefaultSecurityCheckBuildItem rolesAllowed(List<String> rolesAllowed) {
    +        Objects.requireNonNull(rolesAllowed);
    +        return new DefaultSecurityCheckBuildItem(List.copyOf(rolesAllowed));
    +    }
    +
    +    public List<String> getRolesAllowed() {
    +        return rolesAllowed;
    +    }
    +}
    
  • independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java+0 19 modified
    @@ -419,25 +419,6 @@ protected List<ResourceMethod> createEndpoints(ClassInfo currentClassInfo,
             return ret;
         }
     
    -    /**
    -     * Return endpoints defined directly on classInfo.
    -     *
    -     * @param classInfo resource class
    -     * @return classInfo endpoint method info
    -     */
    -    public static Collection<MethodInfo> collectClassEndpoints(ClassInfo classInfo,
    -            Map<DotName, String> httpAnnotationToMethod, IndexView index, ApplicationScanningResult applicationScanningResult) {
    -        Collection<FoundEndpoint> endpoints = collectEndpoints(classInfo, classInfo, new HashSet<>(), new HashSet<>(), true,
    -                httpAnnotationToMethod, index, applicationScanningResult, new AnnotationStore(null));
    -        Collection<MethodInfo> ret = new HashSet<>();
    -        for (FoundEndpoint endpoint : endpoints) {
    -            if (endpoint.classInfo.equals(classInfo)) {
    -                ret.add(endpoint.methodInfo);
    -            }
    -        }
    -        return ret;
    -    }
    -
         private static List<FoundEndpoint> collectEndpoints(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo,
                 Set<String> seenMethods, Set<String> existingClassNameBindings, boolean considerApplication,
                 Map<DotName, String> httpAnnotationToMethod, IndexView index, ApplicationScanningResult applicationScanningResult,
    
c026b1cf6f2e

Fix JAX-RS default security checks for inherited / transformed endpoints

https://github.com/quarkusio/quarkusMichal VavříkJan 23, 2024via ghsa
24 files changed · +339 89
  • extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java+62 4 modified
    @@ -1,17 +1,21 @@
     package io.quarkus.resteasy.deployment;
     
     import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
    +import static io.quarkus.resteasy.deployment.RestPathAnnotationProcessor.getAllClassInterfaces;
     import static io.quarkus.resteasy.deployment.RestPathAnnotationProcessor.isRestEndpointMethod;
     import static io.quarkus.security.spi.SecurityTransformerUtils.hasSecurityAnnotation;
     
     import java.util.ArrayList;
    +import java.util.Collection;
     import java.util.List;
     import java.util.Objects;
    +import java.util.function.Predicate;
     import java.util.stream.Collectors;
     
     import org.jboss.jandex.ClassInfo;
     import org.jboss.jandex.DotName;
     import org.jboss.jandex.MethodInfo;
    +import org.jboss.logging.Logger;
     
     import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
     import io.quarkus.deployment.Capabilities;
    @@ -50,6 +54,7 @@
     public class ResteasyBuiltinsProcessor {
     
         protected static final String META_INF_RESOURCES = "META-INF/resources";
    +    private static final Logger LOG = Logger.getLogger(ResteasyBuiltinsProcessor.class);
     
         @BuildStep
         void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
    @@ -65,10 +70,42 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
                     ClassInfo classInfo = index.getIndex().getClassByName(DotName.createSimple(className));
                     if (classInfo == null)
                         throw new IllegalStateException("Unable to find class info for " + className);
    -                if (!hasSecurityAnnotation(classInfo)) {
    -                    for (MethodInfo methodInfo : classInfo.methods()) {
    -                        if (isRestEndpointMethod(index, methodInfo) && !hasSecurityAnnotation(methodInfo)) {
    -                            methods.add(methodInfo);
    +                // add unannotated class endpoints as well as parent class unannotated endpoints
    +                addAllUnannotatedEndpoints(index, classInfo, methods);
    +
    +                // interface endpoints implemented on resources are already in, now we need to resolve default interface
    +                // methods as there, CDI interceptors won't work, therefore neither will our additional secured methods
    +                Collection<ClassInfo> interfaces = getAllClassInterfaces(index, List.of(classInfo), new ArrayList<>());
    +                if (!interfaces.isEmpty()) {
    +                    final List<MethodInfo> interfaceEndpoints = new ArrayList<>();
    +                    for (ClassInfo anInterface : interfaces) {
    +                        addUnannotatedEndpoints(index, anInterface, interfaceEndpoints);
    +                    }
    +                    // look for implementors as implementors on resource classes are secured by CDI interceptors
    +                    if (!interfaceEndpoints.isEmpty()) {
    +                        interfaceBlock: for (MethodInfo interfaceEndpoint : interfaceEndpoints) {
    +                            if (interfaceEndpoint.isDefault()) {
    +                                for (MethodInfo endpoint : methods) {
    +                                    boolean nameParamsMatch = endpoint.name().equals(interfaceEndpoint.name())
    +                                            && (interfaceEndpoint.parameterTypes().equals(endpoint.parameterTypes()));
    +                                    if (nameParamsMatch) {
    +                                        // whether matched method is declared on class that implements interface endpoint
    +                                        Predicate<DotName> isEndpointInterface = interfaceEndpoint.declaringClass()
    +                                                .name()::equals;
    +                                        if (endpoint.declaringClass().interfaceNames().stream().anyMatch(isEndpointInterface)) {
    +                                            continue interfaceBlock;
    +                                        }
    +                                    }
    +                                }
    +                                String configProperty = config.denyJaxRs ? "quarkus.security.jaxrs.deny-unannotated-endpoints"
    +                                        : "quarkus.security.jaxrs.default-roles-allowed";
    +                                // this is logging only as I'm a bit worried about false positives and breaking things
    +                                // for what is very much edge case
    +                                LOG.warn("Default interface method '" + interfaceEndpoint
    +                                        + "' cannot be secured with the '" + configProperty
    +                                        + "' configuration property. Please implement this method for CDI "
    +                                        + "interceptor binding to work");
    +                            }
                             }
                         }
                     }
    @@ -85,6 +122,27 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
             }
         }
     
    +    private static void addAllUnannotatedEndpoints(CombinedIndexBuildItem index, ClassInfo classInfo,
    +            List<MethodInfo> methods) {
    +        if (classInfo == null) {
    +            return;
    +        }
    +        addUnannotatedEndpoints(index, classInfo, methods);
    +        if (classInfo.superClassType() != null && !classInfo.superClassType().name().equals(DotName.OBJECT_NAME)) {
    +            addAllUnannotatedEndpoints(index, index.getIndex().getClassByName(classInfo.superClassType().name()), methods);
    +        }
    +    }
    +
    +    private static void addUnannotatedEndpoints(CombinedIndexBuildItem index, ClassInfo classInfo, List<MethodInfo> methods) {
    +        if (!hasSecurityAnnotation(classInfo)) {
    +            for (MethodInfo methodInfo : classInfo.methods()) {
    +                if (isRestEndpointMethod(index, methodInfo) && !hasSecurityAnnotation(methodInfo)) {
    +                    methods.add(methodInfo);
    +                }
    +            }
    +        }
    +    }
    +
         /**
          * Install the JAX-RS security provider.
          */
    
  • extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java+1 1 modified
    @@ -182,7 +182,7 @@ static Optional<AnnotationInstance> searchPathAnnotationOnInterfaces(CombinedInd
          * @param resultAcc accumulator for tail-recursion
          * @return Collection of all interfaces und their parents. Never null.
          */
    -    private static Collection<ClassInfo> getAllClassInterfaces(
    +    static Collection<ClassInfo> getAllClassInterfaces(
                 CombinedIndexBuildItem index,
                 Collection<ClassInfo> classInfos,
                 List<ClassInfo> resultAcc) {
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AbstractSecurityEventTest.java+2 1 modified
    @@ -37,7 +37,8 @@ public abstract class AbstractSecurityEventTest {
     
         protected static final Class<?>[] TEST_CLASSES = {
                 RolesAllowedResource.class, TestIdentityProvider.class, TestIdentityController.class,
    -            UnsecuredResource.class, UnsecuredSubResource.class, EventObserver.class
    +            UnsecuredResource.class, UnsecuredSubResource.class, EventObserver.class, UnsecuredResourceInterface.class,
    +            UnsecuredParentResource.class
         };
     
         @Inject
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java+14 2 modified
    @@ -22,8 +22,8 @@ public class DefaultRolesAllowedJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
    +                            TestIdentityController.class, UnsecuredParentResource.class,
                                 UnsecuredSubResource.class, HelloResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = admin\n"),
                                 "application.properties"));
    @@ -41,6 +41,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 200, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedOnParentClass() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedOnInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyDenyAllMethod() {
             String path = "/unsecured/denyAll";
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedStarJaxRsTest.java+2 2 modified
    @@ -17,8 +17,8 @@ public class DefaultRolesAllowedStarJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredParentResource.class,
    +                            TestIdentityController.class, UnsecuredResourceInterface.class,
                                 UnsecuredSubResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = **\n"),
                                 "application.properties"));
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DenyAllJaxRsTest.java+14 2 modified
    @@ -26,8 +26,8 @@ public class DenyAllJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredParentResource.class,
    +                            TestIdentityController.class, UnsecuredResourceInterface.class,
                                 UnsecuredSubResource.class, HelloResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.deny-unannotated-endpoints = true\n"),
                                 "application.properties"));
    @@ -58,6 +58,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedOnParentClass() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedOnInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyDenyAllMethod() {
             String path = "/unsecured/denyAll";
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredParentResource.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public class UnsecuredParentResource {
    +
    +    @Path("/defaultSecurityParent")
    +    @GET
    +    public String defaultSecurityParent() {
    +        return "defaultSecurityParent";
    +    }
    +
    +}
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResourceInterface.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public interface UnsecuredResourceInterface {
    +
    +    @Path("/defaultSecurityInterface")
    +    @GET
    +    default String defaultSecurityInterface() {
    +        return "defaultSecurityInterface";
    +    }
    +
    +}
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResource.java+6 1 modified
    @@ -12,13 +12,18 @@
      * @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com
      */
     @Path("/unsecured")
    -public class UnsecuredResource {
    +public class UnsecuredResource extends UnsecuredParentResource implements UnsecuredResourceInterface {
         @Path("/defaultSecurity")
         @GET
         public String defaultSecurity() {
             return "defaultSecurity";
         }
     
    +    @Override
    +    public String defaultSecurityInterface() {
    +        return UnsecuredResourceInterface.super.defaultSecurityInterface();
    +    }
    +
         @Path("/permitAllPathParam/{index}")
         @GET
         @PermitAll
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java+8 45 modified
    @@ -1,13 +1,9 @@
     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;
    @@ -76,7 +72,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.AdditionalSecuredMethodsBuildItem;
    +import io.quarkus.security.spi.DefaultSecurityCheckBuildItem;
     
     public class ResteasyReactiveCommonProcessor {
     
    @@ -134,46 +130,13 @@ void searchForProviders(Capabilities capabilities,
         }
     
         @BuildStep
    -    void setUpDenyAllJaxRs(
    -            CombinedIndexBuildItem index,
    -            JaxRsSecurityConfig securityConfig,
    -            Optional<ResourceScanningResultBuildItem> resteasyDeployment,
    -            BeanArchiveIndexBuildItem beanArchiveIndexBuildItem,
    -            ApplicationResultBuildItem applicationResultBuildItem,
    -            BuildProducer<AdditionalSecuredMethodsBuildItem> additionalSecuredClasses) {
    -
    -        if (resteasyDeployment.isPresent()
    -                && (securityConfig.denyJaxRs() || securityConfig.defaultRolesAllowed().isPresent())) {
    -            final List<MethodInfo> methods = new ArrayList<>();
    -            Map<DotName, String> httpAnnotationToMethod = resteasyDeployment.get().getResult().getHttpAnnotationToMethod();
    -            Set<DotName> resourceClasses = resteasyDeployment.get().getResult().getScannedResourcePaths().keySet();
    -
    -            for (DotName className : resourceClasses) {
    -                ClassInfo classInfo = index.getIndex().getClassByName(className);
    -                if (classInfo == null)
    -                    throw new IllegalStateException("Unable to find class info for " + className);
    -                if (!hasSecurityAnnotation(classInfo)) {
    -                    // collect class endpoints
    -                    Collection<MethodInfo> classEndpoints = collectClassEndpoints(classInfo, httpAnnotationToMethod,
    -                            beanArchiveIndexBuildItem.getIndex(), applicationResultBuildItem.getResult());
    -
    -                    // add endpoints
    -                    for (MethodInfo classEndpoint : classEndpoints) {
    -                        if (!hasSecurityAnnotation(classEndpoint)) {
    -                            methods.add(classEndpoint);
    -                        }
    -                    }
    -                }
    -            }
    -
    -            if (!methods.isEmpty()) {
    -                if (securityConfig.denyJaxRs()) {
    -                    additionalSecuredClasses.produce(new AdditionalSecuredMethodsBuildItem(methods));
    -                } else {
    -                    additionalSecuredClasses
    -                            .produce(new AdditionalSecuredMethodsBuildItem(methods, securityConfig.defaultRolesAllowed()));
    -                }
    -            }
    +    void setUpDenyAllJaxRs(JaxRsSecurityConfig securityConfig,
    +            BuildProducer<DefaultSecurityCheckBuildItem> defaultSecurityCheckProducer) {
    +        if (securityConfig.denyJaxRs()) {
    +            defaultSecurityCheckProducer.produce(DefaultSecurityCheckBuildItem.denyAll());
    +        } else if (securityConfig.defaultRolesAllowed().isPresent()) {
    +            defaultSecurityCheckProducer
    +                    .produce(DefaultSecurityCheckBuildItem.rolesAllowed(securityConfig.defaultRolesAllowed().get()));
             }
         }
     
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractSecurityEventTest.java+2 1 modified
    @@ -40,7 +40,8 @@ public abstract class AbstractSecurityEventTest {
         protected static final Class<?>[] TEST_CLASSES = {
                 RolesAllowedResource.class, RolesAllowedBlockingResource.class, TestIdentityProvider.class,
                 TestIdentityController.class, UnsecuredResource.class, UnsecuredSubResource.class, RolesAllowedService.class,
    -            RolesAllowedServiceResource.class, EventObserver.class
    +            RolesAllowedServiceResource.class, EventObserver.class, UnsecuredResourceInterface.class,
    +            UnsecuredParentResource.class
         };
     
         @Inject
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedJaxRsTest.java+14 2 modified
    @@ -18,9 +18,9 @@ public class DefaultRolesAllowedJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
                                 TestIdentityController.class,
    -                            UnsecuredSubResource.class, HelloResource.class)
    +                            UnsecuredSubResource.class, HelloResource.class, UnsecuredParentResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed=admin\n"),
                                 "application.properties"));
     
    @@ -37,6 +37,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 200, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedParent() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyDenyAllMethod() {
             String path = "/unsecured/denyAll";
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedStarJaxRsTest.java+2 2 modified
    @@ -17,8 +17,8 @@ public class DefaultRolesAllowedStarJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
    +                            TestIdentityController.class, UnsecuredParentResource.class,
                                 UnsecuredSubResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = **\n"),
                                 "application.properties"));
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DenyAllJaxRsTest.java+80 4 modified
    @@ -3,12 +3,25 @@
     import static io.restassured.RestAssured.given;
     import static io.restassured.RestAssured.when;
     
    +import java.lang.reflect.Modifier;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
     import org.hamcrest.Matchers;
    +import org.jboss.jandex.AnnotationTarget;
    +import org.jboss.jandex.AnnotationValue;
    +import org.jboss.jandex.ClassInfo;
    +import org.jboss.jandex.MethodInfo;
    +import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationsTransformer;
     import org.jboss.shrinkwrap.api.asset.StringAsset;
     import org.junit.jupiter.api.BeforeAll;
     import org.junit.jupiter.api.Test;
     import org.junit.jupiter.api.extension.RegisterExtension;
     
    +import io.quarkus.builder.BuildContext;
    +import io.quarkus.builder.BuildStep;
    +import io.quarkus.resteasy.reactive.server.spi.AnnotationsTransformerBuildItem;
     import io.quarkus.security.test.utils.TestIdentityController;
     import io.quarkus.security.test.utils.TestIdentityProvider;
     import io.quarkus.test.QuarkusUnitTest;
    @@ -21,11 +34,43 @@ public class DenyAllJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    -                            UnsecuredSubResource.class, HelloResource.class)
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
    +                            TestIdentityController.class, SpecialResource.class,
    +                            UnsecuredSubResource.class, HelloResource.class, UnsecuredParentResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.deny-unannotated-endpoints = true\n"),
    -                            "application.properties"));
    +                            "application.properties"))
    +            .addBuildChainCustomizer(builder -> {
    +                builder.addBuildStep(new BuildStep() {
    +                    @Override
    +                    public void execute(BuildContext context) {
    +                        // Here we add an AnnotationsTransformer in order to make sure that the security layer
    +                        // uses the proper set of transformers
    +                        context.produce(
    +                                new AnnotationsTransformerBuildItem(
    +                                        AnnotationsTransformer.builder().appliesTo(AnnotationTarget.Kind.METHOD)
    +                                                .transform(transformerContext -> {
    +                                                    // This transformer auto-adds @GET and @Path if missing, thus emulating Renarde
    +                                                    MethodInfo methodInfo = transformerContext.getTarget().asMethod();
    +                                                    ClassInfo declaringClass = methodInfo.declaringClass();
    +                                                    if (declaringClass.name().toString().equals(SpecialResource.class.getName())
    +                                                            && !methodInfo.isConstructor()
    +                                                            && !Modifier.isStatic(methodInfo.flags())) {
    +                                                        if (methodInfo.declaredAnnotation(GET.class.getName()) == null) {
    +                                                            // auto-add it
    +                                                            transformerContext.transform().add(GET.class).done();
    +                                                        }
    +                                                        if (methodInfo.declaredAnnotation(Path.class.getName()) == null) {
    +                                                            // auto-add it
    +                                                            transformerContext.transform().add(Path.class,
    +                                                                    AnnotationValue.createStringValue("value",
    +                                                                            methodInfo.name()))
    +                                                                    .done();
    +                                                        }
    +                                                    }
    +                                                })));
    +                    }
    +                }).produces(AnnotationsTransformerBuildItem.class).build();
    +            });
     
         @BeforeAll
         public static void setupUsers() {
    @@ -40,6 +85,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedOnParentClass() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedOnInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyUnannotatedNonBlocking() {
             String path = "/unsecured/defaultSecurityNonBlocking";
    @@ -90,6 +147,14 @@ public void testServerExceptionMapper() {
                     .body(Matchers.equalTo("unauthorizedExceptionMapper"));
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedWithAnnotationTransformer() {
    +        String path = "/special/explicit";
    +        assertStatus(path, 403, 401);
    +        path = "/special/implicit";
    +        assertStatus(path, 403, 401);
    +    }
    +
         private void assertStatus(String path, int status, int anonStatus) {
             given().auth().preemptive()
                     .basic("admin", "admin").get(path)
    @@ -105,4 +170,15 @@ private void assertStatus(String path, int status, int anonStatus) {
     
         }
     
    +    @Path("/special")
    +    public static class SpecialResource {
    +        @GET
    +        public String explicit() {
    +            return "explicit";
    +        }
    +
    +        public String implicit() {
    +            return "implicit";
    +        }
    +    }
     }
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/UnsecuredParentResource.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.reactive.server.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public class UnsecuredParentResource {
    +
    +    @Path("/defaultSecurityParent")
    +    @GET
    +    public String defaultSecurityParent() {
    +        return "defaultSecurityParent";
    +    }
    +
    +}
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/UnsecuredResourceInterface.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.reactive.server.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public interface UnsecuredResourceInterface {
    +
    +    @Path("/defaultSecurityInterface")
    +    @GET
    +    default String defaultSecurityInterface() {
    +        return "defaultSecurityInterface";
    +    }
    +
    +}
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/UnsecuredResource.java+1 1 modified
    @@ -12,7 +12,7 @@
      * @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com
      */
     @Path("/unsecured")
    -public class UnsecuredResource {
    +public class UnsecuredResource extends UnsecuredParentResource implements UnsecuredResourceInterface {
         @Path("/defaultSecurity")
         @GET
         public String defaultSecurity() {
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/EagerSecurityHandler.java+14 2 modified
    @@ -66,9 +66,14 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti
             ResteasyReactiveResourceInfo lazyMethod = requestContext.getTarget().getLazyMethod();
             MethodDescription methodDescription = lazyMethodToMethodDescription(lazyMethod);
             if (check == null) {
    -            check = Arc.container().instance(SecurityCheckStorage.class).get().getSecurityCheck(methodDescription);
    +            SecurityCheckStorage storage = Arc.container().instance(SecurityCheckStorage.class).get();
    +            check = storage.getSecurityCheck(methodDescription);
                 if (check == null) {
    -                check = NULL_SENTINEL;
    +                if (storage.getDefaultSecurityCheck() == null || isRequestAlreadyChecked(requestContext)) {
    +                    check = NULL_SENTINEL;
    +                } else {
    +                    check = storage.getDefaultSecurityCheck();
    +                }
                 }
                 this.check = check;
             }
    @@ -193,6 +198,13 @@ private void preventRepeatedSecurityChecks(ResteasyReactiveRequestContext reques
             requestContext.setProperty(STANDARD_SECURITY_CHECK_INTERCEPTOR, methodDescription);
         }
     
    +    private boolean isRequestAlreadyChecked(ResteasyReactiveRequestContext requestContext) {
    +        // when request has already been checked at least once (by another instance of this handler)
    +        // then default security checks, like denied access to all JAX-RS resources by default
    +        // shouldn't be applied; this doesn't mean security checks registered for methods shouldn't be applied
    +        return requestContext.getProperty(STANDARD_SECURITY_CHECK_INTERCEPTOR) != null;
    +    }
    +
         private InjectableInstance<CurrentIdentityAssociation> getCurrentIdentityAssociation() {
             InjectableInstance<CurrentIdentityAssociation> identityAssociation = this.currentIdentityAssociation;
             if (identityAssociation == null) {
    
  • extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java+11 0 modified
    @@ -105,6 +105,7 @@
     import io.quarkus.security.runtime.interceptor.SecurityHandler;
     import io.quarkus.security.spi.AdditionalSecuredClassesBuildItem;
     import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem;
    +import io.quarkus.security.spi.DefaultSecurityCheckBuildItem;
     import io.quarkus.security.spi.RolesAllowedConfigExpResolverBuildItem;
     import io.quarkus.security.spi.runtime.AuthorizationController;
     import io.quarkus.security.spi.runtime.DevModeDisabledAuthorizationController;
    @@ -527,6 +528,7 @@ void gatherSecurityChecks(BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
                 BuildProducer<RunTimeConfigBuilderBuildItem> configBuilderProducer,
                 List<AdditionalSecuredMethodsBuildItem> additionalSecuredMethods,
                 SecurityCheckRecorder recorder,
    +            Optional<DefaultSecurityCheckBuildItem> defaultSecurityCheckBuildItem,
                 BuildProducer<ReflectiveClassBuildItem> reflectiveClassBuildItemBuildProducer,
                 List<AdditionalSecurityCheckBuildItem> additionalSecurityChecks, SecurityBuildTimeConfig config) {
             classPredicate.produce(new ApplicationClassPredicateBuildItem(new SecurityCheckStorageAppPredicate()));
    @@ -559,6 +561,15 @@ void gatherSecurityChecks(BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
                 recorder.addMethod(builder, method.declaringClass().name().toString(), method.name(), params,
                         methodEntry.getValue());
             }
    +
    +        if (defaultSecurityCheckBuildItem.isPresent()) {
    +            var roles = defaultSecurityCheckBuildItem.get().getRolesAllowed();
    +            if (roles == null) {
    +                recorder.registerDefaultSecurityCheck(builder, recorder.denyAll());
    +            } else {
    +                recorder.registerDefaultSecurityCheck(builder, recorder.rolesAllowed(roles.toArray(new String[0])));
    +            }
    +        }
             recorder.create(builder);
     
             syntheticBeans.produce(
    
  • extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/SecurityCheckStorage.java+5 0 modified
    @@ -10,4 +10,9 @@ default SecurityCheck getSecurityCheck(Method method) {
     
         SecurityCheck getSecurityCheck(MethodDescription methodDescription);
     
    +    /**
    +     * {@link SecurityCheck} that should be applied when there is no other check applied on incoming request.
    +     */
    +    SecurityCheck getDefaultSecurityCheck();
    +
     }
    
  • extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityCheckStorageBuilder.java+13 0 modified
    @@ -9,6 +9,7 @@
     
     public class SecurityCheckStorageBuilder {
         private final Map<MethodDescription, SecurityCheck> securityChecks = new HashMap<>();
    +    private SecurityCheck defaultSecurityCheck;
     
         public void registerCheck(String className,
                 String methodName,
    @@ -17,12 +18,24 @@ public void registerCheck(String className,
             securityChecks.put(new MethodDescription(className, methodName, parameterTypes), securityCheck);
         }
     
    +    public void registerDefaultSecurityCheck(SecurityCheck defaultSecurityCheck) {
    +        if (this.defaultSecurityCheck != null) {
    +            throw new IllegalStateException("Default SecurityCheck has already been registered");
    +        }
    +        this.defaultSecurityCheck = defaultSecurityCheck;
    +    }
    +
         public SecurityCheckStorage create() {
             return new SecurityCheckStorage() {
                 @Override
                 public SecurityCheck getSecurityCheck(MethodDescription methodDescription) {
                     return securityChecks.get(methodDescription);
                 }
    +
    +            @Override
    +            public SecurityCheck getDefaultSecurityCheck() {
    +                return defaultSecurityCheck;
    +            }
             };
         }
     }
    
  • extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java+4 0 modified
    @@ -351,4 +351,8 @@ private Class<?> loadClass(String className) {
                 throw new RuntimeException("Unable to load class '" + className + "' for creating permission", e);
             }
         }
    +
    +    public void registerDefaultSecurityCheck(RuntimeValue<SecurityCheckStorageBuilder> builder, SecurityCheck securityCheck) {
    +        builder.getValue().registerDefaultSecurityCheck(securityCheck);
    +    }
     }
    
  • extensions/security/spi/src/main/java/io/quarkus/security/spi/DefaultSecurityCheckBuildItem.java+28 0 added
    @@ -0,0 +1,28 @@
    +package io.quarkus.security.spi;
    +
    +import java.util.List;
    +import java.util.Objects;
    +
    +import io.quarkus.builder.item.SimpleBuildItem;
    +
    +public final class DefaultSecurityCheckBuildItem extends SimpleBuildItem {
    +
    +    public final List<String> rolesAllowed;
    +
    +    private DefaultSecurityCheckBuildItem(List<String> rolesAllowed) {
    +        this.rolesAllowed = rolesAllowed;
    +    }
    +
    +    public static DefaultSecurityCheckBuildItem denyAll() {
    +        return new DefaultSecurityCheckBuildItem(null);
    +    }
    +
    +    public static DefaultSecurityCheckBuildItem rolesAllowed(List<String> rolesAllowed) {
    +        Objects.requireNonNull(rolesAllowed);
    +        return new DefaultSecurityCheckBuildItem(List.copyOf(rolesAllowed));
    +    }
    +
    +    public List<String> getRolesAllowed() {
    +        return rolesAllowed;
    +    }
    +}
    
  • independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java+0 19 modified
    @@ -422,25 +422,6 @@ protected List<ResourceMethod> createEndpoints(ClassInfo currentClassInfo,
             return ret;
         }
     
    -    /**
    -     * Return endpoints defined directly on classInfo.
    -     *
    -     * @param classInfo resource class
    -     * @return classInfo endpoint method info
    -     */
    -    public static Collection<MethodInfo> collectClassEndpoints(ClassInfo classInfo,
    -            Map<DotName, String> httpAnnotationToMethod, IndexView index, ApplicationScanningResult applicationScanningResult) {
    -        Collection<FoundEndpoint> endpoints = collectEndpoints(classInfo, classInfo, new HashSet<>(), new HashSet<>(), true,
    -                httpAnnotationToMethod, index, applicationScanningResult, new AnnotationStore(null));
    -        Collection<MethodInfo> ret = new HashSet<>();
    -        for (FoundEndpoint endpoint : endpoints) {
    -            if (endpoint.classInfo.equals(classInfo)) {
    -                ret.add(endpoint.methodInfo);
    -            }
    -        }
    -        return ret;
    -    }
    -
         private static List<FoundEndpoint> collectEndpoints(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo,
                 Set<String> seenMethods, Set<String> existingClassNameBindings, boolean considerApplication,
                 Map<DotName, String> httpAnnotationToMethod, IndexView index, ApplicationScanningResult applicationScanningResult,
    
b7dd69a3012a

Fix JAX-RS default security checks for inherited / transformed endpoints

https://github.com/quarkusio/quarkusMichal VavříkNov 21, 2023via ghsa
22 files changed · +335 87
  • extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java+62 4 modified
    @@ -1,17 +1,21 @@
     package io.quarkus.resteasy.deployment;
     
     import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
    +import static io.quarkus.resteasy.deployment.RestPathAnnotationProcessor.getAllClassInterfaces;
     import static io.quarkus.resteasy.deployment.RestPathAnnotationProcessor.isRestEndpointMethod;
     import static io.quarkus.security.spi.SecurityTransformerUtils.hasSecurityAnnotation;
     
     import java.util.ArrayList;
    +import java.util.Collection;
     import java.util.List;
     import java.util.Objects;
    +import java.util.function.Predicate;
     import java.util.stream.Collectors;
     
     import org.jboss.jandex.ClassInfo;
     import org.jboss.jandex.DotName;
     import org.jboss.jandex.MethodInfo;
    +import org.jboss.logging.Logger;
     
     import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
     import io.quarkus.deployment.Capabilities;
    @@ -48,6 +52,7 @@
     public class ResteasyBuiltinsProcessor {
     
         protected static final String META_INF_RESOURCES = "META-INF/resources";
    +    private static final Logger LOG = Logger.getLogger(ResteasyBuiltinsProcessor.class);
     
         @BuildStep
         void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
    @@ -63,10 +68,42 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
                     ClassInfo classInfo = index.getIndex().getClassByName(DotName.createSimple(className));
                     if (classInfo == null)
                         throw new IllegalStateException("Unable to find class info for " + className);
    -                if (!hasSecurityAnnotation(classInfo)) {
    -                    for (MethodInfo methodInfo : classInfo.methods()) {
    -                        if (isRestEndpointMethod(index, methodInfo) && !hasSecurityAnnotation(methodInfo)) {
    -                            methods.add(methodInfo);
    +                // add unannotated class endpoints as well as parent class unannotated endpoints
    +                addAllUnannotatedEndpoints(index, classInfo, methods);
    +
    +                // interface endpoints implemented on resources are already in, now we need to resolve default interface
    +                // methods as there, CDI interceptors won't work, therefore neither will our additional secured methods
    +                Collection<ClassInfo> interfaces = getAllClassInterfaces(index, List.of(classInfo), new ArrayList<>());
    +                if (!interfaces.isEmpty()) {
    +                    final List<MethodInfo> interfaceEndpoints = new ArrayList<>();
    +                    for (ClassInfo anInterface : interfaces) {
    +                        addUnannotatedEndpoints(index, anInterface, interfaceEndpoints);
    +                    }
    +                    // look for implementors as implementors on resource classes are secured by CDI interceptors
    +                    if (!interfaceEndpoints.isEmpty()) {
    +                        interfaceBlock: for (MethodInfo interfaceEndpoint : interfaceEndpoints) {
    +                            if (interfaceEndpoint.isDefault()) {
    +                                for (MethodInfo endpoint : methods) {
    +                                    boolean nameParamsMatch = endpoint.name().equals(interfaceEndpoint.name())
    +                                            && (interfaceEndpoint.parameterTypes().equals(endpoint.parameterTypes()));
    +                                    if (nameParamsMatch) {
    +                                        // whether matched method is declared on class that implements interface endpoint
    +                                        Predicate<DotName> isEndpointInterface = interfaceEndpoint.declaringClass()
    +                                                .name()::equals;
    +                                        if (endpoint.declaringClass().interfaceNames().stream().anyMatch(isEndpointInterface)) {
    +                                            continue interfaceBlock;
    +                                        }
    +                                    }
    +                                }
    +                                String configProperty = config.denyJaxRs ? "quarkus.security.jaxrs.deny-unannotated-endpoints"
    +                                        : "quarkus.security.jaxrs.default-roles-allowed";
    +                                // this is logging only as I'm a bit worried about false positives and breaking things
    +                                // for what is very much edge case
    +                                LOG.warn("Default interface method '" + interfaceEndpoint
    +                                        + "' cannot be secured with the '" + configProperty
    +                                        + "' configuration property. Please implement this method for CDI "
    +                                        + "interceptor binding to work");
    +                            }
                             }
                         }
                     }
    @@ -83,6 +120,27 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
             }
         }
     
    +    private static void addAllUnannotatedEndpoints(CombinedIndexBuildItem index, ClassInfo classInfo,
    +            List<MethodInfo> methods) {
    +        if (classInfo == null) {
    +            return;
    +        }
    +        addUnannotatedEndpoints(index, classInfo, methods);
    +        if (classInfo.superClassType() != null && !classInfo.superClassType().name().equals(DotName.OBJECT_NAME)) {
    +            addAllUnannotatedEndpoints(index, index.getIndex().getClassByName(classInfo.superClassType().name()), methods);
    +        }
    +    }
    +
    +    private static void addUnannotatedEndpoints(CombinedIndexBuildItem index, ClassInfo classInfo, List<MethodInfo> methods) {
    +        if (!hasSecurityAnnotation(classInfo)) {
    +            for (MethodInfo methodInfo : classInfo.methods()) {
    +                if (isRestEndpointMethod(index, methodInfo) && !hasSecurityAnnotation(methodInfo)) {
    +                    methods.add(methodInfo);
    +                }
    +            }
    +        }
    +    }
    +
         /**
          * Install the JAX-RS security provider.
          */
    
  • extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java+1 1 modified
    @@ -182,7 +182,7 @@ static Optional<AnnotationInstance> searchPathAnnotationOnInterfaces(CombinedInd
          * @param resultAcc accumulator for tail-recursion
          * @return Collection of all interfaces und their parents. Never null.
          */
    -    private static Collection<ClassInfo> getAllClassInterfaces(
    +    static Collection<ClassInfo> getAllClassInterfaces(
                 CombinedIndexBuildItem index,
                 Collection<ClassInfo> classInfos,
                 List<ClassInfo> resultAcc) {
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java+14 2 modified
    @@ -22,8 +22,8 @@ public class DefaultRolesAllowedJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
    +                            TestIdentityController.class, UnsecuredParentResource.class,
                                 UnsecuredSubResource.class, HelloResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = admin\n"),
                                 "application.properties"));
    @@ -41,6 +41,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 200, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedOnParentClass() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedOnInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyDenyAllMethod() {
             String path = "/unsecured/denyAll";
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedStarJaxRsTest.java+2 2 modified
    @@ -17,8 +17,8 @@ public class DefaultRolesAllowedStarJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredParentResource.class,
    +                            TestIdentityController.class, UnsecuredResourceInterface.class,
                                 UnsecuredSubResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = **\n"),
                                 "application.properties"));
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DenyAllJaxRsTest.java+14 2 modified
    @@ -26,8 +26,8 @@ public class DenyAllJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredParentResource.class,
    +                            TestIdentityController.class, UnsecuredResourceInterface.class,
                                 UnsecuredSubResource.class, HelloResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.deny-unannotated-endpoints = true\n"),
                                 "application.properties"));
    @@ -58,6 +58,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedOnParentClass() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedOnInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyDenyAllMethod() {
             String path = "/unsecured/denyAll";
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredParentResource.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public class UnsecuredParentResource {
    +
    +    @Path("/defaultSecurityParent")
    +    @GET
    +    public String defaultSecurityParent() {
    +        return "defaultSecurityParent";
    +    }
    +
    +}
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResourceInterface.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public interface UnsecuredResourceInterface {
    +
    +    @Path("/defaultSecurityInterface")
    +    @GET
    +    default String defaultSecurityInterface() {
    +        return "defaultSecurityInterface";
    +    }
    +
    +}
    
  • extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResource.java+6 1 modified
    @@ -10,13 +10,18 @@
      * @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com
      */
     @Path("/unsecured")
    -public class UnsecuredResource {
    +public class UnsecuredResource extends UnsecuredParentResource implements UnsecuredResourceInterface {
         @Path("/defaultSecurity")
         @GET
         public String defaultSecurity() {
             return "defaultSecurity";
         }
     
    +    @Override
    +    public String defaultSecurityInterface() {
    +        return UnsecuredResourceInterface.super.defaultSecurityInterface();
    +    }
    +
         @Path("/permitAllPathParam/{index}")
         @GET
         @PermitAll
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java+8 45 modified
    @@ -1,13 +1,9 @@
     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;
    @@ -68,7 +64,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.AdditionalSecuredMethodsBuildItem;
    +import io.quarkus.security.spi.DefaultSecurityCheckBuildItem;
     
     public class ResteasyReactiveCommonProcessor {
     
    @@ -91,46 +87,13 @@ void searchForProviders(Capabilities capabilities,
         }
     
         @BuildStep
    -    void setUpDenyAllJaxRs(
    -            CombinedIndexBuildItem index,
    -            JaxRsSecurityConfig securityConfig,
    -            Optional<ResourceScanningResultBuildItem> resteasyDeployment,
    -            BeanArchiveIndexBuildItem beanArchiveIndexBuildItem,
    -            ApplicationResultBuildItem applicationResultBuildItem,
    -            BuildProducer<AdditionalSecuredMethodsBuildItem> additionalSecuredClasses) {
    -
    -        if (resteasyDeployment.isPresent()
    -                && (securityConfig.denyJaxRs() || securityConfig.defaultRolesAllowed().isPresent())) {
    -            final List<MethodInfo> methods = new ArrayList<>();
    -            Map<DotName, String> httpAnnotationToMethod = resteasyDeployment.get().getResult().getHttpAnnotationToMethod();
    -            Set<DotName> resourceClasses = resteasyDeployment.get().getResult().getScannedResourcePaths().keySet();
    -
    -            for (DotName className : resourceClasses) {
    -                ClassInfo classInfo = index.getIndex().getClassByName(className);
    -                if (classInfo == null)
    -                    throw new IllegalStateException("Unable to find class info for " + className);
    -                if (!hasSecurityAnnotation(classInfo)) {
    -                    // collect class endpoints
    -                    Collection<MethodInfo> classEndpoints = collectClassEndpoints(classInfo, httpAnnotationToMethod,
    -                            beanArchiveIndexBuildItem.getIndex(), applicationResultBuildItem.getResult());
    -
    -                    // add endpoints
    -                    for (MethodInfo classEndpoint : classEndpoints) {
    -                        if (!hasSecurityAnnotation(classEndpoint)) {
    -                            methods.add(classEndpoint);
    -                        }
    -                    }
    -                }
    -            }
    -
    -            if (!methods.isEmpty()) {
    -                if (securityConfig.denyJaxRs()) {
    -                    additionalSecuredClasses.produce(new AdditionalSecuredMethodsBuildItem(methods));
    -                } else {
    -                    additionalSecuredClasses
    -                            .produce(new AdditionalSecuredMethodsBuildItem(methods, securityConfig.defaultRolesAllowed()));
    -                }
    -            }
    +    void setUpDenyAllJaxRs(JaxRsSecurityConfig securityConfig,
    +            BuildProducer<DefaultSecurityCheckBuildItem> defaultSecurityCheckProducer) {
    +        if (securityConfig.denyJaxRs()) {
    +            defaultSecurityCheckProducer.produce(DefaultSecurityCheckBuildItem.denyAll());
    +        } else if (securityConfig.defaultRolesAllowed().isPresent()) {
    +            defaultSecurityCheckProducer
    +                    .produce(DefaultSecurityCheckBuildItem.rolesAllowed(securityConfig.defaultRolesAllowed().get()));
             }
         }
     
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedJaxRsTest.java+14 2 modified
    @@ -18,9 +18,9 @@ public class DefaultRolesAllowedJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
                                 TestIdentityController.class,
    -                            UnsecuredSubResource.class, HelloResource.class)
    +                            UnsecuredSubResource.class, HelloResource.class, UnsecuredParentResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed=admin\n"),
                                 "application.properties"));
     
    @@ -37,6 +37,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 200, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedParent() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 200, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyDenyAllMethod() {
             String path = "/unsecured/denyAll";
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DefaultRolesAllowedStarJaxRsTest.java+2 2 modified
    @@ -17,8 +17,8 @@ public class DefaultRolesAllowedStarJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
    +                            TestIdentityController.class, UnsecuredParentResource.class,
                                 UnsecuredSubResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = **\n"),
                                 "application.properties"));
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/DenyAllJaxRsTest.java+80 4 modified
    @@ -3,12 +3,25 @@
     import static io.restassured.RestAssured.given;
     import static io.restassured.RestAssured.when;
     
    +import java.lang.reflect.Modifier;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
     import org.hamcrest.Matchers;
    +import org.jboss.jandex.AnnotationTarget;
    +import org.jboss.jandex.AnnotationValue;
    +import org.jboss.jandex.ClassInfo;
    +import org.jboss.jandex.MethodInfo;
    +import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationsTransformer;
     import org.jboss.shrinkwrap.api.asset.StringAsset;
     import org.junit.jupiter.api.BeforeAll;
     import org.junit.jupiter.api.Test;
     import org.junit.jupiter.api.extension.RegisterExtension;
     
    +import io.quarkus.builder.BuildContext;
    +import io.quarkus.builder.BuildStep;
    +import io.quarkus.resteasy.reactive.server.spi.AnnotationsTransformerBuildItem;
     import io.quarkus.security.test.utils.TestIdentityController;
     import io.quarkus.security.test.utils.TestIdentityProvider;
     import io.quarkus.test.QuarkusUnitTest;
    @@ -21,11 +34,43 @@ public class DenyAllJaxRsTest {
         static QuarkusUnitTest runner = new QuarkusUnitTest()
                 .withApplicationRoot((jar) -> jar
                         .addClasses(PermitAllResource.class, UnsecuredResource.class,
    -                            TestIdentityProvider.class,
    -                            TestIdentityController.class,
    -                            UnsecuredSubResource.class, HelloResource.class)
    +                            TestIdentityProvider.class, UnsecuredResourceInterface.class,
    +                            TestIdentityController.class, SpecialResource.class,
    +                            UnsecuredSubResource.class, HelloResource.class, UnsecuredParentResource.class)
                         .addAsResource(new StringAsset("quarkus.security.jaxrs.deny-unannotated-endpoints = true\n"),
    -                            "application.properties"));
    +                            "application.properties"))
    +            .addBuildChainCustomizer(builder -> {
    +                builder.addBuildStep(new BuildStep() {
    +                    @Override
    +                    public void execute(BuildContext context) {
    +                        // Here we add an AnnotationsTransformer in order to make sure that the security layer
    +                        // uses the proper set of transformers
    +                        context.produce(
    +                                new AnnotationsTransformerBuildItem(
    +                                        AnnotationsTransformer.builder().appliesTo(AnnotationTarget.Kind.METHOD)
    +                                                .transform(transformerContext -> {
    +                                                    // This transformer auto-adds @GET and @Path if missing, thus emulating Renarde
    +                                                    MethodInfo methodInfo = transformerContext.getTarget().asMethod();
    +                                                    ClassInfo declaringClass = methodInfo.declaringClass();
    +                                                    if (declaringClass.name().toString().equals(SpecialResource.class.getName())
    +                                                            && !methodInfo.isConstructor()
    +                                                            && !Modifier.isStatic(methodInfo.flags())) {
    +                                                        if (methodInfo.declaredAnnotation(GET.class.getName()) == null) {
    +                                                            // auto-add it
    +                                                            transformerContext.transform().add(GET.class).done();
    +                                                        }
    +                                                        if (methodInfo.declaredAnnotation(Path.class.getName()) == null) {
    +                                                            // auto-add it
    +                                                            transformerContext.transform().add(Path.class,
    +                                                                    AnnotationValue.createStringValue("value",
    +                                                                            methodInfo.name()))
    +                                                                    .done();
    +                                                        }
    +                                                    }
    +                                                })));
    +                    }
    +                }).produces(AnnotationsTransformerBuildItem.class).build();
    +            });
     
         @BeforeAll
         public static void setupUsers() {
    @@ -40,6 +85,18 @@ public void shouldDenyUnannotated() {
             assertStatus(path, 403, 401);
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedOnParentClass() {
    +        String path = "/unsecured/defaultSecurityParent";
    +        assertStatus(path, 403, 401);
    +    }
    +
    +    @Test
    +    public void shouldDenyUnannotatedOnInterface() {
    +        String path = "/unsecured/defaultSecurityInterface";
    +        assertStatus(path, 403, 401);
    +    }
    +
         @Test
         public void shouldDenyUnannotatedNonBlocking() {
             String path = "/unsecured/defaultSecurityNonBlocking";
    @@ -90,6 +147,14 @@ public void testServerExceptionMapper() {
                     .body(Matchers.equalTo("unauthorizedExceptionMapper"));
         }
     
    +    @Test
    +    public void shouldDenyUnannotatedWithAnnotationTransformer() {
    +        String path = "/special/explicit";
    +        assertStatus(path, 403, 401);
    +        path = "/special/implicit";
    +        assertStatus(path, 403, 401);
    +    }
    +
         private void assertStatus(String path, int status, int anonStatus) {
             given().auth().preemptive()
                     .basic("admin", "admin").get(path)
    @@ -105,4 +170,15 @@ private void assertStatus(String path, int status, int anonStatus) {
     
         }
     
    +    @Path("/special")
    +    public static class SpecialResource {
    +        @GET
    +        public String explicit() {
    +            return "explicit";
    +        }
    +
    +        public String implicit() {
    +            return "implicit";
    +        }
    +    }
     }
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/UnsecuredParentResource.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.reactive.server.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public class UnsecuredParentResource {
    +
    +    @Path("/defaultSecurityParent")
    +    @GET
    +    public String defaultSecurityParent() {
    +        return "defaultSecurityParent";
    +    }
    +
    +}
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/UnsecuredResourceInterface.java+14 0 added
    @@ -0,0 +1,14 @@
    +package io.quarkus.resteasy.reactive.server.test.security;
    +
    +import jakarta.ws.rs.GET;
    +import jakarta.ws.rs.Path;
    +
    +public interface UnsecuredResourceInterface {
    +
    +    @Path("/defaultSecurityInterface")
    +    @GET
    +    default String defaultSecurityInterface() {
    +        return "defaultSecurityInterface";
    +    }
    +
    +}
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/UnsecuredResource.java+1 1 modified
    @@ -11,7 +11,7 @@
      * @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com
      */
     @Path("/unsecured")
    -public class UnsecuredResource {
    +public class UnsecuredResource extends UnsecuredParentResource implements UnsecuredResourceInterface {
         @Path("/defaultSecurity")
         @GET
         public String defaultSecurity() {
    
  • extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/EagerSecurityHandler.java+14 2 modified
    @@ -60,9 +60,14 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti
             MethodDescription methodDescription = new MethodDescription(lazyMethod.getResourceClass().getName(),
                     lazyMethod.getName(), MethodDescription.typesAsStrings(lazyMethod.getParameterTypes()));
             if (check == null) {
    -            check = Arc.container().instance(SecurityCheckStorage.class).get().getSecurityCheck(methodDescription);
    +            SecurityCheckStorage storage = Arc.container().instance(SecurityCheckStorage.class).get();
    +            check = storage.getSecurityCheck(methodDescription);
                 if (check == null) {
    -                check = NULL_SENTINEL;
    +                if (storage.getDefaultSecurityCheck() == null || isRequestAlreadyChecked(requestContext)) {
    +                    check = NULL_SENTINEL;
    +                } else {
    +                    check = storage.getDefaultSecurityCheck();
    +                }
                 }
                 this.check = check;
             }
    @@ -138,6 +143,13 @@ private void preventRepeatedSecurityChecks(ResteasyReactiveRequestContext reques
             requestContext.setProperty(STANDARD_SECURITY_CHECK_INTERCEPTOR, methodDescription);
         }
     
    +    private boolean isRequestAlreadyChecked(ResteasyReactiveRequestContext requestContext) {
    +        // when request has already been checked at least once (by another instance of this handler)
    +        // then default security checks, like denied access to all JAX-RS resources by default
    +        // shouldn't be applied; this doesn't mean security checks registered for methods shouldn't be applied
    +        return requestContext.getProperty(STANDARD_SECURITY_CHECK_INTERCEPTOR) != null;
    +    }
    +
         private InjectableInstance<CurrentIdentityAssociation> getCurrentIdentityAssociation() {
             InjectableInstance<CurrentIdentityAssociation> identityAssociation = this.currentIdentityAssociation;
             if (identityAssociation == null) {
    
  • extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java+11 0 modified
    @@ -92,6 +92,7 @@
     import io.quarkus.security.runtime.interceptor.SecurityHandler;
     import io.quarkus.security.spi.AdditionalSecuredClassesBuildItem;
     import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem;
    +import io.quarkus.security.spi.DefaultSecurityCheckBuildItem;
     import io.quarkus.security.spi.runtime.AuthorizationController;
     import io.quarkus.security.spi.runtime.DevModeDisabledAuthorizationController;
     import io.quarkus.security.spi.runtime.MethodDescription;
    @@ -511,6 +512,7 @@ void gatherSecurityChecks(BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
                 BuildProducer<RunTimeConfigBuilderBuildItem> configBuilderProducer,
                 List<AdditionalSecuredMethodsBuildItem> additionalSecuredMethods,
                 SecurityCheckRecorder recorder,
    +            Optional<DefaultSecurityCheckBuildItem> defaultSecurityCheckBuildItem,
                 BuildProducer<ReflectiveClassBuildItem> reflectiveClassBuildItemBuildProducer,
                 List<AdditionalSecurityCheckBuildItem> additionalSecurityChecks, SecurityBuildTimeConfig config) {
             classPredicate.produce(new ApplicationClassPredicateBuildItem(new SecurityCheckStorageAppPredicate()));
    @@ -543,6 +545,15 @@ void gatherSecurityChecks(BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
                 recorder.addMethod(builder, method.declaringClass().name().toString(), method.name(), params,
                         methodEntry.getValue());
             }
    +
    +        if (defaultSecurityCheckBuildItem.isPresent()) {
    +            var roles = defaultSecurityCheckBuildItem.get().getRolesAllowed();
    +            if (roles == null) {
    +                recorder.registerDefaultSecurityCheck(builder, recorder.denyAll());
    +            } else {
    +                recorder.registerDefaultSecurityCheck(builder, recorder.rolesAllowed(roles.toArray(new String[0])));
    +            }
    +        }
             recorder.create(builder);
     
             syntheticBeans.produce(
    
  • extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/SecurityCheckStorage.java+5 0 modified
    @@ -10,4 +10,9 @@ default SecurityCheck getSecurityCheck(Method method) {
     
         SecurityCheck getSecurityCheck(MethodDescription methodDescription);
     
    +    /**
    +     * {@link SecurityCheck} that should be applied when there is no other check applied on incoming request.
    +     */
    +    SecurityCheck getDefaultSecurityCheck();
    +
     }
    
  • extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityCheckStorageBuilder.java+13 0 modified
    @@ -9,6 +9,7 @@
     
     public class SecurityCheckStorageBuilder {
         private final Map<MethodDescription, SecurityCheck> securityChecks = new HashMap<>();
    +    private SecurityCheck defaultSecurityCheck;
     
         public void registerCheck(String className,
                 String methodName,
    @@ -17,12 +18,24 @@ public void registerCheck(String className,
             securityChecks.put(new MethodDescription(className, methodName, parameterTypes), securityCheck);
         }
     
    +    public void registerDefaultSecurityCheck(SecurityCheck defaultSecurityCheck) {
    +        if (this.defaultSecurityCheck != null) {
    +            throw new IllegalStateException("Default SecurityCheck has already been registered");
    +        }
    +        this.defaultSecurityCheck = defaultSecurityCheck;
    +    }
    +
         public SecurityCheckStorage create() {
             return new SecurityCheckStorage() {
                 @Override
                 public SecurityCheck getSecurityCheck(MethodDescription methodDescription) {
                     return securityChecks.get(methodDescription);
                 }
    +
    +            @Override
    +            public SecurityCheck getDefaultSecurityCheck() {
    +                return defaultSecurityCheck;
    +            }
             };
         }
     }
    
  • extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java+4 0 modified
    @@ -317,4 +317,8 @@ private Class<?> loadClass(String className) {
                 throw new RuntimeException("Unable to load class '" + className + "' for creating permission", e);
             }
         }
    +
    +    public void registerDefaultSecurityCheck(RuntimeValue<SecurityCheckStorageBuilder> builder, SecurityCheck securityCheck) {
    +        builder.getValue().registerDefaultSecurityCheck(securityCheck);
    +    }
     }
    
  • extensions/security/spi/src/main/java/io/quarkus/security/spi/DefaultSecurityCheckBuildItem.java+28 0 added
    @@ -0,0 +1,28 @@
    +package io.quarkus.security.spi;
    +
    +import java.util.List;
    +import java.util.Objects;
    +
    +import io.quarkus.builder.item.SimpleBuildItem;
    +
    +public final class DefaultSecurityCheckBuildItem extends SimpleBuildItem {
    +
    +    public final List<String> rolesAllowed;
    +
    +    private DefaultSecurityCheckBuildItem(List<String> rolesAllowed) {
    +        this.rolesAllowed = rolesAllowed;
    +    }
    +
    +    public static DefaultSecurityCheckBuildItem denyAll() {
    +        return new DefaultSecurityCheckBuildItem(null);
    +    }
    +
    +    public static DefaultSecurityCheckBuildItem rolesAllowed(List<String> rolesAllowed) {
    +        Objects.requireNonNull(rolesAllowed);
    +        return new DefaultSecurityCheckBuildItem(List.copyOf(rolesAllowed));
    +    }
    +
    +    public List<String> getRolesAllowed() {
    +        return rolesAllowed;
    +    }
    +}
    
  • independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java+0 19 modified
    @@ -415,25 +415,6 @@ protected List<ResourceMethod> createEndpoints(ClassInfo currentClassInfo,
             return ret;
         }
     
    -    /**
    -     * Return endpoints defined directly on classInfo.
    -     *
    -     * @param classInfo resource class
    -     * @return classInfo endpoint method info
    -     */
    -    public static Collection<MethodInfo> collectClassEndpoints(ClassInfo classInfo,
    -            Map<DotName, String> httpAnnotationToMethod, IndexView index, ApplicationScanningResult applicationScanningResult) {
    -        Collection<FoundEndpoint> endpoints = collectEndpoints(classInfo, classInfo, new HashSet<>(), new HashSet<>(), true,
    -                httpAnnotationToMethod, index, applicationScanningResult, new AnnotationStore(null));
    -        Collection<MethodInfo> ret = new HashSet<>();
    -        for (FoundEndpoint endpoint : endpoints) {
    -            if (endpoint.classInfo.equals(classInfo)) {
    -                ret.add(endpoint.methodInfo);
    -            }
    -        }
    -        return ret;
    -    }
    -
         private static List<FoundEndpoint> collectEndpoints(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo,
                 Set<String> seenMethods, Set<String> existingClassNameBindings, boolean considerApplication,
                 Map<DotName, String> httpAnnotationToMethod, IndexView index, ApplicationScanningResult applicationScanningResult,
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

10

News mentions

0

No linked articles in our index yet.