From 2f91fb4474fdcf005f393ce9fbceabb511a86ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenzo=20Dematt=C3=A9?= Date: Wed, 12 Mar 2025 09:44:30 +0100 Subject: [PATCH] [Entitlements] Add support for IT tests of always allowed actions (take 2) (#124429) Writing tests for #123861, turns out that #124195 is not enough. We really need new IT test cases for "always allowed" actions: in order to be sure they are allowed, we need to setup the plugin with no policy. This PR adds test cases for that, plus the support for writing test functions that accept one Environment parameter: many test paths we test and allow/deny are relative to paths in Environment, so it's useful to have access to it (see readAccessConfigDirectory as an example) --- .../qa/test/EntitlementTestPlugin.java | 13 ++++- .../entitlement/qa/test/FileCheckActions.java | 28 +++++++++++ .../qa/test/RestEntitlementsCheckAction.java | 49 ++++++++++++++----- .../qa/EntitlementsAlwaysAllowedIT.java | 36 ++++++++++++++ ...EntitlementsAlwaysAllowedNonModularIT.java | 36 ++++++++++++++ 5 files changed, 148 insertions(+), 14 deletions(-) create mode 100644 libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAlwaysAllowedIT.java create mode 100644 libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAlwaysAllowedNonModularIT.java diff --git a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/EntitlementTestPlugin.java b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/EntitlementTestPlugin.java index 36283cce3c81d..788c5738b6d63 100644 --- a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/EntitlementTestPlugin.java +++ b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/EntitlementTestPlugin.java @@ -15,17 +15,28 @@ import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsFilter; +import org.elasticsearch.env.Environment; import org.elasticsearch.features.NodeFeature; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestHandler; +import java.util.Collection; import java.util.List; import java.util.function.Predicate; import java.util.function.Supplier; public class EntitlementTestPlugin extends Plugin implements ActionPlugin { + + private Environment environment; + + @Override + public Collection createComponents(PluginServices services) { + environment = services.environment(); + return super.createComponents(services); + } + @Override public List getRestHandlers( final Settings settings, @@ -38,6 +49,6 @@ public List getRestHandlers( final Supplier nodesInCluster, Predicate clusterSupportsFeature ) { - return List.of(new RestEntitlementsCheckAction()); + return List.of(new RestEntitlementsCheckAction(environment)); } } diff --git a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/FileCheckActions.java b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/FileCheckActions.java index 2558b0acdba96..e80b0a8580b5e 100644 --- a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/FileCheckActions.java +++ b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/FileCheckActions.java @@ -12,6 +12,7 @@ import org.elasticsearch.core.CheckedRunnable; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.entitlement.qa.entitled.EntitledActions; +import org.elasticsearch.env.Environment; import java.io.File; import java.io.FileDescriptor; @@ -22,9 +23,11 @@ import java.io.FileWriter; import java.io.IOException; import java.io.RandomAccessFile; +import java.net.URISyntaxException; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.GeneralSecurityException; @@ -43,6 +46,7 @@ import static java.util.zip.ZipFile.OPEN_DELETE; import static java.util.zip.ZipFile.OPEN_READ; import static org.elasticsearch.entitlement.qa.entitled.EntitledActions.createTempFileForWrite; +import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.ALWAYS_ALLOWED; import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.ALWAYS_DENIED; import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.PLUGINS; @@ -563,6 +567,30 @@ static void httpResponseBodySubscribersOfFile_FileOpenOptions_readOnly() { HttpResponse.BodySubscribers.ofFile(readFile(), CREATE, WRITE); } + @EntitlementTest(expectedAccess = ALWAYS_ALLOWED) + static void readAccessConfigDirectory(Environment environment) { + Files.exists(environment.configDir()); + } + + @EntitlementTest(expectedAccess = ALWAYS_DENIED) + static void writeAccessConfigDirectory(Environment environment) throws IOException { + var file = environment.configDir().resolve("to_create"); + Files.createFile(file); + } + + @EntitlementTest(expectedAccess = ALWAYS_ALLOWED) + static void readAccessSourcePath() throws URISyntaxException { + var sourcePath = Paths.get(EntitlementTestPlugin.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + Files.exists(sourcePath); + } + + @EntitlementTest(expectedAccess = ALWAYS_DENIED) + static void writeAccessSourcePath() throws IOException, URISyntaxException { + var sourcePath = Paths.get(EntitlementTestPlugin.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + var file = sourcePath.getParent().resolve("to_create"); + Files.createFile(file); + } + @EntitlementTest(expectedAccess = ALWAYS_DENIED) static void javaDesktopFileAccess() throws Exception { // Test file access from a java.desktop class. We explicitly exclude that module from the "system modules", so we expect diff --git a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/RestEntitlementsCheckAction.java b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/RestEntitlementsCheckAction.java index 6905037b2f235..e2422fd32706c 100644 --- a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/RestEntitlementsCheckAction.java +++ b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/RestEntitlementsCheckAction.java @@ -11,9 +11,11 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.Strings; +import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.core.CheckedRunnable; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.entitlement.runtime.api.NotEntitledException; +import org.elasticsearch.env.Environment; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; import org.elasticsearch.rest.BaseRestHandler; @@ -70,7 +72,7 @@ public class RestEntitlementsCheckAction extends BaseRestHandler { private static final Logger logger = LogManager.getLogger(RestEntitlementsCheckAction.class); record CheckAction( - CheckedRunnable action, + CheckedConsumer action, EntitlementTest.ExpectedAccess expectedAccess, Class expectedExceptionIfDenied, Integer fromJavaVersion @@ -79,15 +81,15 @@ record CheckAction( * These cannot be granted to plugins, so our test plugins cannot test the "allowed" case. */ static CheckAction deniedToPlugins(CheckedRunnable action) { - return new CheckAction(action, SERVER_ONLY, NotEntitledException.class, null); + return new CheckAction(env -> action.run(), SERVER_ONLY, NotEntitledException.class, null); } static CheckAction forPlugins(CheckedRunnable action) { - return new CheckAction(action, PLUGINS, NotEntitledException.class, null); + return new CheckAction(env -> action.run(), PLUGINS, NotEntitledException.class, null); } static CheckAction alwaysDenied(CheckedRunnable action) { - return new CheckAction(action, ALWAYS_DENIED, NotEntitledException.class, null); + return new CheckAction(env -> action.run(), ALWAYS_DENIED, NotEntitledException.class, null); } } @@ -135,7 +137,7 @@ static CheckAction alwaysDenied(CheckedRunnable action) { entry( "createInetAddressResolverProvider", new CheckAction( - VersionSpecificNetworkChecks::createInetAddressResolverProvider, + env -> VersionSpecificNetworkChecks.createInetAddressResolverProvider(), SERVER_ONLY, NotEntitledException.class, 18 @@ -215,6 +217,12 @@ static CheckAction alwaysDenied(CheckedRunnable action) { .filter(entry -> entry.getValue().fromJavaVersion() == null || Runtime.version().feature() >= entry.getValue().fromJavaVersion()) .collect(Collectors.toUnmodifiableMap(Entry::getKey, Entry::getValue)); + private final Environment environment; + + public RestEntitlementsCheckAction(Environment environment) { + this.environment = environment; + } + @SuppressForbidden(reason = "Need package private methods so we don't have to make them all public") private static Method[] getDeclaredMethods(Class clazz) { return clazz.getDeclaredMethods(); @@ -230,13 +238,10 @@ private static Stream> getTestEntries(Class action if (Modifier.isStatic(method.getModifiers()) == false) { throw new AssertionError("Entitlement test method [" + method + "] must be static"); } - if (method.getParameterTypes().length != 0) { - throw new AssertionError("Entitlement test method [" + method + "] must not have parameters"); - } - - CheckedRunnable runnable = () -> { + final CheckedConsumer call = createConsumerForMethod(method); + CheckedConsumer runnable = env -> { try { - method.invoke(null); + call.accept(env); } catch (IllegalAccessException e) { throw new AssertionError(e); } catch (InvocationTargetException e) { @@ -258,6 +263,17 @@ private static Stream> getTestEntries(Class action return entries.stream(); } + private static CheckedConsumer createConsumerForMethod(Method method) { + Class[] parameters = method.getParameterTypes(); + if (parameters.length == 0) { + return env -> method.invoke(null); + } + if (parameters.length == 1 && parameters[0].equals(Environment.class)) { + return env -> method.invoke(null, env); + } + throw new AssertionError("Entitlement test method [" + method + "] must have no parameters or 1 parameter (Environment)"); + } + private static void createURLStreamHandlerProvider() { var x = new URLStreamHandlerProvider() { @Override @@ -421,6 +437,14 @@ public static Set getCheckActionsAllowedInPlugins() { .collect(Collectors.toSet()); } + public static Set getAlwaysAllowedCheckActions() { + return checkActions.entrySet() + .stream() + .filter(kv -> kv.getValue().expectedAccess().equals(ALWAYS_ALLOWED)) + .map(Entry::getKey) + .collect(Collectors.toSet()); + } + public static Set getDeniableCheckActions() { return checkActions.entrySet() .stream() @@ -455,7 +479,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli logger.info("Calling check action [{}]", actionName); RestResponse response; try { - checkAction.action().run(); + checkAction.action().accept(environment); response = new RestResponse(RestStatus.OK, Strings.format("Succesfully executed action [%s]", actionName)); } catch (Exception e) { var statusCode = checkAction.expectedExceptionIfDenied.isInstance(e) @@ -468,5 +492,4 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli channel.sendResponse(response); }; } - } diff --git a/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAlwaysAllowedIT.java b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAlwaysAllowedIT.java new file mode 100644 index 0000000000000..36e5b6dd4b8ac --- /dev/null +++ b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAlwaysAllowedIT.java @@ -0,0 +1,36 @@ +/* + * 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.entitlement.qa.test.RestEntitlementsCheckAction; +import org.junit.ClassRule; + +public class EntitlementsAlwaysAllowedIT extends AbstractEntitlementsIT { + + @ClassRule + public static EntitlementsTestRule testRule = new EntitlementsTestRule(true, null); + + public EntitlementsAlwaysAllowedIT(@Name("actionName") String actionName) { + super(actionName, true); + } + + @ParametersFactory + public static Iterable data() { + return RestEntitlementsCheckAction.getAlwaysAllowedCheckActions().stream().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/EntitlementsAlwaysAllowedNonModularIT.java b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAlwaysAllowedNonModularIT.java new file mode 100644 index 0000000000000..42c2732da34a7 --- /dev/null +++ b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAlwaysAllowedNonModularIT.java @@ -0,0 +1,36 @@ +/* + * 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.entitlement.qa.test.RestEntitlementsCheckAction; +import org.junit.ClassRule; + +public class EntitlementsAlwaysAllowedNonModularIT extends AbstractEntitlementsIT { + + @ClassRule + public static EntitlementsTestRule testRule = new EntitlementsTestRule(false, null); + + public EntitlementsAlwaysAllowedNonModularIT(@Name("actionName") String actionName) { + super(actionName, true); + } + + @ParametersFactory + public static Iterable data() { + return RestEntitlementsCheckAction.getAlwaysAllowedCheckActions().stream().map(action -> new Object[] { action }).toList(); + } + + @Override + protected String getTestRestCluster() { + return testRule.cluster.getHttpAddresses(); + } +}