Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Plugins to request to perform cluster actions and index actions with their assigned PluginSubject and prompt on install #15778

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions distribution/tools/plugin-cli/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies {
compileOnly project(":libs:opensearch-cli")
api "org.bouncycastle:bcpg-fips:2.0.9"
api "org.bouncycastle:bc-fips:2.0.0"
testRuntimeOnly project(':libs:opensearch-plugin-classloader')
testImplementation project(":test:framework")
testImplementation 'com.google.jimfs:jimfs:1.3.0'
testRuntimeOnly("com.google.guava:guava:${versions.guava}") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
import org.opensearch.common.SuppressForbidden;
import org.opensearch.common.collect.Tuple;
import org.opensearch.common.hash.MessageDigests;
import org.opensearch.common.settings.Setting;
import org.opensearch.common.settings.Settings;
import org.opensearch.common.util.io.IOUtils;
import org.opensearch.env.Environment;

Expand Down Expand Up @@ -101,6 +103,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.Collectors;

import static org.opensearch.cli.Terminal.Verbosity.VERBOSE;
Expand Down Expand Up @@ -193,6 +196,16 @@ class InstallPluginCommand extends EnvironmentAwareCommand {
static final Set<PosixFilePermission> PLUGIN_DIR_PERMS;
static final Set<PosixFilePermission> PLUGIN_FILES_PERMS;

static final Setting<List<String>> CLUSTER_ACTIONS_SETTING = Setting.listSetting(
"cluster.actions",
Collections.emptyList(),
Function.identity()
);

static final Setting<Settings> INDEX_ACTIONS_SETTING = Setting.groupSetting("index.actions.");

static final Setting<String> DESCRIPTION_SETTING = Setting.simpleString("description");

static {
// Bin directory get chmod 755
BIN_DIR_PERMS = Collections.unmodifiableSet(PosixFilePermissions.fromString("rwxr-xr-x"));
Expand Down Expand Up @@ -880,7 +893,34 @@ private PluginInfo installPlugin(Terminal terminal, boolean isBatch, Path tmpRoo
} else {
permissions = Collections.emptySet();
}
PluginSecurity.confirmPolicyExceptions(terminal, permissions, isBatch);

Path actions = tmpRoot.resolve(PluginInfo.OPENSEARCH_PLUGIN_ACTIONS);
Settings requestedActions = Settings.EMPTY;

if (Files.exists(actions)) {
requestedActions = PluginSecurity.parseRequestedActions(actions);
}

final Map<String, List<String>> requestedIndexActions = new HashMap<>();

final List<String> requestedClusterActions = CLUSTER_ACTIONS_SETTING.get(requestedActions);
final Settings requestedIndexActionsGroup = INDEX_ACTIONS_SETTING.get(requestedActions);
final String pluginActionDescription = DESCRIPTION_SETTING.get(requestedActions);
if (!requestedIndexActionsGroup.keySet().isEmpty()) {
for (String indexPattern : requestedIndexActionsGroup.keySet()) {
List<String> indexActionsForPattern = requestedIndexActionsGroup.getAsList(indexPattern);
requestedIndexActions.put(indexPattern, indexActionsForPattern);
}
}

PluginSecurity.confirmPolicyExceptions(
terminal,
permissions,
pluginActionDescription,
requestedClusterActions,
requestedIndexActions,
isBatch
);

String targetFolderName = info.getTargetFolderName();
final Path destination = env.pluginsDir().resolve(targetFolderName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,22 @@
import org.junit.After;
import org.junit.Before;

import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URI;
Expand Down Expand Up @@ -237,7 +247,13 @@ static Path createPluginDir(Function<String, Path> temp) throws IOException {
static void writeJar(Path jar, String... classes) throws IOException {
try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(jar))) {
for (String clazz : classes) {
stream.putNextEntry(new ZipEntry(clazz + ".class")); // no package names, just support simple classes
clazz = clazz.replace('.', '/');
ZipEntry entry = new ZipEntry(clazz + ".class");
stream.putNextEntry(entry); // no package names, just support simple classes
Path compiledClassPath = jar.getParent().resolve(clazz + ".class");
if (Files.exists(compiledClassPath)) {
Files.copy(compiledClassPath, stream);
}
}
}
}
Expand All @@ -263,7 +279,90 @@ static String createPluginUrl(String name, Path structure, String... additionalP
return createPlugin(name, structure, additionalProps).toUri().toURL().toString();
}

/** creates a plugin .zip and returns the url for testing */
static String createPluginWithRequestedActionsUrl(String name, Path structure, String... additionalProps) throws IOException {
return createPluginWithRequestedActions(name, structure, additionalProps).toUri().toURL().toString();
}

static class JavaSourceFromString extends SimpleJavaFileObject {
private final String code;

public JavaSourceFromString(String className, String code) {
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}

@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}

private static String compileFakePlugin(Path structure) throws IOException {
String pluginClassName = "org.opensearch.plugins.FakePlugin";
String javaSourceCode = "package org.opensearch.plugins;\n" + "\n" + "class FakePlugin extends Plugin {}\n";
if (Files.notExists(structure)) {
Files.createDirectories(structure);
}
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(null, null, null);
JavaFileManager fileManager = new ForwardingJavaFileManager<StandardJavaFileManager>(standardFileManager) {
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) {
Path classFile = structure.resolve(className.replace('.', '/') + ".class");
if (Files.notExists(classFile.getParent())) {
try {
Files.createDirectories(classFile.getParent());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return new SimpleJavaFileObject(classFile.toUri(), kind) {
@Override
public OutputStream openOutputStream() throws IOException {
return Files.newOutputStream(classFile);
}
};
}
};

JavaFileObject javaFileObject = new JavaSourceFromString(pluginClassName, javaSourceCode);
Iterable<String> options = Arrays.asList("-d", structure.toUri().toString());
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, options, null, Arrays.asList(javaFileObject));
boolean success = task.call();
// Close the file manager
fileManager.close();
return pluginClassName;
}

static void writePlugin(String name, Path structure, String... additionalProps) throws IOException {
String pluginClassName = compileFakePlugin(structure);
String[] properties = Stream.concat(
Stream.of(
"description",
"fake desc",
"name",
name,
"version",
"1.0",
"opensearch.version",
Version.CURRENT.toString(),
"java.version",
System.getProperty("java.specification.version"),
"classname",
pluginClassName
),
Arrays.stream(additionalProps)
).toArray(String[]::new);

PluginTestUtil.writePluginProperties(structure, properties);
String className = name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1) + "Plugin";
writeJar(structure.resolve("plugin.jar"), className, pluginClassName);
}

static void writePluginWithRequestedActions(String name, Path structure, String... additionalProps) throws IOException {
String pluginClassName = compileFakePlugin(structure);
String[] properties = Stream.concat(
Stream.of(
"description",
Expand All @@ -277,16 +376,19 @@ static void writePlugin(String name, Path structure, String... additionalProps)
"java.version",
System.getProperty("java.specification.version"),
"classname",
"FakePlugin"
pluginClassName
),
Arrays.stream(additionalProps)
).toArray(String[]::new);

PluginTestUtil.writePluginProperties(structure, properties);
writePluginPermissionsYaml(structure);
String className = name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1) + "Plugin";
writeJar(structure.resolve("plugin.jar"), className);
writeJar(structure.resolve("plugin.jar"), className, pluginClassName);
}

static void writePlugin(String name, Path structure, SemverRange opensearchVersionRange, String... additionalProps) throws IOException {
String pluginClassName = compileFakePlugin(structure);
String[] properties = Stream.concat(
Stream.of(
"description",
Expand All @@ -300,13 +402,13 @@ static void writePlugin(String name, Path structure, SemverRange opensearchVersi
"java.version",
System.getProperty("java.specification.version"),
"classname",
"FakePlugin"
pluginClassName
),
Arrays.stream(additionalProps)
).toArray(String[]::new);
PluginTestUtil.writePluginProperties(structure, properties);
String className = name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1) + "Plugin";
writeJar(structure.resolve("plugin.jar"), className);
writeJar(structure.resolve("plugin.jar"), className, pluginClassName);
}

static Path createPlugin(String name, Path structure, SemverRange opensearchVersionRange, String... additionalProps)
Expand All @@ -326,11 +428,25 @@ static void writePluginSecurityPolicy(Path pluginDir, String... permissions) thr
Files.write(pluginDir.resolve("plugin-security.policy"), securityPolicyContent.toString().getBytes(StandardCharsets.UTF_8));
}

