Skip to content

Commit 05dad86

Browse files
committed
Decentralize plugin permissions (modulo docs and pluginmanager work)
1 parent 2e445d3 commit 05dad86

File tree

15 files changed

+289
-154
lines changed

15 files changed

+289
-154
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.security.Policy;
3030
import java.security.ProtectionDomain;
3131
import java.security.URIParameter;
32+
import java.util.Map;
3233

3334
/** custom policy for union of static and dynamic permissions */
3435
final class ESPolicy extends Policy {
@@ -41,13 +42,15 @@ final class ESPolicy extends Policy {
4142
final Policy template;
4243
final Policy untrusted;
4344
final PermissionCollection dynamic;
45+
final Map<String,PermissionCollection> plugins;
4446

45-
public ESPolicy(PermissionCollection dynamic) throws Exception {
47+
public ESPolicy(PermissionCollection dynamic, Map<String,PermissionCollection> plugins) throws Exception {
4648
URI policyUri = getClass().getResource(POLICY_RESOURCE).toURI();
4749
URI untrustedUri = getClass().getResource(UNTRUSTED_RESOURCE).toURI();
4850
this.template = Policy.getInstance("JavaPolicy", new URIParameter(policyUri));
4951
this.untrusted = Policy.getInstance("JavaPolicy", new URIParameter(untrustedUri));
5052
this.dynamic = dynamic;
53+
this.plugins = plugins;
5154
}
5255

5356
@Override @SuppressForbidden(reason = "fast equals check is desired")
@@ -66,6 +69,11 @@ public boolean implies(ProtectionDomain domain, Permission permission) {
6669
if (BootstrapInfo.UNTRUSTED_CODEBASE.equals(location.getFile())) {
6770
return untrusted.implies(domain, permission);
6871
}
72+
// check for an additional plugin permission
73+
PermissionCollection plugin = plugins.get(location.getFile());
74+
if (plugin != null && plugin.implies(permission)) {
75+
return true;
76+
}
6977
}
7078

7179
// Special handling for broken AWS code which destroys all SSL security

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

Lines changed: 28 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,19 @@
2323
import org.elasticsearch.env.Environment;
2424

2525
import java.io.*;
26+
import java.net.URI;
2627
import java.net.URL;
2728
import java.nio.file.AccessMode;
2829
import java.nio.file.DirectoryStream;
2930
import java.nio.file.FileAlreadyExistsException;
3031
import java.nio.file.Files;
3132
import java.nio.file.NotDirectoryException;
3233
import java.nio.file.Path;
34+
import java.security.NoSuchAlgorithmException;
35+
import java.security.PermissionCollection;
3336
import java.security.Permissions;
3437
import java.security.Policy;
38+
import java.security.URIParameter;
3539
import java.util.Collections;
3640
import java.util.HashMap;
3741
import java.util.IdentityHashMap;
@@ -65,7 +69,7 @@
6569
* when they are so dangerous that general code should not be granted the
6670
* permission, but there are extenuating circumstances.
6771
* <p>
68-
* Groovy scripts are assigned no permissions. This does not provide adequate
72+
* Scripts (groovy, javascript, python) are assigned minimal permissions. This does not provide adequate
6973
* sandboxing, as these scripts still have access to ES classes, and could
7074
* modify members, etc that would cause bad things to happen later on their
7175
* behalf (no package protections are yet in place, this would need some
@@ -81,7 +85,7 @@
8185
* <h1>Debugging Security</h1>
8286
* A good place to start when there is a problem is to turn on security debugging:
8387
* <pre>
84-
* JAVA_OPTS="-Djava.security.debug=access:failure" bin/elasticsearch
88+
* JAVA_OPTS="-Djava.security.debug=access,failure" bin/elasticsearch
8589
* </pre>
8690
* See <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/security/troubleshooting-security.html">
8791
* Troubleshooting Security</a> for information.
@@ -97,11 +101,9 @@ private Security() {}
97101
static void configure(Environment environment) throws Exception {
98102
// set properties for jar locations
99103
setCodebaseProperties();
100-
// set properties for problematic plugins
101-
setPluginCodebaseProperties(environment);
102104

103-
// enable security policy: union of template and environment-based paths.
104-
Policy.setPolicy(new ESPolicy(createPermissions(environment)));
105+
// enable security policy: union of template and environment-based paths, and possibly plugin permissions
106+
Policy.setPolicy(new ESPolicy(createPermissions(environment), getPluginPermissions(environment)));
105107

106108
// enable security manager
107109
System.setSecurityManager(new SecurityManager() {
@@ -157,70 +159,39 @@ static void setCodebaseProperties() {
157159
}
158160
}
159161

160-
// mapping of plugins to plugin class name. see getPluginClass why we need this.
161-
// plugin codebase property is always implicit (es.security.plugin.foobar)
162-
// note that this is only read once, when policy is parsed.
163-
static final Map<String,String> SPECIAL_PLUGINS;
164-
static {
165-
Map<String,String> m = new HashMap<>();
166-
m.put("repository-s3", "org.elasticsearch.plugin.repository.s3.S3RepositoryPlugin");
167-
m.put("discovery-ec2", "org.elasticsearch.plugin.discovery.ec2.Ec2DiscoveryPlugin");
168-
m.put("discovery-gce", "org.elasticsearch.plugin.discovery.gce.GceDiscoveryPlugin");
169-
m.put("lang-expression", "org.elasticsearch.script.expression.ExpressionPlugin");
170-
m.put("lang-groovy", "org.elasticsearch.script.groovy.GroovyPlugin");
171-
m.put("lang-javascript", "org.elasticsearch.plugin.javascript.JavaScriptPlugin");
172-
m.put("lang-python", "org.elasticsearch.plugin.python.PythonPlugin");
173-
SPECIAL_PLUGINS = Collections.unmodifiableMap(m);
174-
}
175-
176-
/**
177-
* Returns policy property for plugin, if it has special permissions.
178-
* otherwise returns null.
179-
*/
180-
static String getPluginProperty(String pluginName) {
181-
if (SPECIAL_PLUGINS.containsKey(pluginName)) {
182-
return "es.security.plugin." + pluginName;
183-
} else {
184-
return null;
185-
}
186-
}
187-
188-
/**
189-
* Returns plugin class name, if it has special permissions.
190-
* otherwise returns null.
191-
*/
192-
// this is only here to support the intellij IDE
193-
// it sucks to duplicate information, but its worth the tradeoff: sanity
194-
// if it gets out of sync, tests will fail.
195-
static String getPluginClass(String pluginName) {
196-
return SPECIAL_PLUGINS.get(pluginName);
197-
}
198-
199162
/**
200163
* Sets properties (codebase URLs) for policy files.
201164
* we look for matching plugins and set URLs to fit
202165
*/
203166
@SuppressForbidden(reason = "proper use of URL")
204-
static void setPluginCodebaseProperties(Environment environment) throws IOException {
167+
static Map<String,PermissionCollection> getPluginPermissions(Environment environment) throws IOException, NoSuchAlgorithmException {
168+
Map<String,PermissionCollection> map = new HashMap<>();
205169
if (Files.exists(environment.pluginsFile())) {
206170
try (DirectoryStream<Path> stream = Files.newDirectoryStream(environment.pluginsFile())) {
207171
for (Path plugin : stream) {
208-
String prop = getPluginProperty(plugin.getFileName().toString());
209-
if (prop != null) {
210-
if (System.getProperty(prop) != null) {
211-
throw new IllegalStateException("property: " + prop + " is unexpectedly set: " + System.getProperty(prop));
172+
Path policyFile = plugin.resolve("plugin-security.policy");
173+
if (Files.exists(policyFile)) {
174+
// parse the plugin's policy file into a set of permissions
175+
Policy policy = Policy.getInstance("JavaPolicy", new URIParameter(policyFile.toUri()));
176+
PermissionCollection permissions = policy.getPermissions(Security.class.getProtectionDomain());
177+
// this method is supported with the specific implementation we use, but just check for safety.
178+
if (permissions == Policy.UNSUPPORTED_EMPTY_COLLECTION) {
179+
throw new UnsupportedOperationException("JavaPolicy implementation does not support retrieving permissions");
180+
}
181+
// grant the permissions to each jar in the plugin
182+
try (DirectoryStream<Path> jarStream = Files.newDirectoryStream(plugin, "*.jar")) {
183+
for (Path jar : jarStream) {
184+
if (map.put(jar.toUri().toURL().getFile(), permissions) != null) {
185+
// just be paranoid ok?
186+
throw new IllegalStateException("per-plugin permissions already granted for jar file: " + jar);
187+
}
188+
}
212189
}
213-
System.setProperty(prop, plugin.toUri().toURL().toString() + "*");
214190
}
215191
}
216192
}
217193
}
218-
for (String plugin : SPECIAL_PLUGINS.keySet()) {
219-
String prop = getPluginProperty(plugin);
220-
if (System.getProperty(prop) == null) {
221-
System.setProperty(prop, "file:/dev/null"); // no chance to be interpreted as "all"
222-
}
223-
}
194+
return map;
224195
}
225196

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

core/src/main/resources/org/elasticsearch/bootstrap/security.policy

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -37,52 +37,6 @@ grant codeBase "${es.security.jar.lucene.core}" {
3737
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
3838
};
3939

40-
//// Special plugin permissions:
41-
//// These are dangerous permissions only needed by special plugins that we don't
42-
//// want to grant in general. Some may be due to problems in third-party library code,
43-
//// others may just be more obscure integrations.
44-
45-
grant codeBase "${es.security.plugin.repository-s3}" {
46-
// needed because of problems in aws-sdk
47-
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
48-
};
49-
50-
grant codeBase "${es.security.plugin.discovery-ec2}" {
51-
// needed because of problems in aws-sdk
52-
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
53-
};
54-
55-
grant codeBase "${es.security.plugin.discovery-gce}" {
56-
// needed because of problems in discovery-gce
57-
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
58-
};
59-
60-
grant codeBase "${es.security.plugin.lang-expression}" {
61-
// needed to generate runtime classes
62-
permission java.lang.RuntimePermission "createClassLoader";
63-
};
64-
65-
grant codeBase "${es.security.plugin.lang-groovy}" {
66-
// needed to generate runtime classes
67-
permission java.lang.RuntimePermission "createClassLoader";
68-
// needed by groovy engine
69-
permission java.lang.RuntimePermission "accessClassInPackage.sun.reflect";
70-
// needed by GroovyScriptEngineService to close its classloader (why?)
71-
permission java.lang.RuntimePermission "closeClassLoader";
72-
// Allow executing groovy scripts with codesource of /untrusted
73-
permission groovy.security.GroovyCodeSourcePermission "/untrusted";
74-
};
75-
76-
grant codeBase "${es.security.plugin.lang-javascript}" {
77-
// needed to generate runtime classes
78-
permission java.lang.RuntimePermission "createClassLoader";
79-
};
80-
81-
grant codeBase "${es.security.plugin.lang-python}" {
82-
// needed to generate runtime classes
83-
permission java.lang.RuntimePermission "createClassLoader";
84-
};
85-
8640
//// test framework permissions.
8741
//// These are mock objects and test management that we allow test framework libs
8842
//// to provide on our behalf. But tests themselves cannot do this stuff!

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

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,21 @@
2525
import org.elasticsearch.bootstrap.Security;
2626
import org.elasticsearch.common.Strings;
2727
import org.elasticsearch.common.io.PathUtils;
28-
import org.elasticsearch.common.logging.Loggers;
2928

3029
import java.io.FilePermission;
30+
import java.io.InputStream;
31+
import java.net.URI;
3132
import java.net.URL;
3233
import java.nio.file.Path;
34+
import java.security.Permission;
35+
import java.security.PermissionCollection;
3336
import java.security.Permissions;
3437
import java.security.Policy;
35-
import java.util.Map;
38+
import java.security.URIParameter;
39+
import java.util.Collections;
40+
import java.util.List;
3641
import java.util.Objects;
42+
import java.util.Properties;
3743

3844
import static com.carrotsearch.randomizedtesting.RandomizedTest.systemPropertyAsBoolean;
3945

@@ -117,33 +123,43 @@ public class BootstrapForTesting {
117123
final Policy policy;
118124
// if its a plugin with special permissions, we use a wrapper policy impl to try
119125
// to simulate what happens with a real distribution
120-
String artifact = System.getProperty("tests.artifact");
121-
// in case we are running from the IDE:
122-
if (artifact == null && System.getProperty("tests.maven") == null) {
123-
// look for plugin classname as a resource to determine what project we are.
124-
// while gross, this will work with any IDE.
125-
for (Map.Entry<String,String> kv : Security.SPECIAL_PLUGINS.entrySet()) {
126-
String resource = kv.getValue().replace('.', '/') + ".class";
127-
if (BootstrapForTesting.class.getClassLoader().getResource(resource) != null) {
128-
artifact = kv.getKey();
129-
break;
126+
List<URL> pluginPolicies = Collections.list(BootstrapForTesting.class.getClassLoader().getResources("plugin-security.policy"));
127+
if (!pluginPolicies.isEmpty()) {
128+
Permissions extra = new Permissions();
129+
for (URL url : pluginPolicies) {
130+
URI uri = url.toURI();
131+
Policy pluginPolicy = Policy.getInstance("JavaPolicy", new URIParameter(uri));
132+
PermissionCollection permissions = pluginPolicy.getPermissions(BootstrapForTesting.class.getProtectionDomain());
133+
// this method is supported with the specific implementation we use, but just check for safety.
134+
if (permissions == Policy.UNSUPPORTED_EMPTY_COLLECTION) {
135+
throw new UnsupportedOperationException("JavaPolicy implementation does not support retrieving permissions");
136+
}
137+
for (Permission permission : Collections.list(permissions.elements())) {
138+
extra.add(permission);
130139
}
131140
}
132-
}
133-
String pluginProp = Security.getPluginProperty(artifact);
134-
if (pluginProp != null) {
135-
policy = new MockPluginPolicy(perms, pluginProp);
141+
// TODO: try to get rid of this class now that the world is simpler?
142+
policy = new MockPluginPolicy(perms, extra);
136143
} else {
137-
policy = new ESPolicy(perms);
144+
policy = new ESPolicy(perms, Collections.emptyMap());
138145
}
139146
Policy.setPolicy(policy);
140147
System.setSecurityManager(new TestSecurityManager());
141148
Security.selfTest();
142149

143-
if (pluginProp != null) {
144-
// initialize the plugin class, in case it has one-time hacks (unit tests often won't do this)
145-
String clazz = Security.getPluginClass(artifact);
146-
Class.forName(clazz);
150+
// guarantee plugin classes are initialized first, in case they have one-time hacks.
151+
// this just makes unit testing more realistic
152+
for (URL url : Collections.list(BootstrapForTesting.class.getClassLoader().getResources("plugin-descriptor.properties"))) {
153+
Properties properties = new Properties();
154+
try (InputStream stream = url.openStream()) {
155+
properties.load(stream);
156+
}
157+
if (Boolean.parseBoolean(properties.getProperty("jvm"))) {
158+
String clazz = properties.getProperty("classname");
159+
if (clazz != null) {
160+
Class.forName(clazz);
161+
}
162+
}
147163
}
148164
} catch (Exception e) {
149165
throw new RuntimeException("unable to install test security manager", e);

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.security.PrivilegedAction;
3333
import java.security.ProtectionDomain;
3434
import java.security.cert.Certificate;
35+
import java.util.Collections;
3536

3637
/**
3738
* Tests for ESPolicy
@@ -54,7 +55,7 @@ public void testNullCodeSource() throws Exception {
5455
Permission all = new AllPermission();
5556
PermissionCollection allCollection = all.newPermissionCollection();
5657
allCollection.add(all);
57-
ESPolicy policy = new ESPolicy(allCollection);
58+
ESPolicy policy = new ESPolicy(allCollection, Collections.emptyMap());
5859
// restrict ourselves to NoPermission
5960
PermissionCollection noPermissions = new Permissions();
6061
assertFalse(policy.implies(new ProtectionDomain(null, noPermissions), new FilePermission("foo", "read")));
@@ -68,7 +69,7 @@ public void testNullCodeSource() throws Exception {
6869
public void testNullLocation() throws Exception {
6970
assumeTrue("test cannot run with security manager", System.getSecurityManager() == null);
7071
PermissionCollection noPermissions = new Permissions();
71-
ESPolicy policy = new ESPolicy(noPermissions);
72+
ESPolicy policy = new ESPolicy(noPermissions, Collections.emptyMap());
7273
assertFalse(policy.implies(new ProtectionDomain(new CodeSource(null, (Certificate[])null), noPermissions), new FilePermission("foo", "read")));
7374
}
7475

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

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import java.security.CodeSource;
3030
import java.security.Permission;
3131
import java.security.PermissionCollection;
32-
import java.security.Permissions;
3332
import java.security.Policy;
3433
import java.security.ProtectionDomain;
3534
import java.security.cert.Certificate;
@@ -58,33 +57,11 @@ final class MockPluginPolicy extends Policy {
5857
* adding the extra plugin permissions from {@code insecurePluginProp} to
5958
* all code except test classes.
6059
*/
61-
MockPluginPolicy(Permissions permissions, String insecurePluginProp) throws Exception {
60+
MockPluginPolicy(PermissionCollection standard, PermissionCollection extra) throws Exception {
6261
// the hack begins!
6362

64-
// parse whole policy file, with and without the substitution, compute the delta
65-
standardPolicy = new ESPolicy(permissions);
66-
67-
URL bogus = new URL("file:/bogus"); // its "any old codebase" this time: generic permissions
68-
PermissionCollection smallPermissions = standardPolicy.template.getPermissions(new CodeSource(bogus, (Certificate[])null));
69-
Set<Permission> small = new HashSet<>(Collections.list(smallPermissions.elements()));
70-
71-
// set the URL for the property substitution, this time it will also have special permissions
72-
System.setProperty(insecurePluginProp, bogus.toString());
73-
ESPolicy biggerPolicy = new ESPolicy(permissions);
74-
System.clearProperty(insecurePluginProp);
75-
PermissionCollection bigPermissions = biggerPolicy.template.getPermissions(new CodeSource(bogus, (Certificate[])null));
76-
Set<Permission> big = new HashSet<>(Collections.list(bigPermissions.elements()));
77-
78-
// compute delta to remove all the generic permissions
79-
// we want equals() vs implies() for this check, in case we need
80-
// to pass along any UnresolvedPermission to the plugin
81-
big.removeAll(small);
82-
83-
// build collection of the special permissions for easy checking
84-
extraPermissions = new Permissions();
85-
for (Permission p : big) {
86-
extraPermissions.add(p);
87-
}
63+
this.standardPolicy = new ESPolicy(standard, Collections.emptyMap());
64+
this.extraPermissions = extra;
8865

8966
excludedSources = new HashSet<CodeSource>();
9067
// exclude some obvious places
@@ -101,7 +78,7 @@ final class MockPluginPolicy extends Policy {
10178
// scripts
10279
excludedSources.add(new CodeSource(new URL("file:" + BootstrapInfo.UNTRUSTED_CODEBASE), (Certificate[])null));
10380

104-
Loggers.getLogger(getClass()).debug("Apply permissions [{}] excluding codebases [{}]", extraPermissions, excludedSources);
81+
Loggers.getLogger(getClass()).debug("Apply extra permissions [{}] excluding codebases [{}]", extraPermissions, excludedSources);
10582
}
10683

10784
@Override

0 commit comments

Comments
 (0)