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 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
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,7 @@
import org.opensearch.common.SuppressForbidden;
import org.opensearch.common.collect.Tuple;
import org.opensearch.common.hash.MessageDigests;
import org.opensearch.common.settings.Settings;
import org.opensearch.common.util.io.IOUtils;
import org.opensearch.env.Environment;

Expand Down Expand Up @@ -880,7 +881,22 @@ private PluginInfo installPlugin(Terminal terminal, boolean isBatch, Path tmpRoo
} else {
permissions = Collections.emptySet();
}
PluginSecurity.confirmPolicyExceptions(terminal, permissions, isBatch);
final PluginsService.Bundle bundle = new PluginsService.Bundle(info, env.pluginsDir());

final Set<String> requestedClusterActions = new HashSet<>();
final Map<String, Set<String>> requestedIndexActions = new HashMap<>();

final IdentityAwarePlugin plugin = PluginsService.maybeLoadIdentityAwarePluginFromBundle(
bundle,
Settings.EMPTY,
env.configDir(),
tmpRoot
);
if (plugin != null) {
requestedClusterActions.addAll(plugin.getClusterActions());
requestedIndexActions.putAll(plugin.getIndexActions());
}
PluginSecurity.confirmPolicyExceptions(terminal, permissions, 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,140 @@ 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 createIdentityAwarePluginUrl(String name, Path structure, String... additionalProps) throws IOException {
return createIdentityAwarePlugin(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;
}

private static String compileIdentityAwareFakePlugin(Path structure) throws IOException {
String pluginClassName = "org.opensearch.plugins.FakePlugin";
String javaSourceCode = "package org.opensearch.plugins;\n"
+ "\n"
+ "import java.util.Set;\n"
+ "\n"
+ "public class FakePlugin extends Plugin implements IdentityAwarePlugin {\n"
+ "\n"
+ " public FakePlugin() {}\n"
+ "\n"
+ " @Override\n"
+ " public Set<String> getClusterActions() {\n"
+ " return Set.of(\"cluster:monitor/health\");\n"
+ " }\n"
+ "}\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 writeIdentityAwarePlugin(String name, Path structure, String... additionalProps) throws IOException {
String pluginClassName = compileIdentityAwareFakePlugin(structure);
String[] properties = Stream.concat(
Stream.of(
"description",
Expand All @@ -277,16 +426,18 @@ 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);
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 +451,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 @@ -331,6 +482,11 @@ static Path createPlugin(String name, Path structure, String... additionalProps)
return writeZip(structure, null);
}

static Path createIdentityAwarePlugin(String name, Path structure, String... additionalProps) throws IOException {
writeIdentityAwarePlugin(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 +1696,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 +1736,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 = createIdentityAwarePluginUrl("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 @@ -12,6 +12,10 @@
import org.opensearch.identity.PluginSubject;
import org.opensearch.identity.Subject;

import java.util.Collections;
import java.util.Map;
import java.util.Set;

/**
* Plugin that performs transport actions with a plugin system context. IdentityAwarePlugins are initialized
* with a {@link Subject} that they can utilize to perform transport actions outside the default subject.
Expand All @@ -31,4 +35,22 @@
* interaction
*/
default void assignSubject(PluginSubject pluginSubject) {}

/**
* Returns a set of cluster actions this plugin can perform within a pluginSubject.runAs(() -> { ... }) block.
*
* @return Set of cluster actions
*/
default Set<String> getClusterActions() {
return Collections.emptySet();

Check warning on line 45 in server/src/main/java/org/opensearch/plugins/IdentityAwarePlugin.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/plugins/IdentityAwarePlugin.java#L45

Added line #L45 was not covered by tests
}

/**
* Returns a map of index pattern -> allowed index actions this plugin can perform within a pluginSubject.runAs(() -> { ... }) block.
*
* @return Map of index pattern -> allowed index actions
*/
default Map<String, Set<String>> getIndexActions() {
return Collections.emptyMap();
}
}
Loading
Loading