[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)
This commit is contained in:
Lorenzo Dematté 2025-03-12 09:44:30 +01:00 committed by GitHub
parent d844c6a847
commit 37a363050e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 148 additions and 14 deletions

View file

@ -15,17 +15,28 @@ import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.IndexScopedSettings;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.SettingsFilter; import org.elasticsearch.common.settings.SettingsFilter;
import org.elasticsearch.env.Environment;
import org.elasticsearch.features.NodeFeature; import org.elasticsearch.features.NodeFeature;
import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.ActionPlugin;
import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestHandler; import org.elasticsearch.rest.RestHandler;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
public class EntitlementTestPlugin extends Plugin implements ActionPlugin { public class EntitlementTestPlugin extends Plugin implements ActionPlugin {
private Environment environment;
@Override
public Collection<?> createComponents(PluginServices services) {
environment = services.environment();
return super.createComponents(services);
}
@Override @Override
public List<RestHandler> getRestHandlers( public List<RestHandler> getRestHandlers(
final Settings settings, final Settings settings,
@ -38,6 +49,6 @@ public class EntitlementTestPlugin extends Plugin implements ActionPlugin {
final Supplier<DiscoveryNodes> nodesInCluster, final Supplier<DiscoveryNodes> nodesInCluster,
Predicate<NodeFeature> clusterSupportsFeature Predicate<NodeFeature> clusterSupportsFeature
) { ) {
return List.of(new RestEntitlementsCheckAction()); return List.of(new RestEntitlementsCheckAction(environment));
} }
} }

View file

@ -12,6 +12,7 @@ package org.elasticsearch.entitlement.qa.test;
import org.elasticsearch.core.CheckedRunnable; import org.elasticsearch.core.CheckedRunnable;
import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.qa.entitled.EntitledActions; import org.elasticsearch.entitlement.qa.entitled.EntitledActions;
import org.elasticsearch.env.Environment;
import java.io.File; import java.io.File;
import java.io.FileDescriptor; import java.io.FileDescriptor;
@ -22,9 +23,11 @@ import java.io.FileReader;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.net.URISyntaxException;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.security.GeneralSecurityException; 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_DELETE;
import static java.util.zip.ZipFile.OPEN_READ; import static java.util.zip.ZipFile.OPEN_READ;
import static org.elasticsearch.entitlement.qa.entitled.EntitledActions.createTempFileForWrite; 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.ALWAYS_DENIED;
import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.PLUGINS; import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.PLUGINS;
@ -563,6 +567,30 @@ class FileCheckActions {
HttpResponse.BodySubscribers.ofFile(readFile(), CREATE, WRITE); 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) @EntitlementTest(expectedAccess = ALWAYS_DENIED)
static void javaDesktopFileAccess() throws Exception { 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 // Test file access from a java.desktop class. We explicitly exclude that module from the "system modules", so we expect

View file

@ -11,9 +11,11 @@ package org.elasticsearch.entitlement.qa.test;
import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.client.internal.node.NodeClient;
import org.elasticsearch.common.Strings; import org.elasticsearch.common.Strings;
import org.elasticsearch.core.CheckedConsumer;
import org.elasticsearch.core.CheckedRunnable; import org.elasticsearch.core.CheckedRunnable;
import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.runtime.api.NotEntitledException; import org.elasticsearch.entitlement.runtime.api.NotEntitledException;
import org.elasticsearch.env.Environment;
import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger; import org.elasticsearch.logging.Logger;
import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.BaseRestHandler;
@ -70,7 +72,7 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
private static final Logger logger = LogManager.getLogger(RestEntitlementsCheckAction.class); private static final Logger logger = LogManager.getLogger(RestEntitlementsCheckAction.class);
record CheckAction( record CheckAction(
CheckedRunnable<Exception> action, CheckedConsumer<Environment, Exception> action,
EntitlementTest.ExpectedAccess expectedAccess, EntitlementTest.ExpectedAccess expectedAccess,
Class<? extends Exception> expectedExceptionIfDenied, Class<? extends Exception> expectedExceptionIfDenied,
Integer fromJavaVersion 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. * These cannot be granted to plugins, so our test plugins cannot test the "allowed" case.
*/ */
static CheckAction deniedToPlugins(CheckedRunnable<Exception> action) { static CheckAction deniedToPlugins(CheckedRunnable<Exception> 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<Exception> action) { static CheckAction forPlugins(CheckedRunnable<Exception> action) {
return new CheckAction(action, PLUGINS, NotEntitledException.class, null); return new CheckAction(env -> action.run(), PLUGINS, NotEntitledException.class, null);
} }
static CheckAction alwaysDenied(CheckedRunnable<Exception> action) { static CheckAction alwaysDenied(CheckedRunnable<Exception> 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( entry(
"createInetAddressResolverProvider", "createInetAddressResolverProvider",
new CheckAction( new CheckAction(
VersionSpecificNetworkChecks::createInetAddressResolverProvider, env -> VersionSpecificNetworkChecks.createInetAddressResolverProvider(),
SERVER_ONLY, SERVER_ONLY,
NotEntitledException.class, NotEntitledException.class,
18 18
@ -215,6 +217,12 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
.filter(entry -> entry.getValue().fromJavaVersion() == null || Runtime.version().feature() >= entry.getValue().fromJavaVersion()) .filter(entry -> entry.getValue().fromJavaVersion() == null || Runtime.version().feature() >= entry.getValue().fromJavaVersion())
.collect(Collectors.toUnmodifiableMap(Entry::getKey, Entry::getValue)); .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") @SuppressForbidden(reason = "Need package private methods so we don't have to make them all public")
private static Method[] getDeclaredMethods(Class<?> clazz) { private static Method[] getDeclaredMethods(Class<?> clazz) {
return clazz.getDeclaredMethods(); return clazz.getDeclaredMethods();
@ -230,13 +238,10 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
if (Modifier.isStatic(method.getModifiers()) == false) { if (Modifier.isStatic(method.getModifiers()) == false) {
throw new AssertionError("Entitlement test method [" + method + "] must be static"); throw new AssertionError("Entitlement test method [" + method + "] must be static");
} }
if (method.getParameterTypes().length != 0) { final CheckedConsumer<Environment, Exception> call = createConsumerForMethod(method);
throw new AssertionError("Entitlement test method [" + method + "] must not have parameters"); CheckedConsumer<Environment, Exception> runnable = env -> {
}
CheckedRunnable<Exception> runnable = () -> {
try { try {
method.invoke(null); call.accept(env);
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
throw new AssertionError(e); throw new AssertionError(e);
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
@ -258,6 +263,17 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
return entries.stream(); return entries.stream();
} }
private static CheckedConsumer<Environment, Exception> 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() { private static void createURLStreamHandlerProvider() {
var x = new URLStreamHandlerProvider() { var x = new URLStreamHandlerProvider() {
@Override @Override
@ -421,6 +437,14 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
public static Set<String> getAlwaysAllowedCheckActions() {
return checkActions.entrySet()
.stream()
.filter(kv -> kv.getValue().expectedAccess().equals(ALWAYS_ALLOWED))
.map(Entry::getKey)
.collect(Collectors.toSet());
}
public static Set<String> getDeniableCheckActions() { public static Set<String> getDeniableCheckActions() {
return checkActions.entrySet() return checkActions.entrySet()
.stream() .stream()
@ -455,7 +479,7 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
logger.info("Calling check action [{}]", actionName); logger.info("Calling check action [{}]", actionName);
RestResponse response; RestResponse response;
try { try {
checkAction.action().run(); checkAction.action().accept(environment);
response = new RestResponse(RestStatus.OK, Strings.format("Succesfully executed action [%s]", actionName)); response = new RestResponse(RestStatus.OK, Strings.format("Succesfully executed action [%s]", actionName));
} catch (Exception e) { } catch (Exception e) {
var statusCode = checkAction.expectedExceptionIfDenied.isInstance(e) var statusCode = checkAction.expectedExceptionIfDenied.isInstance(e)
@ -468,5 +492,4 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
channel.sendResponse(response); channel.sendResponse(response);
}; };
} }
} }

View file

@ -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<Object[]> data() {
return RestEntitlementsCheckAction.getAlwaysAllowedCheckActions().stream().map(action -> new Object[] { action }).toList();
}
@Override
protected String getTestRestCluster() {
return testRule.cluster.getHttpAddresses();
}
}

View file

@ -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<Object[]> data() {
return RestEntitlementsCheckAction.getAlwaysAllowedCheckActions().stream().map(action -> new Object[] { action }).toList();
}
@Override
protected String getTestRestCluster() {
return testRule.cluster.getHttpAddresses();
}
}