Skip to content

Commit 196338d

Browse files
authored
[8.x] [Entitlelments] Server policy patching via system property (#124904) (#125008)
* [Entitlelments] Server policy patching via system property (#124904) This PR adds a mechanism to patch the server layer policy by merging entitlements from a policy provided via a system property (merge, not replace). * change to support 8.x language level
1 parent 9f12bca commit 196338d

File tree

7 files changed

+415
-213
lines changed

7 files changed

+415
-213
lines changed

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

+5
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

+8-2
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.PolicyUtils;
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;
@@ -269,8 +270,13 @@ private static PolicyManager createPolicyManager() {
269270
);
270271
}
271272

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

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

+10-3
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,9 @@ ModuleEntitlements policyEntitlements(String componentName, Path componentPath,
141141

142142
public static final String ALL_UNNAMED = "ALL-UNNAMED";
143143

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

146-
private static Set<Module> findSystemModules() {
146+
private static Set<Module> findSystemLayerModules() {
147147
var systemModulesDescriptors = ModuleFinder.ofSystem()
148148
.findAll()
149149
.stream()
@@ -163,6 +163,13 @@ private static Set<Module> findSystemModules() {
163163
).collect(Collectors.toUnmodifiableSet());
164164
}
165165

166+
// Anything in the boot layer that is not in the system layer, is in the server layer
167+
public static final Set<Module> SERVER_LAYER_MODULES = ModuleLayer.boot()
168+
.modules()
169+
.stream()
170+
.filter(m -> SYSTEM_LAYER_MODULES.contains(m) == false)
171+
.collect(Collectors.toUnmodifiableSet());
172+
166173
private final Map<String, Path> sourcePaths;
167174
/**
168175
* The package name containing classes from the APM agent.
@@ -725,7 +732,7 @@ private static boolean isTriviallyAllowed(Class<?> requestingClass) {
725732
generalLogger.debug("Entitlement trivially allowed: no caller frames outside the entitlement library");
726733
return true;
727734
}
728-
if (systemModules.contains(requestingClass.getModule())) {
735+
if (SYSTEM_LAYER_MODULES.contains(requestingClass.getModule())) {
729736
generalLogger.debug("Entitlement trivially allowed from system module [{}]", requestingClass.getModule().getName());
730737
return true;
731738
}
+82-31
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
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;
15+
import org.elasticsearch.entitlement.runtime.policy.entitlements.WriteSystemPropertiesEntitlement;
1316
import org.elasticsearch.logging.LogManager;
1417
import org.elasticsearch.logging.Logger;
1518

@@ -20,21 +23,23 @@
2023
import java.nio.file.Files;
2124
import java.nio.file.Path;
2225
import java.nio.file.StandardOpenOption;
26+
import java.util.ArrayList;
2327
import java.util.Base64;
2428
import java.util.Collection;
2529
import java.util.HashMap;
2630
import java.util.List;
2731
import java.util.Map;
28-
import java.util.Optional;
2932
import java.util.Set;
33+
import java.util.function.Function;
3034
import java.util.stream.Collectors;
35+
import java.util.stream.Stream;
3136

3237
import static java.util.Objects.requireNonNull;
3338
import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED;
3439

35-
public class PolicyParserUtils {
40+
public class PolicyUtils {
3641

37-
private static final Logger logger = LogManager.getLogger(PolicyParserUtils.class);
42+
private static final Logger logger = LogManager.getLogger(PolicyUtils.class);
3843

3944
public record PluginData(Path pluginPath, boolean isModular, boolean isExternalPlugin) {
4045
public PluginData {
@@ -44,8 +49,6 @@ public record PluginData(Path pluginPath, boolean isModular, boolean isExternalP
4449

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

47-
public static final String POLICY_OVERRIDE_PREFIX = "es.entitlements.policy.";
48-
4952
public static Map<String, Policy> createPluginPolicies(Collection<PluginData> pluginData, Map<String, String> overrides, String version)
5053
throws IOException {
5154
Map<String, Policy> pluginPolicies = new HashMap<>(pluginData.size());
@@ -54,9 +57,15 @@ public static Map<String, Policy> createPluginPolicies(Collection<PluginData> pl
5457
String pluginName = pluginRoot.getFileName().toString();
5558
final Set<String> moduleNames = getModuleNames(pluginRoot, entry.isModular());
5659

57-
var overriddenPolicy = parsePolicyOverrideIfExists(overrides, version, entry.isExternalPlugin(), pluginName, moduleNames);
58-
if (overriddenPolicy.isPresent()) {
59-
pluginPolicies.put(pluginName, overriddenPolicy.get());
60+
var overriddenPolicy = parseEncodedPolicyIfExists(
61+
overrides.get(pluginName),
62+
version,
63+
entry.isExternalPlugin(),
64+
pluginName,
65+
moduleNames
66+
);
67+
if (overriddenPolicy != null) {
68+
pluginPolicies.put(pluginName, overriddenPolicy);
6069
} else {
6170
Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME);
6271
var policy = parsePolicyIfExists(pluginName, policyFile, entry.isExternalPlugin());
@@ -67,59 +76,54 @@ public static Map<String, Policy> createPluginPolicies(Collection<PluginData> pl
6776
return pluginPolicies;
6877
}
6978

70-
static Optional<Policy> parsePolicyOverrideIfExists(
71-
Map<String, String> overrides,
79+
public static Policy parseEncodedPolicyIfExists(
80+
String encodedPolicy,
7281
String version,
7382
boolean externalPlugin,
74-
String pluginName,
83+
String layerName,
7584
Set<String> moduleNames
7685
) {
77-
var policyOverride = overrides.get(pluginName);
78-
if (policyOverride != null) {
86+
if (encodedPolicy != null) {
7987
try {
80-
var versionedPolicy = decodeOverriddenPluginPolicy(policyOverride, pluginName, externalPlugin);
81-
validatePolicyScopes(pluginName, versionedPolicy.policy(), moduleNames, "<override>");
88+
var versionedPolicy = decodeEncodedPolicy(encodedPolicy, layerName, externalPlugin);
89+
validatePolicyScopes(layerName, versionedPolicy.policy(), moduleNames, "<patch>");
8290

8391
// Empty versions defaults to "any"
8492
if (versionedPolicy.versions().isEmpty() || versionedPolicy.versions().contains(version)) {
85-
logger.info("Using policy override for plugin [{}]", pluginName);
86-
return Optional.of(versionedPolicy.policy());
93+
logger.info("Using policy patch for layer [{}]", layerName);
94+
return versionedPolicy.policy();
8795
} else {
8896
logger.warn(
89-
"Found a policy override with version mismatch. The override will not be applied. "
90-
+ "Plugin [{}]; policy versions [{}]; current version [{}]",
91-
pluginName,
97+
"Found a policy patch with version mismatch. The patch will not be applied. "
98+
+ "Layer [{}]; policy versions [{}]; current version [{}]",
99+
layerName,
92100
String.join(",", versionedPolicy.versions()),
93101
version
94102
);
95103
}
96104
} catch (Exception ex) {
97105
logger.warn(
98-
Strings.format(
99-
"Found a policy override with invalid content. The override will not be applied. Plugin [%s]",
100-
pluginName
101-
),
106+
Strings.format("Found a policy patch with invalid content. The patch will not be applied. Layer [%s]", layerName),
102107
ex
103108
);
104109
}
105110
}
106-
return Optional.empty();
111+
return null;
107112
}
108113

109-
static VersionedPolicy decodeOverriddenPluginPolicy(String base64String, String pluginName, boolean isExternalPlugin)
110-
throws IOException {
114+
static VersionedPolicy decodeEncodedPolicy(String base64String, String layerName, boolean isExternalPlugin) throws IOException {
111115
byte[] policyDefinition = Base64.getDecoder().decode(base64String);
112-
return new PolicyParser(new ByteArrayInputStream(policyDefinition), pluginName, isExternalPlugin).parseVersionedPolicy();
116+
return new PolicyParser(new ByteArrayInputStream(policyDefinition), layerName, isExternalPlugin).parseVersionedPolicy();
113117
}
114118

115-
private static void validatePolicyScopes(String pluginName, Policy policy, Set<String> moduleNames, String policyLocation) {
119+
private static void validatePolicyScopes(String layerName, Policy policy, Set<String> moduleNames, String policyLocation) {
116120
// TODO: should this check actually be part of the parser?
117121
for (Scope scope : policy.scopes()) {
118122
if (moduleNames.contains(scope.moduleName()) == false) {
119123
throw new IllegalStateException(
120124
Strings.format(
121-
"Invalid module name in policy: plugin [%s] does not have module [%s]; available modules [%s]; policy path [%s]",
122-
pluginName,
125+
"Invalid module name in policy: layer [%s] does not have module [%s]; available modules [%s]; policy path [%s]",
126+
layerName,
123127
scope.moduleName(),
124128
String.join(", ", moduleNames),
125129
policyLocation
@@ -147,4 +151,51 @@ private static Set<String> getModuleNames(Path pluginRoot, boolean isModular) {
147151
return Set.of(ALL_UNNAMED);
148152
}
149153

154+
public static List<Scope> mergeScopes(List<Scope> mainScopes, List<Scope> additionalScopes) {
155+
var result = new ArrayList<Scope>();
156+
var additionalScopesMap = additionalScopes.stream().collect(Collectors.toMap(Scope::moduleName, Scope::entitlements));
157+
for (var mainScope : mainScopes) {
158+
List<Entitlement> additionalEntitlements = additionalScopesMap.remove(mainScope.moduleName());
159+
if (additionalEntitlements == null) {
160+
result.add(mainScope);
161+
} else {
162+
result.add(new Scope(mainScope.moduleName(), mergeEntitlements(mainScope.entitlements(), additionalEntitlements)));
163+
}
164+
}
165+
166+
for (var remainingEntry : additionalScopesMap.entrySet()) {
167+
result.add(new Scope(remainingEntry.getKey(), remainingEntry.getValue()));
168+
}
169+
return result;
170+
}
171+
172+
static List<Entitlement> mergeEntitlements(List<Entitlement> a, List<Entitlement> b) {
173+
Map<Class<? extends Entitlement>, Entitlement> entitlementMap = a.stream()
174+
.collect(Collectors.toMap(Entitlement::getClass, Function.identity()));
175+
176+
for (var entitlement : b) {
177+
entitlementMap.merge(entitlement.getClass(), entitlement, PolicyUtils::mergeEntitlement);
178+
}
179+
return entitlementMap.values().stream().toList();
180+
}
181+
182+
static Entitlement mergeEntitlement(Entitlement entitlement1, Entitlement entitlement2) {
183+
if (entitlement1 instanceof FilesEntitlement e) {
184+
return merge(e, (FilesEntitlement) entitlement2);
185+
}
186+
if (entitlement1 instanceof WriteSystemPropertiesEntitlement e) {
187+
return merge(e, (WriteSystemPropertiesEntitlement) entitlement2);
188+
}
189+
return entitlement1;
190+
}
191+
192+
private static FilesEntitlement merge(FilesEntitlement a, FilesEntitlement b) {
193+
return new FilesEntitlement(Stream.concat(a.filesData().stream(), b.filesData().stream()).distinct().toList());
194+
}
195+
196+
private static WriteSystemPropertiesEntitlement merge(WriteSystemPropertiesEntitlement a, WriteSystemPropertiesEntitlement b) {
197+
return new WriteSystemPropertiesEntitlement(
198+
Stream.concat(a.properties().stream(), b.properties().stream()).collect(Collectors.toUnmodifiableSet())
199+
);
200+
}
150201
}

0 commit comments

Comments
 (0)