Browse Source

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.
Ryan Ernst 5 years ago
parent
commit
2c58841887

+ 6 - 8
distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java

@@ -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);

+ 141 - 0
qa/evil-tests/src/test/java/org/elasticsearch/bootstrap/PolicyUtilTests.java

@@ -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");
+        }
+    }
+}

+ 19 - 6
qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginSecurityTests.java

@@ -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"));
     }
 }

+ 4 - 0
qa/evil-tests/src/test/resources/org/elasticsearch/bootstrap/dummy.policy

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

+ 8 - 0
qa/evil-tests/src/test/resources/org/elasticsearch/bootstrap/global-and-jar.policy

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

+ 2 - 2
server/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java

@@ -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 {

+ 34 - 0
server/src/main/java/org/elasticsearch/bootstrap/PluginPolicyInfo.java

@@ -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;
+    }
+}

+ 193 - 0
server/src/main/java/org/elasticsearch/bootstrap/PolicyUtil.java

@@ -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;
+    }
+}

+ 13 - 102
server/src/main/java/org/elasticsearch/bootstrap/Security.java

@@ -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);
-                        }
-                    }
-                }
-
-                // parse the plugin's policy file into a set of permissions
-                Policy policy = readPolicy(policyFile.toUri().toURL(), getCodebaseJarMap(codebases));
+            PluginPolicyInfo pluginPolicy = PolicyUtil.getPluginPolicyInfo(plugin);
+            if (pluginPolicy == null) {
+                continue;
+            }
 
-                // 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();

+ 11 - 40
server/src/main/java/org/elasticsearch/plugins/PluginSecurity.java

@@ -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)
-
-        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);
+    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);
         }
-        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());
     }
 }

+ 3 - 3
test/framework/src/main/java/org/elasticsearch/bootstrap/BootstrapForTesting.java

@@ -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