static void writePluginPermissionsYaml(Path pluginDir, String... permissions) throws IOException {
String permissionsYamlContent = "cluster.actions:\n"
+ " - cluster:monitor/health\n"
+ "indices.actions:\n"
+ " example-index*:\n"
+ " - indices:data/write/index*";
Files.write(pluginDir.resolve("plugin-permissions.yml"), permissionsYamlContent.getBytes(StandardCharsets.UTF_8));
}

static Path createPlugin(String name, Path structure, String... additionalProps) throws IOException {
writePlugin(name, structure, additionalProps);
return writeZip(structure, null);
}

static Path createPluginWithRequestedActions(String name, Path structure, String... additionalProps) throws IOException {
writePluginWithRequestedActions(name, structure, additionalProps);
return writeZip(structure, null);
}

void installPlugin(String pluginUrl, Path home) throws Exception {
installPlugin(pluginUrl, home, skipJarHellCommand);
}
Expand Down Expand Up @@ -1540,43 +1656,37 @@ private String signature(final byte[] bytes, final PGPSecretKey secretKey) {
// checks the plugin requires a policy confirmation, and does not install when that is rejected by the user
// the plugin is installed after this method completes
private void assertPolicyConfirmation(Tuple<Path, Environment> env, String pluginZip, String... warnings) throws Exception {
for (int i = 0; i < warnings.length; ++i) {
String warning = warnings[i];
for (int j = 0; j < i; ++j) {
terminal.addTextInput("y"); // accept warnings we have already tested
}
// default answer, does not install
terminal.addTextInput("");
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip, env.v1()));
assertEquals("installation aborted by user", e.getMessage());

assertThat(terminal.getErrorOutput(), containsString("WARNING: " + warning));
try (Stream<Path> fileStream = Files.list(env.v2().pluginsDir())) {
assertThat(fileStream.collect(Collectors.toList()), empty());
}
// default answer, does not install
terminal.addTextInput("");
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip, env.v1()));
assertEquals("installation aborted by user", e.getMessage());

