diff --git a/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAllowedViaOverrideIT.java b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAllowedViaOverrideIT.java new file mode 100644 index 0000000000000..4d9de19bb707e --- /dev/null +++ b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAllowedViaOverrideIT.java @@ -0,0 +1,62 @@ +/* + * 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.qa; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +import org.elasticsearch.core.Strings; +import org.junit.ClassRule; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Base64; +import java.util.Map; +import java.util.stream.Stream; + +import static org.elasticsearch.entitlement.qa.EntitlementsTestRule.ENTITLEMENT_QA_TEST_MODULE_NAME; +import static org.elasticsearch.entitlement.qa.EntitlementsTestRule.ENTITLEMENT_TEST_PLUGIN_NAME; + +public class EntitlementsAllowedViaOverrideIT extends AbstractEntitlementsIT { + + private static Map createPolicyOverrideSystemProperty(Path tempDir) { + String policyOverride = Strings.format(""" + policy: + %s: + - load_native_libraries + - files: + - path: %s + mode: read + """, ENTITLEMENT_QA_TEST_MODULE_NAME, tempDir.resolve("read_dir")); + var encodedPolicyOverride = new String(Base64.getEncoder().encode(policyOverride.getBytes(StandardCharsets.UTF_8))); + return Map.of("es.entitlements.policy." + ENTITLEMENT_TEST_PLUGIN_NAME, encodedPolicyOverride); + } + + @ClassRule + public static EntitlementsTestRule testRule = new EntitlementsTestRule( + true, + null, + EntitlementsAllowedViaOverrideIT::createPolicyOverrideSystemProperty + ); + + public EntitlementsAllowedViaOverrideIT(@Name("actionName") String actionName) { + super(actionName, true); + } + + @ParametersFactory + public static Iterable data() { + return Stream.of("runtime_load_library", "fileList").map(action -> new Object[] { action }).toList(); + } + + @Override + protected String getTestRestCluster() { + return testRule.cluster.getHttpAddresses(); + } +} diff --git a/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsTestRule.java b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsTestRule.java index 177570ba5c0f3..15d470646ea08 100644 --- a/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsTestRule.java +++ b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsTestRule.java @@ -48,17 +48,27 @@ class EntitlementsTestRule implements TestRule { ) ); }; + public static final String ENTITLEMENT_QA_TEST_MODULE_NAME = "org.elasticsearch.entitlement.qa.test"; + public static final String ENTITLEMENT_TEST_PLUGIN_NAME = "entitlement-test-plugin"; interface PolicyBuilder { void build(XContentBuilder builder, Path tempDir) throws IOException; } + interface TempDirSystemPropertyProvider { + Map get(Path tempDir); + } + final TemporaryFolder testDir; final ElasticsearchCluster cluster; final TestRule ruleChain; - @SuppressWarnings("this-escape") EntitlementsTestRule(boolean modular, PolicyBuilder policyBuilder) { + this(modular, policyBuilder, tempDir -> Map.of()); + } + + @SuppressWarnings("this-escape") + EntitlementsTestRule(boolean modular, PolicyBuilder policyBuilder, TempDirSystemPropertyProvider tempDirSystemPropertyProvider) { testDir = new TemporaryFolder(); var tempDirSetup = new ExternalResource() { @Override @@ -72,9 +82,10 @@ protected void before() throws Throwable { }; cluster = ElasticsearchCluster.local() .module("entitled", spec -> buildEntitlements(spec, "org.elasticsearch.entitlement.qa.entitled", ENTITLED_POLICY)) - .module("entitlement-test-plugin", spec -> setupEntitlements(spec, modular, policyBuilder)) + .module(ENTITLEMENT_TEST_PLUGIN_NAME, spec -> setupEntitlements(spec, modular, policyBuilder)) .systemProperty("es.entitlements.enabled", "true") .systemProperty("es.entitlements.testdir", () -> testDir.getRoot().getAbsolutePath()) + .systemProperties(spec -> tempDirSystemPropertyProvider.get(testDir.getRoot().toPath())) .setting("xpack.security.enabled", "false") // Logs in libs/entitlement/qa/build/test-results/javaRestTest/TEST-org.elasticsearch.entitlement.qa.EntitlementsXXX.xml // .setting("logger.org.elasticsearch.entitlement", "DEBUG") @@ -108,14 +119,14 @@ private void buildEntitlements(PluginInstallSpec spec, String moduleName, Policy } private void setupEntitlements(PluginInstallSpec spec, boolean modular, PolicyBuilder policyBuilder) { - String moduleName = modular ? "org.elasticsearch.entitlement.qa.test" : "ALL-UNNAMED"; + String moduleName = modular ? ENTITLEMENT_QA_TEST_MODULE_NAME : "ALL-UNNAMED"; if (policyBuilder != null) { buildEntitlements(spec, moduleName, policyBuilder); } if (modular == false) { spec.withPropertiesOverride(old -> { - String props = old.replace("modulename=org.elasticsearch.entitlement.qa.test", ""); + String props = old.replace("modulename=" + ENTITLEMENT_QA_TEST_MODULE_NAME, ""); System.out.println("Using plugin properties:\n" + props); return Resource.fromString(props); }); diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java index 9698b9e86704a..ce6ce5f17ce01 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java @@ -33,10 +33,12 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -97,6 +99,58 @@ public PolicyParser(InputStream inputStream, String policyName, boolean isExtern this.externalEntitlements = externalEntitlements; } + public VersionedPolicy parseVersionedPolicy() { + Set versions = Set.of(); + Policy policy = emptyPolicy(); + try { + if (policyParser.nextToken() != XContentParser.Token.START_OBJECT) { + throw newPolicyParserException("expected object "); + } + + while (policyParser.nextToken() != XContentParser.Token.END_OBJECT) { + if (policyParser.currentToken() == XContentParser.Token.FIELD_NAME) { + if (policyParser.currentName().equals("versions")) { + versions = parseVersions(); + } else if (policyParser.currentName().equals("policy")) { + policy = parsePolicy(); + } else { + throw newPolicyParserException("expected either or field"); + } + } else { + throw newPolicyParserException("expected either or field"); + } + } + + return new VersionedPolicy(policy, versions); + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + } + + private Policy emptyPolicy() { + return new Policy(policyName, List.of()); + } + + private Set parseVersions() throws IOException { + try { + if (policyParser.nextToken() != XContentParser.Token.START_ARRAY) { + throw newPolicyParserException("expected array of "); + } + Set versions = new HashSet<>(); + while (policyParser.nextToken() != XContentParser.Token.END_ARRAY) { + if (policyParser.currentToken() == XContentParser.Token.VALUE_STRING) { + String version = policyParser.text(); + versions.add(version); + } else { + throw newPolicyParserException("expected "); + } + } + return versions; + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + } + public Policy parsePolicy() { try { if (policyParser.nextToken() != XContentParser.Token.START_OBJECT) { 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/PolicyParserUtils.java index 6e1ea8551825b..b33a00ce7fc5c 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/PolicyParserUtils.java @@ -10,17 +10,22 @@ package org.elasticsearch.entitlement.runtime.policy; import org.elasticsearch.core.Strings; +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.lang.module.ModuleFinder; import java.lang.module.ModuleReference; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +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.stream.Collectors; @@ -29,6 +34,8 @@ public class PolicyParserUtils { + private static final Logger logger = LogManager.getLogger(PolicyParserUtils.class); + public record PluginData(Path pluginPath, boolean isModular, boolean isExternalPlugin) { public PluginData { requireNonNull(pluginPath); @@ -37,41 +44,89 @@ public record PluginData(Path pluginPath, boolean isModular, boolean isExternalP private static final String POLICY_FILE_NAME = "entitlement-policy.yaml"; - public static Map createPluginPolicies(Collection pluginData) throws IOException { + 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()); for (var entry : pluginData) { Path pluginRoot = entry.pluginPath(); String pluginName = pluginRoot.getFileName().toString(); - - final Policy policy = loadPluginPolicy(pluginRoot, entry.isModular(), pluginName, entry.isExternalPlugin()); - - pluginPolicies.put(pluginName, policy); + final Set moduleNames = getModuleNames(pluginRoot, entry.isModular()); + + var overriddenPolicy = parsePolicyOverrideIfExists(overrides, version, entry.isExternalPlugin(), pluginName, moduleNames); + if (overriddenPolicy.isPresent()) { + pluginPolicies.put(pluginName, overriddenPolicy.get()); + } else { + Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME); + var policy = parsePolicyIfExists(pluginName, policyFile, entry.isExternalPlugin()); + validatePolicyScopes(pluginName, policy, moduleNames, policyFile.toString()); + pluginPolicies.put(pluginName, policy); + } } return pluginPolicies; } - private static Policy loadPluginPolicy(Path pluginRoot, boolean isModular, String pluginName, boolean isExternalPlugin) - throws IOException { - Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME); + static Optional parsePolicyOverrideIfExists( + Map overrides, + String version, + boolean externalPlugin, + String pluginName, + Set moduleNames + ) { + var policyOverride = overrides.get(pluginName); + if (policyOverride != null) { + try { + var versionedPolicy = decodeOverriddenPluginPolicy(policyOverride, pluginName, externalPlugin); + validatePolicyScopes(pluginName, 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()); + } else { + logger.warn( + "Found a policy override with version mismatch. The override will not be applied. " + + "Plugin [{}]; policy versions [{}]; current version [{}]", + pluginName, + 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 + ), + ex + ); + } + } + return Optional.empty(); + } - final Set moduleNames = getModuleNames(pluginRoot, isModular); - final Policy policy = parsePolicyIfExists(pluginName, policyFile, isExternalPlugin); + static VersionedPolicy decodeOverriddenPluginPolicy(String base64String, String pluginName, boolean isExternalPlugin) + throws IOException { + byte[] policyDefinition = Base64.getDecoder().decode(base64String); + return new PolicyParser(new ByteArrayInputStream(policyDefinition), pluginName, isExternalPlugin).parseVersionedPolicy(); + } + private static void validatePolicyScopes(String pluginName, 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 file [%s]", + "Invalid module name in policy: plugin [%s] does not have module [%s]; available modules [%s]; policy path [%s]", pluginName, scope.moduleName(), String.join(", ", moduleNames), - policyFile + policyLocation ) ); } } - return policy; } private static Policy parsePolicyIfExists(String pluginName, Path policyFile, boolean isExternalPlugin) throws IOException { diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/VersionedPolicy.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/VersionedPolicy.java new file mode 100644 index 0000000000000..5cb3effd62383 --- /dev/null +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/VersionedPolicy.java @@ -0,0 +1,17 @@ +/* + * 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 java.util.Set; + +/** + * A Policy and associated versions to which the policy applies + */ +public record VersionedPolicy(Policy policy, Set versions) {} diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserTests.java index 0d004b03fb9d8..8a82d85d80eb9 100644 --- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserTests.java +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserTests.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Set; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; @ESTestCase.WithoutSecurityManager @@ -293,6 +294,98 @@ public void testParseLoadNativeLibraries() throws IOException { assertEquals(expected, parsedPolicy); } + public void testVersionedPolicyParsing() throws IOException { + var versionedPolicy = new ByteArrayInputStream(""" + versions: + - x + policy: + entitlement-module-name: + - load_native_libraries + entitlement-module-name-2: + - set_https_connection_properties + """.getBytes(StandardCharsets.UTF_8)); + + var policyParser = new PolicyParser(versionedPolicy, "test-policy.yaml", true); + var parsedPolicy = policyParser.parseVersionedPolicy(); + + Policy expectedPolicy = new Policy( + "test-policy.yaml", + List.of( + new Scope("entitlement-module-name", List.of(new LoadNativeLibrariesEntitlement())), + new Scope("entitlement-module-name-2", List.of(new SetHttpsConnectionPropertiesEntitlement())) + ) + ); + assertEquals(expectedPolicy, parsedPolicy.policy()); + assertThat(parsedPolicy.versions(), contains("x")); + } + + public void testVersionedPolicyParsingMultipleVersions() throws IOException { + var versionedPolicy = new ByteArrayInputStream(""" + versions: + - x + - y + policy: + entitlement-module-name: + - load_native_libraries + entitlement-module-name-2: + - set_https_connection_properties + """.getBytes(StandardCharsets.UTF_8)); + + var policyParser = new PolicyParser(versionedPolicy, "test-policy.yaml", true); + var parsedPolicy = policyParser.parseVersionedPolicy(); + + Policy expectedPolicy = new Policy( + "test-policy.yaml", + List.of( + new Scope("entitlement-module-name", List.of(new LoadNativeLibrariesEntitlement())), + new Scope("entitlement-module-name-2", List.of(new SetHttpsConnectionPropertiesEntitlement())) + ) + ); + assertEquals(expectedPolicy, parsedPolicy.policy()); + assertThat(parsedPolicy.versions(), contains("x", "y")); + } + + public void testVersionedPolicyParsingAnyFieldOrder() throws IOException { + var versionedPolicy = new ByteArrayInputStream(""" + policy: + entitlement-module-name: + - load_native_libraries + entitlement-module-name-2: + - set_https_connection_properties + versions: + - x + - y + """.getBytes(StandardCharsets.UTF_8)); + + var policyParser = new PolicyParser(versionedPolicy, "test-policy.yaml", true); + var parsedPolicy = policyParser.parseVersionedPolicy(); + + Policy expectedPolicy = new Policy( + "test-policy.yaml", + List.of( + new Scope("entitlement-module-name", List.of(new LoadNativeLibrariesEntitlement())), + new Scope("entitlement-module-name-2", List.of(new SetHttpsConnectionPropertiesEntitlement())) + ) + ); + assertEquals(expectedPolicy, parsedPolicy.policy()); + assertThat(parsedPolicy.versions(), contains("x", "y")); + } + + public void testVersionedPolicyParsingEmptyPolicy() throws IOException { + var versionedPolicy = new ByteArrayInputStream(""" + versions: + - x + - y + """.getBytes(StandardCharsets.UTF_8)); + + var policyParser = new PolicyParser(versionedPolicy, "test-policy.yaml", true); + var parsedPolicy = policyParser.parseVersionedPolicy(); + + Policy expectedPolicy = new Policy("test-policy.yaml", List.of()); + assertEquals(expectedPolicy, parsedPolicy.policy()); + assertThat(parsedPolicy.versions(), contains("x", "y")); + } + public void testMultipleConstructorsAnnotated() throws IOException { var parser = new PolicyParser( new ByteArrayInputStream(""" 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 new file mode 100644 index 0000000000000..4512b0b69857c --- /dev/null +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserUtilsTests.java @@ -0,0 +1,169 @@ +/* + * 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/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java b/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java index 271f3c52678e6..92b08a8468ea2 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java @@ -73,6 +73,7 @@ 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; /** @@ -236,7 +237,9 @@ private static void initPhase2(Bootstrap bootstrap) throws IOException { pluginsBundles.stream() .map(bundle -> new PolicyParserUtils.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), true)) ).toList(); - var pluginPolicies = PolicyParserUtils.createPluginPolicies(pluginData); + + var policyOverrides = collectPluginPolicyOverrides(modulesBundles, pluginsBundles, logger); + var pluginPolicies = PolicyParserUtils.createPluginPolicies(pluginData, policyOverrides, Build.current().version()); pluginsLoader = PluginsLoader.createPluginsLoader(modulesBundles, pluginsBundles, findPluginsWithNativeAccess(pluginPolicies)); @@ -276,6 +279,35 @@ private static void initPhase2(Bootstrap bootstrap) throws IOException { bootstrap.setPluginsLoader(pluginsLoader); } + private static Map collectPluginPolicyOverrides( + Set modulesBundles, + Set pluginsBundles, + Logger logger + ) { + var policyOverrides = new HashMap(); + 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); + } + }); + var pluginNames = Stream.concat(modulesBundles.stream(), pluginsBundles.stream()) + .map(bundle -> bundle.pluginDescriptor().getName()) + .collect(Collectors.toUnmodifiableSet()); + + for (var overriddenPluginName : policyOverrides.keySet()) { + if (pluginNames.contains(overriddenPluginName) == false) { + logger.warn( + "Found command-line override for unknown plugin [{}] (available plugins: [{}])", + overriddenPluginName, + String.join(", ", pluginNames) + ); + } + } + return policyOverrides; + } + private static class EntitlementSelfTester { // check entitlements were loaded correctly. note this must be outside the entitlements lib. private static void entitlementSelfTest() {