View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertNotNull;
22  import static org.junit.jupiter.api.Assertions.assertThrows;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  
25  import java.io.File;
26  import java.io.FileReader;
27  import java.lang.reflect.Constructor;
28  import java.util.Collections;
29  
30  import org.apache.commons.jexl3.introspection.JexlPermissions;
31  import org.junit.jupiter.api.Test;
32  
33  import com.google.gson.Gson;
34  
35  /**
36   * Tests for pragmas
37   */
38  class ComposePermissionsTest extends JexlTestCase {
39      static final String SAMPLE_JSON = "src/test/scripts/sample.json";
40  
41      /**
42       * Create a new test case.
43       */
44      public ComposePermissionsTest() {
45          super("PermissionsTest");
46      }
47  
48      void runComposePermissions(final JexlPermissions p) throws Exception {
49          final String check = "http://example.com/content.jpg";
50          final File jsonFile = new File(SAMPLE_JSON);
51          final Gson gson = new Gson();
52          final Object json;
53          try (final FileReader reader = new FileReader(jsonFile)) {
54              json = gson.fromJson(reader, Object.class);
55              assertNotNull(json);
56          }
57  
58          // will succeed because the base permissions (UNRESTRICTED) allow the gson LinkedTreeMap
59          final JexlEngine j0 = createEngine(false, p);
60          final JexlScript s0 = j0.createScript("json.pageInfo.pagePic", "json");
61          final Object r0 = s0.execute(null, json);
62          assertEquals(check, r0);
63  
64          // will fail if gson package is denied
65          JexlEngine j1 = createEngine(false, p.compose("com.google.gson.internal {}"));
66          final JexlScript s1 = j1.createScript("json.pageInfo.pagePic", "json");
67          JexlException.Property xproperty = assertThrows(JexlException.Property.class, () -> s1.execute(null, json));
68          assertEquals("pageInfo", xproperty.getProperty());
69  
70          // will fail since gson package is denied
71          j1 = createEngine(false, p.compose("com.google.gson.internal { LinkedTreeMap {} }"));
72          final JexlScript s2 = j1.createScript("json.pageInfo.pagePic", "json");
73          xproperty = assertThrows(JexlException.Property.class, () -> s2.execute(null, json));
74          assertEquals("pageInfo", xproperty.getProperty());
75  
76          // will succeed once the gson package is explicitly allowed (no reach-through via java.util.Map)
77          j1 = createEngine(false, JexlPermissions.RESTRICTED.compose("com.google.gson.internal +{}"));
78          final JexlScript s3 = j1.createScript("json.pageInfo.pagePic", "json");
79          final Object r3 = s3.execute(null, json);
80          assertEquals(check, r3);
81      }
82  
83      @Test
84      void testComposePermissions() throws Exception {
85          runComposePermissions(JexlPermissions.UNRESTRICTED);
86      }
87  
88      @Test
89      void testComposePreservesBaseDenials() throws Exception {
90          // deny markers must survive the copy performed by compose(): a whole-class denial in the
91          // base must still deny class and constructor visibility after composing unrelated rules
92          final Constructor<?> pbCtor = ProcessBuilder.class.getConstructor(String[].class);
93          assertFalse(JexlPermissions.RESTRICTED.allow(pbCtor));
94          assertFalse(JexlPermissions.RESTRICTED.allow(ProcessBuilder.class));
95          final JexlPermissions composed = JexlPermissions.RESTRICTED.compose("java.math +{}");
96          assertFalse(composed.allow(pbCtor));
97          assertFalse(composed.allow(ProcessBuilder.class));
98          assertFalse(composed.allow(Runtime.class));
99          // a whole-package denial in the base must survive composition as well
100         final JexlPermissions pkgDeny = JexlPermissions.parse("java.lang.*", "java.net {}");
101         assertFalse(pkgDeny.allow(java.net.URI.class));
102         final JexlPermissions pkgDenyComposed = pkgDeny.compose("java.math +{}");
103         assertFalse(pkgDenyComposed.allow(java.net.URI.class));
104     }
105 
106     @Test
107     void testComposePreservesBaseAllows() throws Exception {
108         // the allow marker must survive the copy performed by compose(): a whole-class allowance
109         // in the base must not (fail-closed) revoke constructor visibility after composition
110         final Constructor<?> swCtor = java.io.StringWriter.class.getConstructor();
111         assertTrue(JexlPermissions.RESTRICTED.allow(swCtor));
112         assertTrue(JexlPermissions.RESTRICTED.allow(java.io.StringWriter.class));
113         final JexlPermissions composed = JexlPermissions.RESTRICTED.compose("java.math +{}");
114         assertTrue(composed.allow(swCtor));
115         assertTrue(composed.allow(java.io.StringWriter.class));
116     }
117 
118     @Test
119     void testComposePreservesPackageDenialWithException() throws Exception {
120         // When compose() adds a class-specific exception to a package previously marked NOJEXL_PACKAGE,
121         // ALL other classes in that package must remain denied (deny-all-unlisted semantics preserved).
122         final JexlPermissions base = JexlPermissions.parse("java.lang.*", "java.net {}");
123         // before: all of java.net is denied
124         assertFalse(base.allow(java.net.URI.class));
125         assertFalse(base.allow(java.net.URL.class));
126 
127         // compose adds an exception for URI; URL (unlisted) must remain denied
128         final JexlPermissions withException = base.compose("java.net { +URI {} }");
129         assertTrue(withException.allow(java.net.URI.class));
130         assertFalse(withException.allow(java.net.URL.class));
131 
132         // second compose() on the result must also preserve
133         final JexlPermissions withException2 = withException.compose("java.math +{}");
134         assertTrue(withException2.allow(java.net.URI.class));
135         assertFalse(withException2.allow(java.net.URL.class));
136 
137         // RESTRICTED class-level deny (NOJEXL_CLASS) must survive compose that touches java.lang
138         final JexlPermissions restrictedPlus = JexlPermissions.RESTRICTED.compose("java.lang { +ProcessHandle { Info {} } }");
139         assertFalse(restrictedPlus.allow(Thread.class));
140         assertFalse(restrictedPlus.allow(Runtime.class));
141     }
142 
143     @Test
144     void testComposePermissions1() throws Exception {
145         runComposePermissions(new JexlPermissions.Delegate(JexlPermissions.UNRESTRICTED) {
146             @Override
147             public String toString() {
148                 return "delegate:" + base.toString();
149             }
150         });
151     }
152 
153     @Test
154     void testComposePermissions2() throws Exception {
155         runComposePermissions(new JexlPermissions.ClassPermissions(JexlPermissions.UNRESTRICTED, Collections.emptySet()));
156     }
157 }