From adacab6657113a60ce3da5208dee50238eadbccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Tue, 19 Mar 2024 23:11:09 +0100 Subject: [PATCH] Fix @SecureField detection on subclasses, interfaces etc. --- .../ResteasyReactiveJacksonProcessor.java | 115 ++++++++++++-- .../deployment/test/AbstractNamedPet.java | 17 +++ .../jackson/deployment/test/AbstractPet.java | 23 +++ .../deployment/test/AbstractUnsecuredPet.java | 15 ++ .../reactive/jackson/deployment/test/Cat.java | 17 +++ .../reactive/jackson/deployment/test/Dog.java | 14 ++ .../jackson/deployment/test/Frog.java | 28 ++++ .../deployment/test/FrogBodyParts.java | 45 ++++++ .../jackson/deployment/test/Pond.java | 33 ++++ .../test/SecuredPersonInterface.java | 7 + .../deployment/test/SimpleJsonResource.java | 122 +++++++++++++++ .../deployment/test/SimpleJsonTest.java | 141 +++++++++++++++++- .../jackson/deployment/test/UnsecuredPet.java | 14 ++ .../jackson/deployment/test/Veterinarian.java | 27 ++++ 14 files changed, 601 insertions(+), 17 deletions(-) create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractNamedPet.java create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractPet.java create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractUnsecuredPet.java create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Cat.java create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Dog.java create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Frog.java create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/FrogBodyParts.java create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Pond.java create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SecuredPersonInterface.java create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/UnsecuredPet.java create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Veterinarian.java diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java index ef49d001947a7..84282af70af8f 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java @@ -6,11 +6,14 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; import java.util.function.Supplier; @@ -25,6 +28,7 @@ import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; @@ -368,6 +372,7 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r JaxRsResourceIndexBuildItem index, BuildProducer producer) { IndexView indexView = index.getIndexView(); + Map typeToHasSecureField = new HashMap<>(); List result = new ArrayList<>(); for (ResteasyReactiveResourceMethodEntriesBuildItem.Entry entry : resourceMethodEntries.getEntries()) { MethodInfo methodInfo = entry.getMethodInfo(); @@ -423,22 +428,8 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r if ((effectiveReturnClassInfo == null) || effectiveReturnClassInfo.name().equals(ResteasyReactiveDotNames.OBJECT)) { continue; } - ClassInfo currentClassInfo = effectiveReturnClassInfo; - boolean hasSecureFields = false; - while (true) { - if (currentClassInfo.annotationsMap().containsKey(SECURE_FIELD)) { - hasSecureFields = true; - break; - } - if (currentClassInfo.superName().equals(ResteasyReactiveDotNames.OBJECT)) { - break; - } - currentClassInfo = indexView.getClassByName(currentClassInfo.superName()); - if (currentClassInfo == null) { - break; - } - } - if (hasSecureFields) { + AtomicBoolean needToDeleteCache = new AtomicBoolean(false); + if (hasSecureFields(indexView, effectiveReturnClassInfo, typeToHasSecureField, needToDeleteCache)) { AnnotationInstance customSerializationAtClassAnnotation = methodInfo.declaringClass() .declaredAnnotation(CUSTOM_SERIALIZATION); AnnotationInstance customSerializationAtMethodAnnotation = methodInfo.annotation(CUSTOM_SERIALIZATION); @@ -450,6 +441,9 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r SecurityCustomSerialization.class)); } } + if (needToDeleteCache.get()) { + typeToHasSecureField.clear(); + } } if (!result.isEmpty()) { for (ResourceMethodCustomSerializationBuildItem bi : result) { @@ -458,6 +452,95 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r } } + private static boolean hasSecureFields(IndexView indexView, ClassInfo currentClassInfo, + Map typeToHasSecureField, AtomicBoolean needToDeleteCache) { + // use cached result if there is any + final String className = currentClassInfo.name().toString(); + if (typeToHasSecureField.containsKey(className)) { + Boolean hasSecureFields = typeToHasSecureField.get(className); + if (hasSecureFields == null) { + // this is to avoid false negative for scenario like: + // when 'a' declares field of type 'b' which declares field of type 'a' and both 'a' and 'b' + // are returned from an endpoint and 'b' is detected based on 'a' and processed after 'a' + needToDeleteCache.set(true); + return false; + } + return hasSecureFields; + } + + // prevent cyclic check of the same type + // for example when a field has a same type as the current class has + typeToHasSecureField.put(className, null); + + final boolean hasSecureFields; + if (currentClassInfo.isInterface()) { + // check interface implementors as anyone of them can be returned + hasSecureFields = indexView.getAllKnownImplementors(currentClassInfo.name()).stream() + .anyMatch(ci -> hasSecureFields(indexView, ci, typeToHasSecureField, needToDeleteCache)); + } else { + // figure if any field or parent / subclass field is secured + hasSecureFields = hasSecureFields(currentClassInfo) + || anyFieldHasSecureFields(indexView, currentClassInfo, typeToHasSecureField, needToDeleteCache) + || anySubclassHasSecureFields(indexView, currentClassInfo, typeToHasSecureField, needToDeleteCache) + || anyParentClassHasSecureFields(indexView, currentClassInfo, typeToHasSecureField, needToDeleteCache); + } + typeToHasSecureField.put(className, hasSecureFields); + return hasSecureFields; + } + + private static boolean hasSecureFields(ClassInfo classInfo) { + return classInfo.annotationsMap().containsKey(SECURE_FIELD); + } + + private static boolean anyParentClassHasSecureFields(IndexView indexView, ClassInfo currentClassInfo, + Map typeToHasSecureField, AtomicBoolean needToDeleteCache) { + if (!currentClassInfo.superName().equals(ResteasyReactiveDotNames.OBJECT)) { + final ClassInfo parentClassInfo = indexView.getClassByName(currentClassInfo.superName()); + return parentClassInfo != null + && hasSecureFields(indexView, parentClassInfo, typeToHasSecureField, needToDeleteCache); + } + return false; + } + + private static boolean anySubclassHasSecureFields(IndexView indexView, ClassInfo currentClassInfo, + Map typeToHasSecureField, AtomicBoolean needToDeleteCache) { + return indexView.getAllKnownSubclasses(currentClassInfo.name()).stream() + .anyMatch(subclass -> hasSecureFields(indexView, subclass, typeToHasSecureField, needToDeleteCache)); + } + + private static boolean anyFieldHasSecureFields(IndexView indexView, ClassInfo currentClassInfo, + Map typeToHasSecureField, AtomicBoolean needToDeleteCache) { + return currentClassInfo + .fields() + .stream() + .map(FieldInfo::type) + .anyMatch(fieldType -> fieldTypeHasSecureFields(fieldType, indexView, typeToHasSecureField, needToDeleteCache)); + } + + private static boolean fieldTypeHasSecureFields(Type fieldType, IndexView indexView, + Map typeToHasSecureField, AtomicBoolean needToDeleteCache) { + // this is the best effort and does not cover every possibility (e.g. type variables, wildcards) + if (fieldType.kind() == Type.Kind.CLASS) { + if (fieldType.name().packagePrefix() != null && fieldType.name().packagePrefix().startsWith("java.")) { + return false; + } + final ClassInfo fieldClass = indexView.getClassByName(fieldType.name()); + return fieldClass != null && hasSecureFields(indexView, fieldClass, typeToHasSecureField, needToDeleteCache); + } + if (fieldType.kind() == Type.Kind.ARRAY) { + return fieldTypeHasSecureFields(fieldType.asArrayType().constituent(), indexView, typeToHasSecureField, + needToDeleteCache); + } + if (fieldType.kind() == Type.Kind.PARAMETERIZED_TYPE) { + return fieldType + .asParameterizedType() + .arguments() + .stream() + .anyMatch(t -> fieldTypeHasSecureFields(t, indexView, typeToHasSecureField, needToDeleteCache)); + } + return false; + } + private String getTargetId(AnnotationTarget target) { if (target.kind() == AnnotationTarget.Kind.CLASS) { return getClassId(target.asClass()); diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractNamedPet.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractNamedPet.java new file mode 100644 index 0000000000000..faac9183023dc --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractNamedPet.java @@ -0,0 +1,17 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import io.quarkus.resteasy.reactive.jackson.SecureField; + +public abstract class AbstractNamedPet extends AbstractPet { + + @SecureField(rolesAllowed = "admin") + private String privateName; + + public String getPrivateName() { + return privateName; + } + + public void setPrivateName(String privateName) { + this.privateName = privateName; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractPet.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractPet.java new file mode 100644 index 0000000000000..559a5fbfd1f27 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractPet.java @@ -0,0 +1,23 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +public abstract class AbstractPet implements SecuredPersonInterface { + + private String publicName; + private Veterinarian veterinarian; + + public String getPublicName() { + return publicName; + } + + public void setPublicName(String publicName) { + this.publicName = publicName; + } + + public Veterinarian getVeterinarian() { + return veterinarian; + } + + public void setVeterinarian(Veterinarian veterinarian) { + this.veterinarian = veterinarian; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractUnsecuredPet.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractUnsecuredPet.java new file mode 100644 index 0000000000000..d975a2b794f8b --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractUnsecuredPet.java @@ -0,0 +1,15 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +public abstract class AbstractUnsecuredPet { + + private String publicName; + + public String getPublicName() { + return publicName; + } + + public void setPublicName(String publicName) { + this.publicName = publicName; + } + +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Cat.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Cat.java new file mode 100644 index 0000000000000..0fc63cb142630 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Cat.java @@ -0,0 +1,17 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import io.quarkus.resteasy.reactive.jackson.SecureField; + +public class Cat extends AbstractNamedPet { + + @SecureField(rolesAllowed = "admin") + private int privateAge; + + public int getPrivateAge() { + return privateAge; + } + + public void setPrivateAge(int privateAge) { + this.privateAge = privateAge; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Dog.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Dog.java new file mode 100644 index 0000000000000..2816b3e38f2a0 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Dog.java @@ -0,0 +1,14 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +public class Dog extends AbstractNamedPet { + + private int publicAge; + + public int getPublicAge() { + return publicAge; + } + + public void setPublicAge(int publicAge) { + this.publicAge = publicAge; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Frog.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Frog.java new file mode 100644 index 0000000000000..a04507d9abf8e --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Frog.java @@ -0,0 +1,28 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import java.util.List; + +public class Frog { + + // check no cycle when field has the same type as the class which declares it + private Frog partner; + + private List ponds; + + public Frog getPartner() { + return partner; + } + + public void setPartner(Frog partner) { + this.partner = partner; + } + + public List getPonds() { + return ponds; + } + + public void setPonds(List ponds) { + this.ponds = ponds; + } + +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/FrogBodyParts.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/FrogBodyParts.java new file mode 100644 index 0000000000000..64b6f9abc4aab --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/FrogBodyParts.java @@ -0,0 +1,45 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import io.quarkus.resteasy.reactive.jackson.SecureField; + +public class FrogBodyParts { + + public FrogBodyParts() { + } + + public FrogBodyParts(String bodyPartName) { + this.parts = new BodyPart[] { new BodyPart(bodyPartName) }; + } + + private BodyPart[] parts; + + public BodyPart[] getParts() { + return parts; + } + + public void setParts(BodyPart[] parts) { + this.parts = parts; + } + + public static class BodyPart { + + public BodyPart(String name) { + this.name = name; + } + + public BodyPart() { + } + + @SecureField(rolesAllowed = "admin") + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Pond.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Pond.java new file mode 100644 index 0000000000000..8a75366927450 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Pond.java @@ -0,0 +1,33 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import io.quarkus.resteasy.reactive.jackson.SecureField; + +public class Pond { + + @SecureField(rolesAllowed = "admin") + private WaterQuality waterQuality; + + private String name; + + public WaterQuality getWaterQuality() { + return waterQuality; + } + + public void setWaterQuality(WaterQuality waterQuality) { + this.waterQuality = waterQuality; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public enum WaterQuality { + CLEAR, + DIRTY + } + +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SecuredPersonInterface.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SecuredPersonInterface.java new file mode 100644 index 0000000000000..0215805472b80 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SecuredPersonInterface.java @@ -0,0 +1,7 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +public interface SecuredPersonInterface { + + String getPublicName(); + +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java index 9621a6fdde84a..a7da6fb828626 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java @@ -61,6 +61,128 @@ public Person getPerson() { return person; } + @EnableSecureSerialization + @GET + @Path("/frog") + public Frog getFrog() { + var frog = new Frog(); + frog.setPartner(new Frog()); + var pond = new Pond(); + pond.setName("Atlantic Ocean"); + pond.setWaterQuality(Pond.WaterQuality.CLEAR); + frog.setPonds(List.of(pond)); + return frog; + } + + @EnableSecureSerialization + @GET + @Path("/frog-body-parts") + public FrogBodyParts getFrogBodyParts() { + return new FrogBodyParts("protruding eyes"); + } + + @EnableSecureSerialization + @GET + @Path("/interface-dog") + public SecuredPersonInterface getInterfaceDog() { + return createDog(); + } + + @EnableSecureSerialization + @GET + @Path("/abstract-dog") + public AbstractPet getAbstractDog() { + return createDog(); + } + + @EnableSecureSerialization + @GET + @Path("/abstract-named-dog") + public AbstractNamedPet getAbstractNamedDog() { + return createDog(); + } + + @EnableSecureSerialization + @GET + @Path("/dog") + public Dog getDog() { + return createDog(); + } + + @EnableSecureSerialization + @GET + @Path("/abstract-cat") + public AbstractPet getAbstractCat() { + return createCat(); + } + + @EnableSecureSerialization + @GET + @Path("/interface-cat") + public SecuredPersonInterface getInterfaceCat() { + return createCat(); + } + + @EnableSecureSerialization + @GET + @Path("/abstract-named-cat") + public AbstractNamedPet getAbstractNamedCat() { + return createCat(); + } + + @EnableSecureSerialization + @GET + @Path("/cat") + public Cat getCat() { + return createCat(); + } + + @EnableSecureSerialization + @GET + @Path("/unsecured-pet") + public UnsecuredPet getUnsecuredPet() { + return createUnsecuredPet(); + } + + @EnableSecureSerialization + @GET + @Path("/abstract-unsecured-pet") + public AbstractUnsecuredPet getAbstractUnsecuredPet() { + return createUnsecuredPet(); + } + + private static UnsecuredPet createUnsecuredPet() { + var pet = new UnsecuredPet(); + pet.setPublicName("Unknown"); + pet.setVeterinarian(createVeterinarian()); + return pet; + } + + private static Dog createDog() { + var dog = new Dog(); + dog.setPublicAge(5); + dog.setPrivateName("Jack"); + dog.setPublicName("Leo"); + dog.setVeterinarian(createVeterinarian()); + return dog; + } + + private static Cat createCat() { + var cat = new Cat(); + cat.setPublicName("Garfield"); + cat.setPrivateName("Monday"); + cat.setPrivateAge(4); + cat.setVeterinarian(createVeterinarian()); + return cat; + } + + private static Veterinarian createVeterinarian() { + var vet = new Veterinarian(); + vet.setName("Dolittle"); + vet.setTitle("VMD"); + return vet; + } + @CustomSerialization(UnquotedFieldsPersonSerialization.class) @GET @Path("custom-serialized-person") diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java index a36cd7a2aef68..1c8466653817c 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java @@ -31,7 +31,10 @@ public JavaArchive get() { return ShrinkWrap.create(JavaArchive.class) .addClasses(Person.class, SimpleJsonResource.class, User.class, Views.class, SuperClass.class, OtherPersonResource.class, AbstractPersonResource.class, DataItem.class, Item.class, - NoopReaderInterceptor.class, TestIdentityProvider.class, TestIdentityController.class) + NoopReaderInterceptor.class, TestIdentityProvider.class, TestIdentityController.class, + AbstractPet.class, Dog.class, Cat.class, Veterinarian.class, AbstractNamedPet.class, + AbstractUnsecuredPet.class, UnsecuredPet.class, SecuredPersonInterface.class, Frog.class, + Pond.class, FrogBodyParts.class, FrogBodyParts.BodyPart.class) .addAsResource(new StringAsset("admin-expression=admin\n" + "user-expression=user\n" + "birth-date-roles=alice,bob\n"), "application.properties"); @@ -502,4 +505,140 @@ public void testGenericInput() { .contentType("text/plain") .body(is("foo")); } + + @Test + public void testSecureFieldOnAbstractClass() { + // implementor with / without @SecureField returned + testSecuredFieldOnAbstractClass("cat", "dog"); + // abstract class with @SecureField returned + testSecuredFieldOnAbstractClass("abstract-named-cat", "abstract-named-dog"); + // abstract class without @SecureField directly, but with secured field's field returned + testSecuredFieldOnAbstractClass("abstract-cat", "abstract-dog"); + // interface with implementors that have @SecureField + testSecuredFieldOnAbstractClass("interface-cat", "interface-dog"); + } + + @Test + public void testSecureFieldOnlyOnFieldOfReturnTypeField() { + // returns class with @SecureField is only on field's field + testSecuredFieldOnReturnTypeField("unsecured-pet"); + // returns abstract class and only @SecureField is on implementor's field's field + testSecuredFieldOnReturnTypeField("abstract-unsecured-pet"); + } + + @Test + public void testSecureFieldOnCollectionTypeField() { + TestIdentityController.resetRoles().add("max", "max", "user"); + RestAssured + .with() + .auth().preemptive().basic("max", "max") + .get("/simple/frog") + .then() + .statusCode(200) + .body("partner", Matchers.notNullValue()) + .body("ponds[0].name", Matchers.is("Atlantic Ocean")) + .body("ponds[0].waterQuality", Matchers.nullValue()); + TestIdentityController.resetRoles().add("rolfe", "rolfe", "admin"); + RestAssured + .with() + .auth().preemptive().basic("rolfe", "rolfe") + .get("/simple/frog") + .then() + .statusCode(200) + .body("partner", Matchers.notNullValue()) + .body("ponds[0].name", Matchers.is("Atlantic Ocean")) + .body("ponds[0].waterQuality", Matchers.is("CLEAR")); + } + + @Test + public void testSecureFieldOnArrayTypeField() { + TestIdentityController.resetRoles().add("max", "max", "user"); + RestAssured + .with() + .auth().preemptive().basic("max", "max") + .get("/simple/frog-body-parts") + .then() + .statusCode(200) + .body("parts[0].name", Matchers.nullValue()); + TestIdentityController.resetRoles().add("rolfe", "rolfe", "admin"); + RestAssured + .with() + .auth().preemptive().basic("rolfe", "rolfe") + .get("/simple/frog-body-parts") + .then() + .statusCode(200) + .body("parts[0].name", Matchers.is("protruding eyes")); + } + + private static void testSecuredFieldOnReturnTypeField(String subPath) { + TestIdentityController.resetRoles().add("max", "max", "user"); + RestAssured + .with() + .auth().preemptive().basic("max", "max") + .get("/simple/" + subPath) + .then() + .statusCode(200) + .body("publicName", Matchers.is("Unknown")) + .body("veterinarian.name", Matchers.is("Dolittle")) + .body("veterinarian.title", Matchers.nullValue()); + TestIdentityController.resetRoles().add("rolfe", "rolfe", "admin"); + RestAssured + .with() + .auth().preemptive().basic("rolfe", "rolfe") + .get("/simple/" + subPath) + .then() + .statusCode(200) + .body("publicName", Matchers.is("Unknown")) + .body("veterinarian.name", Matchers.is("Dolittle")) + .body("veterinarian.title", Matchers.is("VMD")); + } + + private static void testSecuredFieldOnAbstractClass(String catPath, String dogPath) { + TestIdentityController.resetRoles().add("max", "max", "user"); + RestAssured + .with() + .auth().preemptive().basic("max", "max") + .get("/simple/" + catPath) + .then() + .statusCode(200) + .body("publicName", Matchers.is("Garfield")) + .body("privateName", Matchers.nullValue()) + .body("veterinarian.name", Matchers.is("Dolittle")) + .body("veterinarian.title", Matchers.nullValue()) + .body("privateAge", Matchers.nullValue()); + RestAssured + .with() + .auth().preemptive().basic("max", "max") + .get("/simple/" + dogPath) + .then() + .statusCode(200) + .body("publicName", Matchers.is("Leo")) + .body("privateName", Matchers.nullValue()) + .body("publicAge", Matchers.is(5)) + .body("veterinarian.name", Matchers.is("Dolittle")) + .body("veterinarian.title", Matchers.nullValue()); + TestIdentityController.resetRoles().add("rolfe", "rolfe", "admin"); + RestAssured + .with() + .auth().preemptive().basic("rolfe", "rolfe") + .get("/simple/" + catPath) + .then() + .statusCode(200) + .body("publicName", Matchers.is("Garfield")) + .body("privateName", Matchers.is("Monday")) + .body("privateAge", Matchers.is(4)) + .body("veterinarian.name", Matchers.is("Dolittle")) + .body("veterinarian.title", Matchers.is("VMD")); + RestAssured + .with() + .auth().preemptive().basic("rolfe", "rolfe") + .get("/simple/" + dogPath) + .then() + .statusCode(200) + .body("publicName", Matchers.is("Leo")) + .body("privateName", Matchers.is("Jack")) + .body("publicAge", Matchers.is(5)) + .body("veterinarian.name", Matchers.is("Dolittle")) + .body("veterinarian.title", Matchers.is("VMD")); + } } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/UnsecuredPet.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/UnsecuredPet.java new file mode 100644 index 0000000000000..df6cac9e533d7 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/UnsecuredPet.java @@ -0,0 +1,14 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +public class UnsecuredPet extends AbstractUnsecuredPet { + + private Veterinarian veterinarian; + + public Veterinarian getVeterinarian() { + return veterinarian; + } + + public void setVeterinarian(Veterinarian veterinarian) { + this.veterinarian = veterinarian; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Veterinarian.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Veterinarian.java new file mode 100644 index 0000000000000..6c7c57e55af78 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Veterinarian.java @@ -0,0 +1,27 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import io.quarkus.resteasy.reactive.jackson.SecureField; + +public class Veterinarian { + + private String name; + + @SecureField(rolesAllowed = "admin") + private String title; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } +}