This is an automated email from the ASF dual-hosted git repository.

henrib pushed a commit to branch JEXL-468b
in repository https://gitbox.apache.org/repos/asf/commons-jexl.git

commit 86ea6a3e36a487c69fe01f869e761340e7a6f917
Author: Henrib <[email protected]>
AuthorDate: Tue Aug 25 17:13:22 2026 +0200

    [JEXL-468] Preserve deny-all package semantics when compose() adds 
class-specific exceptions
    
    When a package was previously marked as NOJEXL_PACKAGE (whole-package 
denial, e.g.
    "com.example.internal {}") and compose() is called with rules that add 
class-specific
    exceptions to that same package, the NOJEXL_PACKAGE sentinel was replaced 
by a plain
    NoJexlPackage whose getNoJexl() returns null for unlisted classes — causing 
deny(Class)
    to fall back to JEXL_CLASS (allow) for every class not explicitly mentioned.
    
    Add DenyAllPackage as the symmetric counterpart of JexlPackage: it returns 
NOJEXL_CLASS
    for any class not explicitly listed, so the deny-all-unlisted semantics of 
the base are
    preserved while individually declared exceptions ("+SafeClass {}") are 
honoured.
    
    PermissionsParser.readPackages() creates a DenyAllPackage (instead of 
NoJexlPackage)
    when merging rules into a package that was previously NOJEXL_PACKAGE or 
DenyAllPackage
    and no explicit polarity sign was given in the compose source.
    
    Co-Authored-By: Claude <[email protected]>
---
 .../jexl3/internal/introspection/Permissions.java  | 22 +++++++++++++++++++
 .../internal/introspection/PermissionsParser.java  | 15 ++++++++++---
 .../commons/jexl3/ComposePermissionsTest.java      | 25 ++++++++++++++++++++++
 3 files changed, 59 insertions(+), 3 deletions(-)

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 5180b740..73e1dd65 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
@@ -251,6 +251,28 @@ public class Permissions implements JexlPermissions {
         }
     }
 
+    /**
+     * A package where ALL unlisted classes are denied.
+     * <p>The symmetric counterpart of {@link JexlPackage}: created by {@code 
compose()} when class-specific
+     * exceptions are added to a package previously marked as {@link 
Markers#NOJEXL_PACKAGE}.
+     * The deny-all-unlisted semantics of the base are preserved while 
allowing individually declared classes.</p>
+     */
+    static class DenyAllPackage extends NoJexlPackage {
+        DenyAllPackage(final Map<String, NoJexlClass> map) {
+            super(map);
+        }
+
+        @Override
+        NoJexlClass getNoJexl(final Class<?> clazz) {
+            final NoJexlClass njc = nojexl.get(classKey(clazz));
+            return njc != null ? njc : NOJEXL_CLASS;
+        }
+
+        @Override public NoJexlPackage copy() {
+            return new DenyAllPackage(copyMap(nojexl));
+        }
+    }
+
     /**
      * Holder for the singleton allow/deny markers and the {@code 
UNRESTRICTED} permission.
      * <p>These live in their own class rather than directly in {@link 
Permissions} to break a
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 4ffd4efc..cdb3baff 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
@@ -404,9 +404,18 @@ public class PermissionsParser {
                             // keep the existing specification if any, 
otherwise default to deny
                             final boolean deny = specified == null ? !(p 
instanceof Permissions.JexlPackage) : specified;
                             final Map<String, Permissions.NoJexlClass> pkgMap 
= p == null ? null : p.nojexl;
-                            return deny
-                                ? new Permissions.NoJexlPackage(pkgMap)
-                                : new Permissions.JexlPackage(pkgMap);
+                            if (deny) {
+                                // when no explicit sign was given and the 
existing entry denies all
+                                // unlisted classes (NOJEXL_PACKAGE or 
DenyAllPackage), preserve that
+                                // deny-all-unlisted semantics for classes not 
covered by the new rules
+                                final boolean denyAll = specified == null
+                                    && (p == Permissions.Markers.NOJEXL_PACKAGE
+                                        || p instanceof 
Permissions.DenyAllPackage);
+                                return denyAll
+                                    ? new Permissions.DenyAllPackage(pkgMap)
+                                    : new Permissions.NoJexlPackage(pkgMap);
+                            }
+                            return new Permissions.JexlPackage(pkgMap);
                         }
                     );
                     i += 1;
diff --git a/src/test/java/org/apache/commons/jexl3/ComposePermissionsTest.java 
b/src/test/java/org/apache/commons/jexl3/ComposePermissionsTest.java
index 10143788..8730c42c 100644
--- a/src/test/java/org/apache/commons/jexl3/ComposePermissionsTest.java
+++ b/src/test/java/org/apache/commons/jexl3/ComposePermissionsTest.java
@@ -115,6 +115,31 @@ class ComposePermissionsTest extends JexlTestCase {
         assertTrue(composed.allow(java.io.StringWriter.class));
     }
 
+    @Test
+    void testComposePreservesPackageDenialWithException() throws Exception {
+        // When compose() adds a class-specific exception to a package 
previously marked NOJEXL_PACKAGE,
+        // ALL other classes in that package must remain denied 
(deny-all-unlisted semantics preserved).
+        final JexlPermissions base = JexlPermissions.parse("java.lang.*", 
"java.net {}");
+        // before: all of java.net is denied
+        assertFalse(base.allow(java.net.URI.class));
+        assertFalse(base.allow(java.net.URL.class));
+
+        // compose adds an exception for URI; URL (unlisted) must remain denied
+        final JexlPermissions withException = base.compose("java.net { +URI {} 
}");
+        assertTrue(withException.allow(java.net.URI.class));
+        assertFalse(withException.allow(java.net.URL.class));
+
+        // second compose() on the result must also preserve
+        final JexlPermissions withException2 = 
withException.compose("java.math +{}");
+        assertTrue(withException2.allow(java.net.URI.class));
+        assertFalse(withException2.allow(java.net.URL.class));
+
+        // RESTRICTED class-level deny (NOJEXL_CLASS) must survive compose 
that touches java.lang
+        final JexlPermissions restrictedPlus = 
JexlPermissions.RESTRICTED.compose("java.lang { +ProcessHandle { Info {} } }");
+        assertFalse(restrictedPlus.allow(Thread.class));
+        assertFalse(restrictedPlus.allow(Runtime.class));
+    }
+
     @Test
     void testComposePermissions1() throws Exception {
         runComposePermissions(new 
JexlPermissions.Delegate(JexlPermissions.UNRESTRICTED) {

Reply via email to