This is an automated email from the ASF dual-hosted git repository. henrib pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-jexl.git
The following commit(s) were added to refs/heads/master by this push: new 1aeec988 JEXL-419: added positive permission syntax; - added tests, updated javadoc, release notes and changes; 1aeec988 is described below commit 1aeec98893992c799b42fcca7cb1a914a250b33e Author: Henri Biestro <hbies...@cloudera.com> AuthorDate: Sun Jan 28 11:35:51 2024 +0100 JEXL-419: added positive permission syntax; - added tests, updated javadoc, release notes and changes; --- RELEASE-NOTES.txt | 1 + src/changes/changes.xml | 3 + .../jexl3/internal/introspection/ClassMap.java | 3 +- .../jexl3/internal/introspection/Permissions.java | 28 +++++-- .../internal/introspection/PermissionsParser.java | 50 ++++++++----- .../jexl3/introspection/JexlPermissions.java | 39 ++++------ .../org/apache/commons/jexl3/Issues400Test.java | 41 ++++++++++ .../internal/introspection/PermissionsTest.java | 87 +++++++++++++++++++++- 8 files changed, 199 insertions(+), 53 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index b53f7d24..a059780d 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -30,6 +30,7 @@ Version 3.3.1 is source and binary compatible with 3.3. New Features in 3.3.1: ==================== +* JEXL-419: Add permission syntax to allow class/method/field * JEXL-408: Using JexlFeatures is tedious * JEXL-404: Support array-access safe navigation (x?[y]) * JEXL-401: Captured variables should be read-only diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 03e9d0dc..3bfb20bd 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -29,6 +29,9 @@ <body> <release version="3.3.1" date="20YY-MM-DD"> <!-- ADD --> + <action dev="henrib" type="add" issue="JEXL-419"> + Add permission syntax to allow class/method/field + </action> <action dev="henrib" type="add" issue="JEXL-408" due-to="sebb"> Using JexlFeatures is tedious </action> diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMap.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMap.java index fac798e5..f3bd5372 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMap.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMap.java @@ -262,8 +262,7 @@ final class ClassMap { } // now that we've got all methods keyed in, lets organize them by name if (!cache.byKey.isEmpty()) { - final List<Method> lm = new ArrayList<>(cache.byKey.size()); - lm.addAll(cache.byKey.values()); + final List<Method> lm = new ArrayList<>(cache.byKey.values()); // sort all methods by name lm.sort(Comparator.comparing(Method::getName)); // put all lists of methods with same name in byName cache diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java index 692a35e8..e3ca6f1a 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java @@ -56,21 +56,21 @@ public class Permissions implements JexlPermissions { */ static class NoJexlPackage { // the NoJexl class names - protected Map<String, NoJexlClass> nojexl; + protected final Map<String, NoJexlClass> nojexl; /** * Ctor. * @param map the map of NoJexl classes */ NoJexlPackage(final Map<String, NoJexlClass> map) { - this.nojexl = map; + this.nojexl = new ConcurrentHashMap<>(map == null ? Collections.emptyMap() : map); } /** * Default ctor. */ NoJexlPackage() { - this(new ConcurrentHashMap<>()); + this(null); } boolean isEmpty() { return nojexl.isEmpty(); } @@ -80,7 +80,11 @@ public class Permissions implements JexlPermissions { } void addNoJexl(final String key, final NoJexlClass njc) { - nojexl.put(key, njc); + if (njc == null) { + nojexl.remove(key); + } else { + nojexl.put(key, njc); + } } } @@ -120,12 +124,13 @@ public class Permissions implements JexlPermissions { /** * Equivalent of @NoJexl on a ctor, a method or a field in a class. + * <p>Field or method that are named are denied access.</p> */ static class NoJexlClass { // the NoJexl method names (including ctor, name of class) - protected Set<String> methodNames; + protected final Set<String> methodNames; // the NoJexl field names - protected Set<String> fieldNames; + protected final Set<String> fieldNames; NoJexlClass(final Set<String> methods, final Set<String> fields) { methodNames = methods; @@ -151,6 +156,16 @@ public class Permissions implements JexlPermissions { } } + /** + * A positive NoJexl construct that defines what is denied by absence in the set. + * <p>Field or method that are named are the only one allowed access.</p> + */ + static class JexlClass extends NoJexlClass { + @Override boolean deny(final Field field) { return !super.deny(field); } + @Override boolean deny(final Method method) { return !super.deny(method); } + @Override boolean deny(final Constructor<?> method) { return !super.deny(method); } + } + /** Marker for whole NoJexl class. */ static final NoJexlClass NOJEXL_CLASS = new NoJexlClass(Collections.emptySet(), Collections.emptySet()) { @Override boolean deny(final Field field) { @@ -501,7 +516,6 @@ public class Permissions implements JexlPermissions { } // let's walk all super classes clazz = clazz.getSuperclass(); - // walk all superclasses while (clazz != null) { if (!allow(clazz, method, explicit)) { return false; diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/PermissionsParser.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/PermissionsParser.java index 7341ee15..12494b78 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/PermissionsParser.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/PermissionsParser.java @@ -45,9 +45,12 @@ import java.util.concurrent.ConcurrentHashMap; * } * # and eol comment * class0(); # constructors - * method(); # method + * method(); # method is not allowed * field; # field * } # end class0 + * +class1 { + * method(); // only allowed method of class1 + * } * } # end package my.package * </pre> */ @@ -65,6 +68,7 @@ public class PermissionsParser { * Basic ctor. */ public PermissionsParser() { + // nothing besides default member initialization } /** @@ -98,13 +102,12 @@ public class PermissionsParser { } this.packages = packages; this.wildcards = wildcards; - for (final String src : srcs) { - this.src = src; - this.size = src.length(); + for (final String source : srcs) { + this.src = source; + this.size = source.length(); readPackages(); } - final Permissions permissions = new Permissions(wildcards, packages); - return permissions; + return new Permissions(wildcards, packages); } finally { clear(); } @@ -239,7 +242,7 @@ public class PermissionsParser { pname = temp.toString(); temp.setLength(0); i = next; - // consume it if it is a wildcard decl + // consume it if it is a wildcard declaration if (pname.endsWith(".*")) { wildcards.add(pname); pname = null; @@ -250,8 +253,9 @@ public class PermissionsParser { // package mode if (njpackage == null) { if (c == '{') { - njpackage = new Permissions.NoJexlPackage(); - packages.put(pname, njpackage); + njpackage = packages.compute(pname, + (n, p) -> new Permissions.NoJexlPackage(p == null? null : p.nojexl) + ); i += 1; } } else if (c == '}') { @@ -263,7 +267,7 @@ public class PermissionsParser { pname = null; i += 1; } else { - i = readClass(njpackage, null, null, i); + i = readClass(njpackage, true,null, null, i); } } } @@ -271,16 +275,18 @@ public class PermissionsParser { /** * Reads a class permission. * @param njpackage the owning package + * @param nojexl whether the restriction is explicitly denying (true) or allowing (false) members * @param outer the outer class (if any) * @param inner the inner class name (if any) * @param offset the initial parsing position in the source * @return the new parsing position */ - private int readClass(final Permissions.NoJexlPackage njpackage, final String outer, final String inner, final int offset) { + private int readClass(final Permissions.NoJexlPackage njpackage, final boolean nojexl, final String outer, final String inner, final int offset) { final StringBuilder temp = new StringBuilder(); Permissions.NoJexlClass njclass = null; String njname = null; String identifier = inner; + boolean deny = nojexl; int i = offset; int j = -1; boolean isMethod = false; @@ -303,15 +309,18 @@ public class PermissionsParser { } // end of class ? if (njclass != null && c == '}') { - // restrict the whole class - if (njclass.isEmpty()) { - njpackage.addNoJexl(njname, Permissions.NOJEXL_CLASS); - } i += 1; break; } // read an identifier, the class name if (identifier == null) { + // negative or positive set ? + if (c == '-') { + i += 1; + } else if (c == '+') { + deny = false; + i += 1; + } final int next = readIdentifier(temp, i); if (i != next) { identifier = temp.toString(); @@ -327,7 +336,7 @@ public class PermissionsParser { throw new IllegalStateException(unexpected(c, i)); } // if we have a class, it has a name - njclass = new Permissions.NoJexlClass(); + njclass = deny ? new Permissions.NoJexlClass() : new Permissions.JexlClass(); njname = outer != null ? outer + "$" + identifier : identifier; njpackage.addNoJexl(njname, njclass); identifier = null; @@ -335,7 +344,7 @@ public class PermissionsParser { // class member mode if (c == '{') { // inner class - i = readClass(njpackage, njname, identifier, i - 1); + i = readClass(njpackage, deny, njname, identifier, i - 1); identifier = null; continue; } @@ -358,6 +367,13 @@ public class PermissionsParser { } i += 1; } + // empty class means allow or deny all + if (njname != null && njclass.isEmpty()) { + njpackage.addNoJexl(njname, njclass instanceof Permissions.JexlClass + ? Permissions.JEXL_CLASS + : Permissions.NOJEXL_CLASS); + + } return i; } } diff --git a/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java b/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java index aa4759c3..71eeb5d5 100644 --- a/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java +++ b/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java @@ -147,11 +147,16 @@ public interface JexlPermissions { * <li>Syntax for restrictions is a list of package restrictions.</li> * <li>A package restriction is a package name followed by a block (as in curly-bracket block {}) * that contains a list of class restrictions.</li> - * <li>A class restriction is a class name followed by a block of member restrictions.</li> + * <li>A class restriction is a class name prefixed by an optional <code>-</code> or <code>+</code> sign + * followed by a block of member restrictions.</li> * <li>A member restriction can be a class restriction - to restrict * nested classes -, a field which is the Java field name suffixed with <code>;</code>, a method composed of * its Java name suffixed with <code>();</code>. Constructor restrictions are specified like methods using the * class name as method name.</li> + * <li>By default or when prefixed with a <code>-</code>, a class restriction is explicitly denying the members + * declared in its block (or the whole class)</li> + * <li>When prefixed with a <code>+</code>, a class restriction is explicitly allowing the members + * declared in its block (or the whole class)</li> * </ul> * <p> * All overrides and overloads of a constructors or method are allowed or restricted at the same time, @@ -165,6 +170,7 @@ public interface JexlPermissions { * # nojexl like restrictions * my.package.internal {} # the whole package is hidden * my.package { + * +class4 { theMethod(); } # only theMethod can be called in class4 * class0 { * class1 {} # the whole class1 is hidden * class2 { @@ -293,30 +299,16 @@ public interface JexlPermissions { * @return true if constructor is not null and public, false otherwise */ default boolean validate(final Constructor<?> constructor) { - if (constructor == null) { - return false; - } - // field must be public - if (!Modifier.isPublic(constructor.getModifiers())) { - return false; - } - return true; + return constructor != null && Modifier.isPublic(constructor.getModifiers()); } /** * Checks that a method is valid for permission check. * @param method the method - * @return true if method is not null, public, ,ot-synthetic, not-bridge, false otherwise + * @return true if method is not null and public, false otherwise */ default boolean validate(final Method method) { - if (method == null) { - return false; - } - // method must be public - if (!Modifier.isPublic(method.getModifiers())) { - return false; - } - return true; + return method != null && Modifier.isPublic(method.getModifiers()); } /** @@ -325,14 +317,7 @@ public interface JexlPermissions { * @return true if field is not null and public, false otherwise */ default boolean validate(final Field field) { - if (field == null) { - return false; - } - // field must be public - if (!Modifier.isPublic(field.getModifiers())) { - return false; - } - return true; + return field != null && Modifier.isPublic(field.getModifiers()); } /** @@ -383,6 +368,8 @@ public interface JexlPermissions { * set of classes. * <p>Typical use case is to deny access to a package - and thus all its classes - but allow * a few specific classes.</p> + * <p>Note that the newer positive restriction syntax is preferable as in: + * <code>RESTRICTED.compose("java.lang { +Class {} }")</code>.</p> */ final class ClassPermissions extends JexlPermissions.Delegate { /** The set of explicitly allowed classes, overriding the delegate permissions. */ diff --git a/src/test/java/org/apache/commons/jexl3/Issues400Test.java b/src/test/java/org/apache/commons/jexl3/Issues400Test.java index e1f79bb1..0a5695bc 100644 --- a/src/test/java/org/apache/commons/jexl3/Issues400Test.java +++ b/src/test/java/org/apache/commons/jexl3/Issues400Test.java @@ -16,17 +16,26 @@ */ package org.apache.commons.jexl3; +import java.lang.reflect.Method; import java.math.BigDecimal; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.commons.jexl3.internal.introspection.Permissions; +import org.apache.commons.jexl3.introspection.JexlPermissions; +import org.apache.commons.jexl3.introspection.JexlSandbox; import org.junit.Assert; import org.junit.Test; +import static org.apache.commons.jexl3.introspection.JexlPermissions.RESTRICTED; +import static org.hamcrest.MatcherAssert.assertThat; + /** * Test cases for reported issue between JEXL-300 and JEXL-399. */ @@ -333,4 +342,36 @@ public class Issues400Test { result = script.execute(null, 42); Assert.assertEquals("--#42",result.toString() ); } + + @Test + public void test419() throws NoSuchMethodException { + // check RESTRICTED permissions denies call to System::currentTimeMillis() + Method currentTimeMillis = System.class.getMethod("currentTimeMillis"); + Assert.assertFalse(RESTRICTED.allow(currentTimeMillis)); + // compose using a positive class permission to allow just System::currentTimeMillis() + JexlPermissions permissions = RESTRICTED.compose("java.lang { +System { currentTimeMillis(); } }"); + // check no side effect on compose + Assert.assertTrue(permissions.allow(currentTimeMillis)); + Assert.assertFalse(RESTRICTED.allow(currentTimeMillis)); + + // An engine with the System class as namespace and the positive permissions + JexlEngine jexl = new JexlBuilder() + .namespaces(Collections.singletonMap("sns", System.class)) + .permissions(permissions) + .create(); + + AtomicLong result = new AtomicLong(); + Assert.assertEquals(0, result.get()); + long now = System.currentTimeMillis(); + // calling System::currentTimeMillis() is allowed and behaves as expected + jexl.createScript("result.set(sns:currentTimeMillis())", "result").execute(null, result); + Assert.assertTrue(result.get() >= now); + + // we still cant call anything else + try { + jexl.createScript("sns:gc()").execute(null); + } catch(JexlException.Method method) { + Assert.assertEquals("gc", method.getMethod()); + } + } } diff --git a/src/test/java/org/apache/commons/jexl3/internal/introspection/PermissionsTest.java b/src/test/java/org/apache/commons/jexl3/internal/introspection/PermissionsTest.java index 62e88ec1..d6bc7532 100644 --- a/src/test/java/org/apache/commons/jexl3/internal/introspection/PermissionsTest.java +++ b/src/test/java/org/apache/commons/jexl3/internal/introspection/PermissionsTest.java @@ -41,6 +41,8 @@ import org.apache.commons.jexl3.introspection.JexlUberspect; import org.junit.Assert; import org.junit.Test; +import static org.apache.commons.jexl3.introspection.JexlPermissions.RESTRICTED; + /** * Checks the CacheMap.MethodKey implementation */ @@ -229,7 +231,7 @@ public class PermissionsTest { } @Test - public void testParsePermissions0() throws Exception { + public void testParsePermissions0a() throws Exception { final String src = "java.lang { Runtime { exit(); exec(); } }\njava.net { URL {} }"; final Permissions p = (Permissions) JexlPermissions.parse(src); final Map<String, Permissions.NoJexlPackage> nojexlmap = p.getPackages(); @@ -242,10 +244,93 @@ public class PermissionsTest { final Method exec = getMethod(java.lang.Runtime.class,"exec"); Assert.assertNotNull(exec); Assert.assertFalse(p.allow(exec)); + final Method avp = getMethod(java.lang.Runtime.class,"availableProcessors"); + Assert.assertNotNull(avp); + Assert.assertTrue(p.allow(avp)); final JexlUberspect uber = new Uberspect(null, null, p); Assert.assertNull(uber.getClassByName("java.net.URL")); } + @Test + public void testParsePermissions0b() throws Exception { + final String src = "java.lang { -Runtime { exit(); } }"; + final Permissions p = (Permissions) JexlPermissions.parse(src); + final Method exit = getMethod(java.lang.Runtime.class,"exit"); + Assert.assertNotNull(exit); + Assert.assertFalse(p.allow(exit)); + } + + @Test + public void testParsePermissions0c() throws Exception { + final String src = "java.lang { +Runtime { availableProcessorCount(); } }"; + final Permissions p = (Permissions) JexlPermissions.parse(src); + final Method exit = getMethod(java.lang.Runtime.class,"exit"); + Assert.assertNotNull(exit); + Assert.assertFalse(p.allow(exit)); + } + + @Test + public void testParsePermissions0d() throws Exception { + final String src = "java.lang { +System { currentTimeMillis(); } }"; + final JexlPermissions p = RESTRICTED.compose(src); + final Field in = System.class.getField("in"); + Assert.assertNotNull(in); + Assert.assertFalse(p.allow(in)); + final Method ctm = getMethod(java.lang.System.class,"currentTimeMillis"); + Assert.assertNotNull(ctm); + Assert.assertTrue(p.allow(ctm)); + } + + @Test + public void testParsePermissions0e() throws Exception { + final String src = "java.lang { +System { in; } }"; + final JexlPermissions p = RESTRICTED.compose(src); + final Field in = System.class.getField("in"); + Assert.assertNotNull(in); + Assert.assertTrue(p.allow(in)); + final Method ctm = getMethod(java.lang.System.class,"currentTimeMillis"); + Assert.assertNotNull(ctm); + Assert.assertFalse(p.allow(ctm)); + } + + @Test + public void testParsePermissions0f() throws Exception { + final String src = "java.lang { +Class { getName(); getSimpleName(); } }"; + final JexlPermissions p = RESTRICTED.compose(src); + final Method getName = getMethod(java.lang.Class.class,"getName"); + Assert.assertNotNull(getName); + Assert.assertTrue(p.allow(getName)); + Assert.assertFalse(RESTRICTED.allow(getName)); + final Method getSimpleName = getMethod(java.lang.Class.class,"getSimpleName"); + Assert.assertNotNull(getSimpleName); + Assert.assertTrue(p.allow(getSimpleName)); + Assert.assertFalse(RESTRICTED.allow(getSimpleName)); + + final Method getMethod = getMethod(java.lang.Class.class,"getMethod"); + Assert.assertNotNull(getMethod); + Assert.assertFalse(p.allow(getMethod)); + + final Method exit = getMethod(java.lang.Runtime.class,"exit"); + Assert.assertNotNull(exit); + Assert.assertFalse(p.allow(exit)); + } + + @Test + public void testParsePermissions0g() throws Exception { + final String src = "java.lang { +Class { } }"; + final JexlPermissions p = RESTRICTED.compose(src); + final Method getName = getMethod(java.lang.Class.class,"getName"); + Assert.assertNotNull(getName); + Assert.assertTrue(p.allow(getName)); + final Method getMethod = getMethod(java.lang.Class.class,"getMethod"); + Assert.assertNotNull(getMethod); + Assert.assertTrue(p.allow(getMethod)); + + final Method exit = getMethod(java.lang.Runtime.class,"exit"); + Assert.assertNotNull(exit); + Assert.assertFalse(p.allow(exit)); + } + @Test public void testParsePermissions1() { final String[] src = {