From 37a363050e7f82cb1b27bedde7ede39c80c092f6 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 36283cce3c81..788c5738b6d6 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.ClusterSettings; 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 class EntitlementTestPlugin extends Plugin implements ActionPlugin { 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 2558b0acdba9..e80b0a8580b5 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 @@ package org.elasticsearch.entitlement.qa.test; 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.FileReader; 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.nio.file.StandardOpenOption.WRITE; 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 @@ class FileCheckActions { 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 6905037b2f23..e2422fd32706 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 @@ package org.elasticsearch.entitlement.qa.test; 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 @@ public class RestEntitlementsCheckAction extends BaseRestHandler { * 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 @@ public class RestEntitlementsCheckAction extends BaseRestHandler { entry( "createInetAddressResolverProvider", new CheckAction( - VersionSpecificNetworkChecks::createInetAddressResolverProvider, + env -> VersionSpecificNetworkChecks.createInetAddressResolverProvider(), SERVER_ONLY, NotEntitledException.class, 18 @@ -215,6 +217,12 @@ public class RestEntitlementsCheckAction extends BaseRestHandler { .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 @@ public class RestEntitlementsCheckAction extends BaseRestHandler { 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 @@ public class RestEntitlementsCheckAction extends BaseRestHandler { 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 class RestEntitlementsCheckAction extends BaseRestHandler { .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 @@ public class RestEntitlementsCheckAction extends BaseRestHandler { 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 @@ public class RestEntitlementsCheckAction extends BaseRestHandler { 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 000000000000..36e5b6dd4b8a --- /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 000000000000..42c2732da34a --- /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(); + } +}