Skip to content

Commit 18463e7

Browse files
authored
Painless: Add whitelist extensions (#28161)
This commit adds a PainlessExtension which may be plugged in via SPI to add additional classes, methods and members to the painless whitelist on a per context basis. An example plugin adding and using a whitelist is also added.
1 parent b82017c commit 18463e7

40 files changed

+319
-91
lines changed

modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.elasticsearch.bootstrap.BootstrapInfo;
2323
import org.elasticsearch.painless.antlr.Walker;
2424
import org.elasticsearch.painless.node.SSource;
25+
import org.elasticsearch.painless.spi.Whitelist;
2526
import org.objectweb.asm.util.Printer;
2627

2728
import java.lang.reflect.Constructor;

modules/lang-painless/src/main/java/org/elasticsearch/painless/Definition.java

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.elasticsearch.painless;
2121

2222
import org.apache.lucene.util.SetOnce;
23+
import org.elasticsearch.painless.spi.Whitelist;
2324

2425
import java.lang.invoke.MethodHandle;
2526
import java.lang.invoke.MethodHandles;
@@ -46,29 +47,6 @@ public final class Definition {
4647

4748
private static final Pattern TYPE_NAME_PATTERN = Pattern.compile("^[_a-zA-Z][._a-zA-Z0-9]*$");
4849

49-
public static final String[] DEFINITION_FILES = new String[] {
50-
"org.elasticsearch.txt",
51-
"java.lang.txt",
52-
"java.math.txt",
53-
"java.text.txt",
54-
"java.time.txt",
55-
"java.time.chrono.txt",
56-
"java.time.format.txt",
57-
"java.time.temporal.txt",
58-
"java.time.zone.txt",
59-
"java.util.txt",
60-
"java.util.function.txt",
61-
"java.util.regex.txt",
62-
"java.util.stream.txt",
63-
"joda.time.txt"
64-
};
65-
66-
/**
67-
* Whitelist that is "built in" to Painless and required by all scripts.
68-
*/
69-
public static final Definition DEFINITION = new Definition(
70-
Collections.singletonList(WhitelistLoader.loadFromResourceFiles(Definition.class, DEFINITION_FILES)));
71-
7250
/** Some native types as constants: */
7351
public final Type voidType;
7452
public final Type booleanType;

modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessPlugin.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,56 @@
2222

2323
import org.elasticsearch.common.settings.Setting;
2424
import org.elasticsearch.common.settings.Settings;
25+
import org.elasticsearch.painless.spi.PainlessExtension;
26+
import org.elasticsearch.painless.spi.Whitelist;
2527
import org.elasticsearch.plugins.ExtensiblePlugin;
2628
import org.elasticsearch.plugins.Plugin;
2729
import org.elasticsearch.plugins.ScriptPlugin;
2830
import org.elasticsearch.script.ScriptContext;
2931
import org.elasticsearch.script.ScriptEngine;
3032

33+
import java.util.ArrayList;
3134
import java.util.Arrays;
3235
import java.util.Collection;
36+
import java.util.HashMap;
3337
import java.util.List;
38+
import java.util.Map;
39+
import java.util.ServiceLoader;
3440

3541
/**
3642
* Registers Painless as a plugin.
3743
*/
3844
public final class PainlessPlugin extends Plugin implements ScriptPlugin, ExtensiblePlugin {
3945

46+
private final Map<ScriptContext<?>, List<Whitelist>> extendedWhitelists = new HashMap<>();
47+
4048
@Override
4149
public ScriptEngine getScriptEngine(Settings settings, Collection<ScriptContext<?>> contexts) {
42-
return new PainlessScriptEngine(settings, contexts);
50+
Map<ScriptContext<?>, List<Whitelist>> contextsWithWhitelists = new HashMap<>();
51+
for (ScriptContext<?> context : contexts) {
52+
// we might have a context that only uses the base whitelists, so would not have been filled in by reloadSPI
53+
List<Whitelist> whitelists = extendedWhitelists.get(context);
54+
if (whitelists == null) {
55+
whitelists = new ArrayList<>(Whitelist.BASE_WHITELISTS);
56+
}
57+
contextsWithWhitelists.put(context, whitelists);
58+
}
59+
return new PainlessScriptEngine(settings, contextsWithWhitelists);
4360
}
4461

4562
@Override
4663
public List<Setting<?>> getSettings() {
4764
return Arrays.asList(CompilerSettings.REGEX_ENABLED);
4865
}
66+
67+
@Override
68+
public void reloadSPI(ClassLoader loader) {
69+
for (PainlessExtension extension : ServiceLoader.load(PainlessExtension.class, loader)) {
70+
for (Map.Entry<ScriptContext<?>, List<Whitelist>> entry : extension.getContextWhitelists().entrySet()) {
71+
List<Whitelist> existing = extendedWhitelists.computeIfAbsent(entry.getKey(),
72+
c -> new ArrayList<>(Whitelist.BASE_WHITELISTS));
73+
existing.addAll(entry.getValue());
74+
}
75+
}
76+
}
4977
}

modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919

2020
package org.elasticsearch.painless;
2121

22-
import org.apache.logging.log4j.core.tools.Generate;
2322
import org.apache.lucene.index.LeafReaderContext;
2423
import org.elasticsearch.SpecialPermission;
2524
import org.elasticsearch.common.component.AbstractComponent;
2625
import org.elasticsearch.common.settings.Settings;
2726
import org.elasticsearch.painless.Compiler.Loader;
27+
import org.elasticsearch.painless.spi.Whitelist;
2828
import org.elasticsearch.script.ExecutableScript;
2929
import org.elasticsearch.script.ScriptContext;
3030
import org.elasticsearch.script.ScriptEngine;
@@ -45,7 +45,6 @@
4545
import java.security.ProtectionDomain;
4646
import java.util.ArrayList;
4747
import java.util.Arrays;
48-
import java.util.Collection;
4948
import java.util.Collections;
5049
import java.util.HashMap;
5150
import java.util.List;
@@ -82,7 +81,7 @@ public final class PainlessScriptEngine extends AbstractComponent implements Scr
8281

8382
/**
8483
* Default compiler settings to be used. Note that {@link CompilerSettings} is mutable but this instance shouldn't be mutated outside
85-
* of {@link PainlessScriptEngine#PainlessScriptEngine(Settings, Collection)}.
84+
* of {@link PainlessScriptEngine#PainlessScriptEngine(Settings, Map)}.
8685
*/
8786
private final CompilerSettings defaultCompilerSettings = new CompilerSettings();
8887

@@ -92,23 +91,19 @@ public final class PainlessScriptEngine extends AbstractComponent implements Scr
9291
* Constructor.
9392
* @param settings The settings to initialize the engine with.
9493
*/
95-
public PainlessScriptEngine(Settings settings, Collection<ScriptContext<?>> contexts) {
94+
public PainlessScriptEngine(Settings settings, Map<ScriptContext<?>, List<Whitelist>> contexts) {
9695
super(settings);
9796

9897
defaultCompilerSettings.setRegexesEnabled(CompilerSettings.REGEX_ENABLED.get(settings));
9998

10099
Map<ScriptContext<?>, Compiler> contextsToCompilers = new HashMap<>();
101100

102-
// Placeholder definition used for all contexts until SPI is fully integrated. Reduces memory foot print
103-
// by re-using the same definition since caching isn't implemented at this time.
104-
Definition definition = new Definition(
105-
Collections.singletonList(WhitelistLoader.loadFromResourceFiles(Definition.class, Definition.DEFINITION_FILES)));
106-
107-
for (ScriptContext<?> context : contexts) {
101+
for (Map.Entry<ScriptContext<?>, List<Whitelist>> entry : contexts.entrySet()) {
102+
ScriptContext<?> context = entry.getKey();
108103
if (context.instanceClazz.equals(SearchScript.class) || context.instanceClazz.equals(ExecutableScript.class)) {
109-
contextsToCompilers.put(context, new Compiler(GenericElasticsearchScript.class, definition));
104+
contextsToCompilers.put(context, new Compiler(GenericElasticsearchScript.class, new Definition(entry.getValue())));
110105
} else {
111-
contextsToCompilers.put(context, new Compiler(context.instanceClazz, definition));
106+
contextsToCompilers.put(context, new Compiler(context.instanceClazz, new Definition(entry.getValue())));
112107
}
113108
}
114109

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.painless.spi;
21+
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
import org.elasticsearch.script.ScriptContext;
26+
27+
public interface PainlessExtension {
28+
29+
Map<ScriptContext<?>, List<Whitelist>> getContextWhitelists();
30+
}

modules/lang-painless/src/main/java/org/elasticsearch/painless/Whitelist.java renamed to modules/lang-painless/src/main/java/org/elasticsearch/painless/spi/Whitelist.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* under the License.
1818
*/
1919

20-
package org.elasticsearch.painless;
20+
package org.elasticsearch.painless.spi;
2121

2222
import java.util.Collections;
2323
import java.util.List;
@@ -34,6 +34,26 @@
3434
*/
3535
public final class Whitelist {
3636

37+
private static final String[] BASE_WHITELIST_FILES = new String[] {
38+
"org.elasticsearch.txt",
39+
"java.lang.txt",
40+
"java.math.txt",
41+
"java.text.txt",
42+
"java.time.txt",
43+
"java.time.chrono.txt",
44+
"java.time.format.txt",
45+
"java.time.temporal.txt",
46+
"java.time.zone.txt",
47+
"java.util.txt",
48+
"java.util.function.txt",
49+
"java.util.regex.txt",
50+
"java.util.stream.txt",
51+
"joda.time.txt"
52+
};
53+
54+
public static final List<Whitelist> BASE_WHITELISTS =
55+
Collections.singletonList(WhitelistLoader.loadFromResourceFiles(Whitelist.class, BASE_WHITELIST_FILES));
56+
3757
/**
3858
* Struct represents the equivalent of a Java class in Painless complete with super classes,
3959
* constructors, methods, and fields. In Painless a class is known as a struct primarily to avoid

modules/lang-painless/src/main/java/org/elasticsearch/painless/WhitelistLoader.java renamed to modules/lang-painless/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@
1717
* under the License.
1818
*/
1919

20-
package org.elasticsearch.painless;
20+
package org.elasticsearch.painless.spi;
2121

2222
import java.io.InputStreamReader;
2323
import java.io.LineNumberReader;
2424
import java.lang.reflect.Constructor;
2525
import java.lang.reflect.Field;
2626
import java.lang.reflect.Method;
2727
import java.nio.charset.StandardCharsets;
28+
import java.security.AccessController;
29+
import java.security.PrivilegedAction;
2830
import java.util.ArrayList;
2931
import java.util.Arrays;
3032
import java.util.List;
@@ -296,8 +298,9 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep
296298
throw new RuntimeException("error in [" + filepath + "] at line [" + number + "]", exception);
297299
}
298300
}
301+
ClassLoader loader = AccessController.doPrivileged((PrivilegedAction<ClassLoader>)resource::getClassLoader);
299302

300-
return new Whitelist(resource.getClassLoader(), whitelistStructs);
303+
return new Whitelist(loader, whitelistStructs);
301304
}
302305

303306
private WhitelistLoader() {}

modules/lang-painless/src/main/plugin-metadata/plugin-security.policy

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,7 @@
2020
grant {
2121
// needed to generate runtime classes
2222
permission java.lang.RuntimePermission "createClassLoader";
23+
24+
// needed to find the classloader to load whitelisted classes from
25+
permission java.lang.RuntimePermission "getClassLoader";
2326
};

0 commit comments

Comments
 (0)