From a7c4dbbb0cdd8c91cab94c816d6d11a1ad615ca9 Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Fri, 12 Oct 2018 15:34:48 -0700 Subject: [PATCH] [Painless] Add a Map for java names to classes for use in the custom classloader (#34424) This fixes a bug that wasn't including the class used for a static import where only the static import is whitelisted. --- .../org/elasticsearch/painless/Compiler.java | 2 +- .../painless/lookup/PainlessLookup.java | 12 +++- .../lookup/PainlessLookupBuilder.java | 55 ++++++++++++++++++- .../ExampleStaticMethodClass.java | 26 +++++++++ .../painlesswhitelist/example_whitelist.txt | 4 ++ .../test/painless_whitelist/30_static.yml | 26 +++++++++ 6 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 plugins/examples/painless-whitelist/src/main/java/org/elasticsearch/example/painlesswhitelist/ExampleStaticMethodClass.java create mode 100644 plugins/examples/painless-whitelist/src/test/resources/rest-api-spec/test/painless_whitelist/30_static.yml diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java index e6ed475a7be..0fbdfa763ea 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java @@ -96,7 +96,7 @@ final class Compiler { if (found != null) { return found; } - found = painlessLookup.canonicalTypeNameToType(name.replace('$', '.')); + found = painlessLookup.javaClassNameToClass(name); return found != null ? found : super.findClass(name); } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java index ed9e7668336..ce31db43eef 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java @@ -34,22 +34,28 @@ import static org.elasticsearch.painless.lookup.PainlessLookupUtility.typeToBoxe public final class PainlessLookup { + private final Map> javaClassNamesToClasses; private final Map> canonicalClassNamesToClasses; private final Map, PainlessClass> classesToPainlessClasses; private final Map painlessMethodKeysToImportedPainlessMethods; private final Map painlessMethodKeysToPainlessClassBindings; - PainlessLookup(Map> canonicalClassNamesToClasses, Map, PainlessClass> classesToPainlessClasses, + PainlessLookup( + Map> javaClassNamesToClasses, + Map> canonicalClassNamesToClasses, + Map, PainlessClass> classesToPainlessClasses, Map painlessMethodKeysToImportedPainlessMethods, Map painlessMethodKeysToPainlessClassBindings) { + Objects.requireNonNull(javaClassNamesToClasses); Objects.requireNonNull(canonicalClassNamesToClasses); Objects.requireNonNull(classesToPainlessClasses); Objects.requireNonNull(painlessMethodKeysToImportedPainlessMethods); Objects.requireNonNull(painlessMethodKeysToPainlessClassBindings); + this.javaClassNamesToClasses = javaClassNamesToClasses; this.canonicalClassNamesToClasses = Collections.unmodifiableMap(canonicalClassNamesToClasses); this.classesToPainlessClasses = Collections.unmodifiableMap(classesToPainlessClasses); @@ -57,6 +63,10 @@ public final class PainlessLookup { this.painlessMethodKeysToPainlessClassBindings = Collections.unmodifiableMap(painlessMethodKeysToPainlessClassBindings); } + public Class javaClassNameToClass(String javaClassName) { + return javaClassNamesToClasses.get(javaClassName); + } + public boolean isValidCanonicalClassName(String canonicalClassName) { Objects.requireNonNull(canonicalClassName); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java index 46bcaf3f1b2..552ad56f68a 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java @@ -120,6 +120,15 @@ public final class PainlessLookupBuilder { return painlessLookupBuilder.build(); } + // javaClassNamesToClasses is all the classes that need to be available to the custom classloader + // including classes used as part of imported methods and class bindings but not necessarily whitelisted + // individually. The values of javaClassNamesToClasses are a superset of the values of + // canonicalClassNamesToClasses. + private final Map> javaClassNamesToClasses; + // canonicalClassNamesToClasses is all the whitelisted classes available in a Painless script including + // classes with imported canonical names but does not include classes from imported methods or class + // bindings unless also whitelisted separately. The values of canonicalClassNamesToClasses are a subset + // of the values of javaClassNamesToClasses. private final Map> canonicalClassNamesToClasses; private final Map, PainlessClassBuilder> classesToPainlessClassBuilders; @@ -127,6 +136,7 @@ public final class PainlessLookupBuilder { private final Map painlessMethodKeysToPainlessClassBindings; public PainlessLookupBuilder() { + javaClassNamesToClasses = new HashMap<>(); canonicalClassNamesToClasses = new HashMap<>(); classesToPainlessClassBuilders = new HashMap<>(); @@ -189,7 +199,16 @@ public final class PainlessLookupBuilder { throw new IllegalArgumentException("invalid class name [" + canonicalClassName + "]"); } - Class existingClass = canonicalClassNamesToClasses.get(canonicalClassName); + Class existingClass = javaClassNamesToClasses.get(clazz.getName()); + + if (existingClass == null) { + javaClassNamesToClasses.put(clazz.getName(), clazz); + } else if (existingClass != clazz) { + throw new IllegalArgumentException("class [" + canonicalClassName + "] " + + "cannot represent multiple java classes with the same name from different class loaders"); + } + + existingClass = canonicalClassNamesToClasses.get(canonicalClassName); if (existingClass != null && existingClass != clazz) { throw new IllegalArgumentException("class [" + canonicalClassName + "] " + @@ -685,6 +704,14 @@ public final class PainlessLookupBuilder { } String targetCanonicalClassName = typeToCanonicalTypeName(targetClass); + Class existingTargetClass = javaClassNamesToClasses.get(targetClass.getName()); + + if (existingTargetClass == null) { + javaClassNamesToClasses.put(targetClass.getName(), targetClass); + } else if (existingTargetClass != targetClass) { + throw new IllegalArgumentException("class [" + targetCanonicalClassName + "] " + + "cannot represent multiple java classes with the same name from different class loaders"); + } if (METHOD_NAME_PATTERN.matcher(methodName).matches() == false) { throw new IllegalArgumentException( @@ -818,6 +845,14 @@ public final class PainlessLookupBuilder { } String targetCanonicalClassName = typeToCanonicalTypeName(targetClass); + Class existingTargetClass = javaClassNamesToClasses.get(targetClass.getName()); + + if (existingTargetClass == null) { + javaClassNamesToClasses.put(targetClass.getName(), targetClass); + } else if (existingTargetClass != targetClass) { + throw new IllegalArgumentException("class [" + targetCanonicalClassName + "] " + + "cannot represent multiple java classes with the same name from different class loaders"); + } Constructor[] javaConstructors = targetClass.getConstructors(); Constructor javaConstructor = null; @@ -952,7 +987,23 @@ public final class PainlessLookupBuilder { classesToPainlessClasses.put(painlessClassBuilderEntry.getKey(), painlessClassBuilderEntry.getValue().build()); } - return new PainlessLookup(canonicalClassNamesToClasses, classesToPainlessClasses, + if (javaClassNamesToClasses.values().containsAll(canonicalClassNamesToClasses.values()) == false) { + throw new IllegalArgumentException("the values of java class names to classes " + + "must be a superset of the values of canonical class names to classes"); + } + + if (javaClassNamesToClasses.values().containsAll(classesToPainlessClasses.keySet()) == false) { + throw new IllegalArgumentException("the values of java class names to classes " + + "must be a superset of the keys of classes to painless classes"); + } + + if (canonicalClassNamesToClasses.values().containsAll(classesToPainlessClasses.keySet()) == false || + classesToPainlessClasses.keySet().containsAll(canonicalClassNamesToClasses.values()) == false) { + throw new IllegalArgumentException("the values of canonical class names to classes " + + "must have the same classes as the keys of classes to painless classes"); + } + + return new PainlessLookup(javaClassNamesToClasses, canonicalClassNamesToClasses, classesToPainlessClasses, painlessMethodKeysToImportedPainlessMethods, painlessMethodKeysToPainlessClassBindings); } diff --git a/plugins/examples/painless-whitelist/src/main/java/org/elasticsearch/example/painlesswhitelist/ExampleStaticMethodClass.java b/plugins/examples/painless-whitelist/src/main/java/org/elasticsearch/example/painlesswhitelist/ExampleStaticMethodClass.java new file mode 100644 index 00000000000..72db9c4979d --- /dev/null +++ b/plugins/examples/painless-whitelist/src/main/java/org/elasticsearch/example/painlesswhitelist/ExampleStaticMethodClass.java @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.example.painlesswhitelist; + +public class ExampleStaticMethodClass { + public static int exampleAddInts(int x, int y) { + return x + y; + } +} diff --git a/plugins/examples/painless-whitelist/src/main/resources/org/elasticsearch/example/painlesswhitelist/example_whitelist.txt b/plugins/examples/painless-whitelist/src/main/resources/org/elasticsearch/example/painlesswhitelist/example_whitelist.txt index 7908d354175..99e5521aebf 100644 --- a/plugins/examples/painless-whitelist/src/main/resources/org/elasticsearch/example/painlesswhitelist/example_whitelist.txt +++ b/plugins/examples/painless-whitelist/src/main/resources/org/elasticsearch/example/painlesswhitelist/example_whitelist.txt @@ -39,4 +39,8 @@ class java.lang.String { # existing classes can be "augmented" to have additional methods, which take the object # to operate on as the first argument to a static method int org.elasticsearch.example.painlesswhitelist.ExampleWhitelistedClass toInt() +} + +static_import { + int exampleAddInts(int, int) from_class org.elasticsearch.example.painlesswhitelist.ExampleStaticMethodClass } \ No newline at end of file diff --git a/plugins/examples/painless-whitelist/src/test/resources/rest-api-spec/test/painless_whitelist/30_static.yml b/plugins/examples/painless-whitelist/src/test/resources/rest-api-spec/test/painless_whitelist/30_static.yml new file mode 100644 index 00000000000..b6592637296 --- /dev/null +++ b/plugins/examples/painless-whitelist/src/test/resources/rest-api-spec/test/painless_whitelist/30_static.yml @@ -0,0 +1,26 @@ +# Example test using whitelisted statically imported method + +"custom static imported method": +- do: + index: + index: test + type: test + id: 1 + body: { "num1": 1 } +- do: + indices.refresh: {} + +- do: + index: test + search: + body: + query: + match_all: {} + script_fields: + sNum1: + script: + source: "exampleAddInts(2, (int)doc['num1'][0])" + lang: painless + +- match: { hits.total: 1 } +- match: { hits.hits.0.fields.sNum1.0: 3 }