diff --git a/wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveConfig.java b/wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveConfig.java index 075724690..c921ce6c1 100644 --- a/wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveConfig.java +++ b/wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveConfig.java @@ -16,7 +16,6 @@ package io.cdap.wrangler.api; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.gson.Gson; @@ -48,33 +47,37 @@ * "json-parser" : "parse-as-json", * "js-parser" : "parse-as-json" * }, - * "jexlAllowlist" : [ - * { - * "className": "java.lang.Runtime", - * "methods": ["*"], - * "properties": ["*"] - * } - * ] + * "jexlConfiguration" : { + * "jexlAllowlistEnabled": true, + * "jexlAllowlist" : [ + * { + * "className": "java.lang.Runtime", + * "methods": ["*"], + * "properties": ["*"] + * } + * ] + * } * } */ @Deprecated public final class DirectiveConfig { - public static final DirectiveConfig EMPTY = new DirectiveConfig(null, null, Collections.emptyList()); + public static final DirectiveConfig EMPTY = + new DirectiveConfig(null, null, new JexlConfiguration(false, Collections.emptyList())); public static final String EXCLUSIONS_KEY = "exclusions"; public static final String ALIASES_KEY = "aliases"; - public static final String JEXL_ALLOWLIST_KEY = "jexlAllowlist"; + public static final String JEXL_CONFIGURATION_KEY = "jexlConfiguration"; private final ImmutableSet exclusions; private final ImmutableMap aliases; - @Nullable private final ImmutableList jexlAllowlist; + @Nullable private final JexlConfiguration jexlConfiguration; public DirectiveConfig( @Nullable Set exclusions, @Nullable Map aliases, - @Nullable List jexlAllowlist) { + @Nullable JexlConfiguration jexlConfiguration) { this.exclusions = exclusions != null ? ImmutableSet.copyOf(exclusions) : ImmutableSet.of(); this.aliases = aliases != null ? ImmutableMap.copyOf(aliases) : ImmutableMap.of(); - this.jexlAllowlist = jexlAllowlist != null ? ImmutableList.copyOf(jexlAllowlist) : null; + this.jexlConfiguration = jexlConfiguration; } /** @@ -96,13 +99,13 @@ public Map getAliases() { } /** - * Gets the list of JEXL inclusions. + * Gets the JEXL configuration. * - * @return the list of JEXL inclusions + * @return the JEXL configuration */ @Nullable - public List getJexlAllowlist() { - return jexlAllowlist; + public JexlConfiguration getJexlConfiguration() { + return jexlConfiguration; } /** @@ -160,7 +163,7 @@ public JsonElement toJson() { JsonObject object = new JsonObject(); object.add(EXCLUSIONS_KEY, gson.toJsonTree(exclusions)); object.add(ALIASES_KEY, gson.toJsonTree(aliases)); - object.add(JEXL_ALLOWLIST_KEY, gson.toJsonTree(jexlAllowlist)); + object.add(JEXL_CONFIGURATION_KEY, gson.toJsonTree(jexlConfiguration)); return object; } } diff --git a/wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveConfigDeserializer.java b/wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveConfigDeserializer.java index 814de8bb3..c1f4853c1 100644 --- a/wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveConfigDeserializer.java +++ b/wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveConfigDeserializer.java @@ -24,7 +24,6 @@ import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; -import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; @@ -35,7 +34,6 @@ public final class DirectiveConfigDeserializer implements JsonDeserializer { private static final Type STRING_SET_TYPE = new TypeToken>() { }.getType(); private static final Type STRING_MAP_TYPE = new TypeToken>() { }.getType(); - private static final Type JEXL_ALLOWLIST_LIST_TYPE = new TypeToken>() { }.getType(); @Override public DirectiveConfig deserialize(JsonElement configJson, Type typeOfT, JsonDeserializationContext ctx) @@ -45,7 +43,7 @@ public DirectiveConfig deserialize(JsonElement configJson, Type typeOfT, JsonDes return new DirectiveConfig( deserializeProperty(configJsonObj, DirectiveConfig.EXCLUSIONS_KEY, STRING_SET_TYPE, ctx), deserializeProperty(configJsonObj, DirectiveConfig.ALIASES_KEY, STRING_MAP_TYPE, ctx), - deserializeProperty(configJsonObj, DirectiveConfig.JEXL_ALLOWLIST_KEY, JEXL_ALLOWLIST_LIST_TYPE, ctx)); + deserializeProperty(configJsonObj, DirectiveConfig.JEXL_CONFIGURATION_KEY, JexlConfiguration.class, ctx)); } @Nullable diff --git a/wrangler-api/src/main/java/io/cdap/wrangler/api/JexlAllowlistDeserializer.java b/wrangler-api/src/main/java/io/cdap/wrangler/api/JexlAllowlistDeserializer.java index 907c0cfa1..ba8bdae5c 100644 --- a/wrangler-api/src/main/java/io/cdap/wrangler/api/JexlAllowlistDeserializer.java +++ b/wrangler-api/src/main/java/io/cdap/wrangler/api/JexlAllowlistDeserializer.java @@ -26,6 +26,7 @@ import java.lang.reflect.Type; import java.util.List; +import javax.annotation.Nullable; /** * Custom GSON deserializer for {@link JexlAllowlist}. @@ -55,13 +56,10 @@ public JexlAllowlist deserialize(JsonElement allowlistJson, Type typeOfT, JsonDe } } + @Nullable private static T deserializeProperty( - JsonObject obj, String key, Type type, T defaultValue, JsonDeserializationContext ctx) { + JsonObject obj, String key, Type type, @Nullable T defaultValue, JsonDeserializationContext ctx) { JsonElement element = obj.get(key); - if (element == null || element.isJsonNull()) { - return defaultValue; - } - T result = ctx.deserialize(element, type); - return result != null ? result : defaultValue; + return (element != null && !element.isJsonNull()) ? ctx.deserialize(element, type) : defaultValue; } } diff --git a/wrangler-api/src/main/java/io/cdap/wrangler/api/JexlConfiguration.java b/wrangler-api/src/main/java/io/cdap/wrangler/api/JexlConfiguration.java new file mode 100644 index 000000000..4c762f306 --- /dev/null +++ b/wrangler-api/src/main/java/io/cdap/wrangler/api/JexlConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2026 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.cdap.wrangler.api; + +import com.google.common.collect.ImmutableList; +import io.cdap.wrangler.api.annotations.PublicEvolving; + +import java.util.List; +import javax.annotation.Nullable; + +/** + * Defines configuration for JEXL allowlisting. + */ +@PublicEvolving +public final class JexlConfiguration { + public static final String JEXL_ALLOWLIST_ENABLED_KEY = "jexlAllowlistEnabled"; + public static final String JEXL_ALLOWLIST_KEY = "jexlAllowlist"; + + private final boolean jexlAllowlistEnabled; + @Nullable + private final List jexlAllowlist; + + public JexlConfiguration(boolean jexlAllowlistEnabled, @Nullable List jexlAllowlist) { + this.jexlAllowlistEnabled = jexlAllowlistEnabled; + this.jexlAllowlist = jexlAllowlist != null ? ImmutableList.copyOf(jexlAllowlist) : null; + } + + public boolean isJexlAllowlistEnabled() { + return jexlAllowlistEnabled; + } + + @Nullable + public List getJexlAllowlist() { + return jexlAllowlist; + } +} diff --git a/wrangler-api/src/main/java/io/cdap/wrangler/api/JexlConfigurationDeserializer.java b/wrangler-api/src/main/java/io/cdap/wrangler/api/JexlConfigurationDeserializer.java new file mode 100644 index 000000000..6a9719770 --- /dev/null +++ b/wrangler-api/src/main/java/io/cdap/wrangler/api/JexlConfigurationDeserializer.java @@ -0,0 +1,58 @@ +/* + * Copyright © 2026 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.cdap.wrangler.api; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; +import io.cdap.wrangler.api.annotations.PublicEvolving; + +import java.lang.reflect.Type; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Custom GSON deserializer for {@link JexlConfiguration}. + */ +@PublicEvolving +public final class JexlConfigurationDeserializer implements JsonDeserializer { + private static final Type JEXL_ALLOWLIST_LIST_TYPE = new TypeToken>() { }.getType(); + + @Override + public JexlConfiguration deserialize(JsonElement jexlConfigJson, Type typeOfT, JsonDeserializationContext ctx) + throws JsonParseException { + if (!jexlConfigJson.isJsonObject()) { + throw new JsonParseException("Expected jexlConfiguration to be a JSON object."); + } + JsonObject jexlConfigJsonObj = jexlConfigJson.getAsJsonObject(); + + return new JexlConfiguration( + deserializeProperty(jexlConfigJsonObj, JexlConfiguration.JEXL_ALLOWLIST_ENABLED_KEY, boolean.class, true, ctx), + deserializeProperty( + jexlConfigJsonObj, JexlConfiguration.JEXL_ALLOWLIST_KEY, JEXL_ALLOWLIST_LIST_TYPE, null, ctx)); + } + + @Nullable + private static T deserializeProperty( + JsonObject obj, String key, Type type, @Nullable T defaultValue, JsonDeserializationContext ctx) { + JsonElement element = obj.get(key); + return (element != null && !element.isJsonNull()) ? ctx.deserialize(element, type) : defaultValue; + } +} diff --git a/wrangler-api/src/test/java/io/cdap/wrangler/api/DirectiveConfigDeserializerTest.java b/wrangler-api/src/test/java/io/cdap/wrangler/api/DirectiveConfigDeserializerTest.java index 8f03f9120..0c2270f99 100644 --- a/wrangler-api/src/test/java/io/cdap/wrangler/api/DirectiveConfigDeserializerTest.java +++ b/wrangler-api/src/test/java/io/cdap/wrangler/api/DirectiveConfigDeserializerTest.java @@ -33,6 +33,7 @@ public class DirectiveConfigDeserializerTest { private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(DirectiveConfig.class, new DirectiveConfigDeserializer()) .registerTypeAdapter(JexlAllowlist.class, new JexlAllowlistDeserializer()) + .registerTypeAdapter(JexlConfiguration.class, new JexlConfigurationDeserializer()) .create(); @Test @@ -40,13 +41,16 @@ public void testDirectiveConfigDeserialization() { String json = "{\n" + " \"exclusions\": [\"drop\"],\n" + " \"aliases\": {\"p\": \"parse-as-json\"},\n" - + " \"jexlAllowlist\": [\n" - + " {\n" - + " \"className\": \"java.lang.String\",\n" - + " \"methods\": [\"*\"],\n" - + " \"properties\": [\"*\"]\n" - + " }\n" - + " ]\n" + + " \"jexlConfiguration\": {\n" + + " \"jexlAllowlistEnabled\": true,\n" + + " \"jexlAllowlist\": [\n" + + " {\n" + + " \"className\": \"java.lang.String\",\n" + + " \"methods\": [\"*\"],\n" + + " \"properties\": [\"*\"]\n" + + " }\n" + + " ]\n" + + " }\n" + "}"; DirectiveConfig config = GSON.fromJson(json, DirectiveConfig.class); @@ -55,10 +59,12 @@ public void testDirectiveConfigDeserialization() { Assert.assertEquals("parse-as-json", config.getAliasName("p")); Assert.assertEquals(Collections.singleton("drop"), config.getExclusions()); Assert.assertEquals(Collections.singletonMap("p", "parse-as-json"), config.getAliases()); - Assert.assertNotNull(config.getJexlAllowlist()); - Assert.assertEquals(1, config.getJexlAllowlist().size()); + Assert.assertNotNull(config.getJexlConfiguration()); + Assert.assertTrue(config.getJexlConfiguration().isJexlAllowlistEnabled()); + Assert.assertNotNull(config.getJexlConfiguration().getJexlAllowlist()); + Assert.assertEquals(1, config.getJexlConfiguration().getJexlAllowlist().size()); - JexlAllowlist allowlist = config.getJexlAllowlist().get(0); + JexlAllowlist allowlist = config.getJexlConfiguration().getJexlAllowlist().get(0); Assert.assertEquals("java.lang.String", allowlist.getClassName()); Assert.assertTrue(allowlist.allowAllMethods()); Assert.assertTrue(allowlist.allowAllProperties()); @@ -69,13 +75,16 @@ public void testDirectiveConfigDeserializationWithSpecificMethodsAndProperties() String json = "{\n" + " \"exclusions\": [\"drop\"],\n" + " \"aliases\": {\"p\": \"parse-as-json\"},\n" - + " \"jexlAllowlist\": [\n" - + " {\n" - + " \"className\": \"java.lang.String\",\n" - + " \"methods\": [\"trim\", \"substring\"],\n" - + " \"properties\": [\"bytes\"]\n" - + " }\n" - + " ]\n" + + " \"jexlConfiguration\": {\n" + + " \"jexlAllowlistEnabled\": false,\n" + + " \"jexlAllowlist\": [\n" + + " {\n" + + " \"className\": \"java.lang.String\",\n" + + " \"methods\": [\"trim\", \"substring\"],\n" + + " \"properties\": [\"bytes\"]\n" + + " }\n" + + " ]\n" + + " }\n" + "}"; DirectiveConfig config = GSON.fromJson(json, DirectiveConfig.class); @@ -84,10 +93,12 @@ public void testDirectiveConfigDeserializationWithSpecificMethodsAndProperties() Assert.assertEquals("parse-as-json", config.getAliasName("p")); Assert.assertEquals(Collections.singleton("drop"), config.getExclusions()); Assert.assertEquals(Collections.singletonMap("p", "parse-as-json"), config.getAliases()); - Assert.assertNotNull(config.getJexlAllowlist()); - Assert.assertEquals(1, config.getJexlAllowlist().size()); + Assert.assertNotNull(config.getJexlConfiguration()); + Assert.assertFalse(config.getJexlConfiguration().isJexlAllowlistEnabled()); + Assert.assertNotNull(config.getJexlConfiguration().getJexlAllowlist()); + Assert.assertEquals(1, config.getJexlConfiguration().getJexlAllowlist().size()); - JexlAllowlist allowlist = config.getJexlAllowlist().get(0); + JexlAllowlist allowlist = config.getJexlConfiguration().getJexlAllowlist().get(0); Assert.assertEquals("java.lang.String", allowlist.getClassName()); Assert.assertEquals(Arrays.asList("trim", "substring"), allowlist.getMethods()); Assert.assertEquals(Arrays.asList("bytes"), allowlist.getProperties()); @@ -103,19 +114,21 @@ public void testDirectiveConfigDeserializationEmpty() { Assert.assertNotNull(config); Assert.assertTrue(config.getExclusions().isEmpty()); Assert.assertTrue(config.getAliases().isEmpty()); - Assert.assertNull(config.getJexlAllowlist()); + Assert.assertNull(config.getJexlConfiguration()); } @Test(expected = JsonParseException.class) public void testDeserializeInvalidJexlAllowlistEntry() { String json = "{\n" - + " \"jexlAllowlist\": [\n" - + " {\n" - + " \"className\": \"123InvalidClass\",\n" - + " \"methods\": [\"*\"],\n" - + " \"properties\": [\"*\"]\n" - + " }\n" - + " ]\n" + + " \"jexlConfiguration\": {\n" + + " \"jexlAllowlist\": [\n" + + " {\n" + + " \"className\": \"123InvalidClass\",\n" + + " \"methods\": [\"*\"],\n" + + " \"properties\": [\"*\"]\n" + + " }\n" + + " ]\n" + + " }\n" + "}"; GSON.fromJson(json, DirectiveConfig.class); } diff --git a/wrangler-api/src/test/java/io/cdap/wrangler/api/JexlConfigurationDeserializerTest.java b/wrangler-api/src/test/java/io/cdap/wrangler/api/JexlConfigurationDeserializerTest.java new file mode 100644 index 000000000..3cb777956 --- /dev/null +++ b/wrangler-api/src/test/java/io/cdap/wrangler/api/JexlConfigurationDeserializerTest.java @@ -0,0 +1,81 @@ +/* + * Copyright © 2026 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.cdap.wrangler.api; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for {@link JexlConfigurationDeserializer}. + */ +public class JexlConfigurationDeserializerTest { + private static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(JexlAllowlist.class, new JexlAllowlistDeserializer()) + .registerTypeAdapter(JexlConfiguration.class, new JexlConfigurationDeserializer()) + .create(); + + @Test + public void testDeserializeValidConfiguration() { + String json = "{\n" + + " \"jexlAllowlistEnabled\": true,\n" + + " \"jexlAllowlist\": [\n" + + " {\n" + + " \"className\": \"java.lang.Math\",\n" + + " \"methods\": [\"*\"],\n" + + " \"properties\": [\"*\"]\n" + + " }\n" + + " ]\n" + + "}"; + + JexlConfiguration config = GSON.fromJson(json, JexlConfiguration.class); + Assert.assertNotNull(config); + Assert.assertTrue(config.isJexlAllowlistEnabled()); + Assert.assertNotNull(config.getJexlAllowlist()); + Assert.assertEquals(1, config.getJexlAllowlist().size()); + Assert.assertEquals("java.lang.Math", config.getJexlAllowlist().get(0).getClassName()); + } + + @Test + public void testDeserializeEmptyObjectDefaultsToTrue() { + JexlConfiguration config = GSON.fromJson("{}", JexlConfiguration.class); + Assert.assertNotNull(config); + Assert.assertTrue(config.isJexlAllowlistEnabled()); + Assert.assertNull(config.getJexlAllowlist()); + } + + @Test + public void testDeserializeNullJexlAllowlistEnabledDefaultsToTrue() { + JexlConfiguration config = GSON.fromJson("{\"jexlAllowlistEnabled\": null}", JexlConfiguration.class); + Assert.assertNotNull(config); + Assert.assertTrue(config.isJexlAllowlistEnabled()); + } + + @Test + public void testDeserializeExplicitFalseEnabled() { + JexlConfiguration config = GSON.fromJson("{\"jexlAllowlistEnabled\": false}", JexlConfiguration.class); + Assert.assertNotNull(config); + Assert.assertFalse(config.isJexlAllowlistEnabled()); + } + + @Test(expected = JsonParseException.class) + public void testDeserializeNonObjectJsonThrowsJsonParseException() { + GSON.fromJson("\"not-an-object\"", JexlConfiguration.class); + } +} diff --git a/wrangler-api/src/test/java/io/cdap/wrangler/api/JexlConfigurationTest.java b/wrangler-api/src/test/java/io/cdap/wrangler/api/JexlConfigurationTest.java new file mode 100644 index 000000000..23639e225 --- /dev/null +++ b/wrangler-api/src/test/java/io/cdap/wrangler/api/JexlConfigurationTest.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2026 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.cdap.wrangler.api; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +/** + * Tests for {@link JexlConfiguration}. + */ +public class JexlConfigurationTest { + + @Test + public void testIsJexlAllowlistEnabled() { + JexlConfiguration trueEnabledConfig = new JexlConfiguration(true, null); + Assert.assertTrue(trueEnabledConfig.isJexlAllowlistEnabled()); + + JexlConfiguration falseEnabledConfig = new JexlConfiguration(false, null); + Assert.assertFalse(falseEnabledConfig.isJexlAllowlistEnabled()); + } + + @Test + public void testGetJexlAllowlist() { + List allowlists = Collections.singletonList( + new JexlAllowlist("java.lang.Math", Collections.singletonList("*"), Collections.singletonList("*"))); + JexlConfiguration config = new JexlConfiguration(true, allowlists); + Assert.assertNotNull(config.getJexlAllowlist()); + Assert.assertEquals(1, config.getJexlAllowlist().size()); + Assert.assertEquals("java.lang.Math", config.getJexlAllowlist().get(0).getClassName()); + } +} diff --git a/wrangler-core/src/main/java/io/cdap/wrangler/clients/DataPrepServiceClient.java b/wrangler-core/src/main/java/io/cdap/wrangler/clients/DataPrepServiceClient.java index 1aec04766..bed7d4344 100644 --- a/wrangler-core/src/main/java/io/cdap/wrangler/clients/DataPrepServiceClient.java +++ b/wrangler-core/src/main/java/io/cdap/wrangler/clients/DataPrepServiceClient.java @@ -34,6 +34,8 @@ import io.cdap.wrangler.api.DirectiveConfigDeserializer; import io.cdap.wrangler.api.JexlAllowlist; import io.cdap.wrangler.api.JexlAllowlistDeserializer; +import io.cdap.wrangler.api.JexlConfiguration; +import io.cdap.wrangler.api.JexlConfigurationDeserializer; import io.cdap.wrangler.proto.ServiceResponse; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; @@ -58,6 +60,7 @@ public class DataPrepServiceClient { private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(DirectiveConfig.class, new DirectiveConfigDeserializer()) .registerTypeAdapter(JexlAllowlist.class, new JexlAllowlistDeserializer()) + .registerTypeAdapter(JexlConfiguration.class, new JexlConfigurationDeserializer()) .create(); private static final String SYSTEM_NAMESPACE = "system"; diff --git a/wrangler-core/src/main/java/io/cdap/wrangler/parser/ConfigDirectiveContext.java b/wrangler-core/src/main/java/io/cdap/wrangler/parser/ConfigDirectiveContext.java index 33ba45cf0..b53df46ba 100644 --- a/wrangler-core/src/main/java/io/cdap/wrangler/parser/ConfigDirectiveContext.java +++ b/wrangler-core/src/main/java/io/cdap/wrangler/parser/ConfigDirectiveContext.java @@ -21,6 +21,7 @@ import io.cdap.wrangler.api.JexlAllowlist; import java.util.List; +import javax.annotation.Nullable; /** * This class {@link ConfigDirectiveContext} manages the context for directive @@ -29,21 +30,25 @@ */ public class ConfigDirectiveContext implements DirectiveContext { private final DirectiveConfig config; - private final boolean jexlAllowlistEnabled; + private final boolean isJexlAllowlistFeatureFlagEnabled; - public ConfigDirectiveContext(DirectiveConfig config, boolean jexlAllowlistEnabled) { + public ConfigDirectiveContext(DirectiveConfig config, boolean isJexlAllowlistFeatureFlagEnabled) { this.config = config; - this.jexlAllowlistEnabled = jexlAllowlistEnabled; + this.isJexlAllowlistFeatureFlagEnabled = isJexlAllowlistFeatureFlagEnabled; } + @Nullable @Override public List getJexlAllowlist() { - return config.getJexlAllowlist(); + return config.getJexlConfiguration() != null + ? config.getJexlConfiguration().getJexlAllowlist() + : null; } @Override public boolean isJexlAllowlistEnabled() { - return jexlAllowlistEnabled; + return isJexlAllowlistFeatureFlagEnabled + && config.getJexlConfiguration().isJexlAllowlistEnabled(); } /** diff --git a/wrangler-core/src/test/java/io/cdap/wrangler/config/DirectiveConfigTest.java b/wrangler-core/src/test/java/io/cdap/wrangler/config/DirectiveConfigTest.java index 616bfe993..22684250f 100644 --- a/wrangler-core/src/test/java/io/cdap/wrangler/config/DirectiveConfigTest.java +++ b/wrangler-core/src/test/java/io/cdap/wrangler/config/DirectiveConfigTest.java @@ -22,6 +22,8 @@ import io.cdap.wrangler.api.DirectiveConfigDeserializer; import io.cdap.wrangler.api.JexlAllowlist; import io.cdap.wrangler.api.JexlAllowlistDeserializer; +import io.cdap.wrangler.api.JexlConfiguration; +import io.cdap.wrangler.api.JexlConfigurationDeserializer; import org.junit.Assert; import org.junit.Test; @@ -70,6 +72,7 @@ public class DirectiveConfigTest { private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(DirectiveConfig.class, new DirectiveConfigDeserializer()) .registerTypeAdapter(JexlAllowlist.class, new JexlAllowlistDeserializer()) + .registerTypeAdapter(JexlConfiguration.class, new JexlConfigurationDeserializer()) .create(); @Test diff --git a/wrangler-core/src/test/java/io/cdap/wrangler/parser/ConfigDirectiveContextTest.java b/wrangler-core/src/test/java/io/cdap/wrangler/parser/ConfigDirectiveContextTest.java index 27c6c2b4e..17d00cf9e 100644 --- a/wrangler-core/src/test/java/io/cdap/wrangler/parser/ConfigDirectiveContextTest.java +++ b/wrangler-core/src/test/java/io/cdap/wrangler/parser/ConfigDirectiveContextTest.java @@ -23,6 +23,8 @@ import io.cdap.wrangler.api.DirectiveConfigDeserializer; import io.cdap.wrangler.api.JexlAllowlist; import io.cdap.wrangler.api.JexlAllowlistDeserializer; +import io.cdap.wrangler.api.JexlConfiguration; +import io.cdap.wrangler.api.JexlConfigurationDeserializer; import io.cdap.wrangler.api.RecipeException; import io.cdap.wrangler.api.RecipeParser; import io.cdap.wrangler.proto.Contexts; @@ -58,6 +60,7 @@ public class ConfigDirectiveContextTest { private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(DirectiveConfig.class, new DirectiveConfigDeserializer()) .registerTypeAdapter(JexlAllowlist.class, new JexlAllowlistDeserializer()) + .registerTypeAdapter(JexlConfiguration.class, new JexlConfigurationDeserializer()) .create(); @Test(expected = RecipeException.class) @@ -133,4 +136,21 @@ public void testWithNoAliasingNoExclusion() throws Exception { Assert.assertEquals(1, steps.size()); } + @Test + public void testJexlAllowlistEnabled() { + DirectiveConfig configDefault = GSON.fromJson("{\"jexlConfiguration\": {}}", DirectiveConfig.class); + Assert.assertTrue(new ConfigDirectiveContext(configDefault, true).isJexlAllowlistEnabled()); + Assert.assertFalse(new ConfigDirectiveContext(configDefault, false).isJexlAllowlistEnabled()); + + DirectiveConfig configTrue = GSON.fromJson( + "{\"jexlConfiguration\": {\"jexlAllowlistEnabled\": true}}", DirectiveConfig.class); + Assert.assertTrue(new ConfigDirectiveContext(configTrue, true).isJexlAllowlistEnabled()); + Assert.assertFalse(new ConfigDirectiveContext(configTrue, false).isJexlAllowlistEnabled()); + + DirectiveConfig configFalse = GSON.fromJson( + "{\"jexlConfiguration\": {\"jexlAllowlistEnabled\": false}}", DirectiveConfig.class); + Assert.assertFalse(new ConfigDirectiveContext(configFalse, true).isJexlAllowlistEnabled()); + Assert.assertFalse(new ConfigDirectiveContext(configFalse, false).isJexlAllowlistEnabled()); + } + } diff --git a/wrangler-service/src/main/java/io/cdap/wrangler/RequestExtractor.java b/wrangler-service/src/main/java/io/cdap/wrangler/RequestExtractor.java index e048e87f4..e4c9d318e 100644 --- a/wrangler-service/src/main/java/io/cdap/wrangler/RequestExtractor.java +++ b/wrangler-service/src/main/java/io/cdap/wrangler/RequestExtractor.java @@ -24,6 +24,8 @@ import io.cdap.wrangler.api.DirectiveConfigDeserializer; import io.cdap.wrangler.api.JexlAllowlist; import io.cdap.wrangler.api.JexlAllowlistDeserializer; +import io.cdap.wrangler.api.JexlConfiguration; +import io.cdap.wrangler.api.JexlConfigurationDeserializer; import io.cdap.wrangler.dataset.workspace.RequestDeserializer; import io.cdap.wrangler.proto.BadRequestException; import io.cdap.wrangler.proto.Request; @@ -44,6 +46,7 @@ public final class RequestExtractor { .registerTypeAdapter(Request.class, new RequestDeserializer()) .registerTypeAdapter(DirectiveConfig.class, new DirectiveConfigDeserializer()) .registerTypeAdapter(JexlAllowlist.class, new JexlAllowlistDeserializer()) + .registerTypeAdapter(JexlConfiguration.class, new JexlConfigurationDeserializer()) .create(); private final HttpServiceRequest request; public static final String CONTENT_TYPE_HEADER = PropertyIds.CONTENT_TYPE; diff --git a/wrangler-service/src/main/java/io/cdap/wrangler/service/directive/DirectivesHandler.java b/wrangler-service/src/main/java/io/cdap/wrangler/service/directive/DirectivesHandler.java index e6cfcc20d..4382d7a13 100644 --- a/wrangler-service/src/main/java/io/cdap/wrangler/service/directive/DirectivesHandler.java +++ b/wrangler-service/src/main/java/io/cdap/wrangler/service/directive/DirectivesHandler.java @@ -48,6 +48,8 @@ import io.cdap.wrangler.api.DirectiveParseException; import io.cdap.wrangler.api.JexlAllowlist; import io.cdap.wrangler.api.JexlAllowlistDeserializer; +import io.cdap.wrangler.api.JexlConfiguration; +import io.cdap.wrangler.api.JexlConfigurationDeserializer; import io.cdap.wrangler.api.Row; import io.cdap.wrangler.datamodel.DataModelGlossary; import io.cdap.wrangler.dataset.workspace.DataType; @@ -101,6 +103,7 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import javax.annotation.Nullable; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; @@ -120,6 +123,7 @@ public class DirectivesHandler extends AbstractDirectiveHandler { .registerTypeAdapter(Schema.class, new SchemaTypeAdapter()) .registerTypeAdapter(DirectiveConfig.class, new DirectiveConfigDeserializer()) .registerTypeAdapter(JexlAllowlist.class, new JexlAllowlistDeserializer()) + .registerTypeAdapter(JexlConfiguration.class, new JexlConfigurationDeserializer()) .create(); private static final String DATA_MODEL_PROPERTY = "dataModel"; @@ -1041,26 +1045,56 @@ public void uploadConfig(HttpServiceRequest request, HttpServiceResponder respon throw new BadRequestException("Config is empty. Please check if the request is sent as HTTP POST body."); } - List userAllowlist = config.getJexlAllowlist(); - if (jexlAllowlistEnabled && userAllowlist == null) { - throw new BadRequestException("JEXL allowlist cannot be null. Please provide a valid JEXL allowlist."); - } - if (!jexlAllowlistEnabled && userAllowlist != null) { - throw new BadRequestException( - "Updates to JEXL allowlist in the wrangler directives config is disabled."); - } - // No updates to JexlAllowlist when JEXL Allowlist feature is disabled - if (!jexlAllowlistEnabled) { - DirectiveConfig existingConfig = configStore.getConfig(); - List existingAllowlist = existingConfig != null ? existingConfig.getJexlAllowlist() : null; - config = new DirectiveConfig(config.getExclusions(), config.getAliases(), existingAllowlist); - } + JexlConfiguration resolvedJexl = resolveJexlConfiguration(config.getJexlConfiguration()); + config = new DirectiveConfig(config.getExclusions(), config.getAliases(), resolvedJexl); configStore.updateConfig(config); return new ServiceResponse("Successfully updated configuration."); }); } + @Nullable + private JexlConfiguration resolveJexlConfiguration(@Nullable JexlConfiguration jexlConfig) + throws BadRequestException, IOException { + if (!jexlAllowlistEnabled) { + if (jexlConfig != null) { + throw new BadRequestException( + "Updates to JEXL configuration in the wrangler directives config is disabled."); + } + return getExistingJexlConfiguration(); + } + + if (jexlConfig == null) { + throw new BadRequestException( + "JEXL configuration cannot be null. Please provide a valid JEXL configuration."); + } + + return buildJexlConfiguration(jexlConfig); + } + + private JexlConfiguration buildJexlConfiguration(JexlConfiguration jexlConfig) + throws BadRequestException, IOException { + boolean isEnabled = jexlConfig.isJexlAllowlistEnabled(); + List allowlist = jexlConfig.getJexlAllowlist(); + + if (allowlist != null) { + return new JexlConfiguration(isEnabled, allowlist); + } + if (isEnabled) { + throw new BadRequestException( + "JEXL allowlist cannot be null when JEXL configuration is enabled. Please provide a valid JEXL allowlist."); + } + + JexlConfiguration existingJexl = getExistingJexlConfiguration(); + return new JexlConfiguration(false, existingJexl != null ? existingJexl.getJexlAllowlist() : null); + } + + @Nullable + private JexlConfiguration getExistingJexlConfiguration() throws IOException { + DirectiveConfig existingConfig = configStore.getConfig(); + return existingConfig != null ? existingConfig.getJexlConfiguration() : null; + } + /** * Updates the configuration. * @@ -1072,13 +1106,13 @@ public void uploadConfig(HttpServiceRequest request, HttpServiceResponder respon @TransactionPolicy(value = TransactionControl.EXPLICIT) public void getConfig(HttpServiceRequest request, HttpServiceResponder responder) { respond(request, responder, () -> { + enforceDirectiveConfigPermission(StandardPermission.GET); DirectiveConfig config = configStore.getConfig(); if (!jexlAllowlistEnabled) { JsonObject configJson = config.toJson().getAsJsonObject(); - configJson.remove(DirectiveConfig.JEXL_ALLOWLIST_KEY); + configJson.remove(DirectiveConfig.JEXL_CONFIGURATION_KEY); return new ServiceResponse<>(configJson); } - enforceDirectiveConfigPermission(StandardPermission.GET); return new ServiceResponse<>(config); }); } diff --git a/wrangler-service/src/main/java/io/cdap/wrangler/service/directive/RemoteExecutionTask.java b/wrangler-service/src/main/java/io/cdap/wrangler/service/directive/RemoteExecutionTask.java index d330354c8..c8682fe9f 100644 --- a/wrangler-service/src/main/java/io/cdap/wrangler/service/directive/RemoteExecutionTask.java +++ b/wrangler-service/src/main/java/io/cdap/wrangler/service/directive/RemoteExecutionTask.java @@ -36,6 +36,8 @@ import io.cdap.wrangler.api.ExecutorContext; import io.cdap.wrangler.api.JexlAllowlist; import io.cdap.wrangler.api.JexlAllowlistDeserializer; +import io.cdap.wrangler.api.JexlConfiguration; +import io.cdap.wrangler.api.JexlConfigurationDeserializer; import io.cdap.wrangler.api.RecipeException; import io.cdap.wrangler.api.RemoteDirectiveResponse; import io.cdap.wrangler.api.Row; @@ -73,6 +75,7 @@ public class RemoteExecutionTask implements RunnableTask { .registerTypeAdapter(Schema.class, new SchemaTypeAdapter()) .registerTypeAdapter(DirectiveConfig.class, new DirectiveConfigDeserializer()) .registerTypeAdapter(JexlAllowlist.class, new JexlAllowlistDeserializer()) + .registerTypeAdapter(JexlConfiguration.class, new JexlConfigurationDeserializer()) .create(); @Override diff --git a/wrangler-storage/src/main/java/io/cdap/wrangler/dataset/workspace/ConfigStore.java b/wrangler-storage/src/main/java/io/cdap/wrangler/dataset/workspace/ConfigStore.java index bd31c1050..87bf096ae 100644 --- a/wrangler-storage/src/main/java/io/cdap/wrangler/dataset/workspace/ConfigStore.java +++ b/wrangler-storage/src/main/java/io/cdap/wrangler/dataset/workspace/ConfigStore.java @@ -33,6 +33,8 @@ import io.cdap.wrangler.api.DirectiveConfigDeserializer; import io.cdap.wrangler.api.JexlAllowlist; import io.cdap.wrangler.api.JexlAllowlistDeserializer; +import io.cdap.wrangler.api.JexlConfiguration; +import io.cdap.wrangler.api.JexlConfigurationDeserializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,8 +56,9 @@ public class ConfigStore { private static final Logger LOG = LoggerFactory.getLogger(ConfigStore.class); private static final Gson GSON = new GsonBuilder() - .registerTypeAdapter(JexlAllowlist.class, new JexlAllowlistDeserializer()) .registerTypeAdapter(DirectiveConfig.class, new DirectiveConfigDeserializer()) + .registerTypeAdapter(JexlAllowlist.class, new JexlAllowlistDeserializer()) + .registerTypeAdapter(JexlConfiguration.class, new JexlConfigurationDeserializer()) .create(); private static final String KEY_COL = "key"; private static final String VAL_COL = "value"; @@ -78,10 +81,11 @@ public void initialize() throws IOException { if (config == null) { LOG.info("Initializing Directive config with default values"); - updateConfig(new DirectiveConfig(null, null, DefaultJexlAllowlist.get())); - } else if (config.getJexlAllowlist() == null) { - LOG.info("Directive config is configured without JEXL allowlist, adding default JEXL allowlist."); - updateConfig(new DirectiveConfig(config.getExclusions(), config.getAliases(), DefaultJexlAllowlist.get())); + updateConfig(new DirectiveConfig(null, null, new JexlConfiguration(true, DefaultJexlAllowlist.get()))); + } else if (config.getJexlConfiguration() == null) { + LOG.info("Directive config is configured without JEXL configuration, adding default JEXL configuration."); + updateConfig(new DirectiveConfig(config.getExclusions(), config.getAliases(), + new JexlConfiguration(true, DefaultJexlAllowlist.get()))); } } diff --git a/wrangler-transform/src/main/java/io/cdap/wrangler/Wrangler.java b/wrangler-transform/src/main/java/io/cdap/wrangler/Wrangler.java index 4656c72ef..26039ad3c 100644 --- a/wrangler-transform/src/main/java/io/cdap/wrangler/Wrangler.java +++ b/wrangler-transform/src/main/java/io/cdap/wrangler/Wrangler.java @@ -65,6 +65,8 @@ import io.cdap.wrangler.api.ExecutorContext; import io.cdap.wrangler.api.JexlAllowlist; import io.cdap.wrangler.api.JexlAllowlistDeserializer; +import io.cdap.wrangler.api.JexlConfiguration; +import io.cdap.wrangler.api.JexlConfigurationDeserializer; import io.cdap.wrangler.api.RecipeParser; import io.cdap.wrangler.api.RecipePipeline; import io.cdap.wrangler.api.RecipeSymbol; @@ -121,6 +123,7 @@ public class Wrangler extends Transform impl private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(DirectiveConfig.class, new DirectiveConfigDeserializer()) .registerTypeAdapter(JexlAllowlist.class, new JexlAllowlistDeserializer()) + .registerTypeAdapter(JexlConfiguration.class, new JexlConfigurationDeserializer()) .create(); private static final String ON_ERROR_DEFAULT = "fail-pipeline"; private static final String ON_ERROR_FAIL_PIPELINE = "fail-pipeline";