Skip to content

Commit 2ee42ed

Browse files
committed
Plugins: Add plugin extension capabilities (#27881)
This commit adds the infrastructure to plugin building and loading to allow one plugin to extend another. That is, one plugin may extend another by the "parent" plugin allowing itself to be extended through java SPI. When all plugins extending a plugin are finished loading, the "parent" plugin has a callback (through the ExtensiblePlugin interface) allowing it to reload SPI. This commit also adds an example plugin which uses as-yet implemented extensibility (adding to the painless whitelist).
1 parent bac53d5 commit 2ee42ed

File tree

34 files changed

+964
-141
lines changed

34 files changed

+964
-141
lines changed

buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesExtension.groovy

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ class PluginPropertiesExtension {
3939
@Input
4040
String classname
4141

42+
/** Other plugins this plugin extends through SPI */
43+
@Input
44+
List<String> extendedPlugins = []
45+
4246
@Input
4347
boolean hasNativeController = false
4448

buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesTask.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class PluginPropertiesTask extends Copy {
8080
'elasticsearchVersion': stringSnap(VersionProperties.elasticsearch),
8181
'javaVersion': project.targetCompatibility as String,
8282
'classname': extension.classname,
83+
'extendedPlugins': extension.extendedPlugins.join(','),
8384
'hasNativeController': extension.hasNativeController,
8485
'requiresKeystore': extension.requiresKeystore
8586
]

buildSrc/src/main/resources/plugin-descriptor.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ java.version=${javaVersion}
4040
elasticsearch.version=${elasticsearchVersion}
4141
### optional elements for plugins:
4242
#
43+
# 'extended.plugins': other plugins this plugin extends through SPI
44+
extended.plugins=${extendedPlugins}
45+
#
4346
# 'has.native.controller': whether or not the plugin has a native controller
4447
has.native.controller=${hasNativeController}
4548
#

core/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ archivesBaseName = 'elasticsearch'
3838

3939
dependencies {
4040

41+
compileOnly project(':libs:plugin-classloader')
42+
testRuntime project(':libs:plugin-classloader')
43+
4144
// lucene
4245
compile "org.apache.lucene:lucene-core:${versions.lucene}"
4346
compile "org.apache.lucene:lucene-analyzers-common:${versions.lucene}"

core/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.security.ProtectionDomain;
3434
import java.util.Collections;
3535
import java.util.Map;
36+
import java.util.Set;
3637
import java.util.function.Predicate;
3738

3839
/** custom policy for union of static and dynamic permissions */
@@ -49,9 +50,9 @@ final class ESPolicy extends Policy {
4950
final PermissionCollection dynamic;
5051
final Map<String,Policy> plugins;
5152

52-
ESPolicy(PermissionCollection dynamic, Map<String,Policy> plugins, boolean filterBadDefaults) {
53-
this.template = Security.readPolicy(getClass().getResource(POLICY_RESOURCE), JarHell.parseClassPath());
54-
this.untrusted = Security.readPolicy(getClass().getResource(UNTRUSTED_RESOURCE), Collections.emptySet());
53+
ESPolicy(Map<String, URL> codebases, PermissionCollection dynamic, Map<String,Policy> plugins, boolean filterBadDefaults) {
54+
this.template = Security.readPolicy(getClass().getResource(POLICY_RESOURCE), codebases);
55+
this.untrusted = Security.readPolicy(getClass().getResource(UNTRUSTED_RESOURCE), Collections.emptyMap());
5556
if (filterBadDefaults) {
5657
this.system = new SystemPolicy(Policy.getPolicy());
5758
} else {

core/src/main/java/org/elasticsearch/bootstrap/Security.java

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,13 @@
4848
import java.util.Collections;
4949
import java.util.HashMap;
5050
import java.util.HashSet;
51+
import java.util.LinkedHashMap;
5152
import java.util.LinkedHashSet;
5253
import java.util.List;
5354
import java.util.Map;
5455
import java.util.Set;
56+
import java.util.function.Function;
57+
import java.util.stream.Collectors;
5558

5659
import static org.elasticsearch.bootstrap.FilePermissionUtils.addDirectoryPath;
5760
import static org.elasticsearch.bootstrap.FilePermissionUtils.addSingleFilePath;
@@ -116,7 +119,8 @@ private Security() {}
116119
static void configure(Environment environment, boolean filterBadDefaults) throws IOException, NoSuchAlgorithmException {
117120

118121
// enable security policy: union of template and environment-based paths, and possibly plugin permissions
119-
Policy.setPolicy(new ESPolicy(createPermissions(environment), getPluginPermissions(environment), filterBadDefaults));
122+
Map<String, URL> codebases = getCodebaseJarMap(JarHell.parseClassPath());
123+
Policy.setPolicy(new ESPolicy(codebases, createPermissions(environment), getPluginPermissions(environment), filterBadDefaults));
120124

121125
// enable security manager
122126
final String[] classesThatCanExit =
@@ -130,6 +134,27 @@ static void configure(Environment environment, boolean filterBadDefaults) throws
130134
selfTest();
131135
}
132136

137+
/**
138+
* Return a map from codebase name to codebase url of jar codebases used by ES core.
139+
*/
140+
@SuppressForbidden(reason = "find URL path")
141+
static Map<String, URL> getCodebaseJarMap(Set<URL> urls) {
142+
Map<String, URL> codebases = new LinkedHashMap<>(); // maintain order
143+
for (URL url : urls) {
144+
try {
145+
String fileName = PathUtils.get(url.toURI()).getFileName().toString();
146+
if (fileName.endsWith(".jar") == false) {
147+
// tests :(
148+
continue;
149+
}
150+
codebases.put(fileName, url);
151+
} catch (URISyntaxException e) {
152+
throw new RuntimeException(e);
153+
}
154+
}
155+
return codebases;
156+
}
157+
133158
/**
134159
* Sets properties (codebase URLs) for policy files.
135160
* we look for matching plugins and set URLs to fit
@@ -174,7 +199,7 @@ static Map<String,Policy> getPluginPermissions(Environment environment) throws I
174199
}
175200

176201
// parse the plugin's policy file into a set of permissions
177-
Policy policy = readPolicy(policyFile.toUri().toURL(), codebases);
202+
Policy policy = readPolicy(policyFile.toUri().toURL(), getCodebaseJarMap(codebases));
178203

179204
// consult this policy for each of the plugin's jars:
180205
for (URL url : codebases) {
@@ -197,21 +222,20 @@ static Map<String,Policy> getPluginPermissions(Environment environment) throws I
197222
* would map to full URL.
198223
*/
199224
@SuppressForbidden(reason = "accesses fully qualified URLs to configure security")
200-
static Policy readPolicy(URL policyFile, Set<URL> codebases) {
225+
static Policy readPolicy(URL policyFile, Map<String, URL> codebases) {
201226
try {
202227
List<String> propertiesSet = new ArrayList<>();
203228
try {
204229
// set codebase properties
205-
for (URL url : codebases) {
206-
String fileName = PathUtils.get(url.toURI()).getFileName().toString();
207-
if (fileName.endsWith(".jar") == false) {
208-
continue; // tests :(
209-
}
230+
for (Map.Entry<String,URL> codebase : codebases.entrySet()) {
231+
String name = codebase.getKey();
232+
URL url = codebase.getValue();
233+
210234
// We attempt to use a versionless identifier for each codebase. This assumes a specific version
211235
// format in the jar filename. While we cannot ensure all jars in all plugins use this format, nonconformity
212236
// only means policy grants would need to include the entire jar filename as they always have before.
213-
String property = "codebase." + fileName;
214-
String aliasProperty = "codebase." + fileName.replaceFirst("-\\d+\\.\\d+.*\\.jar", "");
237+
String property = "codebase." + name;
238+
String aliasProperty = "codebase." + name.replaceFirst("-\\d+\\.\\d+.*\\.jar", "");
215239
if (aliasProperty.equals(property) == false) {
216240
propertiesSet.add(aliasProperty);
217241
String previous = System.setProperty(aliasProperty, url.toString());
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.plugins;
21+
22+
/**
23+
* An extension point for {@link Plugin} implementations to be themselves extensible.
24+
*
25+
* This class provides a callback for extensible plugins to be informed of other plugins
26+
* which extend them.
27+
*/
28+
public interface ExtensiblePlugin {
29+
30+
/**
31+
* Reload any SPI implementations from the given classloader.
32+
*/
33+
default void reloadSPI(ClassLoader loader) {}
34+
}

core/src/main/java/org/elasticsearch/plugins/PluginInfo.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,20 @@
2222
import org.elasticsearch.Version;
2323
import org.elasticsearch.bootstrap.JarHell;
2424
import org.elasticsearch.common.Booleans;
25+
import org.elasticsearch.common.Strings;
2526
import org.elasticsearch.common.io.stream.StreamInput;
2627
import org.elasticsearch.common.io.stream.StreamOutput;
2728
import org.elasticsearch.common.io.stream.Writeable;
28-
import org.elasticsearch.common.xcontent.ToXContent.Params;
2929
import org.elasticsearch.common.xcontent.ToXContentObject;
3030
import org.elasticsearch.common.xcontent.XContentBuilder;
3131

3232
import java.io.IOException;
3333
import java.io.InputStream;
3434
import java.nio.file.Files;
3535
import java.nio.file.Path;
36+
import java.util.Arrays;
37+
import java.util.Collections;
38+
import java.util.List;
3639
import java.util.Locale;
3740
import java.util.Map;
3841
import java.util.Properties;
@@ -51,6 +54,7 @@ public class PluginInfo implements Writeable, ToXContentObject {
5154
private final String description;
5255
private final String version;
5356
private final String classname;
57+
private final List<String> extendedPlugins;
5458
private final boolean hasNativeController;
5559
private final boolean requiresKeystore;
5660

@@ -61,15 +65,17 @@ public class PluginInfo implements Writeable, ToXContentObject {
6165
* @param description a description of the plugin
6266
* @param version the version of Elasticsearch the plugin is built for
6367
* @param classname the entry point to the plugin
68+
* @param extendedPlugins other plugins this plugin extends through SPI
6469
* @param hasNativeController whether or not the plugin has a native controller
6570
* @param requiresKeystore whether or not the plugin requires the elasticsearch keystore to be created
6671
*/
6772
public PluginInfo(String name, String description, String version, String classname,
68-
boolean hasNativeController, boolean requiresKeystore) {
73+
List<String> extendedPlugins, boolean hasNativeController, boolean requiresKeystore) {
6974
this.name = name;
7075
this.description = description;
7176
this.version = version;
7277
this.classname = classname;
78+
this.extendedPlugins = Collections.unmodifiableList(extendedPlugins);
7379
this.hasNativeController = hasNativeController;
7480
this.requiresKeystore = requiresKeystore;
7581
}
@@ -85,6 +91,11 @@ public PluginInfo(final StreamInput in) throws IOException {
8591
this.description = in.readString();
8692
this.version = in.readString();
8793
this.classname = in.readString();
94+
if (in.getVersion().onOrAfter(Version.V_6_2_0)) {
95+
extendedPlugins = in.readList(StreamInput::readString);
96+
} else {
97+
extendedPlugins = Collections.emptyList();
98+
}
8899
if (in.getVersion().onOrAfter(Version.V_5_4_0)) {
89100
hasNativeController = in.readBoolean();
90101
} else {
@@ -103,6 +114,9 @@ public void writeTo(final StreamOutput out) throws IOException {
103114
out.writeString(description);
104115
out.writeString(version);
105116
out.writeString(classname);
117+
if (out.getVersion().onOrAfter(Version.V_6_2_0)) {
118+
out.writeStringList(extendedPlugins);
119+
}
106120
if (out.getVersion().onOrAfter(Version.V_5_4_0)) {
107121
out.writeBoolean(hasNativeController);
108122
}
@@ -176,6 +190,14 @@ public static PluginInfo readFromProperties(final Path path) throws IOException
176190
"property [classname] is missing for plugin [" + name + "]");
177191
}
178192

193+
final String extendedString = propsMap.remove("extended.plugins");
194+
final List<String> extendedPlugins;
195+
if (extendedString == null) {
196+
extendedPlugins = Collections.emptyList();
197+
} else {
198+
extendedPlugins = Arrays.asList(Strings.delimitedListToStringArray(extendedString, ","));
199+
}
200+
179201
final String hasNativeControllerValue = propsMap.remove("has.native.controller");
180202
final boolean hasNativeController;
181203
if (hasNativeControllerValue == null) {
@@ -216,7 +238,7 @@ public static PluginInfo readFromProperties(final Path path) throws IOException
216238
throw new IllegalArgumentException("Unknown properties in plugin descriptor: " + propsMap.keySet());
217239
}
218240

219-
return new PluginInfo(name, description, version, classname, hasNativeController, requiresKeystore);
241+
return new PluginInfo(name, description, version, classname, extendedPlugins, hasNativeController, requiresKeystore);
220242
}
221243

222244
/**
@@ -246,6 +268,15 @@ public String getClassname() {
246268
return classname;
247269
}
248270

271+
/**
272+
* Other plugins this plugin extends through SPI.
273+
*
274+
* @return the names of the plugins extended
275+
*/
276+
public List<String> getExtendedPlugins() {
277+
return extendedPlugins;
278+
}
279+
249280
/**
250281
* The version of Elasticsearch the plugin was built for.
251282
*
@@ -281,6 +312,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
281312
builder.field("version", version);
282313
builder.field("description", description);
283314
builder.field("classname", classname);
315+
builder.field("extended_plugins", extendedPlugins);
284316
builder.field("has_native_controller", hasNativeController);
285317
builder.field("requires_keystore", requiresKeystore);
286318
}
@@ -316,6 +348,7 @@ public String toString() {
316348
.append("Version: ").append(version).append("\n")
317349
.append("Native Controller: ").append(hasNativeController).append("\n")
318350
.append("Requires Keystore: ").append(requiresKeystore).append("\n")
351+
.append("Extended Plugins: ").append(extendedPlugins).append("\n")
319352
.append(" * Classname: ").append(classname);
320353
return information.toString();
321354
}

core/src/main/java/org/elasticsearch/plugins/DummyPluginInfo.java renamed to core/src/main/java/org/elasticsearch/plugins/PluginLoaderIndirection.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,18 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19+
1920
package org.elasticsearch.plugins;
2021

21-
public class DummyPluginInfo extends PluginInfo {
22+
import java.util.List;
2223

23-
private DummyPluginInfo(String name, String description, String version, String classname) {
24-
super(name, description, version, classname, false, false);
25-
}
24+
/**
25+
* This class exists solely as an intermediate layer to avoid causing PluginsService
26+
* to load ExtendedPluginsClassLoader when used in the transport client.
27+
*/
28+
class PluginLoaderIndirection {
2629

27-
public static final DummyPluginInfo INSTANCE =
28-
new DummyPluginInfo(
29-
"dummy_plugin_name",
30-
"dummy plugin description",
31-
"dummy_plugin_version",
32-
"DummyPluginName");
30+
static ClassLoader createLoader(ClassLoader parent, List<ClassLoader> extendedLoaders) {
31+
return ExtendedPluginsClassLoader.create(parent, extendedLoaders);
32+
}
3333
}

0 commit comments

Comments
 (0)