Skip to content

Commit 08233a2

Browse files
committed
Add documentation and pluginmanager support
1 parent 05dad86 commit 08233a2

File tree

14 files changed

+398
-21
lines changed

14 files changed

+398
-21
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121

2222
import org.elasticsearch.common.SuppressForbidden;
2323
import org.elasticsearch.env.Environment;
24+
import org.elasticsearch.plugins.PluginInfo;
2425

2526
import java.io.*;
26-
import java.net.URI;
2727
import java.net.URL;
2828
import java.nio.file.AccessMode;
2929
import java.nio.file.DirectoryStream;
@@ -169,7 +169,7 @@ static Map<String,PermissionCollection> getPluginPermissions(Environment environ
169169
if (Files.exists(environment.pluginsFile())) {
170170
try (DirectoryStream<Path> stream = Files.newDirectoryStream(environment.pluginsFile())) {
171171
for (Path plugin : stream) {
172-
Path policyFile = plugin.resolve("plugin-security.policy");
172+
Path policyFile = plugin.resolve(PluginInfo.ES_PLUGIN_POLICY);
173173
if (Files.exists(policyFile)) {
174174
// parse the plugin's policy file into a set of permissions
175175
Policy policy = Policy.getInstance("JavaPolicy", new URIParameter(policyFile.toUri()));
@@ -191,7 +191,7 @@ static Map<String,PermissionCollection> getPluginPermissions(Environment environ
191191
}
192192
}
193193
}
194-
return map;
194+
return Collections.unmodifiableMap(map);
195195
}
196196

197197
/** returns dynamic Permissions to configured paths */

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
public class PluginInfo implements Streamable, ToXContent {
3737

3838
public static final String ES_PLUGIN_PROPERTIES = "plugin-descriptor.properties";
39+
public static final String ES_PLUGIN_POLICY = "plugin-security.policy";
3940

4041
static final class Fields {
4142
static final XContentBuilderString NAME = new XContentBuilderString("name");

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public PluginManager(Environment environment, URL url, OutputMode outputMode, Ti
100100
this.timeout = timeout;
101101
}
102102

103-
public void downloadAndExtract(String name, Terminal terminal) throws IOException {
103+
public void downloadAndExtract(String name, Terminal terminal, boolean batch) throws IOException {
104104
if (name == null && url == null) {
105105
throw new IllegalArgumentException("plugin name or url must be supplied with install.");
106106
}
@@ -124,7 +124,7 @@ public void downloadAndExtract(String name, Terminal terminal) throws IOExceptio
124124
}
125125

126126
Path pluginFile = download(pluginHandle, terminal);
127-
extract(pluginHandle, terminal, pluginFile);
127+
extract(pluginHandle, terminal, pluginFile, batch);
128128
}
129129

130130
private Path download(PluginHandle pluginHandle, Terminal terminal) throws IOException {
@@ -207,7 +207,7 @@ private Path download(PluginHandle pluginHandle, Terminal terminal) throws IOExc
207207
return pluginFile;
208208
}
209209

210-
private void extract(PluginHandle pluginHandle, Terminal terminal, Path pluginFile) throws IOException {
210+
private void extract(PluginHandle pluginHandle, Terminal terminal, Path pluginFile, boolean batch) throws IOException {
211211
// unzip plugin to a staging temp dir, named for the plugin
212212
Path tmp = Files.createTempDirectory(environment.tmpFile(), null);
213213
Path root = tmp.resolve(pluginHandle.name);
@@ -220,6 +220,13 @@ private void extract(PluginHandle pluginHandle, Terminal terminal, Path pluginFi
220220
PluginInfo info = PluginInfo.readFromProperties(root);
221221
terminal.println(VERBOSE, "%s", info);
222222

223+
// read optional security policy (extra permissions)
224+
// if it exists, confirm or warn the user
225+
Path policy = root.resolve(PluginInfo.ES_PLUGIN_POLICY);
226+
if (Files.exists(policy)) {
227+
PluginSecurity.readPolicy(policy, terminal, environment, batch);
228+
}
229+
223230
// check for jar hell before any copying
224231
if (info.isJvm()) {
225232
jarHellCheck(root, info.isIsolated());
@@ -335,7 +342,7 @@ private static void setPosixFileAttributes(Path path, UserPrincipal owner, Group
335342
fileAttributeView.setPermissions(permissions);
336343
}
337344

338-
private void tryToDeletePath(Terminal terminal, Path ... paths) {
345+
static void tryToDeletePath(Terminal terminal, Path ... paths) {
339346
for (Path path : paths) {
340347
try {
341348
IOUtils.rm(path);

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ static class Install extends Command {
180180

181181
private static final CliToolConfig.Cmd CMD = cmd(NAME, Install.class)
182182
.options(option("t", "timeout").required(false).hasArg(false))
183+
.options(option("b", "batch").required(false))
183184
.build();
184185

185186
static Command parse(Terminal terminal, CommandLine cli) {
@@ -210,21 +211,28 @@ static Command parse(Terminal terminal, CommandLine cli) {
210211
if (cli.hasOption("v")) {
211212
outputMode = OutputMode.VERBOSE;
212213
}
214+
215+
boolean batch = System.console() == null;
216+
if (cli.hasOption("b")) {
217+
batch = true;
218+
}
213219

214-
return new Install(terminal, name, outputMode, optionalPluginUrl, timeout);
220+
return new Install(terminal, name, outputMode, optionalPluginUrl, timeout, batch);
215221
}
216222

217223
final String name;
218224
private OutputMode outputMode;
219225
final URL url;
220226
final TimeValue timeout;
227+
final boolean batch;
221228

222-
Install(Terminal terminal, String name, OutputMode outputMode, URL url, TimeValue timeout) {
229+
Install(Terminal terminal, String name, OutputMode outputMode, URL url, TimeValue timeout, boolean batch) {
223230
super(terminal);
224231
this.name = name;
225232
this.outputMode = outputMode;
226233
this.url = url;
227234
this.timeout = timeout;
235+
this.batch = batch;
228236
}
229237

230238
@Override
@@ -235,7 +243,7 @@ public ExitStatus execute(Settings settings, Environment env) throws Exception {
235243
} else {
236244
terminal.println("-> Installing from " + URLDecoder.decode(url.toString(), "UTF-8") + "...");
237245
}
238-
pluginManager.downloadAndExtract(name, terminal);
246+
pluginManager.downloadAndExtract(name, terminal, batch);
239247
return ExitStatus.OK;
240248
}
241249
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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+
import org.elasticsearch.common.cli.Terminal;
23+
import org.elasticsearch.common.cli.Terminal.Verbosity;
24+
import org.elasticsearch.env.Environment;
25+
26+
import java.io.IOException;
27+
import java.nio.file.Files;
28+
import java.nio.file.Path;
29+
import java.security.NoSuchAlgorithmException;
30+
import java.security.Permission;
31+
import java.security.PermissionCollection;
32+
import java.security.Permissions;
33+
import java.security.Policy;
34+
import java.security.URIParameter;
35+
import java.security.UnresolvedPermission;
36+
import java.util.Collections;
37+
import java.util.Comparator;
38+
import java.util.List;
39+
40+
class PluginSecurity {
41+
42+
/**
43+
* Reads plugin policy, prints/confirms exceptions
44+
*/
45+
static void readPolicy(Path file, Terminal terminal, Environment environment, boolean batch) throws IOException {
46+
PermissionCollection permissions = parsePermissions(terminal, file, environment.tmpFile());
47+
List<Permission> requested = Collections.list(permissions.elements());
48+
if (requested.isEmpty()) {
49+
terminal.print(Verbosity.VERBOSE, "plugin has a policy file with no additional permissions");
50+
return;
51+
}
52+
53+
// sort permissions in a reasonable order
54+
Collections.sort(requested, new Comparator<Permission>() {
55+
@Override
56+
public int compare(Permission o1, Permission o2) {
57+
int cmp = o1.getClass().getName().compareTo(o2.getClass().getName());
58+
if (cmp == 0) {
59+
String name1 = o1.getName();
60+
String name2 = o2.getName();
61+
if (name1 == null) {
62+
name1 = "";
63+
}
64+
if (name2 == null) {
65+
name2 = "";
66+
}
67+
cmp = name1.compareTo(name2);
68+
if (cmp == 0) {
69+
String actions1 = o1.getActions();
70+
String actions2 = o2.getActions();
71+
if (actions1 == null) {
72+
actions1 = "";
73+
}
74+
if (actions2 == null) {
75+
actions2 = "";
76+
}
77+
cmp = actions1.compareTo(actions2);
78+
}
79+
}
80+
return cmp;
81+
}
82+
});
83+
84+
terminal.println(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
85+
terminal.println(Verbosity.NORMAL, "@ WARNING: plugin requires additional permissions @");
86+
terminal.println(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
87+
// print all permissions:
88+
for (Permission permission : requested) {
89+
terminal.println(Verbosity.NORMAL, "* %s", formatPermission(permission));
90+
}
91+
terminal.println(Verbosity.NORMAL, "See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html");
92+
terminal.println(Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks.");
93+
if (!batch) {
94+
terminal.println(Verbosity.NORMAL);
95+
String text = terminal.readText("Continue with installation? [y/N]");
96+
if (!text.equalsIgnoreCase("y")) {
97+
throw new RuntimeException("installation aborted by user");
98+
}
99+
}
100+
}
101+
102+
/** Format permission type, name, and actions into a string */
103+
static String formatPermission(Permission permission) {
104+
StringBuilder sb = new StringBuilder();
105+
106+
String clazz = null;
107+
if (permission instanceof UnresolvedPermission) {
108+
clazz = ((UnresolvedPermission) permission).getUnresolvedType();
109+
} else {
110+
clazz = permission.getClass().getName();
111+
}
112+
sb.append(clazz);
113+
114+
String name = null;
115+
if (permission instanceof UnresolvedPermission) {
116+
name = ((UnresolvedPermission) permission).getUnresolvedName();
117+
} else {
118+
name = permission.getName();
119+
}
120+
if (name != null && name.length() > 0) {
121+
sb.append(' ');
122+
sb.append(name);
123+
}
124+
125+
String actions = null;
126+
if (permission instanceof UnresolvedPermission) {
127+
actions = ((UnresolvedPermission) permission).getUnresolvedActions();
128+
} else {
129+
actions = permission.getActions();
130+
}
131+
if (actions != null && actions.length() > 0) {
132+
sb.append(' ');
133+
sb.append(actions);
134+
}
135+
return sb.toString();
136+
}
137+
138+
/**
139+
* Parses plugin policy into a set of permissions
140+
*/
141+
static PermissionCollection parsePermissions(Terminal terminal, Path file, Path tmpDir) throws IOException {
142+
// create a zero byte file for "comparison"
143+
// this is necessary because the default policy impl automatically grants two permissions:
144+
// 1. permission to exitVM (which we ignore)
145+
// 2. read permission to the code itself (e.g. jar file of the code)
146+
147+
Path emptyPolicyFile = Files.createTempFile(tmpDir, "empty", "tmp");
148+
final Policy emptyPolicy;
149+
try {
150+
emptyPolicy = Policy.getInstance("JavaPolicy", new URIParameter(emptyPolicyFile.toUri()));
151+
} catch (NoSuchAlgorithmException e) {
152+
throw new RuntimeException(e);
153+
}
154+
PluginManager.tryToDeletePath(terminal, emptyPolicyFile);
155+
156+
// parse the plugin's policy file into a set of permissions
157+
final Policy policy;
158+
try {
159+
policy = Policy.getInstance("JavaPolicy", new URIParameter(file.toUri()));
160+
} catch (NoSuchAlgorithmException e) {
161+
throw new RuntimeException(e);
162+
}
163+
PermissionCollection permissions = policy.getPermissions(PluginSecurity.class.getProtectionDomain());
164+
// this method is supported with the specific implementation we use, but just check for safety.
165+
if (permissions == Policy.UNSUPPORTED_EMPTY_COLLECTION) {
166+
throw new UnsupportedOperationException("JavaPolicy implementation does not support retrieving permissions");
167+
}
168+
PermissionCollection actualPermissions = new Permissions();
169+
for (Permission permission : Collections.list(permissions.elements())) {
170+
if (!emptyPolicy.implies(PluginSecurity.class.getProtectionDomain(), permission)) {
171+
actualPermissions.add(permission);
172+
}
173+
}
174+
actualPermissions.setReadOnly();
175+
return actualPermissions;
176+
}
177+
}

core/src/main/resources/org/elasticsearch/plugins/plugin-install.help

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,5 @@ OPTIONS
6161
-v,--verbose Verbose output
6262

6363
-h,--help Shows this message
64+
65+
-b,--batch Enable batch mode explicitly, automatic confirmation of security permissions

core/src/test/java/org/elasticsearch/bootstrap/BootstrapForTesting.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.elasticsearch.bootstrap.Security;
2626
import org.elasticsearch.common.Strings;
2727
import org.elasticsearch.common.io.PathUtils;
28+
import org.elasticsearch.plugins.PluginInfo;
2829

2930
import java.io.FilePermission;
3031
import java.io.InputStream;
@@ -123,7 +124,7 @@ public class BootstrapForTesting {
123124
final Policy policy;
124125
// if its a plugin with special permissions, we use a wrapper policy impl to try
125126
// to simulate what happens with a real distribution
126-
List<URL> pluginPolicies = Collections.list(BootstrapForTesting.class.getClassLoader().getResources("plugin-security.policy"));
127+
List<URL> pluginPolicies = Collections.list(BootstrapForTesting.class.getClassLoader().getResources(PluginInfo.ES_PLUGIN_POLICY));
127128
if (!pluginPolicies.isEmpty()) {
128129
Permissions extra = new Permissions();
129130
for (URL url : pluginPolicies) {
@@ -149,7 +150,7 @@ public class BootstrapForTesting {
149150

150151
// guarantee plugin classes are initialized first, in case they have one-time hacks.
151152
// this just makes unit testing more realistic
152-
for (URL url : Collections.list(BootstrapForTesting.class.getClassLoader().getResources("plugin-descriptor.properties"))) {
153+
for (URL url : Collections.list(BootstrapForTesting.class.getClassLoader().getResources(PluginInfo.ES_PLUGIN_PROPERTIES))) {
153154
Properties properties = new Properties();
154155
try (InputStream stream = url.openStream()) {
155156
properties.load(stream);

0 commit comments

Comments
 (0)