Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions distribution/tools/plugin-cli/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {
compileOnly project(":libs:cli")
implementation project(":libs:plugin-api")
implementation project(":libs:plugin-scanner")
implementation project(":libs:entitlement")
// TODO: asm is picked up from the plugin scanner, we should consolidate so it is not defined twice
implementation 'org.ow2.asm:asm:9.7.1'
implementation 'org.ow2.asm:asm-tree:9.7.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.elasticsearch.Build;
import org.elasticsearch.bootstrap.PluginPolicyInfo;
import org.elasticsearch.bootstrap.PolicyUtil;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
Expand All @@ -36,9 +34,9 @@
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.entitlement.runtime.policy.PolicyUtils;
import org.elasticsearch.env.Environment;
import org.elasticsearch.jdk.JarHell;
import org.elasticsearch.jdk.RuntimeVersionFeature;
import org.elasticsearch.plugin.scanner.ClassReaders;
import org.elasticsearch.plugin.scanner.NamedComponentScanner;
import org.elasticsearch.plugins.Platforms;
Expand Down Expand Up @@ -923,13 +921,11 @@ void jarHellCheck(PluginDescriptor candidateInfo, Path candidateDir, Path plugin
*/
private PluginDescriptor installPlugin(InstallablePlugin descriptor, Path tmpRoot, List<Path> deleteOnFailure) throws Exception {
final PluginDescriptor info = loadPluginInfo(tmpRoot);
if (RuntimeVersionFeature.isSecurityManagerAvailable()) {
PluginPolicyInfo pluginPolicy = PolicyUtil.getPluginPolicyInfo(tmpRoot, env.tmpDir());
if (pluginPolicy != null) {
Set<String> permissions = PluginSecurity.getPermissionDescriptions(pluginPolicy, env.tmpDir());
PluginSecurity.confirmPolicyExceptions(terminal, permissions, batch);
}
}

var pluginPolicy = PolicyUtils.parsePolicyIfExists(info.getName(), tmpRoot, true);

Set<String> entitlements = PolicyUtils.getEntitlementsDescriptions(pluginPolicy);
PluginSecurity.confirmPolicyExceptions(terminal, entitlements, batch);

// Validate that the downloaded plugin's ID matches what we expect from the descriptor. The
// exception is if we install a plugin via `InstallPluginCommand` by specifying a URL or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,65 +9,56 @@

package org.elasticsearch.plugins.cli;

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 java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.security.Permission;
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;

/**
* Contains methods for displaying extended plugin permissions to the user, and confirming that
* Contains methods for displaying extended plugin entitlements to the user, and confirming that
* plugin installation can proceed.
*/
public class PluginSecurity {

public static final String ENTITLEMENTS_DESCRIPTION_URL =
"https://www.elastic.co/guide/en/elasticsearch/plugins/current/creating-classic-plugins.html";

/**
* prints/confirms policy exceptions with the user
*/
static void confirmPolicyExceptions(Terminal terminal, Set<String> permissions, boolean batch) throws UserException {
List<String> requested = new ArrayList<>(permissions);
static void confirmPolicyExceptions(Terminal terminal, Set<String> entitlements, boolean batch) throws UserException {
List<String> requested = new ArrayList<>(entitlements);
if (requested.isEmpty()) {
terminal.println(Verbosity.VERBOSE, "plugin has a policy file with no additional permissions");
terminal.println(Verbosity.VERBOSE, "plugin has a policy file with no additional entitlements");
} else {
// sort permissions in a reasonable order
// sort entitlements in a reasonable order
Collections.sort(requested);

if (terminal.isHeadless()) {
terminal.errorPrintln(
"WARNING: plugin requires additional permissions: ["
"WARNING: plugin requires additional entitlements: ["
+ requested.stream().map(each -> '\'' + each + '\'').collect(Collectors.joining(", "))
+ "]"
);
terminal.errorPrintln(
"See https://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html"
+ " for descriptions of what these permissions allow and the associated risks."
"See " + ENTITLEMENTS_DESCRIPTION_URL + " for descriptions of what these entitlements allow and the associated risks."
);
} else {
terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
terminal.errorPrintln(Verbosity.NORMAL, "@ WARNING: plugin requires additional permissions @");
terminal.errorPrintln(Verbosity.NORMAL, "@ WARNING: plugin requires additional entitlements @");
terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
// print all permissions:
for (String permission : requested) {
terminal.errorPrintln(Verbosity.NORMAL, "* " + permission);
// print all entitlements:
for (String entitlement : requested) {
terminal.errorPrintln(Verbosity.NORMAL, "* " + entitlement);
}
terminal.errorPrintln(
Verbosity.NORMAL,
"See https://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html"
);
terminal.errorPrintln(Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks.");
terminal.errorPrintln(Verbosity.NORMAL, "See " + ENTITLEMENTS_DESCRIPTION_URL);
terminal.errorPrintln(Verbosity.NORMAL, "for descriptions of what these entitlements allow and the associated risks.");

if (batch == false) {
prompt(terminal);
Expand All @@ -83,53 +74,4 @@ private static void prompt(final Terminal terminal) throws UserException {
throw new UserException(ExitCodes.DATA_ERROR, "installation aborted by user");
}
}

/** 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();
}

/**
* Extract a unique set of permissions from the plugin's policy file. Each permission is formatted for output to users.
*/
public 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);
}

return allPermissions.stream().map(PluginSecurity::formatPermission).collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ private void checkFlagEntitlement(
classEntitlements.componentName(),
requestingClass.getModule().getName(),
requestingClass,
PolicyParser.getEntitlementTypeName(entitlementClass)
PolicyParser.buildEntitlementNameFromClass(entitlementClass)
),
callerClass,
classEntitlements
Expand All @@ -529,7 +529,7 @@ private void checkFlagEntitlement(
classEntitlements.componentName(),
requestingClass.getModule().getName(),
requestingClass,
PolicyParser.getEntitlementTypeName(entitlementClass)
PolicyParser.buildEntitlementNameFromClass(entitlementClass)
)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
*/
public class PolicyParser {

private static final Map<String, Class<? extends Entitlement>> EXTERNAL_ENTITLEMENTS = Stream.of(
private static final Map<String, Class<? extends Entitlement>> EXTERNAL_ENTITLEMENT_CLASSES_BY_NAME = Stream.of(
CreateClassLoaderEntitlement.class,
FilesEntitlement.class,
InboundNetworkEntitlement.class,
Expand All @@ -59,14 +59,19 @@ public class PolicyParser {
SetHttpsConnectionPropertiesEntitlement.class,
WriteAllSystemPropertiesEntitlement.class,
WriteSystemPropertiesEntitlement.class
).collect(Collectors.toUnmodifiableMap(PolicyParser::getEntitlementTypeName, Function.identity()));
).collect(Collectors.toUnmodifiableMap(PolicyParser::buildEntitlementNameFromClass, Function.identity()));

private static final Map<Class<? extends Entitlement>, String> EXTERNAL_ENTITLEMENT_NAMES_BY_CLASS =
EXTERNAL_ENTITLEMENT_CLASSES_BY_NAME.entrySet()
.stream()
.collect(Collectors.toUnmodifiableMap(Map.Entry::getValue, Map.Entry::getKey));

protected final XContentParser policyParser;
protected final String policyName;
private final boolean isExternalPlugin;
private final Map<String, Class<? extends Entitlement>> externalEntitlements;

static String getEntitlementTypeName(Class<? extends Entitlement> entitlementClass) {
static String buildEntitlementNameFromClass(Class<? extends Entitlement> entitlementClass) {
var entitlementClassName = entitlementClass.getSimpleName();

if (entitlementClassName.endsWith("Entitlement") == false) {
Expand All @@ -82,8 +87,12 @@ static String getEntitlementTypeName(Class<? extends Entitlement> entitlementCla
.collect(Collectors.joining("_"));
}

public static String getEntitlementName(Class<? extends Entitlement> entitlementClass) {
return EXTERNAL_ENTITLEMENT_NAMES_BY_CLASS.get(entitlementClass);
}

public PolicyParser(InputStream inputStream, String policyName, boolean isExternalPlugin) throws IOException {
this(inputStream, policyName, isExternalPlugin, EXTERNAL_ENTITLEMENTS);
this(inputStream, policyName, isExternalPlugin, EXTERNAL_ENTITLEMENT_CLASSES_BY_NAME);
}

// package private for tests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.Base64;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand All @@ -47,7 +48,7 @@ public record PluginData(Path pluginPath, boolean isModular, boolean isExternalP
}
}

private static final String POLICY_FILE_NAME = "entitlement-policy.yaml";
public static final String POLICY_FILE_NAME = "entitlement-policy.yaml";

public static Map<String, Policy> createPluginPolicies(
Collection<PluginData> pluginData,
Expand All @@ -57,7 +58,6 @@ public static Map<String, Policy> createPluginPolicies(
Map<String, Policy> pluginPolicies = new HashMap<>(pluginData.size());
for (var entry : pluginData) {
Path pluginRoot = entry.pluginPath();
Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME);
String pluginName = pluginRoot.getFileName().toString();
final Set<String> moduleNames = getModuleNames(pluginRoot, entry.isModular());

Expand All @@ -68,8 +68,8 @@ public static Map<String, Policy> createPluginPolicies(
pluginName,
moduleNames
);
var pluginPolicy = parsePolicyIfExists(pluginName, policyFile, entry.isExternalPlugin());
validatePolicyScopes(pluginName, pluginPolicy, moduleNames, policyFile.toString());
var pluginPolicy = parsePolicyIfExists(pluginName, pluginRoot, entry.isExternalPlugin());
validatePolicyScopes(pluginName, pluginPolicy, moduleNames, pluginRoot.resolve(POLICY_FILE_NAME).toString());

pluginPolicies.put(
pluginName,
Expand Down Expand Up @@ -138,7 +138,8 @@ private static void validatePolicyScopes(String layerName, Policy policy, Set<St
}
}

private static Policy parsePolicyIfExists(String pluginName, Path policyFile, boolean isExternalPlugin) throws IOException {
public static Policy parsePolicyIfExists(String pluginName, Path pluginRoot, boolean isExternalPlugin) throws IOException {
Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME);
if (Files.exists(policyFile)) {
return new PolicyParser(Files.newInputStream(policyFile, StandardOpenOption.READ), pluginName, isExternalPlugin).parsePolicy();
}
Expand Down Expand Up @@ -184,21 +185,79 @@ static List<Entitlement> mergeEntitlements(List<Entitlement> a, List<Entitlement
return entitlementMap.values().stream().toList();
}

static Entitlement mergeEntitlement(Entitlement entitlement1, Entitlement entitlement2) {
return switch (entitlement1) {
case FilesEntitlement e -> merge(e, (FilesEntitlement) entitlement2);
case WriteSystemPropertiesEntitlement e -> merge(e, (WriteSystemPropertiesEntitlement) entitlement2);
default -> entitlement1;
static Entitlement mergeEntitlement(Entitlement entitlement, Entitlement other) {
return switch (entitlement) {
case FilesEntitlement e -> mergeFiles(Stream.of(e, (FilesEntitlement) other));
case WriteSystemPropertiesEntitlement e -> mergeWriteSystemProperties(Stream.of(e, (WriteSystemPropertiesEntitlement) other));
default -> entitlement;
};
}

private static FilesEntitlement merge(FilesEntitlement a, FilesEntitlement b) {
return new FilesEntitlement(Stream.concat(a.filesData().stream(), b.filesData().stream()).distinct().toList());
public static List<Entitlement> mergeEntitlements(Stream<Entitlement> entitlements) {
Map<Class<? extends Entitlement>, List<Entitlement>> entitlementMap = entitlements.collect(
Collectors.groupingBy(Entitlement::getClass)
);

List<Entitlement> result = new ArrayList<>();
for (var kv : entitlementMap.entrySet()) {
var entitlementClass = kv.getKey();
var classEntitlements = kv.getValue();
if (classEntitlements.size() == 1) {
result.add(classEntitlements.getFirst());
} else {
result.add(PolicyUtils.mergeEntitlement(entitlementClass, classEntitlements.stream()));
}
}
return result;
}

static Entitlement mergeEntitlement(Class<? extends Entitlement> entitlementClass, Stream<Entitlement> entitlements) {
if (entitlementClass.equals(FilesEntitlement.class)) {
return mergeFiles(entitlements.map(FilesEntitlement.class::cast));
} else if (entitlementClass.equals(WriteSystemPropertiesEntitlement.class)) {
return mergeWriteSystemProperties(entitlements.map(WriteSystemPropertiesEntitlement.class::cast));
}
return entitlements.findFirst().orElseThrow();
}

private static WriteSystemPropertiesEntitlement merge(WriteSystemPropertiesEntitlement a, WriteSystemPropertiesEntitlement b) {
private static FilesEntitlement mergeFiles(Stream<FilesEntitlement> entitlements) {
return new FilesEntitlement(entitlements.flatMap(x -> x.filesData().stream()).distinct().toList());
}

private static WriteSystemPropertiesEntitlement mergeWriteSystemProperties(Stream<WriteSystemPropertiesEntitlement> entitlements) {
return new WriteSystemPropertiesEntitlement(
Stream.concat(a.properties().stream(), b.properties().stream()).collect(Collectors.toUnmodifiableSet())
entitlements.flatMap(x -> x.properties().stream()).collect(Collectors.toUnmodifiableSet())
);
}

static Set<String> describeEntitlement(Entitlement entitlement) {
Set<String> descriptions = new HashSet<>();
if (entitlement instanceof FilesEntitlement f) {
f.filesData()
.stream()
.filter(x -> x.platform() == null || x.platform().isCurrent())
.map(x -> Strings.format("%s %s", PolicyParser.getEntitlementName(FilesEntitlement.class), x.description()))
.forEach(descriptions::add);
} else if (entitlement instanceof WriteSystemPropertiesEntitlement w) {
w.properties()
.stream()
.map(p -> Strings.format("%s [%s]", PolicyParser.getEntitlementName(WriteSystemPropertiesEntitlement.class), p))
.forEach(descriptions::add);
} else {
descriptions.add(PolicyParser.getEntitlementName(entitlement.getClass()));
}
return descriptions;
}

/**
* Extract a unique set of entitlements descriptions from the plugin's policy file. Each entitlement is formatted for output to users.
*/
public static Set<String> getEntitlementsDescriptions(Policy pluginPolicy) {
var allEntitlements = PolicyUtils.mergeEntitlements(pluginPolicy.scopes().stream().flatMap(scope -> scope.entitlements().stream()));
Set<String> descriptions = new HashSet<>();
for (var entitlement : allEntitlements) {
descriptions.addAll(PolicyUtils.describeEntitlement(entitlement));
}
return descriptions;
}
}
Loading
Loading