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 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
43e8974
WIP on requesting perms and displaying on install
cwperks Sep 5, 2024
a841f06
Actually compile a fake plugin in InstallPluginCommandTests
cwperks Sep 5, 2024
edfbe36
Merge branch 'main' into identity-aware-plugin-request-perms
cwperks Sep 16, 2024
5821632
Add test for prompt when installing IdentityAwarePlugin
cwperks Sep 16, 2024
1106978
Update warning message
cwperks Sep 16, 2024
4a29367
Get requested actions from plugin-permissions.yml
cwperks Sep 17, 2024
f4bd921
Allow plugin dev to configure a description to describe why plugin is…
cwperks Sep 17, 2024
454a5dd
Add requested actions to PluginInfo and pass PluginInfo IdentityPlugi…
cwperks Sep 19, 2024
c031a1b
Merge branch 'main' into identity-aware-plugin-request-perms
cwperks Sep 19, 2024
a2fb0fd
Add null check
cwperks Sep 19, 2024
758b671
Check stream version
cwperks Sep 19, 2024
4cdd593
Check version when serializing
cwperks Sep 19, 2024
394c47a
Handle case where requestedActions is null
cwperks Sep 19, 2024
fde386d
Merge branch 'main' into identity-aware-plugin-request-perms
cwperks Sep 20, 2024
157740b
Ensure requestedActions is non-null
cwperks Sep 20, 2024
bf44a29
Simplify tests
cwperks Sep 20, 2024
8b16051
modify correct build.gradle
cwperks Sep 20, 2024
c12df2d
Remove unused code
cwperks Sep 20, 2024
abbb6f0
Remove unused import
cwperks Sep 20, 2024
df6853b
Merge branch 'main' into identity-aware-plugin-request-perms
cwperks Oct 2, 2024
6b64364
Merge branch 'main' into identity-aware-plugin-request-perms
cwperks Oct 25, 2024
51a6cde
Merge branch 'main' into identity-aware-plugin-request-perms
cwperks Oct 29, 2024
5b8c079
Update ToXContent
cwperks Oct 29, 2024
ddeb16b
Set to CURRENT until backport
cwperks Oct 29, 2024
ea1af1f
Merge branch 'main' into identity-aware-plugin-request-perms
cwperks Nov 5, 2024
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