Skip to content

Commit 73d67e2

Browse files
authored
[Entitlements] Replace Permissions with Entitlements in InstallPluginAction (#125207) (#126119)
This PR replaces the parsing and formatting of SecurityManager policies with the parsing and formatting of Entitlements policy during plugin installation. Relates to ES-10923
1 parent be276ae commit 73d67e2

File tree

15 files changed

+261
-259
lines changed

15 files changed

+261
-259
lines changed

distribution/tools/plugin-cli/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ dependencies {
2424
compileOnly project(":libs:cli")
2525
implementation project(":libs:plugin-api")
2626
implementation project(":libs:plugin-scanner")
27-
// TODO: asm is picked up from the plugin scanner, we should consolidate so it is not defined twice
27+
implementation project(":libs:entitlement")
28+
// TODO: asm is picked up from the plugin scanner and entitlements, we should consolidate so it is not defined twice
2829
implementation 'org.ow2.asm:asm:9.7.1'
2930
implementation 'org.ow2.asm:asm-tree:9.7.1'
3031

distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
2525
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
2626
import org.elasticsearch.Build;
27-
import org.elasticsearch.bootstrap.PluginPolicyInfo;
28-
import org.elasticsearch.bootstrap.PolicyUtil;
2927
import org.elasticsearch.cli.ExitCodes;
3028
import org.elasticsearch.cli.Terminal;
3129
import org.elasticsearch.cli.UserException;
@@ -36,9 +34,9 @@
3634
import org.elasticsearch.core.PathUtils;
3735
import org.elasticsearch.core.SuppressForbidden;
3836
import org.elasticsearch.core.Tuple;
37+
import org.elasticsearch.entitlement.runtime.policy.PolicyUtils;
3938
import org.elasticsearch.env.Environment;
4039
import org.elasticsearch.jdk.JarHell;
41-
import org.elasticsearch.jdk.RuntimeVersionFeature;
4240
import org.elasticsearch.plugin.scanner.ClassReaders;
4341
import org.elasticsearch.plugin.scanner.NamedComponentScanner;
4442
import org.elasticsearch.plugins.Platforms;
@@ -934,13 +932,10 @@ private PluginDescriptor installPlugin(InstallablePlugin descriptor, Path tmpRoo
934932
);
935933
}
936934

937-
if (RuntimeVersionFeature.isSecurityManagerAvailable()) {
938-
PluginPolicyInfo pluginPolicy = PolicyUtil.getPluginPolicyInfo(tmpRoot, env.tmpDir());
939-
if (pluginPolicy != null) {
940-
Set<String> permissions = PluginSecurity.getPermissionDescriptions(pluginPolicy, env.tmpDir());
941-
PluginSecurity.confirmPolicyExceptions(terminal, permissions, batch);
942-
}
943-
}
935+
var pluginPolicy = PolicyUtils.parsePolicyIfExists(info.getName(), tmpRoot, true);
936+
937+
Set<String> entitlements = PolicyUtils.getEntitlementsDescriptions(pluginPolicy);
938+
PluginSecurity.confirmPolicyExceptions(terminal, entitlements, batch);
944939

945940
// Validate that the downloaded plugin's ID matches what we expect from the descriptor. The
946941
// exception is if we install a plugin via `InstallPluginCommand` by specifying a URL or

distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginSecurity.java

Lines changed: 16 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,19 @@
99

1010
package org.elasticsearch.plugins.cli;
1111

12-
import org.elasticsearch.bootstrap.PluginPolicyInfo;
13-
import org.elasticsearch.bootstrap.PolicyUtil;
1412
import org.elasticsearch.cli.ExitCodes;
1513
import org.elasticsearch.cli.Terminal;
1614
import org.elasticsearch.cli.Terminal.Verbosity;
1715
import org.elasticsearch.cli.UserException;
1816

19-
import java.io.IOException;
20-
import java.net.URL;
21-
import java.nio.file.Path;
22-
import java.security.Permission;
23-
import java.security.UnresolvedPermission;
2417
import java.util.ArrayList;
2518
import java.util.Collections;
26-
import java.util.HashSet;
2719
import java.util.List;
2820
import java.util.Set;
2921
import java.util.stream.Collectors;
3022

3123
/**
32-
* Contains methods for displaying extended plugin permissions to the user, and confirming that
24+
* Contains methods for displaying extended plugin entitlements to the user, and confirming that
3325
* plugin installation can proceed.
3426
*/
3527
public class PluginSecurity {
@@ -40,37 +32,36 @@ public class PluginSecurity {
4032
/**
4133
* prints/confirms policy exceptions with the user
4234
*/
43-
static void confirmPolicyExceptions(Terminal terminal, Set<String> permissions, boolean batch) throws UserException {
44-
List<String> requested = new ArrayList<>(permissions);
35+
static void confirmPolicyExceptions(Terminal terminal, Set<String> entitlements, boolean batch) throws UserException {
36+
List<String> requested = new ArrayList<>(entitlements);
4537
if (requested.isEmpty()) {
46-
terminal.println(Verbosity.VERBOSE, "plugin has a policy file with no additional permissions");
38+
terminal.println(
39+
Verbosity.NORMAL,
40+
"WARNING: plugin has a policy file with no additional entitlements. Double check this is intentional."
41+
);
4742
} else {
48-
// sort permissions in a reasonable order
43+
// sort entitlements in a reasonable order
4944
Collections.sort(requested);
5045

5146
if (terminal.isHeadless()) {
5247
terminal.errorPrintln(
53-
"WARNING: plugin requires additional permissions: ["
48+
"WARNING: plugin requires additional entitlements: ["
5449
+ requested.stream().map(each -> '\'' + each + '\'').collect(Collectors.joining(", "))
5550
+ "]"
5651
);
5752
terminal.errorPrintln(
58-
"See https://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html"
59-
+ " for descriptions of what these permissions allow and the associated risks."
53+
"See " + ENTITLEMENTS_DESCRIPTION_URL + " for descriptions of what these entitlements allow and the associated risks."
6054
);
6155
} else {
6256
terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
63-
terminal.errorPrintln(Verbosity.NORMAL, "@ WARNING: plugin requires additional permissions @");
57+
terminal.errorPrintln(Verbosity.NORMAL, "@ WARNING: plugin requires additional entitlements @");
6458
terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
65-
// print all permissions:
66-
for (String permission : requested) {
67-
terminal.errorPrintln(Verbosity.NORMAL, "* " + permission);
59+
// print all entitlements:
60+
for (String entitlement : requested) {
61+
terminal.errorPrintln(Verbosity.NORMAL, "* " + entitlement);
6862
}
69-
terminal.errorPrintln(
70-
Verbosity.NORMAL,
71-
"See https://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html"
72-
);
73-
terminal.errorPrintln(Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks.");
63+
terminal.errorPrintln(Verbosity.NORMAL, "See " + ENTITLEMENTS_DESCRIPTION_URL);
64+
terminal.errorPrintln(Verbosity.NORMAL, "for descriptions of what these entitlements allow and the associated risks.");
7465

7566
if (batch == false) {
7667
prompt(terminal);
@@ -86,53 +77,4 @@ private static void prompt(final Terminal terminal) throws UserException {
8677
throw new UserException(ExitCodes.DATA_ERROR, "installation aborted by user");
8778
}
8879
}
89-
90-
/** Format permission type, name, and actions into a string */
91-
static String formatPermission(Permission permission) {
92-
StringBuilder sb = new StringBuilder();
93-
94-
String clazz = null;
95-
if (permission instanceof UnresolvedPermission) {
96-
clazz = ((UnresolvedPermission) permission).getUnresolvedType();
97-
} else {
98-
clazz = permission.getClass().getName();
99-
}
100-
sb.append(clazz);
101-
102-
String name = null;
103-
if (permission instanceof UnresolvedPermission) {
104-
name = ((UnresolvedPermission) permission).getUnresolvedName();
105-
} else {
106-
name = permission.getName();
107-
}
108-
if (name != null && name.length() > 0) {
109-
sb.append(' ');
110-
sb.append(name);
111-
}
112-
113-
String actions = null;
114-
if (permission instanceof UnresolvedPermission) {
115-
actions = ((UnresolvedPermission) permission).getUnresolvedActions();
116-
} else {
117-
actions = permission.getActions();
118-
}
119-
if (actions != null && actions.length() > 0) {
120-
sb.append(' ');
121-
sb.append(actions);
122-
}
123-
return sb.toString();
124-
}
125-
126-
/**
127-
* Extract a unique set of permissions from the plugin's policy file. Each permission is formatted for output to users.
128-
*/
129-
public static Set<String> getPermissionDescriptions(PluginPolicyInfo pluginPolicyInfo, Path tmpDir) throws IOException {
130-
Set<Permission> allPermissions = new HashSet<>(PolicyUtil.getPolicyPermissions(null, pluginPolicyInfo.policy(), tmpDir));
131-
for (URL jar : pluginPolicyInfo.jars()) {
132-
Set<Permission> jarPermissions = PolicyUtil.getPolicyPermissions(jar, pluginPolicyInfo.policy(), tmpDir);
133-
allPermissions.addAll(jarPermissions);
134-
}
135-
136-
return allPermissions.stream().map(PluginSecurity::formatPermission).collect(Collectors.toSet());
137-
}
13880
}

distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,15 @@
4141
import org.elasticsearch.common.hash.MessageDigests;
4242
import org.elasticsearch.common.io.FileSystemUtils;
4343
import org.elasticsearch.common.settings.Settings;
44+
import org.elasticsearch.core.CheckedConsumer;
4445
import org.elasticsearch.core.PathUtils;
4546
import org.elasticsearch.core.PathUtilsForTesting;
4647
import org.elasticsearch.core.Strings;
4748
import org.elasticsearch.core.SuppressForbidden;
4849
import org.elasticsearch.core.Tuple;
50+
import org.elasticsearch.entitlement.runtime.policy.PolicyUtils;
4951
import org.elasticsearch.env.Environment;
5052
import org.elasticsearch.env.TestEnvironment;
51-
import org.elasticsearch.jdk.RuntimeVersionFeature;
5253
import org.elasticsearch.plugin.scanner.NamedComponentScanner;
5354
import org.elasticsearch.plugins.Platforms;
5455
import org.elasticsearch.plugins.PluginDescriptor;
@@ -57,6 +58,8 @@
5758
import org.elasticsearch.test.PosixPermissionsResetter;
5859
import org.elasticsearch.test.compiler.InMemoryJavaCompiler;
5960
import org.elasticsearch.test.jar.JarUtils;
61+
import org.elasticsearch.xcontent.XContentBuilder;
62+
import org.elasticsearch.xcontent.yaml.YamlXContent;
6063
import org.junit.After;
6164
import org.junit.Before;
6265

@@ -102,6 +105,7 @@
102105
import java.util.zip.ZipEntry;
103106
import java.util.zip.ZipOutputStream;
104107

108+
import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED;
105109
import static org.elasticsearch.snapshots.AbstractSnapshotIntegTestCase.forEachFileRecursively;
106110
import static org.hamcrest.CoreMatchers.equalTo;
107111
import static org.hamcrest.Matchers.containsInAnyOrder;
@@ -137,8 +141,6 @@ public class InstallPluginActionTests extends ESTestCase {
137141

138142
@SuppressForbidden(reason = "sets java.io.tmpdir")
139143
public InstallPluginActionTests(FileSystem fs, Function<String, Path> temp) {
140-
assert "false".equals(System.getProperty("tests.security.manager")) : "-Dtests.security.manager=false has to be set";
141-
142144
this.temp = temp;
143145
this.isPosix = fs.supportedFileAttributeViews().contains("posix");
144146
this.isReal = fs == PathUtils.getDefaultFileSystem();
@@ -309,15 +311,20 @@ private static String[] pluginProperties(String name, String[] additionalProps,
309311
).flatMap(Function.identity()).toArray(String[]::new);
310312
}
311313

312-
static void writePluginSecurityPolicy(Path pluginDir, String... permissions) throws IOException {
313-
StringBuilder securityPolicyContent = new StringBuilder("grant {\n ");
314-
for (String permission : permissions) {
315-
securityPolicyContent.append("permission java.lang.RuntimePermission \"");
316-
securityPolicyContent.append(permission);
317-
securityPolicyContent.append("\";");
314+
static void writePluginEntitlementPolicy(Path pluginDir, String moduleName, CheckedConsumer<XContentBuilder, IOException> policyBuilder)
315+
throws IOException {
316+
try (var builder = YamlXContent.contentBuilder()) {
317+
builder.startObject();
318+
builder.field(moduleName);
319+
builder.startArray();
320+
321+
policyBuilder.accept(builder);
322+
builder.endArray();
323+
builder.endObject();
324+
325+
String policy = org.elasticsearch.common.Strings.toString(builder);
326+
Files.writeString(pluginDir.resolve(PolicyUtils.POLICY_FILE_NAME), policy);
318327
}
319-
securityPolicyContent.append("\n};\n");
320-
Files.write(pluginDir.resolve("plugin-security.policy"), securityPolicyContent.toString().getBytes(StandardCharsets.UTF_8));
321328
}
322329

323330
static InstallablePlugin createStablePlugin(String name, Path structure, boolean hasNamedComponentFile, String... additionalProps)
@@ -787,10 +794,10 @@ public void testConfig() throws Exception {
787794
public void testExistingConfig() throws Exception {
788795
Path envConfigDir = env.v2().configDir().resolve("fake");
789796
Files.createDirectories(envConfigDir);
790-
Files.write(envConfigDir.resolve("custom.yml"), "existing config".getBytes(StandardCharsets.UTF_8));
797+
Files.writeString(envConfigDir.resolve("custom.yml"), "existing config");
791798
Path configDir = pluginDir.resolve("config");
792799
Files.createDirectory(configDir);
793-
Files.write(configDir.resolve("custom.yml"), "new config".getBytes(StandardCharsets.UTF_8));
800+
Files.writeString(configDir.resolve("custom.yml"), "new config");
794801
Files.createFile(configDir.resolve("other.yml"));
795802
InstallablePlugin pluginZip = createPluginZip("fake", pluginDir);
796803
installPlugin(pluginZip);
@@ -892,9 +899,8 @@ public void testInstallMisspelledOfficialPlugins() {
892899
}
893900

894901
public void testBatchFlag() throws Exception {
895-
assumeTrue("security policy validation only available with SecurityManager", RuntimeVersionFeature.isSecurityManagerAvailable());
896902
installPlugin(true);
897-
assertThat(terminal.getErrorOutput(), containsString("WARNING: plugin requires additional permissions"));
903+
assertThat(terminal.getErrorOutput(), containsString("WARNING: plugin requires additional entitlements"));
898904
assertThat(terminal.getOutput(), containsString("-> Downloading"));
899905
// No progress bar in batch mode
900906
assertThat(terminal.getOutput(), not(containsString("100%")));
@@ -942,12 +948,12 @@ public void testPluginHasDifferentNameThatDescriptor() throws Exception {
942948
assertThat(e.getMessage(), equalTo("Expected downloaded plugin to have ID [other-fake] but found [fake]"));
943949
}
944950

945-
private void installPlugin(boolean isBatch, String... additionalProperties) throws Exception {
946-
// if batch is enabled, we also want to add a security policy
951+
private void installPlugin(boolean isBatch) throws Exception {
952+
// if batch is enabled, we also want to add an entitlement policy
947953
if (isBatch) {
948-
writePluginSecurityPolicy(pluginDir, "setFactory");
954+
writePluginEntitlementPolicy(pluginDir, ALL_UNNAMED, builder -> builder.value("manage_threads"));
949955
}
950-
InstallablePlugin pluginZip = createPlugin("fake", pluginDir, additionalProperties);
956+
InstallablePlugin pluginZip = createPlugin("fake", pluginDir);
951957
skipJarHellAction.setEnvironment(env.v2());
952958
skipJarHellAction.setBatch(isBatch);
953959
skipJarHellAction.execute(List.of(pluginZip));
@@ -1033,13 +1039,13 @@ URL openUrl(String urlString) throws IOException {
10331039
Path shaFile = temp.apply("shas").resolve("downloaded.zip" + shaExtension);
10341040
byte[] zipbytes = Files.readAllBytes(pluginZipPath);
10351041
String checksum = shaCalculator.apply(zipbytes);
1036-
Files.write(shaFile, checksum.getBytes(StandardCharsets.UTF_8));
1042+
Files.writeString(shaFile, checksum);
10371043
return shaFile.toUri().toURL();
10381044
} else if ((url + ".asc").equals(urlString)) {
10391045
final Path ascFile = temp.apply("asc").resolve("downloaded.zip" + ".asc");
10401046
final byte[] zipBytes = Files.readAllBytes(pluginZipPath);
10411047
final String asc = signature.apply(zipBytes, secretKey);
1042-
Files.write(ascFile, asc.getBytes(StandardCharsets.UTF_8));
1048+
Files.writeString(ascFile, asc);
10431049
return ascFile.toUri().toURL();
10441050
}
10451051
return null;
@@ -1531,11 +1537,13 @@ private void assertPolicyConfirmation(Tuple<Path, Environment> pathEnvironmentTu
15311537
}
15321538

15331539
public void testPolicyConfirmation() throws Exception {
1534-
assumeTrue("security policy parsing only available with SecurityManager", RuntimeVersionFeature.isSecurityManagerAvailable());
1535-
writePluginSecurityPolicy(pluginDir, "getClassLoader", "setFactory");
1540+
writePluginEntitlementPolicy(pluginDir, "test.plugin.module", builder -> {
1541+
builder.value("manage_threads");
1542+
builder.value("outbound_network");
1543+
});
15361544
InstallablePlugin pluginZip = createPluginZip("fake", pluginDir);
15371545

1538-
assertPolicyConfirmation(env, pluginZip, "plugin requires additional permissions");
1546+
assertPolicyConfirmation(env, pluginZip, "plugin requires additional entitlements");
15391547
assertPlugin("fake", pluginDir, env.v2());
15401548
}
15411549

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ private void checkFlagEntitlement(
517517
classEntitlements.componentName(),
518518
getModuleName(requestingClass),
519519
requestingClass,
520-
PolicyParser.getEntitlementTypeName(entitlementClass)
520+
PolicyParser.buildEntitlementNameFromClass(entitlementClass)
521521
),
522522
callerClass,
523523
classEntitlements
@@ -530,7 +530,7 @@ private void checkFlagEntitlement(
530530
classEntitlements.componentName(),
531531
getModuleName(requestingClass),
532532
requestingClass,
533-
PolicyParser.getEntitlementTypeName(entitlementClass)
533+
PolicyParser.buildEntitlementNameFromClass(entitlementClass)
534534
)
535535
);
536536
}

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
*/
5050
public class PolicyParser {
5151

52-
private static final Map<String, Class<? extends Entitlement>> EXTERNAL_ENTITLEMENTS = Stream.of(
52+
private static final Map<String, Class<? extends Entitlement>> EXTERNAL_ENTITLEMENT_CLASSES_BY_NAME = Stream.of(
5353
CreateClassLoaderEntitlement.class,
5454
FilesEntitlement.class,
5555
InboundNetworkEntitlement.class,
@@ -59,14 +59,19 @@ public class PolicyParser {
5959
SetHttpsConnectionPropertiesEntitlement.class,
6060
WriteAllSystemPropertiesEntitlement.class,
6161
WriteSystemPropertiesEntitlement.class
62-
).collect(Collectors.toUnmodifiableMap(PolicyParser::getEntitlementTypeName, Function.identity()));
62+
).collect(Collectors.toUnmodifiableMap(PolicyParser::buildEntitlementNameFromClass, Function.identity()));
63+
64+
private static final Map<Class<? extends Entitlement>, String> EXTERNAL_ENTITLEMENT_NAMES_BY_CLASS =
65+
EXTERNAL_ENTITLEMENT_CLASSES_BY_NAME.entrySet()
66+
.stream()
67+
.collect(Collectors.toUnmodifiableMap(Map.Entry::getValue, Map.Entry::getKey));
6368

6469
protected final XContentParser policyParser;
6570
protected final String policyName;
6671
private final boolean isExternalPlugin;
6772
private final Map<String, Class<? extends Entitlement>> externalEntitlements;
6873

69-
static String getEntitlementTypeName(Class<? extends Entitlement> entitlementClass) {
74+
static String buildEntitlementNameFromClass(Class<? extends Entitlement> entitlementClass) {
7075
var entitlementClassName = entitlementClass.getSimpleName();
7176

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

90+
public static String getEntitlementName(Class<? extends Entitlement> entitlementClass) {
91+
return EXTERNAL_ENTITLEMENT_NAMES_BY_CLASS.get(entitlementClass);
92+
}
93+
8594
public PolicyParser(InputStream inputStream, String policyName, boolean isExternalPlugin) throws IOException {
86-
this(inputStream, policyName, isExternalPlugin, EXTERNAL_ENTITLEMENTS);
95+
this(inputStream, policyName, isExternalPlugin, EXTERNAL_ENTITLEMENT_CLASSES_BY_NAME);
8796
}
8897

8998
// package private for tests

0 commit comments

Comments
 (0)