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));
// 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);
}
}
}
}
return Collections.unmodifiableMap(map);
}
/**
* Reads and returns the specified {@code policyFile}.
*
* Jar files listed in {@code codebases} location will be provided to the policy file via
* a system property of the short name: e.g. ${codebase.joda-convert-1.2.jar}
* would map to full URL.
*/
@SuppressForbidden(reason = "accesses fully qualified URLs to configure security")
static Policy readPolicy(URL policyFile, Map codebases) {
try {
List propertiesSet = new ArrayList<>();
try {
// set codebase properties
for (Map.Entry 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();
addClasspathPermissions(policy);
addFilePermissions(policy, environment);
addBindPermissions(policy, environment.settings());
return policy;
}
private static Permissions createRecursiveDataPathPermission(Environment environment) throws IOException {
Permissions policy = new Permissions();
for (Path path : environment.dataFiles()) {
addDirectoryPath(policy, Environment.PATH_DATA_SETTING.getKey(), path, "read,readlink,write,delete", true);
}
return policy;
}
/** Adds access to classpath jars/classes for jar hell scan, etc */
@SuppressForbidden(reason = "accesses fully qualified URLs to configure security")
static void addClasspathPermissions(Permissions policy) throws IOException {
// add permissions to everything in classpath
// really it should be covered by lib/, but there could be e.g. agents or similar configured)
for (URL url : JarHell.parseClassPath()) {
Path path;
try {
path = PathUtils.get(url.toURI());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
// resource itself
if (Files.isDirectory(path)) {
addDirectoryPath(policy, "class.path", path, "read,readlink", false);
} else {
addSingleFilePath(policy, path, "read,readlink");
}
}
}
/**
* Adds access to all configurable paths.
*/
static void addFilePermissions(Permissions policy, Environment environment) throws IOException {
// read-only dirs
addDirectoryPath(policy, Environment.PATH_HOME_SETTING.getKey(), environment.binDir(), "read,readlink", false);
addDirectoryPath(policy, Environment.PATH_HOME_SETTING.getKey(), environment.libDir(), "read,readlink", false);
addDirectoryPath(policy, Environment.PATH_HOME_SETTING.getKey(), environment.modulesDir(), "read,readlink", false);
addDirectoryPath(policy, Environment.PATH_HOME_SETTING.getKey(), environment.pluginsDir(), "read,readlink", false);
addDirectoryPath(policy, "path.conf'", environment.configDir(), "read,readlink", false);
// read-write dirs
addDirectoryPath(policy, "java.io.tmpdir", environment.tmpDir(), "read,readlink,write,delete", false);
addDirectoryPath(policy, Environment.PATH_LOGS_SETTING.getKey(), environment.logsDir(), "read,readlink,write,delete", false);
if (environment.sharedDataDir() != null) {
addDirectoryPath(
policy,
Environment.PATH_SHARED_DATA_SETTING.getKey(),
environment.sharedDataDir(),
"read,readlink,write,delete",
false
);
}
final Set dataFilesPaths = new HashSet<>();
for (Path path : environment.dataFiles()) {
addDirectoryPath(policy, Environment.PATH_DATA_SETTING.getKey(), path, "read,readlink,write,delete", false);
/*
* We have to do this after adding the path because a side effect of that is that the directory is created; the Path#toRealPath
* invocation will fail if the directory does not already exist. We use Path#toRealPath to follow symlinks and handle issues
* like unicode normalization or case-insensitivity on some filesystems (e.g., the case-insensitive variant of HFS+ on macOS).
*/
try {
final Path realPath = path.toRealPath();
if (!dataFilesPaths.add(realPath)) {
throw new IllegalStateException("path [" + realPath + "] is duplicated by [" + path + "]");
}
} catch (final IOException e) {
throw new IllegalStateException("unable to access [" + path + "]", e);
}
}
for (Path path : environment.repoFiles()) {
addDirectoryPath(policy, Environment.PATH_REPO_SETTING.getKey(), path, "read,readlink,write,delete", false);
}
if (environment.pidFile() != null) {
// we just need permission to remove the file if its elsewhere.
addSingleFilePath(policy, environment.pidFile(), "delete");
}
}
/**
* Add dynamic {@link SocketPermission}s based on HTTP and transport settings.
*
* @param policy the {@link Permissions} instance to apply the dynamic {@link SocketPermission}s to.
* @param settings the {@link Settings} instance to read the HTTP and transport settings from
*/
private static void addBindPermissions(Permissions policy, Settings settings) {
addSocketPermissionForHttp(policy, settings);
addSocketPermissionForTransportProfiles(policy, settings);
}
/**
* Add dynamic {@link SocketPermission} based on HTTP settings.
*
* @param policy the {@link Permissions} instance to apply the dynamic {@link SocketPermission}s to.
* @param settings the {@link Settings} instance to read the HTTP settings from
*/
private static void addSocketPermissionForHttp(final Permissions policy, final Settings settings) {
// http is simple
final String httpRange = HttpTransportSettings.SETTING_HTTP_PORT.get(settings).getPortRangeString();
addSocketPermissionForPortRange(policy, httpRange);
}
/**
* Add dynamic {@link SocketPermission} based on transport settings. This method will first check if there is a port range specified in
* the transport profile specified by {@code profileSettings} and will fall back to {@code settings}.
*
* @param policy the {@link Permissions} instance to apply the dynamic {@link SocketPermission}s to
* @param settings the {@link Settings} instance to read the transport settings from
*/
private static void addSocketPermissionForTransportProfiles(final Permissions policy, final Settings settings) {
// transport is way over-engineered
Set profiles = TcpTransport.getProfileSettings(settings);
Set uniquePortRanges = new HashSet<>();
// loop through all profiles and add permissions for each one
for (final TcpTransport.ProfileSettings profile : profiles) {
if (uniquePortRanges.add(profile.portOrRange)) {
// profiles fall back to the transport.port if it's not explicit but we want to only add one permission per range
addSocketPermissionForPortRange(policy, profile.portOrRange);
}
}
}
/**
* Add dynamic {@link SocketPermission} for the specified port range.
*
* @param policy the {@link Permissions} instance to apply the dynamic {@link SocketPermission} to.
* @param portRange the port range
*/
private static void addSocketPermissionForPortRange(final Permissions policy, final String portRange) {
// listen is always called with 'localhost' but use wildcard to be sure, no name service is consulted.
// see SocketPermission implies() code
policy.add(new SocketPermission("*:" + portRange, "listen,resolve"));
}
/**
* Ensures configured directory {@code path} exists.
* @throws IOException if {@code path} exists, but is not a directory, not accessible, or broken symbolic link.
*/
static void ensureDirectoryExists(Path path) throws IOException {
// this isn't atomic, but neither is createDirectories.
if (Files.isDirectory(path)) {
// verify access, following links (throws exception if something is wrong)
// we only check READ as a sanity test
path.getFileSystem().provider().checkAccess(path.toRealPath(), AccessMode.READ);
} else {
// doesn't exist, or not a directory
try {
Files.createDirectories(path);
} catch (FileAlreadyExistsException e) {
// convert optional specific exception so the context is clear
IOException e2 = new NotDirectoryException(path.toString());
e2.addSuppressed(e);
throw e2;
}
}
}
/** Simple checks that everything is ok */
@SuppressForbidden(reason = "accesses jvm default tempdir as a self-test")
static void selfTest() throws IOException {
// check we can manipulate temporary files
try {
Path p = Files.createTempFile(null, null);
try {
Files.delete(p);
} catch (IOException ignored) {
// potentially virus scanner
}
} catch (SecurityException problem) {
throw new SecurityException("Security misconfiguration: cannot access java.io.tmpdir", problem);
}
}
}