Reuse policy parsing for plugins (#64089)

Plugin policy parsing is currently split, with different code executed
for Elasticsearch startup vs installing a plugin. This commit
refactors the policy parsing to be utilized by both places. The main
benefit is policy files in both places now handle permissions not only
for a global grant, but also codebase specific grants.
This commit is contained in:
Ryan Ernst 2020-10-29 14:11:38 -07:00 committed by GitHub
parent a740a6dd80
commit 2c58841887
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 435 additions and 162 deletions

View file

@ -38,6 +38,8 @@ import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProv
import org.elasticsearch.Build;
import org.elasticsearch.Version;
import org.elasticsearch.bootstrap.JarHell;
import org.elasticsearch.bootstrap.PluginPolicyInfo;
import org.elasticsearch.bootstrap.PolicyUtil;
import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
@ -848,15 +850,11 @@ class InstallPluginCommand extends EnvironmentAwareCommand {
private PluginInfo installPlugin(Terminal terminal, boolean isBatch, Path tmpRoot, Environment env, List<Path> deleteOnFailure)
throws Exception {
final PluginInfo info = loadPluginInfo(terminal, tmpRoot, env);
// read optional security policy (extra permissions), if it exists, confirm or warn the user
Path policy = tmpRoot.resolve(PluginInfo.ES_PLUGIN_POLICY);
final Set<String> permissions;
if (Files.exists(policy)) {
permissions = PluginSecurity.parsePermissions(policy, env.tmpFile());
} else {
permissions = Collections.emptySet();
PluginPolicyInfo pluginPolicy = PolicyUtil.getPluginPolicyInfo(tmpRoot);
if (pluginPolicy != null) {
Set<String> permissions = PluginSecurity.getPermissionDescriptions(pluginPolicy, env.tmpFile());
PluginSecurity.confirmPolicyExceptions(terminal, permissions, isBatch);
}
PluginSecurity.confirmPolicyExceptions(terminal, permissions, isBatch);
final Path destination = env.pluginsFile().resolve(info.getName());
deleteOnFailure.add(destination);

View file

@ -0,0 +1,141 @@
/*
* 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.bootstrap;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.plugins.PluginInfo;
import org.elasticsearch.test.ESTestCase;
import org.junit.Before;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Permission;
import java.security.Policy;
import java.security.URIParameter;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
import static org.hamcrest.collection.IsMapContaining.hasKey;
public class PolicyUtilTests extends ESTestCase {
@Before
public void assumeSecurityManagerDisabled() {
assumeTrue(
"test cannot run with security manager enabled",
System.getSecurityManager() == null);
}
URL makeUrl(String s) {
try {
return new URL(s);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
Path makeDummyPlugin(String policy, String... files) throws IOException {
Path plugin = createTempDir();
Files.copy(this.getDataPath(policy), plugin.resolve(PluginInfo.ES_PLUGIN_POLICY));
for (String file : files) {
Files.createFile(plugin.resolve(file));
}
return plugin;
}
@SuppressForbidden(reason = "set for test")
void setProperty(String key, String value) {
System.setProperty(key, value);
}
@SuppressForbidden(reason = "cleanup test")
void clearProperty(String key) {
System.clearProperty(key);
}
public void testCodebaseJarMap() throws Exception {
Set<URL> urls = new LinkedHashSet<>(List.of(
makeUrl("file:///foo.jar"),
makeUrl("file:///bar.txt"),
makeUrl("file:///a/bar.jar")
));
Map<String, URL> jarMap = PolicyUtil.getCodebaseJarMap(urls);
assertThat(jarMap, hasKey("foo.jar"));
assertThat(jarMap, hasKey("bar.jar"));
// only jars are grabbed
assertThat(jarMap, not(hasKey("bar.txt")));
// order matters
assertThat(jarMap.keySet(), contains("foo.jar", "bar.jar"));
}
public void testPluginPolicyInfoEmpty() throws Exception {
assertThat(PolicyUtil.getPluginPolicyInfo(createTempDir()), is(nullValue()));
}
public void testPluginPolicyInfoNoJars() throws Exception {
Path noJarsPlugin = makeDummyPlugin("dummy.policy");
PluginPolicyInfo info = PolicyUtil.getPluginPolicyInfo(noJarsPlugin);
assertThat(info.policy, is(not(nullValue())));
assertThat(info.jars, emptyIterable());
}
public void testPluginPolicyInfo() throws Exception {
Path plugin = makeDummyPlugin("dummy.policy",
"foo.jar", "foo.txt", "bar.jar");
PluginPolicyInfo info = PolicyUtil.getPluginPolicyInfo(plugin);
assertThat(info.policy, is(not(nullValue())));
assertThat(info.jars, containsInAnyOrder(
plugin.resolve("foo.jar").toUri().toURL(),
plugin.resolve("bar.jar").toUri().toURL()));
}
public void testPolicyPermissions() throws Exception {
Path plugin = makeDummyPlugin("global-and-jar.policy", "foo.jar", "bar.jar");
Path tmpDir = createTempDir();
try {
URL jarUrl = plugin.resolve("foo.jar").toUri().toURL();
setProperty("jarUrl", jarUrl.toString());
URL policyFile = plugin.resolve(PluginInfo.ES_PLUGIN_POLICY).toUri().toURL();
Policy policy = Policy.getInstance("JavaPolicy", new URIParameter(policyFile.toURI()));
Set<Permission> globalPermissions = PolicyUtil.getPolicyPermissions(null, policy, tmpDir);
assertThat(globalPermissions, contains(new RuntimePermission("queuePrintJob")));
Set<Permission> jarPermissions = PolicyUtil.getPolicyPermissions(jarUrl, policy, tmpDir);
assertThat(jarPermissions,
containsInAnyOrder(new RuntimePermission("getClassLoader"), new RuntimePermission("queuePrintJob")));
} finally {
clearProperty("jarUrl");
}
}
}

View file

@ -19,8 +19,12 @@
package org.elasticsearch.plugins;
import org.elasticsearch.bootstrap.PluginPolicyInfo;
import org.elasticsearch.bootstrap.PolicyUtil;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
@ -30,14 +34,23 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
/** Tests plugin manager security check */
public class PluginSecurityTests extends ESTestCase {
PluginPolicyInfo makeDummyPlugin(String policy, String... files) throws IOException {
Path plugin = createTempDir();
Files.copy(this.getDataPath(policy), plugin.resolve(PluginInfo.ES_PLUGIN_POLICY));
for (String file : files) {
Files.createFile(plugin.resolve(file));
}
return PolicyUtil.getPluginPolicyInfo(plugin);
}
/** Test that we can parse the set of permissions correctly for a simple policy */
public void testParsePermissions() throws Exception {
assumeTrue(
"test cannot run with security manager enabled",
System.getSecurityManager() == null);
Path scratch = createTempDir();
Path testFile = this.getDataPath("security/simple-plugin-security.policy");
Set<String> actual = PluginSecurity.parsePermissions(testFile, scratch);
PluginPolicyInfo info = makeDummyPlugin("security/simple-plugin-security.policy");
Set<String> actual = PluginSecurity.getPermissionDescriptions(info, scratch);
assertThat(actual, contains(PluginSecurity.formatPermission(new RuntimePermission("queuePrintJob"))));
}
@ -47,8 +60,8 @@ public class PluginSecurityTests extends ESTestCase {
"test cannot run with security manager enabled",
System.getSecurityManager() == null);
Path scratch = createTempDir();
Path testFile = this.getDataPath("security/complex-plugin-security.policy");
Set<String> actual = PluginSecurity.parsePermissions(testFile, scratch);
PluginPolicyInfo info = makeDummyPlugin("security/complex-plugin-security.policy");
Set<String> actual = PluginSecurity.getPermissionDescriptions(info, scratch);
assertThat(actual, containsInAnyOrder(
PluginSecurity.formatPermission(new RuntimePermission("getClassLoader")),
PluginSecurity.formatPermission(new RuntimePermission("closeClassLoader"))));
@ -67,8 +80,8 @@ public class PluginSecurityTests extends ESTestCase {
"test cannot run with security manager enabled",
System.getSecurityManager() == null);
Path scratch = createTempDir();
Path testFile = this.getDataPath("security/unresolved-plugin-security.policy");
Set<String> permissions = PluginSecurity.parsePermissions(testFile, scratch);
PluginPolicyInfo info = makeDummyPlugin("security/unresolved-plugin-security.policy");
Set<String> permissions = PluginSecurity.getPermissionDescriptions(info, scratch);
assertThat(permissions, contains("org.fake.FakePermission fakeName"));
}
}

View file

@ -0,0 +1,4 @@
grant {
// needed to waste paper
permission java.lang.RuntimePermission "queuePrintJob";
};

View file

@ -0,0 +1,8 @@
grant {
// needed to waste paper
permission java.lang.RuntimePermission "queuePrintJob";
};
grant codeBase "${jarUrl}" {
permission java.lang.RuntimePermission "getClassLoader";
};

View file

@ -52,9 +52,9 @@ final class ESPolicy extends Policy {
ESPolicy(Map<String, URL> codebases, PermissionCollection dynamic, Map<String,Policy> plugins, boolean filterBadDefaults,
PermissionCollection dataPathPermission) {
this.template = Security.readPolicy(getClass().getResource(POLICY_RESOURCE), codebases);
this.template = PolicyUtil.readPolicy(getClass().getResource(POLICY_RESOURCE), codebases);
this.dataPathPermission = dataPathPermission;
this.untrusted = Security.readPolicy(getClass().getResource(UNTRUSTED_RESOURCE), Collections.emptyMap());
this.untrusted = PolicyUtil.readPolicy(getClass().getResource(UNTRUSTED_RESOURCE), Collections.emptyMap());
if (filterBadDefaults) {
this.system = new SystemPolicy(Policy.getPolicy());
} else {

View file

@ -0,0 +1,34 @@
/*
* 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.bootstrap;
import java.net.URL;
import java.security.Policy;
import java.util.Set;
public class PluginPolicyInfo {
public final Set<URL> jars;
public final Policy policy;
PluginPolicyInfo(Set<URL> jars, Policy policy) {
this.jars = jars;
this.policy = policy;
}
}

View file

@ -0,0 +1,193 @@
/*
* 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.bootstrap;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.core.internal.io.IOUtils;
import org.elasticsearch.plugins.PluginInfo;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.CodeSource;
import java.security.NoSuchAlgorithmException;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Policy;
import java.security.ProtectionDomain;
import java.security.URIParameter;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class PolicyUtil {
/**
* Return a map from codebase name to codebase url of jar codebases used by ES core.
*/
@SuppressForbidden(reason = "find URL path")
public static Map<String, URL> getCodebaseJarMap(Set<URL> urls) {
Map<String, URL> codebases = new LinkedHashMap<>(); // maintain order
for (URL url : urls) {
try {
String fileName = PathUtils.get(url.toURI()).getFileName().toString();
if (fileName.endsWith(".jar") == false) {
// tests :(
continue;
}
codebases.put(fileName, url);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
return codebases;
}
/**
* Reads and returns the specified {@code policyFile}.
* <p>
* Jar files listed in {@code codebases} location will be provided to the policy file via
* a system property of the short name: e.g. <code>${codebase.joda-convert-1.2.jar}</code>
* would map to full URL.
*/
@SuppressForbidden(reason = "accesses fully qualified URLs to configure security")
public static Policy readPolicy(URL policyFile, Map<String, URL> codebases) {
try {
List<String> propertiesSet = new ArrayList<>();
try {
// set codebase properties
for (Map.Entry<String,URL> codebase : codebases.entrySet()) {
String name = codebase.getKey();
URL url = codebase.getValue();
// We attempt to use a versionless identifier for each codebase. This assumes a specific version
// format in the jar filename. While we cannot ensure all jars in all plugins use this format, nonconformity
// only means policy grants would need to include the entire jar filename as they always have before.
String property = "codebase." + name;
String aliasProperty = "codebase." + name.replaceFirst("-\\d+\\.\\d+.*\\.jar", "");
if (aliasProperty.equals(property) == false) {
propertiesSet.add(aliasProperty);
String previous = System.setProperty(aliasProperty, url.toString());
if (previous != null) {
throw new IllegalStateException("codebase property already set: " + aliasProperty + " -> " + previous +
", cannot set to " + url.toString());
}
}
propertiesSet.add(property);
String previous = System.setProperty(property, url.toString());
if (previous != null) {
throw new IllegalStateException("codebase property already set: " + property + " -> " + previous +
", cannot set to " + url.toString());
}
}
return Policy.getInstance("JavaPolicy", new URIParameter(policyFile.toURI()));
} finally {
// clear codebase properties
for (String property : propertiesSet) {
System.clearProperty(property);
}
}
} catch (NoSuchAlgorithmException | URISyntaxException e) {
throw new IllegalArgumentException("unable to parse policy file `" + policyFile + "`", e);
}
}
/**
* Return info about the security policy for a plugin.
*/
public static PluginPolicyInfo getPluginPolicyInfo(Path pluginRoot) throws IOException {
Path policyFile = pluginRoot.resolve(PluginInfo.ES_PLUGIN_POLICY);
if (Files.exists(policyFile) == false) {
return null;
}
// first get a list of URLs for the plugins' jars:
// we resolve symlinks so map is keyed on the normalize codebase name
Set<URL> jars = new LinkedHashSet<>(); // order is already lost, but some filesystems have it
try (DirectoryStream<Path> jarStream = Files.newDirectoryStream(pluginRoot, "*.jar")) {
for (Path jar : jarStream) {
URL url = jar.toRealPath().toUri().toURL();
if (jars.add(url) == false) {
throw new IllegalStateException("duplicate module/plugin: " + url);
}
}
}
// parse the plugin's policy file into a set of permissions
Policy policy = readPolicy(policyFile.toUri().toURL(), getCodebaseJarMap(jars));
return new PluginPolicyInfo(jars, policy);
}
/**
* Return permissions for a policy that apply to a jar.
*
* @param url The url of a jar to find permissions for, or {@code null} for global permissions.
*/
public static Set<Permission> getPolicyPermissions(URL url, Policy policy, Path tmpDir) throws IOException {
// create a zero byte file for "comparison"
// this is necessary because the default policy impl automatically grants two permissions:
// 1. permission to exitVM (which we ignore)
// 2. read permission to the code itself (e.g. jar file of the code)
Path emptyPolicyFile = Files.createTempFile(tmpDir, "empty", "tmp");
final Policy emptyPolicy;
try {
emptyPolicy = Policy.getInstance("JavaPolicy", new URIParameter(emptyPolicyFile.toUri()));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
IOUtils.rm(emptyPolicyFile);
final ProtectionDomain protectionDomain;
if (url == null) {
// global, use PolicyUtil since it is part of core ES
protectionDomain = PolicyUtil.class.getProtectionDomain();
} else {
// we may not have the url loaded, so create a fake protection domain
protectionDomain = new ProtectionDomain(new CodeSource(url, (Certificate[]) null), null);
}
PermissionCollection permissions = policy.getPermissions(protectionDomain);
// this method is supported with the specific implementation we use, but just check for safety.
if (permissions == Policy.UNSUPPORTED_EMPTY_COLLECTION) {
throw new UnsupportedOperationException("JavaPolicy implementation does not support retrieving permissions");
}
Set<Permission> actualPermissions = new HashSet<>();
for (Permission permission : Collections.list(permissions.elements())) {
if (emptyPolicy.implies(protectionDomain, permission) == false) {
actualPermissions.add(permission);
}
}
return actualPermissions;
}
}

View file

@ -25,7 +25,6 @@ import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.http.HttpTransportSettings;
import org.elasticsearch.plugins.PluginInfo;
import org.elasticsearch.plugins.PluginsService;
import org.elasticsearch.secure_sm.SecureSM;
import org.elasticsearch.transport.TcpTransport;
@ -35,7 +34,6 @@ import java.net.SocketPermission;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.AccessMode;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.NotDirectoryException;
@ -43,14 +41,10 @@ import java.nio.file.Path;
import java.security.NoSuchAlgorithmException;
import java.security.Permissions;
import java.security.Policy;
import java.security.URIParameter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -117,9 +111,9 @@ final class Security {
static void configure(Environment environment, boolean filterBadDefaults) throws IOException, NoSuchAlgorithmException {
// enable security policy: union of template and environment-based paths, and possibly plugin permissions
Map<String, URL> codebases = getCodebaseJarMap(JarHell.parseClassPath());
Policy.setPolicy(new ESPolicy(codebases, createPermissions(environment), getPluginPermissions(environment), filterBadDefaults,
createRecursiveDataPathPermission(environment)));
Map<String, URL> codebases = PolicyUtil.getCodebaseJarMap(JarHell.parseClassPath());
Policy.setPolicy(new ESPolicy(codebases, createPermissions(environment),
getPluginAndModulePermissions(environment), filterBadDefaults, createRecursiveDataPathPermission(environment)));
// enable security manager
final String[] classesThatCanExit =
@ -133,33 +127,12 @@ final class Security {
selfTest();
}
/**
* Return a map from codebase name to codebase url of jar codebases used by ES core.
*/
@SuppressForbidden(reason = "find URL path")
static Map<String, URL> getCodebaseJarMap(Set<URL> urls) {
Map<String, URL> codebases = new LinkedHashMap<>(); // maintain order
for (URL url : urls) {
try {
String fileName = PathUtils.get(url.toURI()).getFileName().toString();
if (fileName.endsWith(".jar") == false) {
// tests :(
continue;
}
codebases.put(fileName, url);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
return codebases;
}
/**
* Sets properties (codebase URLs) for policy files.
* we look for matching plugins and set URLs to fit
*/
@SuppressForbidden(reason = "proper use of URL")
static Map<String,Policy> getPluginPermissions(Environment environment) throws IOException, NoSuchAlgorithmException {
static Map<String,Policy> getPluginAndModulePermissions(Environment environment) throws IOException {
Map<String,Policy> map = new HashMap<>();
// collect up set of plugins and modules by listing directories.
Set<Path> pluginsAndModules = new LinkedHashSet<>(PluginsService.findPluginDirs(environment.pluginsFile()));
@ -167,29 +140,16 @@ final class Security {
// now process each one
for (Path plugin : pluginsAndModules) {
Path policyFile = plugin.resolve(PluginInfo.ES_PLUGIN_POLICY);
if (Files.exists(policyFile)) {
// first get a list of URLs for the plugins' jars:
// we resolve symlinks so map is keyed on the normalize codebase name
Set<URL> codebases = new LinkedHashSet<>(); // order is already lost, but some filesystems have it
try (DirectoryStream<Path> jarStream = Files.newDirectoryStream(plugin, "*.jar")) {
for (Path jar : jarStream) {
URL url = jar.toRealPath().toUri().toURL();
if (codebases.add(url) == false) {
throw new IllegalStateException("duplicate module/plugin: " + url);
}
}
}
PluginPolicyInfo pluginPolicy = PolicyUtil.getPluginPolicyInfo(plugin);
if (pluginPolicy == null) {
continue;
}
// parse the plugin's policy file into a set of permissions
Policy policy = readPolicy(policyFile.toUri().toURL(), getCodebaseJarMap(codebases));
// consult this policy for each of the plugin's jars:
for (URL url : codebases) {
if (map.put(url.getFile(), policy) != null) {
// just be paranoid ok?
throw new IllegalStateException("per-plugin permissions already granted for jar file: " + url);
}
// consult this policy for each of the plugin's jars:
for (URL jar : pluginPolicy.jars) {
if (map.put(jar.getFile(), pluginPolicy.policy) != null) {
// just be paranoid ok?
throw new IllegalStateException("per-plugin permissions already granted for jar file: " + jar);
}
}
}
@ -197,55 +157,6 @@ final class Security {
return Collections.unmodifiableMap(map);
}
/**
* Reads and returns the specified {@code policyFile}.
* <p>
* Jar files listed in {@code codebases} location will be provided to the policy file via
* a system property of the short name: e.g. <code>${codebase.joda-convert-1.2.jar}</code>
* would map to full URL.
*/
@SuppressForbidden(reason = "accesses fully qualified URLs to configure security")
static Policy readPolicy(URL policyFile, Map<String, URL> codebases) {
try {
List<String> propertiesSet = new ArrayList<>();
try {
// set codebase properties
for (Map.Entry<String,URL> codebase : codebases.entrySet()) {
String name = codebase.getKey();
URL url = codebase.getValue();
// We attempt to use a versionless identifier for each codebase. This assumes a specific version
// format in the jar filename. While we cannot ensure all jars in all plugins use this format, nonconformity
// only means policy grants would need to include the entire jar filename as they always have before.
String property = "codebase." + name;
String aliasProperty = "codebase." + name.replaceFirst("-\\d+\\.\\d+.*\\.jar", "");
if (aliasProperty.equals(property) == false) {
propertiesSet.add(aliasProperty);
String previous = System.setProperty(aliasProperty, url.toString());
if (previous != null) {
throw new IllegalStateException("codebase property already set: " + aliasProperty + " -> " + previous +
", cannot set to " + url.toString());
}
}
propertiesSet.add(property);
String previous = System.setProperty(property, url.toString());
if (previous != null) {
throw new IllegalStateException("codebase property already set: " + property + " -> " + previous +
", cannot set to " + url.toString());
}
}
return Policy.getInstance("JavaPolicy", new URIParameter(policyFile.toURI()));
} finally {
// clear codebase properties
for (String property : propertiesSet) {
System.clearProperty(property);
}
}
} catch (NoSuchAlgorithmException | URISyntaxException e) {
throw new IllegalArgumentException("unable to parse policy file `" + policyFile + "`", e);
}
}
/** returns dynamic Permissions to configured paths and bind ports */
static Permissions createPermissions(Environment environment) throws IOException {
Permissions policy = new Permissions();

View file

@ -19,24 +19,21 @@
package org.elasticsearch.plugins;
import org.elasticsearch.bootstrap.PluginPolicyInfo;
import org.elasticsearch.bootstrap.PolicyUtil;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.Terminal.Verbosity;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.core.internal.io.IOUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.net.URL;
import java.nio.file.Path;
import java.security.NoSuchAlgorithmException;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.Policy;
import java.security.URIParameter;
import java.security.UnresolvedPermission;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@ -115,41 +112,15 @@ class PluginSecurity {
}
/**
* Parses plugin policy into a set of permissions. Each permission is formatted for output to users.
* Extract a unique set of permissions from the plugin's policy file. Each permission is formatted for output to users.
*/
public static Set<String> parsePermissions(Path file, Path tmpDir) throws IOException {
// create a zero byte file for "comparison"
// this is necessary because the default policy impl automatically grants two permissions:
// 1. permission to exitVM (which we ignore)
// 2. read permission to the code itself (e.g. jar file of the code)
static Set<String> getPermissionDescriptions(PluginPolicyInfo pluginPolicyInfo, Path tmpDir) throws IOException {
Set<Permission> allPermissions = new HashSet<>(PolicyUtil.getPolicyPermissions(null, pluginPolicyInfo.policy, tmpDir));
for (URL jar : pluginPolicyInfo.jars) {
Set<Permission> jarPermissions = PolicyUtil.getPolicyPermissions(jar, pluginPolicyInfo.policy, tmpDir);
allPermissions.addAll(jarPermissions);
}
Path emptyPolicyFile = Files.createTempFile(tmpDir, "empty", "tmp");
final Policy emptyPolicy;
try {
emptyPolicy = Policy.getInstance("JavaPolicy", new URIParameter(emptyPolicyFile.toUri()));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
IOUtils.rm(emptyPolicyFile);
// parse the plugin's policy file into a set of permissions
final Policy policy;
try {
policy = Policy.getInstance("JavaPolicy", new URIParameter(file.toUri()));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
PermissionCollection permissions = policy.getPermissions(PluginSecurity.class.getProtectionDomain());
// this method is supported with the specific implementation we use, but just check for safety.
if (permissions == Policy.UNSUPPORTED_EMPTY_COLLECTION) {
throw new UnsupportedOperationException("JavaPolicy implementation does not support retrieving permissions");
}
PermissionCollection actualPermissions = new Permissions();
for (Permission permission : Collections.list(permissions.elements())) {
if (!emptyPolicy.implies(PluginSecurity.class.getProtectionDomain(), permission)) {
actualPermissions.add(permission);
}
}
return Collections.list(actualPermissions.elements()).stream().map(PluginSecurity::formatPermission).collect(Collectors.toSet());
return allPermissions.stream().map(PluginSecurity::formatPermission).collect(Collectors.toSet());
}
}

View file

@ -138,7 +138,7 @@ public class BootstrapForTesting {
perms.add(new SocketPermission("localhost:1024-", "listen,resolve"));
// read test-framework permissions
Map<String, URL> codebases = Security.getCodebaseJarMap(JarHell.parseClassPath());
Map<String, URL> codebases = PolicyUtil.getCodebaseJarMap(JarHell.parseClassPath());
// when testing server, the main elasticsearch code is not yet in a jar, so we need to manually add it
addClassCodebase(codebases,"elasticsearch", "org.elasticsearch.plugins.PluginsService");
if (System.getProperty("tests.gradle") == null) {
@ -148,7 +148,7 @@ public class BootstrapForTesting {
addClassCodebase(codebases, "elasticsearch-secure-sm", "org.elasticsearch.secure_sm.SecureSM");
addClassCodebase(codebases, "elasticsearch-rest-client", "org.elasticsearch.client.RestClient");
}
final Policy testFramework = Security.readPolicy(Bootstrap.class.getResource("test-framework.policy"), codebases);
final Policy testFramework = PolicyUtil.readPolicy(Bootstrap.class.getResource("test-framework.policy"), codebases);
final Policy esPolicy = new ESPolicy(codebases, perms, getPluginPermissions(), true, new Permissions());
Policy.setPolicy(new Policy() {
@Override
@ -224,7 +224,7 @@ public class BootstrapForTesting {
// parse each policy file, with codebase substitution from the classpath
final List<Policy> policies = new ArrayList<>(pluginPolicies.size());
for (URL policyFile : pluginPolicies) {
policies.add(Security.readPolicy(policyFile, Security.getCodebaseJarMap(codebases)));
policies.add(PolicyUtil.readPolicy(policyFile, PolicyUtil.getCodebaseJarMap(codebases)));
}
// consult each policy file for those codebases