From 0b83f71649b09a6ca34faa8e8fcd5797435d74fd Mon Sep 17 00:00:00 2001 From: jasmith-hs Date: Tue, 14 Jul 2026 14:27:44 -0400 Subject: [PATCH] feat: allowlist enum method invocation and return types Enum values with constant-specific class bodies are realized at runtime as anonymous subclasses (e.g. Op$1) whose getCanonicalName() returns null. The method and return-type allowlists key off canonical class name, so they bailed on null and such enums could never be allowlisted even when the user explicitly added the enum type. Resolve the runtime enum-constant class to its base enum type before the canonical-name check so allowlisting a specific enum works for its values and methods, including constant-specific bodies. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../el/ext/AllowlistMethodValidator.java | 6 ++ .../el/ext/AllowlistReturnTypeValidator.java | 2 +- .../jinjava/el/ext/AllowlistEnumTest.java | 95 +++++++++++++++++++ .../jinjava/testobjects/OperationEnum.java | 18 ++++ .../jinjava/testobjects/SimpleColorEnum.java | 16 ++++ 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/hubspot/jinjava/el/ext/AllowlistEnumTest.java create mode 100644 src/test/java/com/hubspot/jinjava/testobjects/OperationEnum.java create mode 100644 src/test/java/com/hubspot/jinjava/testobjects/SimpleColorEnum.java diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AllowlistMethodValidator.java b/src/main/java/com/hubspot/jinjava/el/ext/AllowlistMethodValidator.java index 7e19b70c1..44f67a700 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/AllowlistMethodValidator.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/AllowlistMethodValidator.java @@ -51,6 +51,12 @@ public Method validateMethod(Method m) { m1 -> { Class clazz = m1.getDeclaringClass(); String canonicalClassName = clazz.getCanonicalName(); + if (canonicalClassName == null) { + Class superclass = clazz.getSuperclass(); + if (superclass != null && superclass.isEnum()) { + canonicalClassName = superclass.getCanonicalName(); + } + } if (canonicalClassName == null) { return false; } diff --git a/src/main/java/com/hubspot/jinjava/el/ext/AllowlistReturnTypeValidator.java b/src/main/java/com/hubspot/jinjava/el/ext/AllowlistReturnTypeValidator.java index e42d79888..58195170a 100644 --- a/src/main/java/com/hubspot/jinjava/el/ext/AllowlistReturnTypeValidator.java +++ b/src/main/java/com/hubspot/jinjava/el/ext/AllowlistReturnTypeValidator.java @@ -50,7 +50,7 @@ public Object validateReturnType(Object o) { if (o instanceof String || o instanceof Number || o instanceof Boolean) { return o; } - Class clazz = o.getClass(); + Class clazz = o instanceof Enum ? ((Enum) o).getDeclaringClass() : o.getClass(); if (clazz.isArray() && allowArrays) { return o; } diff --git a/src/test/java/com/hubspot/jinjava/el/ext/AllowlistEnumTest.java b/src/test/java/com/hubspot/jinjava/el/ext/AllowlistEnumTest.java new file mode 100644 index 000000000..39ff75357 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/el/ext/AllowlistEnumTest.java @@ -0,0 +1,95 @@ +package com.hubspot.jinjava.el.ext; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseJinjavaTest; +import com.hubspot.jinjava.testobjects.OperationEnum; +import com.hubspot.jinjava.testobjects.SimpleColorEnum; +import java.lang.reflect.Method; +import java.time.Month; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; + +public class AllowlistEnumTest extends BaseJinjavaTest { + + private static final AllowlistReturnTypeValidator ENUM_RETURN_TYPE_VALIDATOR = + AllowlistReturnTypeValidator.create( + ReturnTypeValidatorConfig + .builder() + .addAllowedCanonicalClassNames( + SimpleColorEnum.class.getCanonicalName(), + OperationEnum.class.getCanonicalName() + ) + .build() + ); + + private static final AllowlistMethodValidator ENUM_METHOD_VALIDATOR = + AllowlistMethodValidator.create( + MethodValidatorConfig + .builder() + .addAllowedDeclaredMethodsFromCanonicalClassNames( + SimpleColorEnum.class.getCanonicalName(), + OperationEnum.class.getCanonicalName() + ) + .build() + ); + + @Test + public void itAllowsReturningSimpleEnumValue() { + assertThat(ENUM_RETURN_TYPE_VALIDATOR.validateReturnType(SimpleColorEnum.RED)) + .isEqualTo(SimpleColorEnum.RED); + } + + @Test + public void itAllowsReturningConstantSpecificBodyEnumValue() { + assertThat(ENUM_RETURN_TYPE_VALIDATOR.validateReturnType(OperationEnum.PLUS)) + .isEqualTo(OperationEnum.PLUS); + } + + @Test + public void itRejectsReturningNonAllowlistedEnumValue() { + assertThat(ENUM_RETURN_TYPE_VALIDATOR.validateReturnType(Month.JANUARY)).isNull(); + } + + @Test + public void itAllowsInvokingSimpleEnumGetter() throws NoSuchMethodException { + Method getLabel = SimpleColorEnum.class.getMethod("getLabel"); + assertThat(ENUM_METHOD_VALIDATOR.validateMethod(getLabel)).isEqualTo(getLabel); + } + + @Test + public void itAllowsInvokingConstantSpecificBodyEnumMethod() + throws NoSuchMethodException { + Method apply = OperationEnum.PLUS.getClass().getMethod("apply", int.class, int.class); + assertThat(apply.getDeclaringClass().getCanonicalName()).isNull(); + assertThat(ENUM_METHOD_VALIDATOR.validateMethod(apply)).isEqualTo(apply); + } + + @Test + public void itRejectsInvokingNonAllowlistedEnumMethod() throws NoSuchMethodException { + Method getValue = Month.class.getMethod("getValue"); + assertThat(ENUM_METHOD_VALIDATOR.validateMethod(getValue)).isNull(); + } + + @Test + public void itRendersSimpleEnumGetter() { + Map context = new HashMap<>(); + context.put("color", SimpleColorEnum.RED); + assertThat(jinjava.render("{{ color.label }}", context)).isEqualTo("red-label"); + } + + @Test + public void itRendersConstantSpecificBodyEnumMethodInvocation() { + Map context = new HashMap<>(); + context.put("op", OperationEnum.PLUS); + assertThat(jinjava.render("{{ op.apply(2, 3) }}", context)).isEqualTo("5"); + } + + @Test + public void itRendersEnumValueAsName() { + Map context = new HashMap<>(); + context.put("op", OperationEnum.TIMES); + assertThat(jinjava.render("{{ op }}", context)).isEqualTo("TIMES"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/OperationEnum.java b/src/test/java/com/hubspot/jinjava/testobjects/OperationEnum.java new file mode 100644 index 000000000..871b333c4 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/OperationEnum.java @@ -0,0 +1,18 @@ +package com.hubspot.jinjava.testobjects; + +public enum OperationEnum { + PLUS { + @Override + public int apply(int a, int b) { + return a + b; + } + }, + TIMES { + @Override + public int apply(int a, int b) { + return a * b; + } + }; + + public abstract int apply(int a, int b); +} diff --git a/src/test/java/com/hubspot/jinjava/testobjects/SimpleColorEnum.java b/src/test/java/com/hubspot/jinjava/testobjects/SimpleColorEnum.java new file mode 100644 index 000000000..048eccf21 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/testobjects/SimpleColorEnum.java @@ -0,0 +1,16 @@ +package com.hubspot.jinjava.testobjects; + +public enum SimpleColorEnum { + RED("red-label"), + GREEN("green-label"); + + private final String label; + + SimpleColorEnum(String label) { + this.label = label; + } + + public String getLabel() { + return label; + } +}