Skip to content

Commit a74551e

Browse files
committed
server policy patching via system property (merge, not replace)
1 parent 49254b0 commit a74551e

File tree

6 files changed

+119
-55
lines changed

6 files changed

+119
-55
lines changed

libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import com.sun.tools.attach.AttachNotSupportedException;
1515
import com.sun.tools.attach.VirtualMachine;
1616

17+
import org.elasticsearch.core.Nullable;
1718
import org.elasticsearch.core.SuppressForbidden;
1819
import org.elasticsearch.entitlement.initialization.EntitlementInitialization;
1920
import org.elasticsearch.entitlement.runtime.policy.Policy;
@@ -33,6 +34,7 @@
3334
public class EntitlementBootstrap {
3435

3536
public record BootstrapArgs(
37+
@Nullable Policy serverPolicyPatch,
3638
Map<String, Policy> pluginPolicies,
3739
Function<Class<?>, String> pluginResolver,
3840
Function<String, Stream<String>> settingResolver,
@@ -78,6 +80,7 @@ public static BootstrapArgs bootstrapArgs() {
7880
* Activates entitlement checking. Once this method returns, calls to methods protected by Entitlements from classes without a valid
7981
* policy will throw {@link org.elasticsearch.entitlement.runtime.api.NotEntitledException}.
8082
*
83+
* @param serverPolicyPatch a policy with additional entitlements to patch the embedded server layer policy
8184
* @param pluginPolicies a map holding policies for plugins (and modules), by plugin (or module) name.
8285
* @param pluginResolver a functor to map a Java Class to the plugin it belongs to (the plugin name).
8386
* @param settingResolver a functor to resolve a setting name pattern for one or more Elasticsearch settings.
@@ -94,6 +97,7 @@ public static BootstrapArgs bootstrapArgs() {
9497
* @param suppressFailureLogClasses classes for which we do not need or want to log Entitlements failures
9598
*/
9699
public static void bootstrap(
100+
Policy serverPolicyPatch,
97101
Map<String, Policy> pluginPolicies,
98102
Function<Class<?>, String> pluginResolver,
99103
Function<String, Stream<String>> settingResolver,
@@ -114,6 +118,7 @@ public static void bootstrap(
114118
throw new IllegalStateException("plugin data is already set");
115119
}
116120
EntitlementBootstrap.bootstrapArgs = new BootstrapArgs(
121+
serverPolicyPatch,
117122
pluginPolicies,
118123
pluginResolver,
119124
settingResolver,

libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.elasticsearch.entitlement.runtime.policy.PathLookup;
2424
import org.elasticsearch.entitlement.runtime.policy.Policy;
2525
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
26+
import org.elasticsearch.entitlement.runtime.policy.PolicyParserUtils;
2627
import org.elasticsearch.entitlement.runtime.policy.Scope;
2728
import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement;
2829
import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement;
@@ -268,8 +269,13 @@ private static PolicyManager createPolicyManager() {
268269
);
269270
}
270271

271-
// TODO(ES-10031): Decide what goes in the elasticsearch default policy and extend it
272-
var serverPolicy = new Policy("server", serverScopes);
272+
var serverPolicy = new Policy(
273+
"server",
274+
bootstrapArgs.serverPolicyPatch() == null
275+
? serverScopes
276+
: PolicyParserUtils.mergeScopes(serverScopes, bootstrapArgs.serverPolicyPatch().scopes())
277+
);
278+
273279
// agents run without a module, so this is a special hack for the apm agent
274280
// this should be removed once https://github.com/elastic/elasticsearch/issues/109335 is completed
275281
// See also modules/apm/src/main/plugin-metadata/entitlement-policy.yaml

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,9 @@ ModuleEntitlements policyEntitlements(String componentName, Path componentPath,
131131

132132
public static final String ALL_UNNAMED = "ALL-UNNAMED";
133133

134-
private static final Set<Module> systemModules = findSystemModules();
134+
private static final Set<Module> SYSTEM_LAYER_MODULES = findSystemLayerModules();
135135

136-
private static Set<Module> findSystemModules() {
136+
private static Set<Module> findSystemLayerModules() {
137137
var systemModulesDescriptors = ModuleFinder.ofSystem()
138138
.findAll()
139139
.stream()
@@ -153,6 +153,13 @@ private static Set<Module> findSystemModules() {
153153
).collect(Collectors.toUnmodifiableSet());
154154
}
155155

156+
// Anything in the boot layer that is not in the system layer, is in the server layer
157+
public static final Set<Module> SERVER_LAYER_MODULES = ModuleLayer.boot()
158+
.modules()
159+
.stream()
160+
.filter(m -> SYSTEM_LAYER_MODULES.contains(m) == false)
161+
.collect(Collectors.toUnmodifiableSet());
162+
156163
private final Map<String, Path> sourcePaths;
157164
/**
158165
* The package name containing classes from the APM agent.
@@ -705,7 +712,7 @@ private static boolean isTriviallyAllowed(Class<?> requestingClass) {
705712
logger.debug("Entitlement trivially allowed: no caller frames outside the entitlement library");
706713
return true;
707714
}
708-
if (systemModules.contains(requestingClass.getModule())) {
715+
if (SYSTEM_LAYER_MODULES.contains(requestingClass.getModule())) {
709716
logger.debug("Entitlement trivially allowed from system module [{}]", requestingClass.getModule().getName());
710717
return true;
711718
}

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

Lines changed: 68 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
package org.elasticsearch.entitlement.runtime.policy;
1111

1212
import org.elasticsearch.core.Strings;
13+
import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement;
14+
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
1315
import org.elasticsearch.logging.LogManager;
1416
import org.elasticsearch.logging.Logger;
1517

@@ -20,14 +22,15 @@
2022
import java.nio.file.Files;
2123
import java.nio.file.Path;
2224
import java.nio.file.StandardOpenOption;
25+
import java.util.ArrayList;
2326
import java.util.Base64;
2427
import java.util.Collection;
2528
import java.util.HashMap;
2629
import java.util.List;
2730
import java.util.Map;
28-
import java.util.Optional;
2931
import java.util.Set;
3032
import java.util.stream.Collectors;
33+
import java.util.stream.Stream;
3134

3235
import static java.util.Objects.requireNonNull;
3336
import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED;
@@ -36,6 +39,45 @@ public class PolicyParserUtils {
3639

3740
private static final Logger logger = LogManager.getLogger(PolicyParserUtils.class);
3841

42+
public static List<Scope> mergeScopes(List<Scope> mainScopes, List<Scope> additionalScopes) {
43+
var result = new ArrayList<Scope>();
44+
var additionalScopesMap = additionalScopes.stream().collect(Collectors.toMap(Scope::moduleName, Scope::entitlements));
45+
for (var mainScope : mainScopes) {
46+
List<Entitlement> additionalEntitlements = additionalScopesMap.remove(mainScope.moduleName());
47+
if (additionalEntitlements != null) {
48+
result.add(new Scope(mainScope.moduleName(), mergeEntitlements(mainScope.entitlements(), additionalEntitlements)));
49+
} else {
50+
result.add(mainScope);
51+
}
52+
}
53+
54+
for (var remainingEntry : additionalScopesMap.entrySet()) {
55+
result.add(new Scope(remainingEntry.getKey(), remainingEntry.getValue()));
56+
}
57+
return result;
58+
}
59+
60+
static List<Entitlement> mergeEntitlements(List<Entitlement> a, List<Entitlement> b) {
61+
Map<Class<? extends Entitlement>, List<Entitlement>> allEntitlements = Stream.concat(a.stream(), b.stream())
62+
.collect(Collectors.groupingBy(Entitlement::getClass));
63+
64+
List<Entitlement> result = new ArrayList<>();
65+
for (var entitlements : allEntitlements.entrySet()) {
66+
var entitlementClass = entitlements.getKey();
67+
if (entitlementClass.equals(FilesEntitlement.class)) {
68+
var filesData = entitlements.getValue().stream().flatMap(entitlement -> {
69+
FilesEntitlement filesEntitlement = (FilesEntitlement) entitlement;
70+
return filesEntitlement.filesData().stream();
71+
}).filter(x -> x.platform().isCurrent()).distinct();
72+
73+
result.add(new FilesEntitlement(filesData.toList()));
74+
} else {
75+
result.add(entitlements.getValue().get(0));
76+
}
77+
}
78+
return result;
79+
}
80+
3981
public record PluginData(Path pluginPath, boolean isModular, boolean isExternalPlugin) {
4082
public PluginData {
4183
requireNonNull(pluginPath);
@@ -44,8 +86,6 @@ public record PluginData(Path pluginPath, boolean isModular, boolean isExternalP
4486

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

47-
public static final String POLICY_OVERRIDE_PREFIX = "es.entitlements.policy.";
48-
4989
public static Map<String, Policy> createPluginPolicies(Collection<PluginData> pluginData, Map<String, String> overrides, String version)
5090
throws IOException {
5191
Map<String, Policy> pluginPolicies = new HashMap<>(pluginData.size());
@@ -54,9 +94,15 @@ public static Map<String, Policy> createPluginPolicies(Collection<PluginData> pl
5494
String pluginName = pluginRoot.getFileName().toString();
5595
final Set<String> moduleNames = getModuleNames(pluginRoot, entry.isModular());
5696

57-
var overriddenPolicy = parsePolicyOverrideIfExists(overrides, version, entry.isExternalPlugin(), pluginName, moduleNames);
58-
if (overriddenPolicy.isPresent()) {
59-
pluginPolicies.put(pluginName, overriddenPolicy.get());
97+
var overriddenPolicy = parsePolicyOverrideIfExists(
98+
overrides.get(pluginName),
99+
version,
100+
entry.isExternalPlugin(),
101+
pluginName,
102+
moduleNames
103+
);
104+
if (overriddenPolicy != null) {
105+
pluginPolicies.put(pluginName, overriddenPolicy);
60106
} else {
61107
Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME);
62108
var policy = parsePolicyIfExists(pluginName, policyFile, entry.isExternalPlugin());
@@ -67,59 +113,55 @@ public static Map<String, Policy> createPluginPolicies(Collection<PluginData> pl
67113
return pluginPolicies;
68114
}
69115

70-
static Optional<Policy> parsePolicyOverrideIfExists(
71-
Map<String, String> overrides,
116+
public static Policy parsePolicyOverrideIfExists(
117+
String policyOverride,
72118
String version,
73119
boolean externalPlugin,
74-
String pluginName,
120+
String layerName,
75121
Set<String> moduleNames
76122
) {
77-
var policyOverride = overrides.get(pluginName);
78123
if (policyOverride != null) {
79124
try {
80-
var versionedPolicy = decodeOverriddenPluginPolicy(policyOverride, pluginName, externalPlugin);
81-
validatePolicyScopes(pluginName, versionedPolicy.policy(), moduleNames, "<override>");
125+
var versionedPolicy = decodeOverriddenPluginPolicy(policyOverride, layerName, externalPlugin);
126+
validatePolicyScopes(layerName, versionedPolicy.policy(), moduleNames, "<override>");
82127

83128
// Empty versions defaults to "any"
84129
if (versionedPolicy.versions().isEmpty() || versionedPolicy.versions().contains(version)) {
85-
logger.info("Using policy override for plugin [{}]", pluginName);
86-
return Optional.of(versionedPolicy.policy());
130+
logger.info("Using policy override for layer [{}]", layerName);
131+
return versionedPolicy.policy();
87132
} else {
88133
logger.warn(
89134
"Found a policy override with version mismatch. The override will not be applied. "
90-
+ "Plugin [{}]; policy versions [{}]; current version [{}]",
91-
pluginName,
135+
+ "Layer [{}]; policy versions [{}]; current version [{}]",
136+
layerName,
92137
String.join(",", versionedPolicy.versions()),
93138
version
94139
);
95140
}
96141
} catch (Exception ex) {
97142
logger.warn(
98-
Strings.format(
99-
"Found a policy override with invalid content. The override will not be applied. Plugin [%s]",
100-
pluginName
101-
),
143+
Strings.format("Found a policy override with invalid content. The override will not be applied. Layer [%s]", layerName),
102144
ex
103145
);
104146
}
105147
}
106-
return Optional.empty();
148+
return null;
107149
}
108150

109-
static VersionedPolicy decodeOverriddenPluginPolicy(String base64String, String pluginName, boolean isExternalPlugin)
151+
static VersionedPolicy decodeOverriddenPluginPolicy(String base64String, String layerName, boolean isExternalPlugin)
110152
throws IOException {
111153
byte[] policyDefinition = Base64.getDecoder().decode(base64String);
112-
return new PolicyParser(new ByteArrayInputStream(policyDefinition), pluginName, isExternalPlugin).parseVersionedPolicy();
154+
return new PolicyParser(new ByteArrayInputStream(policyDefinition), layerName, isExternalPlugin).parseVersionedPolicy();
113155
}
114156

115-
private static void validatePolicyScopes(String pluginName, Policy policy, Set<String> moduleNames, String policyLocation) {
157+
private static void validatePolicyScopes(String layerName, Policy policy, Set<String> moduleNames, String policyLocation) {
116158
// TODO: should this check actually be part of the parser?
117159
for (Scope scope : policy.scopes()) {
118160
if (moduleNames.contains(scope.moduleName()) == false) {
119161
throw new IllegalStateException(
120162
Strings.format(
121-
"Invalid module name in policy: plugin [%s] does not have module [%s]; available modules [%s]; policy path [%s]",
122-
pluginName,
163+
"Invalid module name in policy: layer [%s] does not have module [%s]; available modules [%s]; policy path [%s]",
164+
layerName,
123165
scope.moduleName(),
124166
String.join(", ", moduleNames),
125167
policyLocation

libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserUtilsTests.java

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@
1616
import java.nio.charset.StandardCharsets;
1717
import java.util.Base64;
1818
import java.util.List;
19-
import java.util.Map;
2019
import java.util.Set;
2120

22-
import static org.elasticsearch.test.hamcrest.OptionalMatchers.isEmpty;
23-
import static org.elasticsearch.test.hamcrest.OptionalMatchers.isPresentWith;
21+
import static org.hamcrest.Matchers.equalTo;
22+
import static org.hamcrest.Matchers.nullValue;
2423

2524
@ESTestCase.WithoutSecurityManager
2625
public class PolicyParserUtilsTests extends ESTestCase {
@@ -41,8 +40,6 @@ public void testCreatePluginPolicyWithOverride() {
4140
Base64.getEncoder().encode(policyForOverride.getBytes(StandardCharsets.UTF_8)),
4241
StandardCharsets.UTF_8
4342
);
44-
var overrides = Map.of("test-plugin", base64EncodedPolicy);
45-
4643
final Policy expectedPolicy = new Policy(
4744
"test-plugin",
4845
List.of(
@@ -52,14 +49,14 @@ public void testCreatePluginPolicyWithOverride() {
5249
);
5350

5451
var policy = PolicyParserUtils.parsePolicyOverrideIfExists(
55-
overrides,
52+
base64EncodedPolicy,
5653
"9.0.0",
5754
true,
5855
"test-plugin",
5956
Set.of("entitlement-module-name", "entitlement-module-name-2")
6057
);
6158

62-
assertThat(policy, isPresentWith(expectedPolicy));
59+
assertThat(policy, equalTo(expectedPolicy));
6360
}
6461

6562
public void testCreatePluginPolicyWithOverrideAnyVersion() {
@@ -75,7 +72,6 @@ public void testCreatePluginPolicyWithOverrideAnyVersion() {
7572
Base64.getEncoder().encode(policyForOverride.getBytes(StandardCharsets.UTF_8)),
7673
StandardCharsets.UTF_8
7774
);
78-
var overrides = Map.of("test-plugin", base64EncodedPolicy);
7975

8076
final Policy expectedPolicy = new Policy(
8177
"test-plugin",
@@ -86,14 +82,14 @@ public void testCreatePluginPolicyWithOverrideAnyVersion() {
8682
);
8783

8884
var policy = PolicyParserUtils.parsePolicyOverrideIfExists(
89-
overrides,
85+
base64EncodedPolicy,
9086
"abcdef",
9187
true,
9288
"test-plugin",
9389
Set.of("entitlement-module-name", "entitlement-module-name-2")
9490
);
9591

96-
assertThat(policy, isPresentWith(expectedPolicy));
92+
assertThat(policy, equalTo(expectedPolicy));
9793
}
9894

9995
public void testNoOverriddenPolicyWithVersionMismatch() {
@@ -112,17 +108,16 @@ public void testNoOverriddenPolicyWithVersionMismatch() {
112108
Base64.getEncoder().encode(policyForOverride.getBytes(StandardCharsets.UTF_8)),
113109
StandardCharsets.UTF_8
114110
);
115-
var overrides = Map.of("test-plugin", base64EncodedPolicy);
116111

117112
var policy = PolicyParserUtils.parsePolicyOverrideIfExists(
118-
overrides,
113+
base64EncodedPolicy,
119114
"9.1.0",
120115
true,
121116
"test-plugin",
122117
Set.of("entitlement-module-name", "entitlement-module-name-2")
123118
);
124119

125-
assertThat(policy, isEmpty());
120+
assertThat(policy, nullValue());
126121
}
127122

128123
public void testNoOverriddenPolicyWithValidationError() {
@@ -141,11 +136,10 @@ public void testNoOverriddenPolicyWithValidationError() {
141136
Base64.getEncoder().encode(policyForOverride.getBytes(StandardCharsets.UTF_8)),
142137
StandardCharsets.UTF_8
143138
);
144-
var overrides = Map.of("test-plugin", base64EncodedPolicy);
145139

146-
var policy = PolicyParserUtils.parsePolicyOverrideIfExists(overrides, "9.0.0", true, "test-plugin", Set.of());
140+
var policy = PolicyParserUtils.parsePolicyOverrideIfExists(base64EncodedPolicy, "9.0.0", true, "test-plugin", Set.of());
147141

148-
assertThat(policy, isEmpty());
142+
assertThat(policy, nullValue());
149143
}
150144

151145
public void testNoOverriddenPolicyWithParsingError() {
@@ -160,10 +154,9 @@ public void testNoOverriddenPolicyWithParsingError() {
160154
Base64.getEncoder().encode(policyForOverride.getBytes(StandardCharsets.UTF_8)),
161155
StandardCharsets.UTF_8
162156
);
163-
var overrides = Map.of("test-plugin", base64EncodedPolicy);
164157

165-
var policy = PolicyParserUtils.parsePolicyOverrideIfExists(overrides, "9.0.0", true, "test-plugin", Set.of());
158+
var policy = PolicyParserUtils.parsePolicyOverrideIfExists(base64EncodedPolicy, "9.0.0", true, "test-plugin", Set.of());
166159

167-
assertThat(policy, isEmpty());
160+
assertThat(policy, nullValue());
168161
}
169162
}

0 commit comments

Comments
 (0)