diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 27dc90d504481..5efc8f12098d7 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -27,6 +27,8 @@ 2.12.8 4.1.1 + 2.22.1 + diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java index 7546e6263bf8d..a151f4cc84723 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java @@ -1,23 +1,44 @@ package io.quarkus.security.deployment; +import java.lang.reflect.Method; import java.security.Provider; import java.security.Security; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.Type; import org.jboss.logging.Logger; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; +import io.quarkus.arc.deployment.BeanRegistrarBuildItem; import io.quarkus.arc.deployment.InterceptorBindingRegistrarBuildItem; +import io.quarkus.arc.processor.AnnotationStore; +import io.quarkus.arc.processor.BeanConfigurator; +import io.quarkus.arc.processor.BeanRegistrar; +import io.quarkus.arc.processor.BuildExtension; +import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CapabilityBuildItem; +import io.quarkus.deployment.builditem.ApplicationIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; import io.quarkus.security.runtime.IdentityProviderManagerCreator; import io.quarkus.security.runtime.SecurityBuildTimeConfig; import io.quarkus.security.runtime.SecurityIdentityAssociation; @@ -26,6 +47,8 @@ import io.quarkus.security.runtime.interceptor.DenyAllInterceptor; import io.quarkus.security.runtime.interceptor.PermitAllInterceptor; import io.quarkus.security.runtime.interceptor.RolesAllowedInterceptor; +import io.quarkus.security.runtime.interceptor.SecurityCheckStorage; +import io.quarkus.security.runtime.interceptor.SecurityCheckStorageBuilder; import io.quarkus.security.runtime.interceptor.SecurityConstrainer; import io.quarkus.security.runtime.interceptor.SecurityHandler; @@ -85,6 +108,145 @@ void registerSecurityInterceptors(BuildProducer beanRegistrars, + ApplicationIndexBuildItem indexBuildItem) { + + beanRegistrars.produce(new BeanRegistrarBuildItem(new BeanRegistrar() { + + @Override + public void register(RegistrationContext registrationContext) { + Map methodAnnotations = gatherSecurityAnnotations(indexBuildItem, + registrationContext); + + DotName name = DotName.createSimple(SecurityCheckStorage.class.getName()); + + BeanConfigurator configurator = registrationContext.configure(name); + configurator.addType(name); + configurator.scope(BuiltinScope.APPLICATION.getInfo()); + configurator.creator(m -> { + ResultHandle storageBuilder = m + .newInstance(MethodDescriptor.ofConstructor(SecurityCheckStorageBuilder.class)); + for (Map.Entry methodEntry : methodAnnotations.entrySet()) { + registerSecuredMethod(storageBuilder, m, methodEntry); + } + ResultHandle ret = m.invokeVirtualMethod( + MethodDescriptor.ofMethod(SecurityCheckStorageBuilder.class, "create", + SecurityCheckStorage.class), + storageBuilder); + m.returnValue(ret); + }); + configurator.done(); + } + })); + } + + private void registerSecuredMethod(ResultHandle checkStorage, + MethodCreator methodCreator, + Map.Entry methodEntry) { + try { + MethodInfo method = methodEntry.getKey(); + ResultHandle aClass = methodCreator.load(method.declaringClass().name().toString()); + ResultHandle methodName = methodCreator.load(method.name()); + ResultHandle params = paramTypes(methodCreator, method.parameters()); + + AnnotationInstance instance = methodEntry.getValue(); + ResultHandle securityAnnotation = methodCreator.load(instance.name().toString()); + + ResultHandle annotationParameters = annotationValues(methodCreator, instance); + + Method registerAnnotation = SecurityCheckStorageBuilder.class.getDeclaredMethod("registerAnnotation", + String.class, String.class, String[].class, String.class, String[].class); + methodCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(registerAnnotation), checkStorage, + aClass, methodName, params, securityAnnotation, annotationParameters); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("registerAnnotation method not found on on SecurityCheckStorage", e); + } + } + + private ResultHandle annotationValues(MethodCreator methodCreator, AnnotationInstance instance) { + AnnotationValue value = instance.value(); + if (value != null && value.asStringArray() != null) { + String[] values = value.asStringArray(); + ResultHandle result = methodCreator.newArray(String.class, methodCreator.load(values.length)); + int i = 0; + for (String val : values) { + methodCreator.writeArrayValue(result, i, methodCreator.load(val)); + } + return result; + } + return methodCreator.loadNull(); + } + + private ResultHandle paramTypes(MethodCreator ctor, List parameters) { + ResultHandle result = ctor.newArray(String.class, ctor.load(parameters.size())); + + for (int i = 0; i < parameters.size(); i++) { + ctor.writeArrayValue(result, i, ctor.load(parameters.get(i).toString())); + } + + return result; + } + + private Map gatherSecurityAnnotations(ApplicationIndexBuildItem indexBuildItem, + BeanRegistrar.RegistrationContext registrationContext) { + Set securityAnnotations = SecurityAnnotationsRegistrar.SECURITY_BINDINGS.keySet(); + AnnotationStore annotationStore = registrationContext.get(BuildExtension.Key.ANNOTATION_STORE); + Set classesWithSecurity = new HashSet<>(); + + Collection classes = indexBuildItem.getIndex().getKnownClasses(); + for (ClassInfo classInfo : classes) { + boolean hasSecurityAnnotations = annotationStore.hasAnyAnnotation(classInfo, securityAnnotations); + if (!hasSecurityAnnotations) { + for (MethodInfo method : classInfo.methods()) { + if (annotationStore.hasAnyAnnotation(method, securityAnnotations)) { + hasSecurityAnnotations = true; + break; + } + } + } + if (hasSecurityAnnotations) { + classesWithSecurity.add(classInfo); + } + } + + return gatherSecurityAnnotations(securityAnnotations, + classesWithSecurity, annotationStore); + } + + private Map gatherSecurityAnnotations(Set securityAnnotations, + Set classesWithSecurity, + AnnotationStore annotationStore) { + Map methodAnnotations = new HashMap<>(); + for (ClassInfo classInfo : classesWithSecurity) { + Collection classAnnotations = annotationStore.getAnnotations(classInfo); + AnnotationInstance classLevelAnnotation = getSingle(classAnnotations, securityAnnotations); + + for (MethodInfo method : classInfo.methods()) { + AnnotationInstance methodAnnotation = getSingle(annotationStore.getAnnotations(method), securityAnnotations); + methodAnnotation = methodAnnotation == null ? classLevelAnnotation : methodAnnotation; + if (methodAnnotation != null) { + methodAnnotations.put(method, methodAnnotation); + } + } + } + return methodAnnotations; + } + + private AnnotationInstance getSingle(Collection classAnnotations, Set securityAnnotations) { + AnnotationInstance result = null; + for (AnnotationInstance annotation : classAnnotations) { + if (securityAnnotations.contains(annotation.name())) { + if (result != null) { + throw new IllegalStateException("Duplicate security annotations on class " + annotation.target()); + } + result = annotation; + } + } + + return result; + } + /** * Determine the classes that make up the provider and its services * diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityCheckStorage.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityCheckStorage.java new file mode 100644 index 0000000000000..62f91426e424f --- /dev/null +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityCheckStorage.java @@ -0,0 +1,9 @@ +package io.quarkus.security.runtime.interceptor; + +import java.lang.reflect.Method; + +import io.quarkus.security.runtime.interceptor.check.SecurityCheck; + +public interface SecurityCheckStorage { + SecurityCheck getSecurityCheck(Method method); +} diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityCheckStorageBuilder.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityCheckStorageBuilder.java new file mode 100644 index 0000000000000..d328e31d960d1 --- /dev/null +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityCheckStorageBuilder.java @@ -0,0 +1,97 @@ +package io.quarkus.security.runtime.interceptor; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.runtime.interceptor.check.AuthenticatedCheck; +import io.quarkus.security.runtime.interceptor.check.DenyAllCheck; +import io.quarkus.security.runtime.interceptor.check.PermitAllCheck; +import io.quarkus.security.runtime.interceptor.check.RolesAllowedCheck; +import io.quarkus.security.runtime.interceptor.check.SecurityCheck; + +public class SecurityCheckStorageBuilder { + private final Map securityChecks = new HashMap<>(); + + public void registerAnnotation(String aClass, + String methodName, + String[] parameterTypes, + String securityAnnotation, + String[] value) { + securityChecks.put(new MethodDescription(aClass, methodName, parameterTypes), + determineCheck(securityAnnotation, value)); + } + + public SecurityCheckStorage create() { + return new SecurityCheckStorage() { + @Override + public SecurityCheck getSecurityCheck(Method method) { + MethodDescription descriptor = new MethodDescription(method.getDeclaringClass().getName(), method.getName(), + typesAsStrings(method.getParameterTypes())); + return securityChecks.get(descriptor); + } + }; + } + + private SecurityCheck determineCheck(String securityAnnotation, String[] value) { + if (DenyAll.class.getName().equals(securityAnnotation)) { + return new DenyAllCheck(); + } + if (RolesAllowed.class.getName().equals(securityAnnotation)) { + return new RolesAllowedCheck(value); + } + if (PermitAll.class.getName().equals(securityAnnotation)) { + return new PermitAllCheck(); + } + if (Authenticated.class.getName().equals(securityAnnotation)) { + return new AuthenticatedCheck(); + } + throw new IllegalArgumentException("Unsupported security check " + securityAnnotation); + } + + private String[] typesAsStrings(Class[] parameterTypes) { + String[] result = new String[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + result[i] = parameterTypes[i].getName(); + } + return result; + } + + static class MethodDescription { + private final String className; + private final String methodName; + private final String[] parameterTypes; + + public MethodDescription(String aClass, String methodName, String[] parameterTypes) { + this.className = aClass; + this.methodName = methodName; + this.parameterTypes = parameterTypes; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + MethodDescription that = (MethodDescription) o; + return className.equals(that.className) && + methodName.equals(that.methodName) && + Arrays.equals(parameterTypes, that.parameterTypes); + } + + @Override + public int hashCode() { + int result = Objects.hash(className, methodName); + result = 31 * result + Arrays.hashCode(parameterTypes); + return result; + } + } +} diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityConstrainer.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityConstrainer.java index 7925c7753bc51..665c6e60b844c 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityConstrainer.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityConstrainer.java @@ -1,28 +1,15 @@ package io.quarkus.security.runtime.interceptor; -import static java.util.Arrays.asList; - -import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Supplier; -import javax.annotation.security.DenyAll; -import javax.annotation.security.PermitAll; -import javax.annotation.security.RolesAllowed; import javax.inject.Inject; import javax.inject.Singleton; -import io.quarkus.security.Authenticated; -import io.quarkus.security.ForbiddenException; -import io.quarkus.security.UnauthorizedException; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.interceptor.check.SecurityCheck; /** * @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com @@ -30,159 +17,19 @@ @Singleton public class SecurityConstrainer { - private static final List> SECURITY_ANNOTATIONS = asList(Authenticated.class, DenyAll.class, - PermitAll.class, RolesAllowed.class); - - private final Map> checkForMethod = new ConcurrentHashMap<>(); + private final Map> checkForMethod = new ConcurrentHashMap<>(); @Inject SecurityIdentity identity; - public void checkRoles(Method method, Collection interceptorBindings) { - Optional check = getCheck(method, interceptorBindings); - if (check.isPresent()) { - check.get().apply(identity); - } - } - - private Optional getCheck(Method method, Collection interceptorBindings) { - Optional check = checkForMethod.get(method); - if (check == null) { - // intentionally no synchronization - check = determineSecurityCheck(method, interceptorBindings); - checkForMethod.put(method, check); - } - return check; - } - - private Optional determineSecurityCheck(Method method, Collection interceptorBindings) { - Annotation securityAnnotation = determineSecurityAnnotation(method.getDeclaredAnnotations(), method::toString); - if (securityAnnotation == null) { - Class declaringClass = method.getDeclaringClass(); - securityAnnotation = determineSecurityAnnotation(declaringClass.getDeclaredAnnotations(), - declaringClass::getCanonicalName); - } - if (securityAnnotation == null) { - securityAnnotation = determineSecurityAnnotationFromBindings(interceptorBindings, method::toString); - } - return checkForAnnotation(securityAnnotation); - } - - private Optional checkForAnnotation(Annotation securityAnnotation) { - if (securityAnnotation instanceof DenyAll) { - return Optional.of(new DenyAllCheck()); - } - if (securityAnnotation instanceof RolesAllowed) { - RolesAllowed rolesAllowed = (RolesAllowed) securityAnnotation; - return Optional.of(new RolesAllowedCheck(rolesAllowed.value())); - } - if (securityAnnotation instanceof PermitAll) { - return Optional.of(new PermitAllCheck()); - } - if (securityAnnotation instanceof Authenticated) { - return Optional.of(new AuthenticatedCheck()); - } - return Optional.empty(); - } - - private Annotation determineSecurityAnnotationFromBindings(Collection interceptorBindings, - Supplier annotationPlacement) { - - List securityAnnotations = new ArrayList<>(); - for (Annotation binding : interceptorBindings) { - if (isSecurityAnnotation(binding)) { - securityAnnotations.add(binding); - } - } - return getExactlyOne(securityAnnotations, annotationPlacement); - } - - private Annotation determineSecurityAnnotation(Annotation[] annotations, Supplier annotationPlacement) { - List securityAnnotations = new ArrayList<>(); - for (Annotation binding : annotations) { - if (isSecurityAnnotation(binding)) { - securityAnnotations.add(binding); - } - } - return getExactlyOne(securityAnnotations, annotationPlacement); - } - - private boolean isSecurityAnnotation(Annotation binding) { - boolean isSecurityAnnotation = false; - for (Class annotationClass : SECURITY_ANNOTATIONS) { - if (annotationClass == binding.annotationType()) { - isSecurityAnnotation = true; - } - } - return isSecurityAnnotation; - } - - private Annotation getExactlyOne(List securityAnnotations, Supplier annotationPlacement) { - switch (securityAnnotations.size()) { - case 0: - return null; - case 1: - return securityAnnotations.get(0); - default: - throw new IllegalStateException("Duplicate security annotations found on " - + annotationPlacement.get() + - ". Expected at most 1 annotation, found: " + securityAnnotations); - } - } - - private static class RolesAllowedCheck implements Check { - private final String[] allowedRoles; - - private RolesAllowedCheck(String[] allowedRoles) { - this.allowedRoles = allowedRoles; - } - - @Override - public void apply(SecurityIdentity identity) { - Set roles = identity.getRoles(); - if (roles != null) { - for (String role : allowedRoles) { - if (roles.contains(role)) { - return; - } - } - } - if (identity.isAnonymous()) { - throw new UnauthorizedException(); - } else { - throw new ForbiddenException(); - } - } - } - - private static class DenyAllCheck implements Check { - @Override - public void apply(SecurityIdentity identity) { - if (identity.isAnonymous()) { - throw new UnauthorizedException(); - } else { - throw new ForbiddenException(); - } - } - } - - private static class PermitAllCheck implements Check { - @Override - public void apply(SecurityIdentity identity) { - } - } + @Inject + SecurityCheckStorage storage; - private static class AuthenticatedCheck implements Check { + public void checkRoles(Method method) { - @Override - public void apply(SecurityIdentity identity) { - if (identity.isAnonymous()) { - throw new UnauthorizedException(); - } + SecurityCheck securityCheck = storage.getSecurityCheck(method); + if (securityCheck != null) { + securityCheck.apply(identity); } } - - private interface Check { - void apply(SecurityIdentity identity); - } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityHandler.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityHandler.java index b0dd792b96f26..dd697ba297b0a 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityHandler.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityHandler.java @@ -4,8 +4,6 @@ import javax.inject.Singleton; import javax.interceptor.InvocationContext; -import io.quarkus.arc.runtime.InterceptorBindings; - /** * @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com */ @@ -22,7 +20,7 @@ public Object handle(InvocationContext ic) throws Exception { if (alreadyHandled(ic)) { return ic.proceed(); } - constrainer.checkRoles(ic.getMethod(), InterceptorBindings.getInterceptorBindings(ic)); + constrainer.checkRoles(ic.getMethod()); return ic.proceed(); } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/AuthenticatedCheck.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/AuthenticatedCheck.java new file mode 100644 index 0000000000000..f624e539e20b2 --- /dev/null +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/AuthenticatedCheck.java @@ -0,0 +1,14 @@ +package io.quarkus.security.runtime.interceptor.check; + +import io.quarkus.security.UnauthorizedException; +import io.quarkus.security.identity.SecurityIdentity; + +public class AuthenticatedCheck implements SecurityCheck { + + @Override + public void apply(SecurityIdentity identity) { + if (identity.isAnonymous()) { + throw new UnauthorizedException(); + } + } +} diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/DenyAllCheck.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/DenyAllCheck.java new file mode 100644 index 0000000000000..68aab05c19bf0 --- /dev/null +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/DenyAllCheck.java @@ -0,0 +1,16 @@ +package io.quarkus.security.runtime.interceptor.check; + +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.UnauthorizedException; +import io.quarkus.security.identity.SecurityIdentity; + +public class DenyAllCheck implements SecurityCheck { + @Override + public void apply(SecurityIdentity identity) { + if (identity.isAnonymous()) { + throw new UnauthorizedException(); + } else { + throw new ForbiddenException(); + } + } +} \ No newline at end of file diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/PermitAllCheck.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/PermitAllCheck.java new file mode 100644 index 0000000000000..4e1857ded00d2 --- /dev/null +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/PermitAllCheck.java @@ -0,0 +1,9 @@ +package io.quarkus.security.runtime.interceptor.check; + +import io.quarkus.security.identity.SecurityIdentity; + +public class PermitAllCheck implements SecurityCheck { + @Override + public void apply(SecurityIdentity identity) { + } +} \ No newline at end of file diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/RolesAllowedCheck.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/RolesAllowedCheck.java new file mode 100644 index 0000000000000..28f8f71884db0 --- /dev/null +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/RolesAllowedCheck.java @@ -0,0 +1,32 @@ +package io.quarkus.security.runtime.interceptor.check; + +import java.util.Set; + +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.UnauthorizedException; +import io.quarkus.security.identity.SecurityIdentity; + +public class RolesAllowedCheck implements SecurityCheck { + private final String[] allowedRoles; + + public RolesAllowedCheck(String[] allowedRoles) { + this.allowedRoles = allowedRoles; + } + + @Override + public void apply(SecurityIdentity identity) { + Set roles = identity.getRoles(); + if (roles != null) { + for (String role : allowedRoles) { + if (roles.contains(role)) { + return; + } + } + } + if (identity.isAnonymous()) { + throw new UnauthorizedException(); + } else { + throw new ForbiddenException(); + } + } +} diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/SecurityCheck.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/SecurityCheck.java new file mode 100644 index 0000000000000..31b36c98540b0 --- /dev/null +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/SecurityCheck.java @@ -0,0 +1,7 @@ +package io.quarkus.security.runtime.interceptor.check; + +import io.quarkus.security.identity.SecurityIdentity; + +public interface SecurityCheck { + void apply(SecurityIdentity identity); +} \ No newline at end of file