Painless: Add whitelist extensions ()

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.
This commit is contained in:
Ryan Ernst 2018-01-15 11:28:31 -08:00 committed by GitHub
parent b82017cbfe
commit 18463e7e9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 319 additions and 91 deletions

@ -22,6 +22,7 @@ package org.elasticsearch.painless;
import org.elasticsearch.bootstrap.BootstrapInfo;
import org.elasticsearch.painless.antlr.Walker;
import org.elasticsearch.painless.node.SSource;
import org.elasticsearch.painless.spi.Whitelist;
import org.objectweb.asm.util.Printer;
import java.lang.reflect.Constructor;

@ -20,6 +20,7 @@
package org.elasticsearch.painless;
import org.apache.lucene.util.SetOnce;
import org.elasticsearch.painless.spi.Whitelist;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
@ -46,29 +47,6 @@ public final class Definition {
private static final Pattern TYPE_NAME_PATTERN = Pattern.compile("^[_a-zA-Z][._a-zA-Z0-9]*$");
public static final String[] DEFINITION_FILES = new String[] {
"org.elasticsearch.txt",
"java.lang.txt",
"java.math.txt",
"java.text.txt",
"java.time.txt",
"java.time.chrono.txt",
"java.time.format.txt",
"java.time.temporal.txt",
"java.time.zone.txt",
"java.util.txt",
"java.util.function.txt",
"java.util.regex.txt",
"java.util.stream.txt",
"joda.time.txt"
};
/**
* Whitelist that is "built in" to Painless and required by all scripts.
*/
public static final Definition DEFINITION = new Definition(
Collections.singletonList(WhitelistLoader.loadFromResourceFiles(Definition.class, DEFINITION_FILES)));
/** Some native types as constants: */
public final Type voidType;
public final Type booleanType;

@ -22,28 +22,56 @@ package org.elasticsearch.painless;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.painless.spi.PainlessExtension;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.plugins.ExtensiblePlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.ScriptPlugin;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
/**
* Registers Painless as a plugin.
*/
public final class PainlessPlugin extends Plugin implements ScriptPlugin, ExtensiblePlugin {
private final Map<ScriptContext<?>, List<Whitelist>> extendedWhitelists = new HashMap<>();
@Override
public ScriptEngine getScriptEngine(Settings settings, Collection<ScriptContext<?>> contexts) {
return new PainlessScriptEngine(settings, contexts);
Map<ScriptContext<?>, List<Whitelist>> contextsWithWhitelists = new HashMap<>();
for (ScriptContext<?> context : contexts) {
// we might have a context that only uses the base whitelists, so would not have been filled in by reloadSPI
List<Whitelist> whitelists = extendedWhitelists.get(context);
if (whitelists == null) {
whitelists = new ArrayList<>(Whitelist.BASE_WHITELISTS);
}
contextsWithWhitelists.put(context, whitelists);
}
return new PainlessScriptEngine(settings, contextsWithWhitelists);
}
@Override
public List<Setting<?>> getSettings() {
return Arrays.asList(CompilerSettings.REGEX_ENABLED);
}
@Override
public void reloadSPI(ClassLoader loader) {
for (PainlessExtension extension : ServiceLoader.load(PainlessExtension.class, loader)) {
for (Map.Entry<ScriptContext<?>, List<Whitelist>> entry : extension.getContextWhitelists().entrySet()) {
List<Whitelist> existing = extendedWhitelists.computeIfAbsent(entry.getKey(),
c -> new ArrayList<>(Whitelist.BASE_WHITELISTS));
existing.addAll(entry.getValue());
}
}
}
}

@ -19,12 +19,12 @@
package org.elasticsearch.painless;
import org.apache.logging.log4j.core.tools.Generate;
import org.apache.lucene.index.LeafReaderContext;
import org.elasticsearch.SpecialPermission;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.painless.Compiler.Loader;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine;
@ -45,7 +45,6 @@ import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -82,7 +81,7 @@ public final class PainlessScriptEngine extends AbstractComponent implements Scr
/**
* Default compiler settings to be used. Note that {@link CompilerSettings} is mutable but this instance shouldn't be mutated outside
* of {@link PainlessScriptEngine#PainlessScriptEngine(Settings, Collection)}.
* of {@link PainlessScriptEngine#PainlessScriptEngine(Settings, Map)}.
*/
private final CompilerSettings defaultCompilerSettings = new CompilerSettings();
@ -92,23 +91,19 @@ public final class PainlessScriptEngine extends AbstractComponent implements Scr
* Constructor.
* @param settings The settings to initialize the engine with.
*/
public PainlessScriptEngine(Settings settings, Collection<ScriptContext<?>> contexts) {
public PainlessScriptEngine(Settings settings, Map<ScriptContext<?>, List<Whitelist>> contexts) {
super(settings);
defaultCompilerSettings.setRegexesEnabled(CompilerSettings.REGEX_ENABLED.get(settings));
Map<ScriptContext<?>, Compiler> contextsToCompilers = new HashMap<>();
// Placeholder definition used for all contexts until SPI is fully integrated. Reduces memory foot print
// by re-using the same definition since caching isn't implemented at this time.
Definition definition = new Definition(
Collections.singletonList(WhitelistLoader.loadFromResourceFiles(Definition.class, Definition.DEFINITION_FILES)));
for (ScriptContext<?> context : contexts) {
for (Map.Entry<ScriptContext<?>, List<Whitelist>> entry : contexts.entrySet()) {
ScriptContext<?> context = entry.getKey();
if (context.instanceClazz.equals(SearchScript.class) || context.instanceClazz.equals(ExecutableScript.class)) {
contextsToCompilers.put(context, new Compiler(GenericElasticsearchScript.class, definition));
contextsToCompilers.put(context, new Compiler(GenericElasticsearchScript.class, new Definition(entry.getValue())));
} else {
contextsToCompilers.put(context, new Compiler(context.instanceClazz, definition));
contextsToCompilers.put(context, new Compiler(context.instanceClazz, new Definition(entry.getValue())));
}
}

@ -0,0 +1,30 @@
/*
* 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.painless.spi;
import java.util.List;
import java.util.Map;
import org.elasticsearch.script.ScriptContext;
public interface PainlessExtension {
Map<ScriptContext<?>, List<Whitelist>> getContextWhitelists();
}

@ -17,7 +17,7 @@
* under the License.
*/
package org.elasticsearch.painless;
package org.elasticsearch.painless.spi;
import java.util.Collections;
import java.util.List;
@ -34,6 +34,26 @@ import java.util.Objects;
*/
public final class Whitelist {
private static final String[] BASE_WHITELIST_FILES = new String[] {
"org.elasticsearch.txt",
"java.lang.txt",
"java.math.txt",
"java.text.txt",
"java.time.txt",
"java.time.chrono.txt",
"java.time.format.txt",
"java.time.temporal.txt",
"java.time.zone.txt",
"java.util.txt",
"java.util.function.txt",
"java.util.regex.txt",
"java.util.stream.txt",
"joda.time.txt"
};
public static final List<Whitelist> BASE_WHITELISTS =
Collections.singletonList(WhitelistLoader.loadFromResourceFiles(Whitelist.class, BASE_WHITELIST_FILES));
/**
* Struct represents the equivalent of a Java class in Painless complete with super classes,
* constructors, methods, and fields. In Painless a class is known as a struct primarily to avoid

@ -17,7 +17,7 @@
* under the License.
*/
package org.elasticsearch.painless;
package org.elasticsearch.painless.spi;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
@ -25,6 +25,8 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -296,8 +298,9 @@ public final class WhitelistLoader {
throw new RuntimeException("error in [" + filepath + "] at line [" + number + "]", exception);
}
}
ClassLoader loader = AccessController.doPrivileged((PrivilegedAction<ClassLoader>)resource::getClassLoader);
return new Whitelist(resource.getClassLoader(), whitelistStructs);
return new Whitelist(loader, whitelistStructs);
}
private WhitelistLoader() {}

@ -20,4 +20,7 @@
grant {
// needed to generate runtime classes
permission java.lang.RuntimePermission "createClassLoader";
// needed to find the classloader to load whitelisted classes from
permission java.lang.RuntimePermission "getClassLoader";
};

@ -21,16 +21,12 @@ package org.elasticsearch.painless;
import org.elasticsearch.painless.Definition.Cast;
import org.elasticsearch.painless.Definition.Type;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.test.ESTestCase;
import java.util.Collections;
import static org.elasticsearch.painless.Definition.DEFINITION_FILES;
public class AnalyzerCasterTests extends ESTestCase {
private static final Definition definition = new Definition(
Collections.singletonList(WhitelistLoader.loadFromResourceFiles(Definition.class, DEFINITION_FILES)));
private static final Definition definition = new Definition(Whitelist.BASE_WHITELISTS);
private static void assertCast(Type actual, Type expected, boolean mustBeExplicit) {
Location location = new Location("dummy", 0);

@ -19,13 +19,12 @@
package org.elasticsearch.painless;
import org.elasticsearch.script.ScriptContext;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.elasticsearch.painless.spi.Whitelist;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static org.hamcrest.Matchers.containsString;
@ -37,8 +36,7 @@ import static org.hamcrest.Matchers.startsWith;
*/
public class BaseClassTests extends ScriptTestCase {
private final Definition definition = new Definition(
Collections.singletonList(WhitelistLoader.loadFromResourceFiles(Definition.class, Definition.DEFINITION_FILES)));
private final Definition definition = new Definition(Whitelist.BASE_WHITELISTS);
public abstract static class Gets {

@ -22,10 +22,10 @@ package org.elasticsearch.painless;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.script.ScriptException;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import static java.util.Collections.singletonList;
@ -35,8 +35,7 @@ import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.not;
public class DebugTests extends ScriptTestCase {
private final Definition definition = new Definition(
Collections.singletonList(WhitelistLoader.loadFromResourceFiles(Definition.class, Definition.DEFINITION_FILES)));
private final Definition definition = new Definition(Whitelist.BASE_WHITELISTS);
public void testExplain() {
// Debug.explain can explain an object

@ -20,11 +20,11 @@
package org.elasticsearch.painless;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.painless.spi.Whitelist;
import org.objectweb.asm.util.Textifier;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collections;
/** quick and dirty tools for debugging */
final class Debugger {
@ -40,8 +40,7 @@ final class Debugger {
PrintWriter outputWriter = new PrintWriter(output);
Textifier textifier = new Textifier();
try {
new Compiler(iface, new Definition(
Collections.singletonList(WhitelistLoader.loadFromResourceFiles(Definition.class, Definition.DEFINITION_FILES))))
new Compiler(iface, new Definition(Whitelist.BASE_WHITELISTS))
.compile("<debugging>", source, settings, textifier);
} catch (Exception e) {
textifier.print(outputWriter);

@ -27,11 +27,11 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.test.ESTestCase;
public class DefBootstrapTests extends ESTestCase {
private final Definition definition = new Definition(
Collections.singletonList(WhitelistLoader.loadFromResourceFiles(Definition.class, Definition.DEFINITION_FILES)));
private final Definition definition = new Definition(Whitelist.BASE_WHITELISTS);
/** calls toString() on integers, twice */
public void testOneType() throws Throwable {

@ -19,21 +19,23 @@
package org.elasticsearch.painless;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.TemplateScript;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class FactoryTests extends ScriptTestCase {
protected Collection<ScriptContext<?>> scriptContexts() {
Collection<ScriptContext<?>> contexts = super.scriptContexts();
contexts.add(StatefulFactoryTestScript.CONTEXT);
contexts.add(FactoryTestScript.CONTEXT);
contexts.add(EmptyTestScript.CONTEXT);
contexts.add(TemplateScript.CONTEXT);
@Override
protected Map<ScriptContext<?>, List<Whitelist>> scriptContexts() {
Map<ScriptContext<?>, List<Whitelist>> contexts = super.scriptContexts();
contexts.put(StatefulFactoryTestScript.CONTEXT, Whitelist.BASE_WHITELISTS);
contexts.put(FactoryTestScript.CONTEXT, Whitelist.BASE_WHITELISTS);
contexts.put(EmptyTestScript.CONTEXT, Whitelist.BASE_WHITELISTS);
contexts.put(TemplateScript.CONTEXT, Whitelist.BASE_WHITELISTS);
return contexts;
}

@ -22,14 +22,17 @@ package org.elasticsearch.painless;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.IndexService;
import org.elasticsearch.index.query.QueryShardContext;
import org.elasticsearch.index.shard.IndexShard;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.SearchScript;
import org.elasticsearch.search.lookup.SearchLookup;
import org.elasticsearch.test.ESSingleNodeTestCase;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Test that needsScores() is reported correctly depending on whether _score is used
@ -40,8 +43,10 @@ public class NeedsScoreTests extends ESSingleNodeTestCase {
public void testNeedsScores() {
IndexService index = createIndex("test", Settings.EMPTY, "type", "d", "type=double");
PainlessScriptEngine service = new PainlessScriptEngine(Settings.EMPTY,
Arrays.asList(SearchScript.CONTEXT, ExecutableScript.CONTEXT));
Map<ScriptContext<?>, List<Whitelist>> contexts = new HashMap<>();
contexts.put(SearchScript.CONTEXT, Whitelist.BASE_WHITELISTS);
contexts.put(ExecutableScript.CONTEXT, Whitelist.BASE_WHITELISTS);
PainlessScriptEngine service = new PainlessScriptEngine(Settings.EMPTY, contexts);
QueryShardContext shardContext = index.newQueryShardContext(0, null, () -> 0, null);
SearchLookup lookup = new SearchLookup(index.mapperService(), shardContext::getForField, null);

@ -27,7 +27,7 @@ import org.elasticsearch.painless.Definition.Field;
import org.elasticsearch.painless.Definition.Method;
import org.elasticsearch.painless.Definition.Struct;
import org.elasticsearch.painless.Definition.Type;
import org.elasticsearch.painless.api.Augmentation;
import org.elasticsearch.painless.spi.Whitelist;
import java.io.IOException;
import java.io.PrintStream;
@ -36,7 +36,6 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@ -68,8 +67,7 @@ public class PainlessDocGenerator {
Files.newOutputStream(indexPath, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE),
false, StandardCharsets.UTF_8.name())) {
emitGeneratedWarning(indexStream);
List<Type> types = new Definition(Collections.singletonList(
WhitelistLoader.loadFromResourceFiles(Definition.class, Definition.DEFINITION_FILES))).
List<Type> types = new Definition(Whitelist.BASE_WHITELISTS).
allSimpleTypes().stream().sorted(comparing(t -> t.name)).collect(toList());
for (Type type : types) {
if (type.clazz.isPrimitive()) {

@ -24,6 +24,7 @@ import org.apache.lucene.search.Scorer;
import org.elasticsearch.common.lucene.ScorerAware;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.painless.antlr.Walker;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptException;
@ -31,10 +32,8 @@ import org.elasticsearch.script.SearchScript;
import org.elasticsearch.test.ESTestCase;
import org.junit.Before;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.elasticsearch.painless.node.SSource.MainMethodReserved;
@ -63,11 +62,10 @@ public abstract class ScriptTestCase extends ESTestCase {
/**
* Script contexts used to build the script engine. Override to customize which script contexts are available.
*/
protected Collection<ScriptContext<?>> scriptContexts() {
Collection<ScriptContext<?>> contexts = new ArrayList<>();
contexts.add(SearchScript.CONTEXT);
contexts.add(ExecutableScript.CONTEXT);
protected Map<ScriptContext<?>, List<Whitelist>> scriptContexts() {
Map<ScriptContext<?>, List<Whitelist>> contexts = new HashMap<>();
contexts.put(SearchScript.CONTEXT, Whitelist.BASE_WHITELISTS);
contexts.put(ExecutableScript.CONTEXT, Whitelist.BASE_WHITELISTS);
return contexts;
}
@ -92,8 +90,7 @@ public abstract class ScriptTestCase extends ESTestCase {
public Object exec(String script, Map<String, Object> vars, Map<String,String> compileParams, Scorer scorer, boolean picky) {
// test for ambiguity errors before running the actual script if picky is true
if (picky) {
Definition definition = new Definition(
Collections.singletonList(WhitelistLoader.loadFromResourceFiles(Definition.class, Definition.DEFINITION_FILES)));
Definition definition = new Definition(Whitelist.BASE_WHITELISTS);
ScriptClassInfo scriptClassInfo = new ScriptClassInfo(definition, GenericElasticsearchScript.class);
CompilerSettings pickySettings = new CompilerSettings();
pickySettings.setPicky(true);

@ -37,20 +37,25 @@ import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.elasticsearch.index.similarity.ScriptedSimilarity;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.SimilarityScript;
import org.elasticsearch.script.SimilarityWeightScript;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class SimilarityScriptTests extends ScriptTestCase {
@Override
protected Collection<ScriptContext<?>> scriptContexts() {
return Arrays.asList(SimilarityScript.CONTEXT, SimilarityWeightScript.CONTEXT);
protected Map<ScriptContext<?>, List<Whitelist>> scriptContexts() {
Map<ScriptContext<?>, List<Whitelist>> contexts = new HashMap<>();
contexts.put(SimilarityScript.CONTEXT, Whitelist.BASE_WHITELISTS);
contexts.put(SimilarityWeightScript.CONTEXT, Whitelist.BASE_WHITELISTS);
return contexts;
}
public void testBasics() throws IOException {

@ -33,12 +33,11 @@ import org.elasticsearch.painless.Locals.Variable;
import org.elasticsearch.painless.Location;
import org.elasticsearch.painless.Operation;
import org.elasticsearch.painless.ScriptClassInfo;
import org.elasticsearch.painless.WhitelistLoader;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.painless.antlr.Walker;
import org.elasticsearch.test.ESTestCase;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -50,8 +49,7 @@ import static org.elasticsearch.painless.node.SSource.MainMethodReserved;
* Tests {@link Object#toString} implementations on all extensions of {@link ANode}.
*/
public class NodeToStringTests extends ESTestCase {
private final Definition definition = new Definition(
Collections.singletonList(WhitelistLoader.loadFromResourceFiles(Definition.class, Definition.DEFINITION_FILES)));
private final Definition definition = new Definition(Whitelist.BASE_WHITELISTS);
public void testEAssignment() {
assertToString(

@ -26,6 +26,10 @@ esplugin {
extendedPlugins = ['lang-painless']
}
dependencies {
compileOnly project(':modules:lang-painless')
}
integTestCluster {
distribution = 'zip'
}

@ -0,0 +1,42 @@
/*
* 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;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.elasticsearch.painless.spi.PainlessExtension;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.painless.spi.WhitelistLoader;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.SearchScript;
/** An extension of painless which adds a whitelist. */
public class ExampleWhitelistExtension implements PainlessExtension {
private static final Whitelist WHITELIST =
WhitelistLoader.loadFromResourceFiles(ExampleWhitelistExtension.class, "example_whitelist.txt");
@Override
public Map<ScriptContext<?>, List<Whitelist>> getContextWhitelists() {
return Collections.singletonMap(SearchScript.CONTEXT, Collections.singletonList(WHITELIST));
}
}

@ -0,0 +1,57 @@
/*
* 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;
/**
* An example of a class to be whitelisted for use by painless scripts
*
* Each of the members and methods below are whitelisted for use in search scripts.
* See <a href="file:example_whitelist.txt">example_whitelist.txt</a>.
*/
public class ExampleWhitelistedClass {
public static final int CONSTANT = 42;
public int publicMember;
private int privateMember;
public ExampleWhitelistedClass(int publicMember, int privateMember) {
this.publicMember = publicMember;
this.privateMember = privateMember;
}
public int getPrivateMemberAccessor() {
return this.privateMember;
}
public void setPrivateMemberAccessor(int privateMember) {
this.privateMember = privateMember;
}
public static void staticMethod() {
// electricity
}
// example augmentation method
public static int toInt(String x) {
return Integer.parseInt(x);
}
}

@ -22,4 +22,5 @@ package org.elasticsearch.example.painlesswhitelist;
import org.elasticsearch.plugins.Plugin;
public class MyWhitelistPlugin extends Plugin {
// we don't actually need anything here, since whitelists are extended through SPI
}

@ -0,0 +1 @@
org.elasticsearch.example.painlesswhitelist.ExampleWhitelistExtension

@ -0,0 +1,42 @@
#
# 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.
#
# This file contains a whitelist for an example class which may be access from painless
class org.elasticsearch.example.painlesswhitelist.ExampleWhitelistedClass {
# constructor
(int, int)
# static constants and methods look the same as instance members and methods
int CONSTANT
void staticMethod()
# members lack parenthesis that methods have
int publicMember
# getter and setter for private member
int getPrivateMemberAccessor()
void setPrivateMemberAccessor(int)
}
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()
}

@ -0,0 +1,26 @@
# Example test using whitelisted members and methods
"Whitelisted custom class":
- do:
index:
index: test
type: test
id: 1
body: { "num1": 1.0 }
- do:
indices.refresh: {}
- do:
index: test
search:
body:
query:
match_all: {}
script_fields:
sNum1:
script:
source: "def e = new ExampleWhitelistedClass(6, 42); ExampleWhitelistedClass.staticMethod(); return e.publicMember + e.privateMemberAccessor + ExampleWhitelistedClass.CONSTANT + '2'.toInt()"
lang: painless
- match: { hits.total: 1 }
- match: { hits.hits.0.fields.sNum1.0: 92 }