Add support for a policy file (x-pack-extension-security.policy) in an x-pack extension

Fix elastic/elasticsearch#2094

Original commit: elastic/x-pack-elasticsearch@bc017064d0
This commit is contained in:
Jim Ferenczi 2016-06-02 14:43:00 +02:00
parent 3c26a64f4a
commit ce8ffab7f2
14 changed files with 501 additions and 24 deletions

View File

@ -37,6 +37,7 @@ processResources {
task buildZip(type:Zip, dependsOn: [jar]) {
from 'build/resources/main/x-pack-extension-descriptor.properties'
from 'build/resources/main/x-pack-extension-security.policy'
from project.jar
}

View File

@ -11,6 +11,9 @@ import org.elasticsearch.example.realm.CustomRealmFactory;
import org.elasticsearch.shield.authc.AuthenticationModule;
import org.elasticsearch.xpack.extensions.XPackExtension;
import java.security.AccessController;
import java.security.PrivilegedAction;
public class ExampleRealmExtension extends XPackExtension {
@Override
public String name() {
@ -25,5 +28,10 @@ public class ExampleRealmExtension extends XPackExtension {
public void onModule(AuthenticationModule authenticationModule) {
authenticationModule.addCustomRealm(CustomRealm.TYPE, CustomRealmFactory.class);
authenticationModule.setAuthenticationFailureHandler(CustomAuthenticationFailureHandler.class);
// check that the extension's policy works.
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
System.getSecurityManager().checkPrintJobAccess();
return null;
});
}
}

View File

@ -0,0 +1,3 @@
grant {
permission java.lang.RuntimePermission "queuePrintJob";
};

View File

@ -8,6 +8,7 @@ package org.elasticsearch.xpack.extensions;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.SpecialPermission;
import org.elasticsearch.bootstrap.JarHell;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.SettingCommand;
@ -26,13 +27,26 @@ import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.security.Policy;
import java.security.PermissionCollection;
import java.security.Permission;
import java.security.NoSuchAlgorithmException;
import java.security.Permissions;
import java.security.PrivilegedAction;
import java.security.AccessController;
import java.security.UnresolvedPermission;
import java.security.URIParameter;
import static org.elasticsearch.cli.Terminal.Verbosity.VERBOSE;
import static org.elasticsearch.xpack.XPackPlugin.resolveXPackExtensionsFile;
@ -49,18 +63,19 @@ import static org.elasticsearch.xpack.XPackPlugin.resolveXPackExtensionsFile;
* <ul>
* <li>The property file exists and contains valid metadata. See {@link XPackExtensionInfo#readFromProperties(Path)}</li>
* <li>Jar hell does not exist, either between the extension's own jars or with the parent classloader (elasticsearch + x-pack)</li>
* <li>If the extension contains extra security permissions, the policy file is validated</li>
* </ul>
*/
class InstallXPackExtensionCommand extends SettingCommand {
final class InstallXPackExtensionCommand extends SettingCommand {
private final OptionSpec<Void> batchOption;
private final OptionSpec<String> arguments;
InstallXPackExtensionCommand() {
super("Install a plugin");
super("Install an extension");
this.batchOption = parser.acceptsAll(Arrays.asList("b", "batch"),
"Enable batch mode explicitly, automatic confirmation of security permission");
this.arguments = parser.nonOptions("plugin id");
this.arguments = parser.nonOptions("extension id");
}
@Override
@ -86,7 +101,7 @@ class InstallXPackExtensionCommand extends SettingCommand {
Path extensionZip = download(terminal, extensionId, env.tmpFile());
Path extractedZip = unzip(extensionZip, resolveXPackExtensionsFile(env));
install(terminal, extractedZip, env);
install(terminal, extractedZip, env, isBatch);
}
/** Downloads the extension and returns the file it was downloaded to. */
@ -133,13 +148,21 @@ class InstallXPackExtensionCommand extends SettingCommand {
}
/** Load information about the extension, and verify it can be installed with no errors. */
private XPackExtensionInfo verify(Terminal terminal, Path extensionRoot, Environment env) throws Exception {
private XPackExtensionInfo verify(Terminal terminal, Path extensionRoot, Environment env, boolean isBatch) throws Exception {
// read and validate the extension descriptor
XPackExtensionInfo info = XPackExtensionInfo.readFromProperties(extensionRoot);
terminal.println(VERBOSE, info.toString());
// check for jar hell before any copying
jarHellCheck(extensionRoot);
// read optional security policy (extra permissions)
// if it exists, confirm or warn the user
Path policy = extensionRoot.resolve(XPackExtensionInfo.XPACK_EXTENSION_POLICY);
if (Files.exists(policy)) {
readPolicy(policy, terminal, env, isBatch);
}
return info;
}
@ -165,11 +188,11 @@ class InstallXPackExtensionCommand extends SettingCommand {
/**
* Installs the extension from {@code tmpRoot} into the extensions dir.
*/
private void install(Terminal terminal, Path tmpRoot, Environment env) throws Exception {
private void install(Terminal terminal, Path tmpRoot, Environment env, boolean isBatch) throws Exception {
List<Path> deleteOnFailure = new ArrayList<>();
deleteOnFailure.add(tmpRoot);
try {
XPackExtensionInfo info = verify(terminal, tmpRoot, env);
XPackExtensionInfo info = verify(terminal, tmpRoot, env, isBatch);
final Path destination = resolveXPackExtensionsFile(env).resolve(info.getName());
if (Files.exists(destination)) {
throw new UserError(ExitCodes.USAGE,
@ -188,4 +211,150 @@ class InstallXPackExtensionCommand extends SettingCommand {
throw installProblem;
}
}
/** Format permission type, name, and actions into a string */
static String formatPermission(Permission permission) {
StringBuilder sb = new StringBuilder();
String clazz = null;
if (permission instanceof UnresolvedPermission) {
clazz = ((UnresolvedPermission) permission).getUnresolvedType();
} else {
clazz = permission.getClass().getName();
}
sb.append(clazz);
String name = null;
if (permission instanceof UnresolvedPermission) {
name = ((UnresolvedPermission) permission).getUnresolvedName();
} else {
name = permission.getName();
}
if (name != null && name.length() > 0) {
sb.append(' ');
sb.append(name);
}
String actions = null;
if (permission instanceof UnresolvedPermission) {
actions = ((UnresolvedPermission) permission).getUnresolvedActions();
} else {
actions = permission.getActions();
}
if (actions != null && actions.length() > 0) {
sb.append(' ');
sb.append(actions);
}
return sb.toString();
}
/**
* Parses extension policy into a set of permissions
*/
static PermissionCollection 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;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
emptyPolicy =
AccessController.doPrivileged((PrivilegedAction<Policy>) () -> {
try {
return Policy.getInstance("JavaPolicy", new URIParameter(emptyPolicyFile.toUri()));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
});
IOUtils.rm(emptyPolicyFile);
// parse the extension's policy file into a set of permissions
final Policy policy =
AccessController.doPrivileged((PrivilegedAction<Policy>) () -> {
try {
return Policy.getInstance("JavaPolicy", new URIParameter(file.toUri()));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
});
PermissionCollection permissions = policy.getPermissions(XPackExtensionSecurity.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(XPackExtensionSecurity.class.getProtectionDomain(), permission)) {
actualPermissions.add(permission);
}
}
actualPermissions.setReadOnly();
return actualPermissions;
}
/**
* Reads extension policy, prints/confirms exceptions
*/
static void readPolicy(Path file, Terminal terminal, Environment environment, boolean batch) throws IOException {
PermissionCollection permissions = parsePermissions(file, environment.tmpFile());
List<Permission> requested = Collections.list(permissions.elements());
if (requested.isEmpty()) {
terminal.println(Terminal.Verbosity.VERBOSE, "extension has a policy file with no additional permissions");
return;
}
// sort permissions in a reasonable order
Collections.sort(requested, new Comparator<Permission>() {
@Override
public int compare(Permission o1, Permission o2) {
int cmp = o1.getClass().getName().compareTo(o2.getClass().getName());
if (cmp == 0) {
String name1 = o1.getName();
String name2 = o2.getName();
if (name1 == null) {
name1 = "";
}
if (name2 == null) {
name2 = "";
}
cmp = name1.compareTo(name2);
if (cmp == 0) {
String actions1 = o1.getActions();
String actions2 = o2.getActions();
if (actions1 == null) {
actions1 = "";
}
if (actions2 == null) {
actions2 = "";
}
cmp = actions1.compareTo(actions2);
}
}
return cmp;
}
});
terminal.println(Terminal.Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
terminal.println(Terminal.Verbosity.NORMAL, "@ WARNING: x-pack extension requires additional permissions @");
terminal.println(Terminal.Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
// print all permissions:
for (Permission permission : requested) {
terminal.println(Terminal.Verbosity.NORMAL, "* " + formatPermission(permission));
}
terminal.println(Terminal.Verbosity.NORMAL, "See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html");
terminal.println(Terminal.Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks.");
if (!batch) {
terminal.println(Terminal.Verbosity.NORMAL, "");
String text = terminal.readText("Continue with installation? [y/N]");
if (!text.equalsIgnoreCase("y")) {
throw new RuntimeException("installation aborted by user");
}
}
}
}

View File

@ -16,6 +16,7 @@ import java.util.Properties;
public class XPackExtensionInfo {
public static final String XPACK_EXTENSION_PROPERTIES = "x-pack-extension-descriptor.properties";
public static final String XPACK_EXTENSION_POLICY = "x-pack-extension-security.policy";
private String name;
private String description;

View File

@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.extensions;
import org.elasticsearch.common.SuppressForbidden;
import java.net.URL;
import java.security.Policy;
import java.security.ProtectionDomain;
import java.security.CodeSource;
import java.security.Permission;
import java.security.SecurityPermission;
import java.util.Map;
final class XPackExtensionPolicy extends Policy {
static final Permission SET_POLICY_PERMISSION = new SecurityPermission("setPolicy");
static final Permission GET_POLICY_PERMISSION = new SecurityPermission("getPolicy");
static final Permission CREATE_POLICY_PERMISSION = new SecurityPermission("createPolicy.JavaPolicy");
// the base policy (es + plugins)
final Policy basePolicy;
// policy extensions
final Map<String, Policy> extensions;
// xpack code source location
final URL xpackURL;
/**
*
* @param basePolicy The base policy
* @param extensions Extra code source extension's policy
*/
public XPackExtensionPolicy(Policy basePolicy, Map<String, Policy> extensions) {
this.basePolicy = basePolicy;
this.extensions = extensions;
xpackURL = XPackExtensionPolicy.class.getProtectionDomain().getCodeSource().getLocation();
}
private boolean isPolicyPermission(Permission permission) {
return GET_POLICY_PERMISSION.equals(permission) ||
CREATE_POLICY_PERMISSION.equals(permission) ||
SET_POLICY_PERMISSION.equals(permission);
}
@Override @SuppressForbidden(reason = "fast equals check is desired")
public boolean implies(ProtectionDomain domain, Permission permission) {
CodeSource codeSource = domain.getCodeSource();
if (codeSource != null && codeSource.getLocation() != null) {
if (codeSource.getLocation().equals(xpackURL) &&
isPolicyPermission(permission)) {
// forbid to get, create and set java policy in xpack codesource
// it is only granted at startup in order to let xpack add the extensions policy
// and make this policy the default.
return false;
}
// check for an additional extension permission: extension policy is
// only consulted for its codesources.
Policy extension = extensions.get(codeSource.getLocation().getFile());
if (extension != null && extension.implies(domain, permission)) {
return true;
}
}
return basePolicy.implies(domain, permission);
}
}

View File

@ -0,0 +1,144 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.extensions;
import org.elasticsearch.SpecialPermission;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.io.PathUtils;
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.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.security.Policy;
import java.security.PrivilegedAction;
import java.security.AccessController;
import java.security.URIParameter;
import java.security.NoSuchAlgorithmException;
final class XPackExtensionSecurity {
private XPackExtensionSecurity() {}
/**
* Initializes the XPackExtensionPolicy
* Can only happen once!
*
* @param extsDirectory the directory where the extensions are installed
*/
static void configure(Path extsDirectory) throws IOException {
Map<String, Policy> map = getExtensionsPermissions(extsDirectory);
if (map.size() > 0) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
Policy newPolicy = new XPackExtensionPolicy(Policy.getPolicy(), map);
Policy.setPolicy(newPolicy);
return null;
});
}
}
/**
* Sets properties (codebase URLs) for policy files.
* we look for matching extensions and set URLs to fit
*/
@SuppressForbidden(reason = "proper use of URL")
static Map<String, Policy> getExtensionsPermissions(Path extsDirectory) throws IOException {
Map<String, Policy> map = new HashMap<>();
// collect up lists of extensions
List<Path> extensionPaths = new ArrayList<>();
if (Files.exists(extsDirectory)) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(extsDirectory)) {
for (Path extension : stream) {
extensionPaths.add(extension);
}
}
}
// now process each one
for (Path extension : extensionPaths) {
Path policyFile = extension.resolve(XPackExtensionInfo.XPACK_EXTENSION_POLICY);
if (Files.exists(policyFile)) {
// first get a list of URLs for the extension's jars:
// we resolve symlinks so map is keyed on the normalize codebase name
List<URL> codebases = new ArrayList<>();
try (DirectoryStream<Path> jarStream = Files.newDirectoryStream(extension, "*.jar")) {
for (Path jar : jarStream) {
codebases.add(jar.toRealPath().toUri().toURL());
}
}
// parse the extension's policy file into a set of permissions
Policy policy = readPolicy(policyFile.toUri().toURL(), codebases.toArray(new URL[codebases.size()]));
// consult this policy for each of the extension's jars:
for (URL url : codebases) {
if (map.put(url.getFile(), policy) != null) {
// just be paranoid ok?
throw new IllegalStateException("per-extension permissions already granted for jar file: " + url);
}
}
}
}
return Collections.unmodifiableMap(map);
}
/**
* Reads and returns the specified {@code policyFile}.
* <p>
* Resources (e.g. jar files and directories) 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, URL codebases[]) throws IOException {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
try {
try {
// set codebase properties
for (URL url : codebases) {
String shortName = PathUtils.get(url.toURI()).getFileName().toString();
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
System.setProperty("codebase." + shortName, url.toString());
return null;
});
}
URIParameter uri = new URIParameter(policyFile.toURI());
return AccessController.doPrivileged((PrivilegedAction<Policy>) () -> {
try {
return Policy.getInstance("JavaPolicy", uri);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
});
} finally {
// clear codebase properties
for (URL url : codebases) {
String shortName = PathUtils.get(url.toURI()).getFileName().toString();
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
System.clearProperty("codebase." + shortName);
return null;
});
}
}
} catch (URISyntaxException e) {
throw new IllegalArgumentException("unable to parse policy file `" + policyFile + "`", e);
}
}
}

View File

@ -20,10 +20,10 @@ import java.net.URLClassLoader;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import static org.elasticsearch.common.io.FileSystemUtils.isAccessibleDirectory;
@ -41,14 +41,21 @@ public class XPackExtensionsService {
/**
* Constructs a new XPackExtensionsService
*
* @param settings The settings of the system
* @param extsDirectory The directory extensions exist in, or null if extensions should not be loaded from the filesystem
* @param classpathExtensions Extensions that exist in the classpath which should be loaded
*/
public XPackExtensionsService(Settings settings, Path extsDirectory, Collection<Class<? extends XPackExtension>> classpathExtensions) {
public XPackExtensionsService(Settings settings, Path extsDirectory,
Collection<Class<? extends XPackExtension>> classpathExtensions) {
try {
XPackExtensionSecurity.configure(extsDirectory);
} catch (Exception e) {
throw new IllegalStateException("Unable to configure extension policy", e);
}
this.settings = settings;
List<Tuple<XPackExtensionInfo, XPackExtension>> extensionsLoaded = new ArrayList<>();
// first we load extensions that are on the classpath. this is for tests
for (Class<? extends XPackExtension> extClass : classpathExtensions) {
XPackExtension ext = loadExtension(extClass, settings);

View File

@ -16,4 +16,9 @@ grant {
// bouncy castle
permission java.security.SecurityPermission "putProviderProperty.BC";
// needed for x-pack security extension
permission java.security.SecurityPermission "createPolicy.JavaPolicy";
permission java.security.SecurityPermission "getPolicy";
permission java.security.SecurityPermission "setPolicy";
};

View File

@ -95,7 +95,7 @@ public class InstallXPackExtensionCommandTests extends ESTestCase {
return terminal;
}
void assertExtension(String name, Path original, Environment env) throws IOException {
void assertExtension(String name, Environment env) throws IOException {
Path got = env.pluginsFile().resolve("x-pack").resolve("extensions").resolve(name);
assertTrue("dir " + name + " exists", Files.exists(got));
assertTrue("jar was copied", Files.exists(got.resolve("extension.jar")));
@ -116,7 +116,7 @@ public class InstallXPackExtensionCommandTests extends ESTestCase {
Path extDir = createTempDir();
String extZip = createExtension("fake", extDir);
installExtension(extZip, home);
assertExtension("fake", extDir, env);
assertExtension("fake", env);
}
public void testSpaceInUrl() throws Exception {
@ -127,7 +127,7 @@ public class InstallXPackExtensionCommandTests extends ESTestCase {
Files.copy(in, extZipWithSpaces, StandardCopyOption.REPLACE_EXISTING);
}
installExtension(extZipWithSpaces.toUri().toURL().toString(), home);
assertExtension("fake", extDir, env);
assertExtension("fake", env);
}
public void testMalformedUrlNotMaven() throws Exception {
@ -155,8 +155,8 @@ public class InstallXPackExtensionCommandTests extends ESTestCase {
Path extDir2 = createTempDir();
String extZip2 = createExtension("fake2", extDir2);
installExtension(extZip2, home);
assertExtension("fake1", extDir1, env);
assertExtension("fake2", extDir2, env);
assertExtension("fake1", env);
assertExtension("fake2", env);
}
public void testExistingExtension() throws Exception {

View File

@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.extensions;
import org.elasticsearch.test.ESTestCase;
import java.nio.file.Path;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.util.Collections;
import java.util.List;
public class XPackExtensionSecurityTests extends ESTestCase {
/** Test that we can parse the set of permissions correctly for a simple policy */
public void testParsePermissions() throws Exception {
Path scratch = createTempDir();
Path testFile = this.getDataPath("security/simple-x-pack-extension-security.policy");
Permissions expected = new Permissions();
expected.add(new RuntimePermission("queuePrintJob"));
PermissionCollection actual = InstallXPackExtensionCommand.parsePermissions(testFile, scratch);
assertEquals(expected, actual);
}
/** Test that we can parse the set of permissions correctly for a complex policy */
public void testParseTwoPermissions() throws Exception {
Path scratch = createTempDir();
Path testFile = this.getDataPath("security/complex-x-pack-extension-security.policy");
Permissions expected = new Permissions();
expected.add(new RuntimePermission("getClassLoader"));
expected.add(new RuntimePermission("closeClassLoader"));
PermissionCollection actual = InstallXPackExtensionCommand.parsePermissions(testFile, scratch);
assertEquals(expected, actual);
}
/** Test that we can format some simple permissions properly */
public void testFormatSimplePermission() throws Exception {
assertEquals("java.lang.RuntimePermission queuePrintJob",
InstallXPackExtensionCommand.formatPermission(new RuntimePermission("queuePrintJob")));
}
/** Test that we can format an unresolved permission properly */
public void testFormatUnresolvedPermission() throws Exception {
Path scratch = createTempDir();
Path testFile = this.getDataPath("security/unresolved-x-pack-extension-security.policy");
PermissionCollection actual = InstallXPackExtensionCommand.parsePermissions(testFile, scratch);
List<Permission> permissions = Collections.list(actual.elements());
assertEquals(1, permissions.size());
assertEquals("org.fake.FakePermission fakeName", InstallXPackExtensionCommand.formatPermission(permissions.get(0)));
}
/** no guaranteed equals on these classes, we assert they contain the same set */
private void assertEquals(PermissionCollection expected, PermissionCollection actual) {
assertEquals(asSet(Collections.list(expected.elements())), asSet(Collections.list(actual.elements())));
}
}

View File

@ -0,0 +1,5 @@
grant {
// needed to cause problems
permission java.lang.RuntimePermission "getClassLoader";
permission java.lang.RuntimePermission "closeClassLoader";
};

View File

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

View File

@ -0,0 +1,4 @@
grant {
// an unresolved permission
permission org.fake.FakePermission "fakeName";
};