// explicitly do not install
terminal.reset();
for (int j = 0; j < i; ++j) {
terminal.addTextInput("y"); // accept warnings we have already tested
}
terminal.addTextInput("n");
e = expectThrows(UserException.class, () -> installPlugin(pluginZip, env.v1()));
assertEquals("installation aborted by user", e.getMessage());
assertThat(terminal.getErrorOutput(), containsString("WARNING: " + warning));
try (Stream<Path> fileStream = Files.list(env.v2().pluginsDir())) {
assertThat(fileStream.collect(Collectors.toList()), empty());
}
try (Stream<Path> fileStream = Files.list(env.v2().pluginsDir())) {
assertThat(fileStream.collect(Collectors.toList()), empty());
}

// allow installation
for (String warning : warnings) {
assertThat(terminal.getErrorOutput(), containsString(warning));
}

// explicitly do not install
terminal.reset();
for (int j = 0; j < warnings.length; ++j) {
terminal.addTextInput("y");
terminal.addTextInput("n");
e = expectThrows(UserException.class, () -> installPlugin(pluginZip, env.v1()));
assertEquals("installation aborted by user", e.getMessage());
try (Stream<Path> fileStream = Files.list(env.v2().pluginsDir())) {
assertThat(fileStream.collect(Collectors.toList()), empty());
}
for (String warning : warnings) {
assertThat(terminal.getErrorOutput(), containsString(warning));
}

// allow installation
terminal.reset();
terminal.addTextInput("y");
installPlugin(pluginZip, env.v1());
for (String warning : warnings) {
assertThat(terminal.getErrorOutput(), containsString("WARNING: " + warning));
assertThat(terminal.getErrorOutput(), containsString(warning));
}
}

Expand All @@ -1586,7 +1696,16 @@ public void testPolicyConfirmation() throws Exception {
writePluginSecurityPolicy(pluginDir, "setAccessible", "setFactory");
String pluginZip = createPluginUrl("fake", pluginDir);

assertPolicyConfirmation(env, pluginZip, "plugin requires additional permissions");
assertPolicyConfirmation(env, pluginZip, "WARNING: plugin requires additional permissions");
assertPlugin("fake", pluginDir, env.v2());
}

public void testRequestedActionsConfirmation() throws Exception {
Tuple<Path, Environment> env = createEnv(fs, temp);
Path pluginDir = createPluginDir(temp);
String pluginZip = createPluginWithRequestedActionsUrl("fake", pluginDir);

assertPolicyConfirmation(env, pluginZip, "WARNING: plugin requires additional permissions", "Cluster Actions", "Index Actions");
assertPlugin("fake", pluginDir, env.v2());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.opensearch.plugins.ActionPlugin;
import org.opensearch.plugins.IdentityPlugin;
import org.opensearch.plugins.Plugin;
import org.opensearch.plugins.PluginInfo;
import org.opensearch.repositories.RepositoriesService;
import org.opensearch.rest.BytesRestResponse;
import org.opensearch.rest.RestChannel;
Expand Down Expand Up @@ -138,7 +139,8 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c
}
}

public PluginSubject getPluginSubject(Plugin plugin) {
@Override
public PluginSubject getPluginSubject(PluginInfo pluginInfo) {
return new ShiroPluginSubject(threadPool);
}
}
Loading
Loading