diff --git a/pom.xml b/pom.xml index a2dfc67d7..58b1ccfd0 100644 --- a/pom.xml +++ b/pom.xml @@ -134,6 +134,9 @@ Ales Dolecek + + Nick Müller + diff --git a/src/main/java/org/apache/commons/scxml2/env/jexl/JexlEvaluator.java b/src/main/java/org/apache/commons/scxml2/env/jexl/JexlEvaluator.java index 643e37360..75f08de51 100644 --- a/src/main/java/org/apache/commons/scxml2/env/jexl/JexlEvaluator.java +++ b/src/main/java/org/apache/commons/scxml2/env/jexl/JexlEvaluator.java @@ -74,11 +74,21 @@ public Evaluator getEvaluator(final SCXML document) { /** The internal JexlEngine instance to use. */ private transient volatile JexlEngine jexlEngine; + /** Optional: saves user defined packages, which JEXL should allow or deny for evaluation */ + private String[] customJexlPermissions; + /** Constructor. */ public JexlEvaluator() { jexlEngine = getJexlEngine(); } + /** Constructor with a given builder to set up specific options */ + public JexlEvaluator(JexlEvaluatorBuilder builder) { + customJexlPermissions = builder.getJexlPermissions(); + jexlEngine = getJexlEngine(); + } + + @Override public String getSupportedDatamodel() { return SUPPORTED_DATA_MODEL; @@ -176,7 +186,7 @@ public Context newContext(final Context parent) { /** * Create the internal JexlEngine member during the initialization. - * This method can be overriden to specify more detailed options + * This method can be overridden to specify more detailed options * into the JexlEngine. * @return new JexlEngine instance */ @@ -185,7 +195,13 @@ protected JexlEngine createJexlEngine() { // See javadoc of org.apache.commons.jexl2.JexlEngine#setFunctions(Map funcs) for detail. final Map funcs = new HashMap<>(); funcs.put(null, JexlBuiltin.class); + JexlPermissions permissions = JexlPermissions.RESTRICTED.compose("org.apache.commons.scxml2.*"); + + if (customJexlPermissions != null && customJexlPermissions.length > 0) { + permissions = permissions.compose(customJexlPermissions); + } + return new JexlBuilder().permissions(permissions).namespaces(funcs).cache(256).create(); } diff --git a/src/main/java/org/apache/commons/scxml2/env/jexl/JexlEvaluatorBuilder.java b/src/main/java/org/apache/commons/scxml2/env/jexl/JexlEvaluatorBuilder.java new file mode 100644 index 000000000..dd62f87bb --- /dev/null +++ b/src/main/java/org/apache/commons/scxml2/env/jexl/JexlEvaluatorBuilder.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.scxml2.env.jexl; + +import java.util.ArrayList; +import java.util.List; + +public class JexlEvaluatorBuilder { + + private final List allowedPackages = new ArrayList<>(); + private final List deniedClasses = new ArrayList<>(); + + /** + * Adds a whole package to the JEXL permissions and allows to use this classes in JEXL expressions. + * + * @param fullQualifiedPackageName expects the complete path f.e. "org.apache.commons.logging" + */ + public JexlEvaluatorBuilder addAllowedPackage(String fullQualifiedPackageName) { + allowedPackages.add(fullQualifiedPackageName); + return this; + } + + /** + * Adds a specific class, which is not allowed to be used in JEXL expressions. + * + * @param fullQualifiedClassName expects a complete path f.e. "org.apache.commons.logging.Logger" + */ + public JexlEvaluatorBuilder addDeniedClass(String fullQualifiedClassName) { + deniedClasses.add(fullQualifiedClassName); + return this; + } + + /** + * Creates a JexlEvaluator by the defined options. + */ + public JexlEvaluator build() { + return new JexlEvaluator(this); + } + + /** + * Package-private: converts the user given information to JEXL understandable src-list + */ + String[] getJexlPermissions() { + List permissions = new ArrayList<>(); + + for (String allowedPackage : allowedPackages) { + permissions.add(allowedPackage.replace(".*", "") + ".*"); + } + + for (String deniedClass : deniedClasses) { + final int lastDot = deniedClass.lastIndexOf('.'); + String packageName = deniedClass.substring(0, lastDot); + String className = deniedClass.substring(lastDot + 1); + permissions.add(packageName + " { " + className + " {} }"); + } + + return permissions.toArray(new String[]{}); + } +} + diff --git a/src/test/java/com/custom/Payload.java b/src/test/java/com/custom/Payload.java new file mode 100644 index 000000000..d32ca832f --- /dev/null +++ b/src/test/java/com/custom/Payload.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.custom; + +public class Payload { + + private final int id; + private final String someString; + + public Payload(int id, String someString) { + this.id = id; + this.someString = someString; + } + + public int getId() { + return id; + } + + public String getSomeString() { + return someString; + } +} diff --git a/src/test/java/org/apache/commons/scxml2/SCXMLExecutorTest.java b/src/test/java/org/apache/commons/scxml2/SCXMLExecutorTest.java index 90b2cf6cf..af14aebc7 100644 --- a/src/test/java/org/apache/commons/scxml2/SCXMLExecutorTest.java +++ b/src/test/java/org/apache/commons/scxml2/SCXMLExecutorTest.java @@ -21,6 +21,9 @@ import java.util.Map; import java.util.Set; +import com.custom.Payload; +import org.apache.commons.scxml2.env.jexl.JexlEvaluator; +import org.apache.commons.scxml2.env.jexl.JexlEvaluatorBuilder; import org.apache.commons.scxml2.model.EnterableState; import org.apache.commons.scxml2.model.TransitionTarget; import org.junit.jupiter.api.Assertions; @@ -289,6 +292,20 @@ public void testSCXMLExecutorFinalDoneData() throws Exception { Assertions.assertEquals("done", exec.getFinalDoneData()); } + @Test + public void testSCXMLExecutorWithExternalPayloadObject() throws Exception { + final SCXMLExecutor exec = SCXMLTestHelper.getExecutor("org/apache/commons/scxml2/external-payload.xml"); + final JexlEvaluator evaluator = new JexlEvaluatorBuilder() + .addAllowedPackage("com.custom") + .build(); + + exec.setEvaluator(evaluator); + + exec.go(); + SCXMLTestHelper.assertPostTriggerState(exec, "done", new Payload(1, "someString"), "end"); + Assertions.assertEquals(1, exec.getGlobalContext().getVars().get("idFromEventPayload")); + } + private void checkMicrowave01Sample(final SCXMLExecutor exec) throws Exception { final Set currentStates = SCXMLTestHelper.fireEvent(exec, "turn_on"); Assertions.assertEquals(1, currentStates.size()); diff --git a/src/test/java/org/apache/commons/scxml2/env/jexl/JexlEvaluatorBuilderTest.java b/src/test/java/org/apache/commons/scxml2/env/jexl/JexlEvaluatorBuilderTest.java new file mode 100644 index 000000000..514c031dc --- /dev/null +++ b/src/test/java/org/apache/commons/scxml2/env/jexl/JexlEvaluatorBuilderTest.java @@ -0,0 +1,35 @@ +package org.apache.commons.scxml2.env.jexl; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class JexlEvaluatorBuilderTest { + + @Test + public void testAddAllowedPackages() { + + final String[] jexlPermissions = new JexlEvaluatorBuilder() + .addAllowedPackage("org.apache.commons.scxml2") + .addAllowedPackage("org.apache.commons.logging") + .getJexlPermissions(); + + Assertions.assertEquals(2, jexlPermissions.length); + Assertions.assertEquals("org.apache.commons.scxml2.*", jexlPermissions[0]); + Assertions.assertEquals("org.apache.commons.logging.*", jexlPermissions[1]); + } + + @Test + public void testAllowedPackagesAndDeniedClasses() { + + final String[] jexlPermissions = new JexlEvaluatorBuilder() + .addAllowedPackage("org.apache.commons.scxml2") + .addDeniedClass("org.apache.commons.scxml2.Builtin") + .addDeniedClass("org.apache.commons.logging.Logger") + .getJexlPermissions(); + + Assertions.assertEquals(3, jexlPermissions.length); + Assertions.assertEquals("org.apache.commons.scxml2.*", jexlPermissions[0]); + Assertions.assertEquals("org.apache.commons.scxml2 { Builtin {} }", jexlPermissions[1]); + Assertions.assertEquals("org.apache.commons.logging { Logger {} }", jexlPermissions[2]); + } +} \ No newline at end of file diff --git a/src/test/java/org/apache/commons/scxml2/env/jexl/JexlEvaluatorTest.java b/src/test/java/org/apache/commons/scxml2/env/jexl/JexlEvaluatorTest.java index 2c0c01495..8b462b805 100644 --- a/src/test/java/org/apache/commons/scxml2/env/jexl/JexlEvaluatorTest.java +++ b/src/test/java/org/apache/commons/scxml2/env/jexl/JexlEvaluatorTest.java @@ -16,6 +16,7 @@ */ package org.apache.commons.scxml2.env.jexl; +import com.custom.Payload; import org.apache.commons.scxml2.Context; import org.apache.commons.scxml2.Evaluator; import org.apache.commons.scxml2.SCXMLExpressionException; @@ -60,4 +61,20 @@ public void testErrorMessage() { "JexlEvaluator: Incorrect error message"); } + @Test + void testEvalInCustomClass() throws SCXMLExpressionException { + + // Arrange + final JexlEvaluator eval = new JexlEvaluatorBuilder() + .addAllowedPackage("com.custom") + .build(); + + ctx.set("payload", new Payload(1, "hello")); + + // Act + final Object result = eval.evalScript(ctx, "payload.getId()"); + + // Assert + Assertions.assertEquals(1, result); + } } diff --git a/src/test/java/org/apache/commons/scxml2/external-payload.xml b/src/test/java/org/apache/commons/scxml2/external-payload.xml new file mode 100644 index 000000000..47cd63dd7 --- /dev/null +++ b/src/test/java/org/apache/commons/scxml2/external-payload.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + +