diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java index 483890b4c70c9..42aeb33c501e8 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java @@ -9,6 +9,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -33,6 +34,7 @@ import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.BeanProcessor; import io.quarkus.arc.processor.BuiltinScope; +import io.quarkus.arc.processor.BytecodeTransformer; import io.quarkus.arc.processor.ContextConfigurator; import io.quarkus.arc.processor.ContextRegistrar; import io.quarkus.arc.processor.ReflectionRegistration; @@ -47,6 +49,7 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; import io.quarkus.deployment.builditem.ApplicationClassPredicateBuildItem; +import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; import io.quarkus.deployment.builditem.ExecutorBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; @@ -239,7 +242,8 @@ public BeanRegistrationPhaseBuildItem registerBeans(ContextRegistrationPhaseBuil // PHASE 3 - initialize and validate the bean deployment @BuildStep public ValidationPhaseBuildItem validate(BeanRegistrationPhaseBuildItem beanRegistrationPhase, - List beanConfigurators) { + List beanConfigurators, + BuildProducer bytecodeTransformer) { for (BeanConfiguratorBuildItem beanConfigurator : beanConfigurators) { for (BeanConfigurator value : beanConfigurator.getValues()) { @@ -248,7 +252,12 @@ public ValidationPhaseBuildItem validate(BeanRegistrationPhaseBuildItem beanRegi } } - beanRegistrationPhase.getBeanProcessor().initialize(); + beanRegistrationPhase.getBeanProcessor().initialize(new Consumer() { + @Override + public void accept(BytecodeTransformer t) { + bytecodeTransformer.produce(new BytecodeTransformerBuildItem(t.getClassToTransform(), t.getVisitorFunction())); + } + }); return new ValidationPhaseBuildItem(beanRegistrationPhase.getBeanProcessor().validate(), beanRegistrationPhase.getBeanProcessor()); } @@ -364,4 +373,4 @@ public boolean test(T t) { return false; } } -} \ No newline at end of file +} diff --git a/extensions/security/deployment/src/test/java/io/quarkus/security/test/cdi/SecurityAnnotationOnFinalMethodTest.java b/extensions/security/deployment/src/test/java/io/quarkus/security/test/cdi/SecurityAnnotationOnFinalMethodTest.java new file mode 100644 index 0000000000000..9e241498ddebc --- /dev/null +++ b/extensions/security/deployment/src/test/java/io/quarkus/security/test/cdi/SecurityAnnotationOnFinalMethodTest.java @@ -0,0 +1,61 @@ +package io.quarkus.security.test.cdi; + +import static io.quarkus.security.test.cdi.SecurityTestUtils.assertFailureFor; +import static io.quarkus.security.test.cdi.SecurityTestUtils.assertSuccess; +import static io.quarkus.security.test.utils.IdentityMock.ADMIN; +import static io.quarkus.security.test.utils.IdentityMock.ANONYMOUS; +import static io.quarkus.security.test.utils.IdentityMock.USER; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.RolesAllowed; +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.UnauthorizedException; +import io.quarkus.security.test.utils.AuthData; +import io.quarkus.security.test.utils.IdentityMock; +import io.quarkus.test.QuarkusUnitTest; + +public class SecurityAnnotationOnFinalMethodTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(BeanWithSecuredFinalMethod.class, IdentityMock.class, + AuthData.class, SecurityTestUtils.class)); + + @Inject + BeanWithSecuredFinalMethod bean; + + @Test + public void shouldRestrictAccessToSpecificRole() { + assertFailureFor(() -> bean.securedMethod(), UnauthorizedException.class, ANONYMOUS); + assertSuccess(() -> bean.securedMethod(), "accessibleForAdminOnly", ADMIN); + } + + @Test + public void shouldFailToAccessCompletely() { + assertFailureFor(() -> bean.otherSecuredMethod("whatever"), UnauthorizedException.class, ANONYMOUS); + assertFailureFor(() -> bean.otherSecuredMethod("whatever"), ForbiddenException.class, USER, ADMIN); + } + + @Singleton + public static class BeanWithSecuredFinalMethod { + + @RolesAllowed("admin") + public final String securedMethod() { + return "accessibleForAdminOnly"; + } + + @DenyAll + public final String otherSecuredMethod(String input) { + return "denied"; + } + } +} diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java index 11148f0bb3c11..5d9a32d86342b 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java @@ -25,6 +25,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -203,19 +204,19 @@ BeanRegistrar.RegistrationContext registerBeans(List beanRegistra return registerSyntheticBeans(beanRegistrars, buildContext); } - void init() { + void init(Consumer bytecodeTransformerConsumer) { long start = System.currentTimeMillis(); // Collect dependency resolution errors List errors = new ArrayList<>(); for (BeanInfo bean : beans) { - bean.init(errors); + bean.init(errors, bytecodeTransformerConsumer); } for (ObserverInfo observer : observers) { observer.init(errors); } for (InterceptorInfo interceptor : interceptors) { - interceptor.init(errors); + interceptor.init(errors, bytecodeTransformerConsumer); } processErrors(errors); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java index 92daeaba796dd..cc7b1f4696870 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java @@ -396,7 +396,7 @@ void validate(List errors, List validators) } } - void init(List errors) { + void init(List errors, Consumer bytecodeTransformerConsumer) { for (Injection injection : injections) { for (InjectionPointInfo injectionPoint : injection.injectionPoints) { Beans.resolveInjectionPoint(beanDeployment, this, injectionPoint, errors); @@ -405,7 +405,7 @@ void init(List errors) { if (disposer != null) { disposer.init(errors); } - interceptedMethods.putAll(initInterceptedMethods()); + interceptedMethods.putAll(initInterceptedMethods(bytecodeTransformerConsumer)); lifecycleInterceptors.putAll(initLifecycleInterceptors()); } @@ -421,7 +421,8 @@ protected String getType() { } } - private Map initInterceptedMethods() { + private Map initInterceptedMethods( + Consumer bytecodeTransformerConsumer) { if (!isInterceptor() && isClassBean()) { Map interceptedMethods = new HashMap<>(); Map> candidates = new HashMap<>(); @@ -434,7 +435,8 @@ private Map initInterceptedMethods() { } } - Methods.addInterceptedMethodCandidates(beanDeployment, target.get().asClass(), candidates, classLevelBindings); + Methods.addInterceptedMethodCandidates(beanDeployment, target.get().asClass(), candidates, classLevelBindings, + bytecodeTransformerConsumer); for (Entry> entry : candidates.entrySet()) { List interceptors = beanDeployment.getInterceptorResolver() diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java index 206d0a1e1406f..073a8c2d6e17f 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java @@ -15,6 +15,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; import org.jboss.jandex.AnnotationInstance; @@ -29,7 +30,7 @@ *
    *
  1. {@link #registerCustomContexts()}
  2. *
  3. {@link #registerBeans()}
  4. - *
  5. {@link #initialize()}
  6. + *
  7. {@link #initialize(Consumer)}
  8. *
  9. {@link #validate()}
  10. *
  11. {@link #processValidationErrors(io.quarkus.arc.processor.BeanDeploymentValidator.ValidationContext)}
  12. *
  13. {@link #generateResources(ReflectionRegistration)}
  14. @@ -102,8 +103,8 @@ public BeanRegistrar.RegistrationContext registerBeans() { return beanDeployment.registerBeans(beanRegistrars); } - public void initialize() { - beanDeployment.init(); + public void initialize(Consumer bytecodeTransformerConsumer) { + beanDeployment.init(bytecodeTransformerConsumer); } public BeanDeploymentValidator.ValidationContext validate() { @@ -201,7 +202,12 @@ public BeanDeployment getBeanDeployment() { public BeanDeployment process() throws IOException { registerCustomContexts(); registerBeans(); - initialize(); + initialize(new Consumer() { + @Override + public void accept(BytecodeTransformer transformer) { + + } + }); ValidationContext validationContext = validate(); processValidationErrors(validationContext); generateResources(null); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BytecodeTransformer.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BytecodeTransformer.java new file mode 100644 index 0000000000000..ed939481d353a --- /dev/null +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BytecodeTransformer.java @@ -0,0 +1,24 @@ +package io.quarkus.arc.processor; + +import java.util.function.BiFunction; +import org.objectweb.asm.ClassVisitor; + +public class BytecodeTransformer { + + final String classToTransform; + final BiFunction visitorFunction; + + public BytecodeTransformer(String classToTransform, + BiFunction visitorFunction) { + this.classToTransform = classToTransform; + this.visitorFunction = visitorFunction; + } + + public String getClassToTransform() { + return classToTransform; + } + + public BiFunction getVisitorFunction() { + return visitorFunction; + } +} diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java index 6f927a8493a54..8b75e3611a476 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java @@ -9,6 +9,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; @@ -18,6 +20,9 @@ import org.jboss.jandex.Type; import org.jboss.jandex.TypeVariable; import org.jboss.logging.Logger; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; /** * @@ -115,7 +120,9 @@ static boolean isObjectToString(MethodInfo method) { static void addInterceptedMethodCandidates(BeanDeployment beanDeployment, ClassInfo classInfo, Map> candidates, - List classLevelBindings) { + List classLevelBindings, Consumer bytecodeTransformerConsumer) { + + Set methodsFromWhichToRemoveFinal = new HashSet<>(); for (MethodInfo method : classInfo.methods()) { if (skipForSubclass(method)) { continue; @@ -136,20 +143,35 @@ static void addInterceptedMethodCandidates(BeanDeployment beanDeployment, ClassI if (Modifier.isFinal(method.flags())) { String className = method.declaringClass().name().toString(); if (!className.startsWith("java.")) { - LOGGER.warn( - String.format( - "Method %s.%s() is final, skipped during generation of the corresponding intercepted subclass", - className, method.name())); + methodsFromWhichToRemoveFinal.add(method.name()); } - } else { - candidates.computeIfAbsent(new Methods.MethodKey(method), key -> merged); } + candidates.computeIfAbsent(new Methods.MethodKey(method), key -> merged); } } + if (!methodsFromWhichToRemoveFinal.isEmpty()) { + bytecodeTransformerConsumer.accept( + new BytecodeTransformer(classInfo.name().toString(), new BiFunction() { + @Override + public ClassVisitor apply(String s, ClassVisitor classVisitor) { + return new ClassVisitor(Opcodes.ASM7, classVisitor) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, + String[] exceptions) { + if (methodsFromWhichToRemoveFinal.contains(name)) { + access = access & (~Opcodes.ACC_FINAL); + } + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + }; + } + })); + } if (classInfo.superClassType() != null) { ClassInfo superClassInfo = getClassByName(beanDeployment.getIndex(), classInfo.superName()); if (superClassInfo != null) { - addInterceptedMethodCandidates(beanDeployment, superClassInfo, candidates, classLevelBindings); + addInterceptedMethodCandidates(beanDeployment, superClassInfo, candidates, classLevelBindings, + bytecodeTransformerConsumer); } } }