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 java.io.ByteArrayInputStream;
20  import java.io.InputStream;
21  import java.math.MathContext;
22  import java.nio.charset.StandardCharsets;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.function.Predicate;
26  
27  import org.apache.commons.jexl3.introspection.JexlUberspect;
28  import org.junit.jupiter.api.Assertions;
29  import org.junit.jupiter.api.Test;
30  
31  /**
32   * Tests for {@link JexlConfigLoader}.
33   */
34  public class JexlConfigLoaderTest {
35  
36      /** Namespace example: static math helpers callable as {@code mymath:square(n)}. */
37      public static class MathNs {
38          public static int square(final int n) { return n * n; }
39          public static double abs(final double x) { return Math.abs(x); }
40      }
41  
42      /** Permissions example: explicitly allowed by a compose() rule in the YAML. */
43      public static class Permitted {
44          public String greet(final String name) { return "Hello, " + name; }
45      }
46  
47      private static InputStream yaml(final String content) {
48          return new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
49      }
50  
51      private InputStream resource(final String name) {
52          final InputStream in = getClass().getResourceAsStream(name);
53          Assertions.assertNotNull(in, "test resource not found: " + name);
54          return in;
55      }
56  
57      @Test
58      void testLoadFromResource() throws Exception {
59          try (InputStream in = resource("test-config.yaml")) {
60              final JexlBuilder builder = JexlConfigLoader.load(in);
61              Assertions.assertNotNull(builder);
62          }
63      }
64  
65      @Test
66      void testStrictModeFromYaml() throws Exception {
67          try (InputStream in = yaml("strict: true\nsafe: false\n")) {
68              final JexlEngine engine = JexlConfigLoader.load(in).create();
69              Assertions.assertThrows(JexlException.class,
70                  () -> engine.createExpression("undeclaredVar + 1").evaluate(new MapContext()));
71          }
72      }
73  
74      @Test
75      void testFeaturesFromYaml() throws Exception {
76          try (InputStream in = yaml("features:\n  loops: true\n  lambda: true\n  sideEffect: true\n")) {
77              final JexlEngine engine = JexlConfigLoader.load(in).create();
78              final JexlScript loop = engine.createScript(
79                  "var s = 0; for (var i : [1,2,3]) { s += i; } s");
80              Assertions.assertEquals(6, ((Number) loop.execute(new MapContext())).intValue());
81          }
82      }
83  
84      @Test
85      void testNamespaceFromYaml() throws Exception {
86          final String ns = MathNs.class.getName();
87          try (InputStream in = yaml("permissions:\n  base: RESTRICTED\nnamespaces:\n  mymath: " + ns + "\n")) {
88              final JexlEngine engine = JexlConfigLoader.load(in).create();
89              final Object result = engine.createScript("mymath:square(4)")
90                  .execute(new MapContext());
91              Assertions.assertEquals(16, result);
92          }
93      }
94  
95      @Test
96      void testPermissionsFromYaml() throws Exception {
97          try (InputStream in = resource("test-config.yaml")) {
98              final JexlEngine engine = JexlConfigLoader.load(in).create();
99              Assertions.assertThrows(JexlException.class,
100                 () -> engine.createScript("new('java.lang.Runtime')").execute(new MapContext()));
101         }
102     }
103 
104     @Test
105     void testFullResourceConfig() throws Exception {
106         try (InputStream in = resource("test-config.yaml")) {
107             final JexlEngine engine = JexlConfigLoader.load(in).create();
108             Assertions.assertNotNull(engine);
109 
110             Assertions.assertThrows(JexlException.class,
111                 () -> engine.createExpression("ghost + 1").evaluate(new MapContext()));
112 
113             final Object r = engine.createScript("math:random() >= 0").execute(new MapContext());
114             Assertions.assertEquals(Boolean.TRUE, r);
115 
116             final Number sum = (Number) engine.createScript(
117                 "var s = 0; for (var i : [1,2,3]) { s += i; } s").execute(new MapContext());
118             Assertions.assertEquals(6, sum.intValue());
119         }
120     }
121 
122     @Test
123     void testEngineConvenience() throws Exception {
124         try (InputStream in = yaml("strict: false\n")) {
125             final JexlEngine engine = JexlConfigLoader.engine(in);
126             Assertions.assertNotNull(engine);
127         }
128     }
129 
130     @Test
131     void testInvalidArithmeticClass() throws Exception {
132         try (InputStream in = yaml("arithmetic:\n  clazz: com.example.NoSuchClass\n  strict: true\n")) {
133             Assertions.assertThrows(IllegalArgumentException.class,
134                 () -> JexlConfigLoader.load(in));
135         }
136     }
137 
138     @Test
139     void testInvalidNamespaceClass() throws Exception {
140         try (InputStream in = yaml("namespaces:\n  bad: com.example.NoSuchClass\n")) {
141             Assertions.assertThrows(IllegalArgumentException.class,
142                 () -> JexlConfigLoader.load(in));
143         }
144     }
145 
146     @Test
147     void testEmptyYaml() throws Exception {
148         try (InputStream in = yaml("")) {
149             Assertions.assertNotNull(JexlConfigLoader.load(in).create());
150         }
151     }
152 
153     @Test
154     void testCommentOnlyYaml() throws Exception {
155         try (InputStream in = yaml("# just a comment\n")) {
156             Assertions.assertNotNull(JexlConfigLoader.load(in).create());
157         }
158     }
159 
160     @Test
161     void testInlineComment() throws Exception {
162         try (InputStream in = yaml("strict: true  # enable strict mode\ncache: 256 # cache size\n")) {
163             Assertions.assertNotNull(JexlConfigLoader.load(in).create());
164         }
165     }
166 
167     @Test
168     void testImportsList() throws Exception {
169         try (InputStream in = yaml("imports:\n  - java.lang\n  - java.util\n")) {
170             Assertions.assertNotNull(JexlConfigLoader.load(in).create());
171         }
172     }
173 
174     @Test
175     void testPermissionsWithRules() throws Exception {
176         final String canonical = Permitted.class.getCanonicalName();
177         final String binary    = Permitted.class.getName();
178         final String content =
179             "permissions:\n"
180             + "  base: RESTRICTED\n"
181             + "  classes:\n"
182             + "    - " + canonical + "\n";
183         try (InputStream in = yaml(content)) {
184             final JexlEngine engine = JexlConfigLoader.load(in).create();
185             final Object result = engine.createScript(
186                 "new('" + binary + "').greet('World')").execute(new MapContext());
187             Assertions.assertEquals("Hello, World", result);
188         }
189     }
190 
191     @Test
192     void testUnrestrictedBase() throws Exception {
193         try (InputStream in = yaml("permissions:\n  base: UNRESTRICTED\n")) {
194             Assertions.assertNotNull(JexlConfigLoader.load(in).create());
195         }
196     }
197 
198     @Test
199     void testSecureBase() throws Exception {
200         try (InputStream in = yaml("permissions:\n  base: SECURE\n")) {
201             final JexlBuilder builder = JexlConfigLoader.load(in);
202             Assertions.assertSame(org.apache.commons.jexl3.introspection.JexlPermissions.SECURE,
203                 builder.permissions());
204         }
205         // no base: → defaults to NONE (deny everything, build from scratch)
206         try (InputStream in = yaml("permissions:\n  rules:\n")) {
207             final JexlBuilder builder = JexlConfigLoader.load(in);
208             Assertions.assertSame(org.apache.commons.jexl3.introspection.JexlPermissions.NONE,
209                 builder.permissions());
210         }
211     }
212 
213     @Test
214     void testNoneBaseFromYaml() throws Exception {
215         try (InputStream in = yaml("permissions:\n  base: NONE\n  rules:\n    - \"java.lang { +String{} }\"\n")) {
216             final JexlBuilder builder = JexlConfigLoader.load(in);
217             final org.apache.commons.jexl3.introspection.JexlPermissions perms = builder.permissions();
218             Assertions.assertTrue(perms.allow(String.class));
219             Assertions.assertFalse(perms.allow(Integer.class));
220             Assertions.assertFalse(perms.allow(System.class));
221         }
222     }
223 
224     @Test
225     void testMigrationYaml() throws Exception {
226         // jexl.yaml restores RESTRICTED permissions + legacy features (new, loops, side-effects)
227         try (InputStream in = resource("jexl.yaml")) {
228             final JexlEngine engine = JexlConfigLoader.load(in).create();
229             // loop works
230             final Number sum = (Number) engine.createScript(
231                 "var s = 0; for (var i : [1,2,3]) { s += i; } s").execute(new MapContext());
232             Assertions.assertEquals(6, sum.intValue());
233             // new() works
234             final Object list = engine.createScript(
235                 "new('java.util.ArrayList')").execute(new MapContext());
236             Assertions.assertNotNull(list);
237         }
238     }
239 
240     @Test
241     void testLoggingFromYaml() throws Exception {
242         // a named logger wraps the permissions in a logging view
243         try (InputStream in = yaml("permissions:\n  base: RESTRICTED\n  logging: jexl.config.permissions\n")) {
244             final JexlBuilder builder = JexlConfigLoader.load(in);
245             Assertions.assertTrue(builder.permissions()
246                 instanceof org.apache.commons.jexl3.introspection.JexlPermissions.LoggingPermissions);
247         }
248         // a bare "logging:" entry uses the default logger, still a logging view
249         try (InputStream in = yaml("permissions:\n  base: RESTRICTED\n  logging:\n")) {
250             Assertions.assertTrue(JexlConfigLoader.load(in).permissions()
251                 instanceof org.apache.commons.jexl3.introspection.JexlPermissions.LoggingPermissions);
252         }
253         // without a logging entry, no wrapper is applied
254         try (InputStream in = yaml("permissions:\n  base: RESTRICTED\n")) {
255             Assertions.assertFalse(JexlConfigLoader.load(in).permissions()
256                 instanceof org.apache.commons.jexl3.introspection.JexlPermissions.LoggingPermissions);
257         }
258     }
259 
260     @Test
261     void testOptionFlagsFromYaml() throws Exception {
262         final String content =
263             "silent: true\n"
264             + "safe: false\n"
265             + "cancellable: false\n"
266             + "antish: false\n"
267             + "lexical: true\n"
268             + "lexicalShade: true\n"
269             + "strictInterpolation: true\n"
270             + "booleanLogical: true\n";
271         try (InputStream in = yaml(content)) {
272             final JexlBuilder builder = JexlConfigLoader.load(in);
273             final JexlOptions o = builder.options();
274             Assertions.assertTrue(o.isSilent());
275             Assertions.assertFalse(o.isSafe());
276             Assertions.assertFalse(o.isCancellable());
277             Assertions.assertFalse(o.isAntish());
278             Assertions.assertFalse(builder.antish());
279             Assertions.assertTrue(o.isLexical());
280             Assertions.assertTrue(builder.lexical());
281             Assertions.assertTrue(o.isLexicalShade());
282             Assertions.assertTrue(o.isStrictInterpolation());
283             Assertions.assertTrue(o.isBooleanLogical());
284         }
285     }
286 
287     @Test
288     void testNumericAndMiscOptionsFromYaml() throws Exception {
289         final String content =
290             "cache: 256\n"
291             + "cacheThreshold: 32\n"
292             + "stackOverflow: 1024\n"
293             + "collectMode: 1\n"
294             + "debug: true\n"
295             + "charset: UTF-16\n"
296             + "strategy: MAP_STRATEGY\n";
297         try (InputStream in = yaml(content)) {
298             final JexlBuilder builder = JexlConfigLoader.load(in);
299             Assertions.assertEquals(256, builder.cache());
300             Assertions.assertEquals(java.nio.charset.StandardCharsets.UTF_16, builder.charset());
301             Assertions.assertSame(JexlUberspect.MAP_STRATEGY, builder.strategy());
302             Assertions.assertNotNull(builder.create());
303         }
304     }
305 
306     @Test
307     void testParseBoolVariants() throws Exception {
308         // "yes" and "on" are truthy; "off" is falsy
309         try (InputStream in = yaml("strict: yes\nsafe: on\ncancellable: off\n")) {
310             final JexlOptions o = JexlConfigLoader.load(in).options();
311             Assertions.assertTrue(o.isStrict());
312             Assertions.assertTrue(o.isSafe());
313             Assertions.assertFalse(o.isCancellable());
314         }
315     }
316 
317     @Test
318     void testArithmeticMathContextFromYaml() throws Exception {
319         // reflective 3-arg ctor branch: strict + mathContext + mathScale
320         try (InputStream in = yaml("arithmetic:\n  strict: false\n  mathContext: DECIMAL64\n  mathScale: 5\n")) {
321             final JexlArithmetic a = JexlConfigLoader.load(in).arithmetic();
322             Assertions.assertNotNull(a);
323             Assertions.assertEquals(MathContext.DECIMAL64, a.getMathContext());
324             Assertions.assertEquals(5, a.getMathScale());
325             Assertions.assertFalse(a.isStrict());
326         }
327         // 1-arg ctor branch: no mathContext
328         try (InputStream in = yaml("arithmetic:\n  strict: true\n")) {
329             final JexlArithmetic a = JexlConfigLoader.load(in).arithmetic();
330             Assertions.assertNotNull(a);
331             Assertions.assertTrue(a.isStrict());
332         }
333     }
334 
335     @Test
336     void testStrategyJexl() throws Exception {
337         // any non-MAP strategy value routes through parseStrategy's else branch → JEXL_STRATEGY
338         try (InputStream in = yaml("strategy: JEXL_STRATEGY\n")) {
339             Assertions.assertSame(JexlUberspect.JEXL_STRATEGY, JexlConfigLoader.load(in).strategy());
340         }
341         // no strategy: key → left unset (null), engine uses its internal default
342         try (InputStream in = yaml("cache: 16\n")) {
343             Assertions.assertNull(JexlConfigLoader.load(in).strategy());
344         }
345     }
346 
347     @Test
348     void testPragmasFromYaml() throws Exception {
349         final String config = "pragmas:\n  - dlm.*   # any dlm pragma\n  - jexl.import\n";
350         try (InputStream in = yaml(config)) {
351             final JexlBuilder builder = JexlConfigLoader.load(in);
352             final Predicate<String> test = builder.features().pragmaTest();
353             Assertions.assertNotSame(JexlFeatures.TEST_STR_TRUE, test);
354             Assertions.assertTrue(test.test("dlm.foo"));
355             Assertions.assertTrue(test.test("dlm.foo.bar"));
356             Assertions.assertTrue(test.test("jexl.import"));
357             Assertions.assertFalse(test.test("dlm"));
358             Assertions.assertFalse(test.test("jexl.importer"));
359             Assertions.assertFalse(test.test("jexl.options"));
360             final JexlEngine engine = builder.create();
361             final JexlScript script = engine.createScript("#pragma dlm.foo 42\n#pragma jexl.import 'java.util'\n1");
362             Assertions.assertEquals(42, script.getPragmas().get("dlm.foo"));
363             final JexlException.Feature xfeature = Assertions.assertThrows(JexlException.Feature.class,
364                 () -> engine.createScript("#pragma dlm.foo 42\n#pragma jexl.options '+strict'\n1"));
365             // the parser reports the offending pragma line and key
366             Assertions.assertEquals(2, xfeature.getInfo().getLine());
367             Assertions.assertTrue(xfeature.getMessage().contains("pragma 'jexl.options' not allowed"),
368                 xfeature.getMessage());
369             Assertions.assertThrows(JexlException.Feature.class,
370                 () -> engine.createExpression("#pragma other.foo 42\n1"));
371         }
372     }
373 
374     @Test
375     void testInlinePragmaFromYaml() throws Exception {
376         try (InputStream in = yaml("pragmas: dlm.*   # a single pattern\nstrict: true\n")) {
377             final Predicate<String> test = JexlConfigLoader.load(in).features().pragmaTest();
378             Assertions.assertTrue(test.test("dlm.foo"));
379             Assertions.assertFalse(test.test("jexl.options"));
380         }
381         try (InputStream in = yaml("pragmas: 'jexl.import'\n")) {
382             final Predicate<String> test = JexlConfigLoader.load(in).features().pragmaTest();
383             Assertions.assertTrue(test.test("jexl.import"));
384             Assertions.assertFalse(test.test("dlm.foo"));
385         }
386     }
387 
388     @Test
389     void testInvalidPragmasFromYaml() throws Exception {
390         final String[] configs = {
391             "pragmas:\n  dlm: true\n",
392             "pragmas:\n  - dlm.*\n  rules:\n    - jexl.import\n",
393             "pragmas: [dlm.*, jexl.import]\n",
394             "pragmas: dlm.*, jexl.import\n",
395             "pragmas:\n  - dlm.*, jexl.import\n"
396         };
397         for (final String config : configs) {
398             try (InputStream in = yaml(config)) {
399                 Assertions.assertThrows(IllegalArgumentException.class, () -> JexlConfigLoader.load(in), config);
400             }
401         }
402     }
403 
404     @Test
405     void testEmptyPragmaItemFromYaml() throws Exception {
406         // an empty item allows nothing
407         try (InputStream in = yaml("pragmas:\n  - \n")) {
408             Assertions.assertSame(JexlFeatures.TEST_STR_FALSE, JexlConfigLoader.load(in).features().pragmaTest());
409         }
410         // and is ignored among other items
411         try (InputStream in = yaml("pragmas:\n  - dlm.*\n  - \n")) {
412             final Predicate<String> test = JexlConfigLoader.load(in).features().pragmaTest();
413             Assertions.assertTrue(test.test("dlm.foo"));
414             Assertions.assertFalse(test.test(""));
415         }
416     }
417 
418     @Test
419     void testPragmasAnywhereFromYaml() throws Exception {
420         final String config = "features:\n  pragmaAnywhere: true\npragmas:\n  - dlm.*\n";
421         try (InputStream in = yaml(config)) {
422             final JexlEngine engine = JexlConfigLoader.load(in).create();
423             final JexlScript script = engine.createScript("var x = 1;\n#pragma dlm.foo 42\nx");
424             Assertions.assertEquals(42, script.getPragmas().get("dlm.foo"));
425             final JexlException.Feature xfeature = Assertions.assertThrows(JexlException.Feature.class,
426                 () -> engine.createScript("var x = 1;\n#pragma dlm.foo 42\nx;\n#pragma other.foo 43\nx"));
427             Assertions.assertEquals(4, xfeature.getInfo().getLine());
428             Assertions.assertTrue(xfeature.getMessage().contains("pragma 'other.foo' not allowed"),
429                 xfeature.getMessage());
430         }
431     }
432 
433     @Test
434     void testStarPragmaFromYamlAllowsAll() throws Exception {
435         try (InputStream in = yaml("pragmas:\n  - '*'\n")) {
436             Assertions.assertSame(JexlFeatures.TEST_STR_TRUE, JexlConfigLoader.load(in).features().pragmaTest());
437         }
438     }
439 
440     @Test
441     void testEmptyPragmasFromYamlAllowsNone() throws Exception {
442         try (InputStream in = yaml("pragmas:\nstrict: true\n")) {
443             final JexlEngine engine = JexlConfigLoader.load(in).create();
444             Assertions.assertThrows(JexlException.Feature.class,
445                 () -> engine.createScript("#pragma dlm.foo 42\n1"));
446             Assertions.assertEquals(1, engine.createScript("1").execute(null));
447         }
448     }
449 
450     @Test
451     void testNoPragmasFromYamlAllowsAll() throws Exception {
452         try (InputStream in = yaml("strict: true\n")) {
453             final JexlBuilder builder = JexlConfigLoader.load(in);
454             Assertions.assertSame(JexlFeatures.TEST_STR_TRUE, builder.features().pragmaTest());
455             final JexlScript script = builder.create().createScript("#pragma any.thing 42\n1");
456             Assertions.assertEquals(42, script.getPragmas().get("any.thing"));
457         }
458     }
459 
460     @Test
461     void testPragmasFromYamlEquality() throws Exception {
462         final String config = "pragmas:\n  - dlm.*\n  - jexl.import\n";
463         final JexlFeatures f0;
464         final JexlFeatures f1;
465         try (InputStream in = yaml(config)) {
466             f0 = JexlConfigLoader.load(in).features();
467         }
468         try (InputStream in = yaml(config)) {
469             f1 = JexlConfigLoader.load(in).features();
470         }
471         // same patterns, same features
472         Assertions.assertNotSame(f0.pragmaTest(), f1.pragmaTest());
473         Assertions.assertEquals(f0, f1);
474         Assertions.assertEquals(f0.hashCode(), f1.hashCode());
475         // pattern order and spacing do not matter, the patterns do
476         final JexlFeatures f2 = new JexlFeatures(f0)
477             .pragmaTest(JexlFeatures.pragmaPatterns(Arrays.asList(" jexl.import", "dlm.* ")));
478         Assertions.assertEquals(f0, f2);
479         final JexlFeatures f3 = new JexlFeatures(f0)
480             .pragmaTest(JexlFeatures.pragmaPatterns(Collections.singletonList("dlm.*")));
481         Assertions.assertNotEquals(f0, f3);
482     }
483 
484     @Test
485     void testPragmaPatterns() {
486         Assertions.assertSame(JexlFeatures.TEST_STR_FALSE, JexlFeatures.pragmaPatterns(null));
487         Assertions.assertSame(JexlFeatures.TEST_STR_FALSE, JexlFeatures.pragmaPatterns(Collections.emptyList()));
488         Assertions.assertSame(JexlFeatures.TEST_STR_FALSE, JexlFeatures.pragmaPatterns(Arrays.asList("", " ", null)));
489         Assertions.assertSame(JexlFeatures.TEST_STR_TRUE, JexlFeatures.pragmaPatterns(Arrays.asList("dlm.*", "*")));
490         final Predicate<String> test = JexlFeatures.pragmaPatterns(Arrays.asList("dlm.*", "jexl.import"));
491         Assertions.assertTrue(test.test("dlm.foo"));
492         Assertions.assertTrue(test.test("jexl.import"));
493         Assertions.assertFalse(test.test("jexl.importer"));
494         Assertions.assertFalse(test.test(null));
495         Assertions.assertEquals("jexl.import, dlm.*", test.toString());
496     }
497 
498     @Test
499     void testFeaturesPragmaTest() {
500         final JexlFeatures features = new JexlFeatures().pragmaTest(k -> k.startsWith("dlm."));
501         // copies keep the pragma test
502         Assertions.assertEquals(features, new JexlFeatures(features));
503         final JexlEngine engine = new JexlBuilder().features(features).create();
504         Assertions.assertNotNull(engine.createScript("#pragma dlm.good 1\n1"));
505         Assertions.assertThrows(JexlException.Feature.class, () -> engine.createScript("#pragma other.good 1\n1"));
506         // explicit features use their own pragma test
507         Assertions.assertNotNull(engine.createScript(new JexlFeatures(), null, "#pragma other.good 1\n1"));
508     }
509 }