diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java index bae1a0c463cc9..a2c6307185b29 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java @@ -14,6 +14,7 @@ import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.VirtualMachine; +import org.elasticsearch.core.Nullable; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.entitlement.initialization.EntitlementInitialization; import org.elasticsearch.entitlement.runtime.policy.Policy; @@ -33,6 +34,7 @@ public class EntitlementBootstrap { public record BootstrapArgs( + @Nullable Policy serverPolicyPatch, Map pluginPolicies, Function, String> pluginResolver, Function> settingResolver, @@ -78,6 +80,7 @@ public static BootstrapArgs bootstrapArgs() { * Activates entitlement checking. Once this method returns, calls to methods protected by Entitlements from classes without a valid * policy will throw {@link org.elasticsearch.entitlement.runtime.api.NotEntitledException}. * + * @param serverPolicyPatch a policy with additional entitlements to patch the embedded server layer policy * @param pluginPolicies a map holding policies for plugins (and modules), by plugin (or module) name. * @param pluginResolver a functor to map a Java Class to the plugin it belongs to (the plugin name). * @param settingResolver a functor to resolve a setting name pattern for one or more Elasticsearch settings. @@ -94,6 +97,7 @@ public static BootstrapArgs bootstrapArgs() { * @param suppressFailureLogClasses classes for which we do not need or want to log Entitlements failures */ public static void bootstrap( + Policy serverPolicyPatch, Map pluginPolicies, Function, String> pluginResolver, Function> settingResolver, @@ -114,6 +118,7 @@ public static void bootstrap( throw new IllegalStateException("plugin data is already set"); } EntitlementBootstrap.bootstrapArgs = new BootstrapArgs( + serverPolicyPatch, pluginPolicies, pluginResolver, settingResolver, diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java index d5c0d3e9472d9..88e44c59bc5ef 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java @@ -23,6 +23,7 @@ import org.elasticsearch.entitlement.runtime.policy.PathLookup; import org.elasticsearch.entitlement.runtime.policy.Policy; import org.elasticsearch.entitlement.runtime.policy.PolicyManager; +import org.elasticsearch.entitlement.runtime.policy.PolicyUtils; import org.elasticsearch.entitlement.runtime.policy.Scope; import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement; import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement; @@ -269,8 +270,13 @@ private static PolicyManager createPolicyManager() { ); } - // TODO(ES-10031): Decide what goes in the elasticsearch default policy and extend it - var serverPolicy = new Policy("server", serverScopes); + var serverPolicy = new Policy( + "server", + bootstrapArgs.serverPolicyPatch() == null + ? serverScopes + : PolicyUtils.mergeScopes(serverScopes, bootstrapArgs.serverPolicyPatch().scopes()) + ); + // agents run without a module, so this is a special hack for the apm agent // this should be removed once https://github.com/elastic/elasticsearch/issues/109335 is completed // See also modules/apm/src/main/plugin-metadata/entitlement-policy.yaml diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java index 6502b9a935034..2aed7e001d762 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java @@ -141,9 +141,9 @@ ModuleEntitlements policyEntitlements(String componentName, Path componentPath, public static final String ALL_UNNAMED = "ALL-UNNAMED"; - private static final Set systemModules = findSystemModules(); + private static final Set SYSTEM_LAYER_MODULES = findSystemLayerModules(); - private static Set findSystemModules() { + private static Set findSystemLayerModules() { var systemModulesDescriptors = ModuleFinder.ofSystem() .findAll() .stream() @@ -163,6 +163,13 @@ private static Set findSystemModules() { ).collect(Collectors.toUnmodifiableSet()); } + // Anything in the boot layer that is not in the system layer, is in the server layer + public static final Set SERVER_LAYER_MODULES = ModuleLayer.boot() + .modules() + .stream() + .filter(m -> SYSTEM_LAYER_MODULES.contains(m) == false) + .collect(Collectors.toUnmodifiableSet()); + private final Map sourcePaths; /** * The package name containing classes from the APM agent. @@ -725,7 +732,7 @@ private static boolean isTriviallyAllowed(Class requestingClass) { generalLogger.debug("Entitlement trivially allowed: no caller frames outside the entitlement library"); return true; } - if (systemModules.contains(requestingClass.getModule())) { + if (SYSTEM_LAYER_MODULES.contains(requestingClass.getModule())) { generalLogger.debug("Entitlement trivially allowed from system module [{}]", requestingClass.getModule().getName()); return true; } diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserUtils.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtils.java similarity index 51% rename from libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserUtils.java rename to libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtils.java index b33a00ce7fc5c..a9b93409ea5fb 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserUtils.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtils.java @@ -10,6 +10,9 @@ package org.elasticsearch.entitlement.runtime.policy; import org.elasticsearch.core.Strings; +import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement; +import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement; +import org.elasticsearch.entitlement.runtime.policy.entitlements.WriteSystemPropertiesEntitlement; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; @@ -20,21 +23,23 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; import java.util.Base64; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import static java.util.Objects.requireNonNull; import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED; -public class PolicyParserUtils { +public class PolicyUtils { - private static final Logger logger = LogManager.getLogger(PolicyParserUtils.class); + private static final Logger logger = LogManager.getLogger(PolicyUtils.class); public record PluginData(Path pluginPath, boolean isModular, boolean isExternalPlugin) { public PluginData { @@ -44,8 +49,6 @@ public record PluginData(Path pluginPath, boolean isModular, boolean isExternalP private static final String POLICY_FILE_NAME = "entitlement-policy.yaml"; - public static final String POLICY_OVERRIDE_PREFIX = "es.entitlements.policy."; - public static Map createPluginPolicies(Collection pluginData, Map overrides, String version) throws IOException { Map pluginPolicies = new HashMap<>(pluginData.size()); @@ -54,9 +57,15 @@ public static Map createPluginPolicies(Collection pl String pluginName = pluginRoot.getFileName().toString(); final Set moduleNames = getModuleNames(pluginRoot, entry.isModular()); - var overriddenPolicy = parsePolicyOverrideIfExists(overrides, version, entry.isExternalPlugin(), pluginName, moduleNames); - if (overriddenPolicy.isPresent()) { - pluginPolicies.put(pluginName, overriddenPolicy.get()); + var overriddenPolicy = parseEncodedPolicyIfExists( + overrides.get(pluginName), + version, + entry.isExternalPlugin(), + pluginName, + moduleNames + ); + if (overriddenPolicy != null) { + pluginPolicies.put(pluginName, overriddenPolicy); } else { Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME); var policy = parsePolicyIfExists(pluginName, policyFile, entry.isExternalPlugin()); @@ -67,59 +76,54 @@ public static Map createPluginPolicies(Collection pl return pluginPolicies; } - static Optional parsePolicyOverrideIfExists( - Map overrides, + public static Policy parseEncodedPolicyIfExists( + String encodedPolicy, String version, boolean externalPlugin, - String pluginName, + String layerName, Set moduleNames ) { - var policyOverride = overrides.get(pluginName); - if (policyOverride != null) { + if (encodedPolicy != null) { try { - var versionedPolicy = decodeOverriddenPluginPolicy(policyOverride, pluginName, externalPlugin); - validatePolicyScopes(pluginName, versionedPolicy.policy(), moduleNames, ""); + var versionedPolicy = decodeEncodedPolicy(encodedPolicy, layerName, externalPlugin); + validatePolicyScopes(layerName, versionedPolicy.policy(), moduleNames, ""); // Empty versions defaults to "any" if (versionedPolicy.versions().isEmpty() || versionedPolicy.versions().contains(version)) { - logger.info("Using policy override for plugin [{}]", pluginName); - return Optional.of(versionedPolicy.policy()); + logger.info("Using policy patch for layer [{}]", layerName); + return versionedPolicy.policy(); } else { logger.warn( - "Found a policy override with version mismatch. The override will not be applied. " - + "Plugin [{}]; policy versions [{}]; current version [{}]", - pluginName, + "Found a policy patch with version mismatch. The patch will not be applied. " + + "Layer [{}]; policy versions [{}]; current version [{}]", + layerName, String.join(",", versionedPolicy.versions()), version ); } } catch (Exception ex) { logger.warn( - Strings.format( - "Found a policy override with invalid content. The override will not be applied. Plugin [%s]", - pluginName - ), + Strings.format("Found a policy patch with invalid content. The patch will not be applied. Layer [%s]", layerName), ex ); } } - return Optional.empty(); + return null; } - static VersionedPolicy decodeOverriddenPluginPolicy(String base64String, String pluginName, boolean isExternalPlugin) - throws IOException { + static VersionedPolicy decodeEncodedPolicy(String base64String, String layerName, boolean isExternalPlugin) throws IOException { byte[] policyDefinition = Base64.getDecoder().decode(base64String); - return new PolicyParser(new ByteArrayInputStream(policyDefinition), pluginName, isExternalPlugin).parseVersionedPolicy(); + return new PolicyParser(new ByteArrayInputStream(policyDefinition), layerName, isExternalPlugin).parseVersionedPolicy(); } - private static void validatePolicyScopes(String pluginName, Policy policy, Set moduleNames, String policyLocation) { + private static void validatePolicyScopes(String layerName, Policy policy, Set moduleNames, String policyLocation) { // TODO: should this check actually be part of the parser? for (Scope scope : policy.scopes()) { if (moduleNames.contains(scope.moduleName()) == false) { throw new IllegalStateException( Strings.format( - "Invalid module name in policy: plugin [%s] does not have module [%s]; available modules [%s]; policy path [%s]", - pluginName, + "Invalid module name in policy: layer [%s] does not have module [%s]; available modules [%s]; policy path [%s]", + layerName, scope.moduleName(), String.join(", ", moduleNames), policyLocation @@ -147,4 +151,51 @@ private static Set getModuleNames(Path pluginRoot, boolean isModular) { return Set.of(ALL_UNNAMED); } + public static List mergeScopes(List mainScopes, List additionalScopes) { + var result = new ArrayList(); + var additionalScopesMap = additionalScopes.stream().collect(Collectors.toMap(Scope::moduleName, Scope::entitlements)); + for (var mainScope : mainScopes) { + List additionalEntitlements = additionalScopesMap.remove(mainScope.moduleName()); + if (additionalEntitlements == null) { + result.add(mainScope); + } else { + result.add(new Scope(mainScope.moduleName(), mergeEntitlements(mainScope.entitlements(), additionalEntitlements))); + } + } + + for (var remainingEntry : additionalScopesMap.entrySet()) { + result.add(new Scope(remainingEntry.getKey(), remainingEntry.getValue())); + } + return result; + } + + static List mergeEntitlements(List a, List b) { + Map, Entitlement> entitlementMap = a.stream() + .collect(Collectors.toMap(Entitlement::getClass, Function.identity())); + + for (var entitlement : b) { + entitlementMap.merge(entitlement.getClass(), entitlement, PolicyUtils::mergeEntitlement); + } + return entitlementMap.values().stream().toList(); + } + + static Entitlement mergeEntitlement(Entitlement entitlement1, Entitlement entitlement2) { + if (entitlement1 instanceof FilesEntitlement e) { + return merge(e, (FilesEntitlement) entitlement2); + } + if (entitlement1 instanceof WriteSystemPropertiesEntitlement e) { + return merge(e, (WriteSystemPropertiesEntitlement) entitlement2); + } + return entitlement1; + } + + private static FilesEntitlement merge(FilesEntitlement a, FilesEntitlement b) { + return new FilesEntitlement(Stream.concat(a.filesData().stream(), b.filesData().stream()).distinct().toList()); + } + + private static WriteSystemPropertiesEntitlement merge(WriteSystemPropertiesEntitlement a, WriteSystemPropertiesEntitlement b) { + return new WriteSystemPropertiesEntitlement( + Stream.concat(a.properties().stream(), b.properties().stream()).collect(Collectors.toUnmodifiableSet()) + ); + } } diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserUtilsTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserUtilsTests.java deleted file mode 100644 index 4512b0b69857c..0000000000000 --- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserUtilsTests.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.entitlement.runtime.policy; - -import org.elasticsearch.entitlement.runtime.policy.entitlements.LoadNativeLibrariesEntitlement; -import org.elasticsearch.entitlement.runtime.policy.entitlements.SetHttpsConnectionPropertiesEntitlement; -import org.elasticsearch.test.ESTestCase; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static org.elasticsearch.test.hamcrest.OptionalMatchers.isEmpty; -import static org.elasticsearch.test.hamcrest.OptionalMatchers.isPresentWith; - -@ESTestCase.WithoutSecurityManager -public class PolicyParserUtilsTests extends ESTestCase { - - public void testCreatePluginPolicyWithOverride() { - - var policyForOverride = """ - versions: - - 9.0.0 - - 9.0.0-SNAPSHOT - policy: - entitlement-module-name: - - load_native_libraries - entitlement-module-name-2: - - set_https_connection_properties - """; - var base64EncodedPolicy = new String( - Base64.getEncoder().encode(policyForOverride.getBytes(StandardCharsets.UTF_8)), - StandardCharsets.UTF_8 - ); - var overrides = Map.of("test-plugin", base64EncodedPolicy); - - final Policy expectedPolicy = new Policy( - "test-plugin", - List.of( - new Scope("entitlement-module-name", List.of(new LoadNativeLibrariesEntitlement())), - new Scope("entitlement-module-name-2", List.of(new SetHttpsConnectionPropertiesEntitlement())) - ) - ); - - var policy = PolicyParserUtils.parsePolicyOverrideIfExists( - overrides, - "9.0.0", - true, - "test-plugin", - Set.of("entitlement-module-name", "entitlement-module-name-2") - ); - - assertThat(policy, isPresentWith(expectedPolicy)); - } - - public void testCreatePluginPolicyWithOverrideAnyVersion() { - - var policyForOverride = """ - policy: - entitlement-module-name: - - load_native_libraries - entitlement-module-name-2: - - set_https_connection_properties - """; - var base64EncodedPolicy = new String( - Base64.getEncoder().encode(policyForOverride.getBytes(StandardCharsets.UTF_8)), - StandardCharsets.UTF_8 - ); - var overrides = Map.of("test-plugin", base64EncodedPolicy); - - final Policy expectedPolicy = new Policy( - "test-plugin", - List.of( - new Scope("entitlement-module-name", List.of(new LoadNativeLibrariesEntitlement())), - new Scope("entitlement-module-name-2", List.of(new SetHttpsConnectionPropertiesEntitlement())) - ) - ); - - var policy = PolicyParserUtils.parsePolicyOverrideIfExists( - overrides, - "abcdef", - true, - "test-plugin", - Set.of("entitlement-module-name", "entitlement-module-name-2") - ); - - assertThat(policy, isPresentWith(expectedPolicy)); - } - - public void testNoOverriddenPolicyWithVersionMismatch() { - - var policyForOverride = """ - versions: - - 9.0.0 - - 9.0.0-SNAPSHOT - policy: - entitlement-module-name: - - load_native_libraries - entitlement-module-name-2: - - set_https_connection_properties - """; - var base64EncodedPolicy = new String( - Base64.getEncoder().encode(policyForOverride.getBytes(StandardCharsets.UTF_8)), - StandardCharsets.UTF_8 - ); - var overrides = Map.of("test-plugin", base64EncodedPolicy); - - var policy = PolicyParserUtils.parsePolicyOverrideIfExists( - overrides, - "9.1.0", - true, - "test-plugin", - Set.of("entitlement-module-name", "entitlement-module-name-2") - ); - - assertThat(policy, isEmpty()); - } - - public void testNoOverriddenPolicyWithValidationError() { - - var policyForOverride = """ - versions: - - 9.0.0 - - 9.0.0-SNAPSHOT - policy: - entitlement-module-name: - - load_native_libraries - entitlement-module-name-2: - - set_https_connection_properties - """; - var base64EncodedPolicy = new String( - Base64.getEncoder().encode(policyForOverride.getBytes(StandardCharsets.UTF_8)), - StandardCharsets.UTF_8 - ); - var overrides = Map.of("test-plugin", base64EncodedPolicy); - - var policy = PolicyParserUtils.parsePolicyOverrideIfExists(overrides, "9.0.0", true, "test-plugin", Set.of()); - - assertThat(policy, isEmpty()); - } - - public void testNoOverriddenPolicyWithParsingError() { - - var policyForOverride = """ - entitlement-module-name: - - load_native_libraries - entitlement-module-name-2: - - set_https_connection_properties - """; - var base64EncodedPolicy = new String( - Base64.getEncoder().encode(policyForOverride.getBytes(StandardCharsets.UTF_8)), - StandardCharsets.UTF_8 - ); - var overrides = Map.of("test-plugin", base64EncodedPolicy); - - var policy = PolicyParserUtils.parsePolicyOverrideIfExists(overrides, "9.0.0", true, "test-plugin", Set.of()); - - assertThat(policy, isEmpty()); - } -} diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtilsTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtilsTests.java new file mode 100644 index 0000000000000..5742d45f83aef --- /dev/null +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtilsTests.java @@ -0,0 +1,291 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.entitlement.runtime.policy; + +import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement; +import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement; +import org.elasticsearch.entitlement.runtime.policy.entitlements.InboundNetworkEntitlement; +import org.elasticsearch.entitlement.runtime.policy.entitlements.LoadNativeLibrariesEntitlement; +import org.elasticsearch.entitlement.runtime.policy.entitlements.ManageThreadsEntitlement; +import org.elasticsearch.entitlement.runtime.policy.entitlements.OutboundNetworkEntitlement; +import org.elasticsearch.entitlement.runtime.policy.entitlements.SetHttpsConnectionPropertiesEntitlement; +import org.elasticsearch.entitlement.runtime.policy.entitlements.WriteAllSystemPropertiesEntitlement; +import org.elasticsearch.entitlement.runtime.policy.entitlements.WriteSystemPropertiesEntitlement; +import org.elasticsearch.test.ESTestCase; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Base64; +import java.util.List; +import java.util.Set; + +import static org.elasticsearch.entitlement.runtime.policy.PolicyUtils.mergeEntitlement; +import static org.elasticsearch.entitlement.runtime.policy.PolicyUtils.mergeEntitlements; +import static org.elasticsearch.test.LambdaMatchers.transformedMatch; +import static org.hamcrest.Matchers.both; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + +@ESTestCase.WithoutSecurityManager +public class PolicyUtilsTests extends ESTestCase { + + public void testCreatePluginPolicyWithPatch() { + + var policyPatch = """ + versions: + - 9.0.0 + - 9.0.0-SNAPSHOT + policy: + entitlement-module-name: + - load_native_libraries + entitlement-module-name-2: + - set_https_connection_properties + """; + var base64EncodedPolicy = new String( + Base64.getEncoder().encode(policyPatch.getBytes(StandardCharsets.UTF_8)), + StandardCharsets.UTF_8 + ); + final Policy expectedPolicy = new Policy( + "test-plugin", + List.of( + new Scope("entitlement-module-name", List.of(new LoadNativeLibrariesEntitlement())), + new Scope("entitlement-module-name-2", List.of(new SetHttpsConnectionPropertiesEntitlement())) + ) + ); + + var policy = PolicyUtils.parseEncodedPolicyIfExists( + base64EncodedPolicy, + "9.0.0", + true, + "test-plugin", + Set.of("entitlement-module-name", "entitlement-module-name-2") + ); + + assertThat(policy, equalTo(expectedPolicy)); + } + + public void testCreatePluginPolicyWithPatchAnyVersion() { + + var policyPatch = """ + policy: + entitlement-module-name: + - load_native_libraries + entitlement-module-name-2: + - set_https_connection_properties + """; + var base64EncodedPolicy = new String( + Base64.getEncoder().encode(policyPatch.getBytes(StandardCharsets.UTF_8)), + StandardCharsets.UTF_8 + ); + + final Policy expectedPolicy = new Policy( + "test-plugin", + List.of( + new Scope("entitlement-module-name", List.of(new LoadNativeLibrariesEntitlement())), + new Scope("entitlement-module-name-2", List.of(new SetHttpsConnectionPropertiesEntitlement())) + ) + ); + + var policy = PolicyUtils.parseEncodedPolicyIfExists( + base64EncodedPolicy, + "abcdef", + true, + "test-plugin", + Set.of("entitlement-module-name", "entitlement-module-name-2") + ); + + assertThat(policy, equalTo(expectedPolicy)); + } + + public void testNoPatchWithVersionMismatch() { + + var policyPatch = """ + versions: + - 9.0.0 + - 9.0.0-SNAPSHOT + policy: + entitlement-module-name: + - load_native_libraries + entitlement-module-name-2: + - set_https_connection_properties + """; + var base64EncodedPolicy = new String( + Base64.getEncoder().encode(policyPatch.getBytes(StandardCharsets.UTF_8)), + StandardCharsets.UTF_8 + ); + + var policy = PolicyUtils.parseEncodedPolicyIfExists( + base64EncodedPolicy, + "9.1.0", + true, + "test-plugin", + Set.of("entitlement-module-name", "entitlement-module-name-2") + ); + + assertThat(policy, nullValue()); + } + + public void testNoPatchWithValidationError() { + + var policyPatch = """ + versions: + - 9.0.0 + - 9.0.0-SNAPSHOT + policy: + entitlement-module-name: + - load_native_libraries + entitlement-module-name-2: + - set_https_connection_properties + """; + var base64EncodedPolicy = new String( + Base64.getEncoder().encode(policyPatch.getBytes(StandardCharsets.UTF_8)), + StandardCharsets.UTF_8 + ); + + var policy = PolicyUtils.parseEncodedPolicyIfExists(base64EncodedPolicy, "9.0.0", true, "test-plugin", Set.of()); + + assertThat(policy, nullValue()); + } + + public void testNoPatchWithParsingError() { + + var policyPatch = """ + entitlement-module-name: + - load_native_libraries + entitlement-module-name-2: + - set_https_connection_properties + """; + var base64EncodedPolicy = new String( + Base64.getEncoder().encode(policyPatch.getBytes(StandardCharsets.UTF_8)), + StandardCharsets.UTF_8 + ); + + var policy = PolicyUtils.parseEncodedPolicyIfExists(base64EncodedPolicy, "9.0.0", true, "test-plugin", Set.of()); + + assertThat(policy, nullValue()); + } + + public void testMergeScopes() { + var originalPolicy = List.of( + new Scope("module1", List.of(new LoadNativeLibrariesEntitlement())), + new Scope("module2", List.of(new ManageThreadsEntitlement())), + new Scope("module3", List.of(new InboundNetworkEntitlement())) + ); + + var patchPolicy = List.of( + new Scope("module2", List.of(new ManageThreadsEntitlement())), + new Scope("module3", List.of(new OutboundNetworkEntitlement())), + new Scope("module4", List.of(new WriteAllSystemPropertiesEntitlement())) + ); + + var resultPolicy = PolicyUtils.mergeScopes(originalPolicy, patchPolicy); + assertThat( + resultPolicy, + containsInAnyOrder( + equalTo(new Scope("module1", List.of(new LoadNativeLibrariesEntitlement()))), + equalTo(new Scope("module2", List.of(new ManageThreadsEntitlement()))), + both(transformedMatch(Scope::moduleName, equalTo("module3"))).and( + transformedMatch( + Scope::entitlements, + containsInAnyOrder(new InboundNetworkEntitlement(), new OutboundNetworkEntitlement()) + ) + ), + equalTo(new Scope("module4", List.of(new WriteAllSystemPropertiesEntitlement()))) + ) + ); + } + + public void testMergeSameFlagEntitlement() { + var e1 = new InboundNetworkEntitlement(); + var e2 = new InboundNetworkEntitlement(); + + assertThat(mergeEntitlement(e1, e2), equalTo(new InboundNetworkEntitlement())); + } + + public void testMergeFilesEntitlement() { + var e1 = new FilesEntitlement( + List.of( + FilesEntitlement.FileData.ofPath(Path.of("/a/b"), FilesEntitlement.Mode.READ), + FilesEntitlement.FileData.ofPath(Path.of("/a/c"), FilesEntitlement.Mode.READ_WRITE), + FilesEntitlement.FileData.ofRelativePath(Path.of("c/d"), FilesEntitlement.BaseDir.CONFIG, FilesEntitlement.Mode.READ) + ) + ); + var e2 = new FilesEntitlement( + List.of( + FilesEntitlement.FileData.ofPath(Path.of("/a/b"), FilesEntitlement.Mode.READ), // identical + FilesEntitlement.FileData.ofPath(Path.of("/a/c"), FilesEntitlement.Mode.READ), // different mode + FilesEntitlement.FileData.ofPath(Path.of("/c/d"), FilesEntitlement.Mode.READ) // different type + ) + ); + + var merged = mergeEntitlement(e1, e2); + assertThat( + merged, + transformedMatch( + x -> ((FilesEntitlement) x).filesData(), + containsInAnyOrder( + FilesEntitlement.FileData.ofPath(Path.of("/a/b"), FilesEntitlement.Mode.READ), + FilesEntitlement.FileData.ofPath(Path.of("/a/c"), FilesEntitlement.Mode.READ), + FilesEntitlement.FileData.ofPath(Path.of("/a/c"), FilesEntitlement.Mode.READ_WRITE), + FilesEntitlement.FileData.ofRelativePath(Path.of("c/d"), FilesEntitlement.BaseDir.CONFIG, FilesEntitlement.Mode.READ), + FilesEntitlement.FileData.ofPath(Path.of("/c/d"), FilesEntitlement.Mode.READ) + ) + ) + ); + } + + public void testMergeWritePropertyEntitlement() { + var e1 = new WriteSystemPropertiesEntitlement(List.of("a", "b", "c")); + var e2 = new WriteSystemPropertiesEntitlement(List.of("b", "c", "d")); + + var merged = mergeEntitlement(e1, e2); + assertThat( + merged, + transformedMatch(x -> ((WriteSystemPropertiesEntitlement) x).properties(), containsInAnyOrder("a", "b", "c", "d")) + ); + } + + public void testMergeEntitlements() { + List a = List.of( + new InboundNetworkEntitlement(), + new OutboundNetworkEntitlement(), + new FilesEntitlement( + List.of( + FilesEntitlement.FileData.ofPath(Path.of("/a/b"), FilesEntitlement.Mode.READ), + FilesEntitlement.FileData.ofPath(Path.of("/a/c"), FilesEntitlement.Mode.READ_WRITE) + ) + ) + ); + List b = List.of( + new InboundNetworkEntitlement(), + new LoadNativeLibrariesEntitlement(), + new FilesEntitlement(List.of()), + new WriteSystemPropertiesEntitlement(List.of("a")) + ); + + var merged = mergeEntitlements(a, b); + assertThat( + merged, + containsInAnyOrder( + new InboundNetworkEntitlement(), + new OutboundNetworkEntitlement(), + new LoadNativeLibrariesEntitlement(), + new FilesEntitlement( + List.of( + FilesEntitlement.FileData.ofPath(Path.of("/a/b"), FilesEntitlement.Mode.READ), + FilesEntitlement.FileData.ofPath(Path.of("/a/c"), FilesEntitlement.Mode.READ_WRITE) + ) + ), + new WriteSystemPropertiesEntitlement(List.of("a")) + ) + ); + } +} diff --git a/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java b/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java index 92b08a8468ea2..4dd9b4055f2a4 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java @@ -35,7 +35,8 @@ import org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap; import org.elasticsearch.entitlement.runtime.api.NotEntitledException; import org.elasticsearch.entitlement.runtime.policy.Policy; -import org.elasticsearch.entitlement.runtime.policy.PolicyParserUtils; +import org.elasticsearch.entitlement.runtime.policy.PolicyManager; +import org.elasticsearch.entitlement.runtime.policy.PolicyUtils; import org.elasticsearch.entitlement.runtime.policy.entitlements.LoadNativeLibrariesEntitlement; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexVersion; @@ -73,7 +74,6 @@ import java.util.stream.Stream; import static org.elasticsearch.bootstrap.BootstrapSettings.SECURITY_FILTER_BAD_DEFAULTS_SETTING; -import static org.elasticsearch.entitlement.runtime.policy.PolicyParserUtils.POLICY_OVERRIDE_PREFIX; import static org.elasticsearch.nativeaccess.WindowsFunctions.ConsoleCtrlHandler.CTRL_CLOSE_EVENT; /** @@ -81,6 +81,9 @@ */ class Elasticsearch { + private static final String PLUGIN_POLICY_OVERRIDE_PREFIX = "es.entitlements.policy."; + private static final String SERVER_POLICY_OVERRIDE = "es.entitlements.server_policy"; + /** * Main entry point for starting elasticsearch. */ @@ -233,13 +236,20 @@ private static void initPhase2(Bootstrap bootstrap) throws IOException { var pluginData = Stream.concat( modulesBundles.stream() - .map(bundle -> new PolicyParserUtils.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), false)), + .map(bundle -> new PolicyUtils.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), false)), pluginsBundles.stream() - .map(bundle -> new PolicyParserUtils.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), true)) + .map(bundle -> new PolicyUtils.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), true)) ).toList(); - var policyOverrides = collectPluginPolicyOverrides(modulesBundles, pluginsBundles, logger); - var pluginPolicies = PolicyParserUtils.createPluginPolicies(pluginData, policyOverrides, Build.current().version()); + var pluginPolicyOverrides = collectPluginPolicyOverrides(modulesBundles, pluginsBundles, logger); + var pluginPolicies = PolicyUtils.createPluginPolicies(pluginData, pluginPolicyOverrides, Build.current().version()); + var serverPolicyPatch = PolicyUtils.parseEncodedPolicyIfExists( + System.getProperty(SERVER_POLICY_OVERRIDE), + Build.current().version(), + false, + "server", + PolicyManager.SERVER_LAYER_MODULES.stream().map(Module::getName).collect(Collectors.toUnmodifiableSet()) + ); pluginsLoader = PluginsLoader.createPluginsLoader(modulesBundles, pluginsBundles, findPluginsWithNativeAccess(pluginPolicies)); @@ -247,6 +257,7 @@ private static void initPhase2(Bootstrap bootstrap) throws IOException { Map sourcePaths = Stream.concat(modulesBundles.stream(), pluginsBundles.stream()) .collect(Collectors.toUnmodifiableMap(bundle -> bundle.pluginDescriptor().getName(), PluginBundle::getDir)); EntitlementBootstrap.bootstrap( + serverPolicyPatch, pluginPolicies, pluginsResolver::resolveClassToPluginName, nodeEnv.settings()::getValues, @@ -288,8 +299,8 @@ private static Map collectPluginPolicyOverrides( var systemProperties = BootstrapInfo.getSystemProperties(); systemProperties.keys().asIterator().forEachRemaining(key -> { var value = systemProperties.get(key); - if (key instanceof String k && k.startsWith(POLICY_OVERRIDE_PREFIX) && value instanceof String v) { - policyOverrides.put(k.substring(POLICY_OVERRIDE_PREFIX.length()), v); + if (key instanceof String k && k.startsWith(PLUGIN_POLICY_OVERRIDE_PREFIX) && value instanceof String v) { + policyOverrides.put(k.substring(PLUGIN_POLICY_OVERRIDE_PREFIX.length()), v); } }); var pluginNames = Stream.concat(modulesBundles.stream(), pluginsBundles.stream())