diff --git a/distribution/tools/plugin-cli/src/main/java/org/opensearch/tools/cli/plugin/InstallPluginCommand.java b/distribution/tools/plugin-cli/src/main/java/org/opensearch/tools/cli/plugin/InstallPluginCommand.java index 1ab2697d5ced8..aa9b71dfcedcf 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/opensearch/tools/cli/plugin/InstallPluginCommand.java +++ b/distribution/tools/plugin-cli/src/main/java/org/opensearch/tools/cli/plugin/InstallPluginCommand.java @@ -60,6 +60,7 @@ import org.opensearch.common.cli.EnvironmentAwareCommand; 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; import org.opensearch.plugins.Platforms; @@ -95,6 +96,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -106,6 +108,10 @@ import java.util.stream.Collectors; import static org.opensearch.cli.Terminal.Verbosity.VERBOSE; +import static org.opensearch.plugins.PluginInfo.CLUSTER_ACTIONS_SETTING; +import static org.opensearch.plugins.PluginInfo.DESCRIPTION_SETTING; +import static org.opensearch.plugins.PluginInfo.INDEX_ACTIONS_SETTING; +import static org.opensearch.plugins.PluginInfo.parseRequestedActions; /** * A command for the plugin cli to install a plugin into opensearch. @@ -850,7 +856,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 = parseRequestedActions(actions); + } + + final Map> requestedIndexActions = new HashMap<>(); + + final List 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 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); diff --git a/distribution/tools/plugin-cli/src/main/java/org/opensearch/tools/cli/plugin/PluginSecurity.java b/distribution/tools/plugin-cli/src/main/java/org/opensearch/tools/cli/plugin/PluginSecurity.java index 81d7824812361..da202024811e0 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/opensearch/tools/cli/plugin/PluginSecurity.java +++ b/distribution/tools/plugin-cli/src/main/java/org/opensearch/tools/cli/plugin/PluginSecurity.java @@ -51,6 +51,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -64,10 +65,17 @@ class PluginSecurity { /** * prints/confirms policy exceptions with the user */ - static void confirmPolicyExceptions(Terminal terminal, Set permissions, boolean batch) throws UserException { + static void confirmPolicyExceptions( + Terminal terminal, + Set permissions, + String description, + List requestedClusterActions, + Map> requestedIndexActions, + boolean batch + ) throws UserException { List requested = new ArrayList<>(permissions); - if (requested.isEmpty()) { - terminal.println(Verbosity.VERBOSE, "plugin has a policy file with no additional permissions"); + if (requested.isEmpty() && requestedClusterActions.isEmpty() && requestedIndexActions.isEmpty()) { + terminal.println(Verbosity.VERBOSE, "plugin has not requested any additional permissions"); } else { // sort permissions in a reasonable order @@ -76,12 +84,56 @@ static void confirmPolicyExceptions(Terminal terminal, Set permissions, terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); terminal.errorPrintln(Verbosity.NORMAL, "@ WARNING: plugin requires additional permissions @"); terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); - // print all permissions: - for (String permission : requested) { - terminal.errorPrintln(Verbosity.NORMAL, "* " + permission); + if (!requested.isEmpty()) { + // print all permissions: + for (String permission : requested) { + terminal.errorPrintln(Verbosity.NORMAL, "* " + permission); + } + terminal.errorPrintln( + Verbosity.NORMAL, + "See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html" + ); + terminal.errorPrintln(Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks."); + terminal.errorPrintln(Verbosity.NORMAL, ""); + terminal.errorPrintln(Verbosity.NORMAL, "Plugin requests permission to perform the following transport actions. Any index"); + terminal.errorPrintln( + Verbosity.NORMAL, + "pattern that appears below is a default value and may change depending on plugin settings." + ); } - terminal.errorPrintln(Verbosity.NORMAL, "See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html"); - terminal.errorPrintln(Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks."); + if (!requestedClusterActions.isEmpty() || !requestedIndexActions.isEmpty()) { + terminal.errorPrintln(Verbosity.NORMAL, ""); + if (description != null) { + terminal.errorPrintln(Verbosity.NORMAL, description); + terminal.errorPrintln(Verbosity.NORMAL, ""); + } + terminal.errorPrintln(Verbosity.NORMAL, "Cluster Actions"); + terminal.errorPrintln(Verbosity.NORMAL, "---------------"); + terminal.errorPrintln(Verbosity.NORMAL, ""); + if (requestedClusterActions.isEmpty()) { + terminal.errorPrintln(Verbosity.NORMAL, "None"); + } else { + for (String clusterAction : requestedClusterActions) { + terminal.errorPrintln(Verbosity.NORMAL, "* " + clusterAction); + } + } + terminal.errorPrintln(Verbosity.NORMAL, ""); + terminal.errorPrintln(Verbosity.NORMAL, "Index Actions"); + terminal.errorPrintln(Verbosity.NORMAL, "-------------"); + terminal.errorPrintln(Verbosity.NORMAL, ""); + if (requestedIndexActions.isEmpty()) { + terminal.errorPrintln(Verbosity.NORMAL, "None"); + } else { + for (Map.Entry> entry : requestedIndexActions.entrySet()) { + terminal.errorPrintln(Verbosity.NORMAL, "Index Pattern: " + entry.getKey()); + terminal.errorPrintln(Verbosity.NORMAL, ""); + for (String indexAction : entry.getValue()) { + terminal.errorPrintln(Verbosity.NORMAL, "* " + indexAction); + } + } + } + } + prompt(terminal, batch); } } diff --git a/distribution/tools/plugin-cli/src/test/java/org/opensearch/tools/cli/plugin/InstallPluginCommandTests.java b/distribution/tools/plugin-cli/src/test/java/org/opensearch/tools/cli/plugin/InstallPluginCommandTests.java index 56ef09c5c9128..c660933e3acb2 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/opensearch/tools/cli/plugin/InstallPluginCommandTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/opensearch/tools/cli/plugin/InstallPluginCommandTests.java @@ -266,6 +266,11 @@ 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 void writePlugin(String name, Path structure, String... additionalProps) throws IOException { String[] properties = Stream.concat( Stream.of( @@ -289,6 +294,31 @@ static void writePlugin(String name, Path structure, String... additionalProps) writeJar(structure.resolve("plugin.jar"), className); } + static void writePluginWithRequestedActions(String name, Path structure, String... additionalProps) throws IOException { + 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", + "FakePlugin" + ), + 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); + } + static void writePlugin(String name, Path structure, SemverRange opensearchVersionRange, String... additionalProps) throws IOException { String[] properties = Stream.concat( Stream.of( @@ -329,11 +359,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); } @@ -1410,43 +1454,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 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 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 fileStream = Files.list(env.v2().pluginsDir())) { - assertThat(fileStream.collect(Collectors.toList()), empty()); - } + try (Stream 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 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)); } } @@ -1456,7 +1494,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 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()); } diff --git a/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroIdentityPlugin.java b/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroIdentityPlugin.java index 4ca61ddeb6bcd..a844b8800883d 100644 --- a/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroIdentityPlugin.java +++ b/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroIdentityPlugin.java @@ -28,6 +28,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; @@ -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); } } diff --git a/server/src/main/java/org/opensearch/identity/IdentityService.java b/server/src/main/java/org/opensearch/identity/IdentityService.java index 33066fae5a80d..debba695c1a0a 100644 --- a/server/src/main/java/org/opensearch/identity/IdentityService.java +++ b/server/src/main/java/org/opensearch/identity/IdentityService.java @@ -9,12 +9,14 @@ import org.apache.logging.log4j.Logger; import org.opensearch.OpenSearchException; import org.opensearch.common.annotation.InternalApi; +import org.opensearch.common.collect.Tuple; import org.opensearch.common.settings.Settings; import org.opensearch.identity.noop.NoopIdentityPlugin; import org.opensearch.identity.tokens.TokenManager; import org.opensearch.plugins.IdentityAwarePlugin; import org.opensearch.plugins.IdentityPlugin; import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.PluginInfo; import org.opensearch.threadpool.ThreadPool; import java.util.List; @@ -63,11 +65,11 @@ public TokenManager getTokenManager() { return identityPlugin.getTokenManager(); } - public void initializeIdentityAwarePlugins(final List identityAwarePlugins) { + public void initializeIdentityAwarePlugins(final List> identityAwarePlugins) { if (identityAwarePlugins != null) { - for (IdentityAwarePlugin plugin : identityAwarePlugins) { - PluginSubject pluginSubject = identityPlugin.getPluginSubject((Plugin) plugin); - plugin.assignSubject(pluginSubject); + for (Tuple pluginTuple : identityAwarePlugins) { + PluginSubject pluginSubject = identityPlugin.getPluginSubject(pluginTuple.v1()); + ((IdentityAwarePlugin) pluginTuple.v2()).assignSubject(pluginSubject); } } } diff --git a/server/src/main/java/org/opensearch/identity/noop/NoopIdentityPlugin.java b/server/src/main/java/org/opensearch/identity/noop/NoopIdentityPlugin.java index 6279388c76f96..977f88c1a24d0 100644 --- a/server/src/main/java/org/opensearch/identity/noop/NoopIdentityPlugin.java +++ b/server/src/main/java/org/opensearch/identity/noop/NoopIdentityPlugin.java @@ -12,7 +12,7 @@ import org.opensearch.identity.Subject; import org.opensearch.identity.tokens.TokenManager; import org.opensearch.plugins.IdentityPlugin; -import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.PluginInfo; import org.opensearch.threadpool.ThreadPool; /** @@ -49,7 +49,7 @@ public TokenManager getTokenManager() { } @Override - public PluginSubject getPluginSubject(Plugin plugin) { + public PluginSubject getPluginSubject(PluginInfo pluginInfo) { return new NoopPluginSubject(threadPool); } } diff --git a/server/src/main/java/org/opensearch/node/Node.java b/server/src/main/java/org/opensearch/node/Node.java index 222c6e8ba36c4..0b37c258a1dc3 100644 --- a/server/src/main/java/org/opensearch/node/Node.java +++ b/server/src/main/java/org/opensearch/node/Node.java @@ -89,6 +89,7 @@ import org.opensearch.common.StopWatch; import org.opensearch.common.cache.module.CacheModule; import org.opensearch.common.cache.service.CacheService; +import org.opensearch.common.collect.Tuple; import org.opensearch.common.inject.Injector; import org.opensearch.common.inject.Key; import org.opensearch.common.inject.Module; @@ -212,6 +213,7 @@ import org.opensearch.plugins.NetworkPlugin; import org.opensearch.plugins.PersistentTaskPlugin; import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.PluginInfo; import org.opensearch.plugins.PluginsService; import org.opensearch.plugins.RepositoryPlugin; import org.opensearch.plugins.ScriptPlugin; @@ -1052,8 +1054,8 @@ protected Node( // Add the telemetryAwarePlugin components to the existing pluginComponents collection. pluginComponents.addAll(telemetryAwarePluginComponents); - List identityAwarePlugins = pluginsService.filterPlugins(IdentityAwarePlugin.class); - identityService.initializeIdentityAwarePlugins(identityAwarePlugins); + List> identityAwarePluginTuples = pluginsService.filterPluginTuples(IdentityAwarePlugin.class); + identityService.initializeIdentityAwarePlugins(identityAwarePluginTuples); final QueryGroupResourceUsageTrackerService queryGroupResourceUsageTrackerService = new QueryGroupResourceUsageTrackerService( taskResourceTrackingService diff --git a/server/src/main/java/org/opensearch/plugins/IdentityPlugin.java b/server/src/main/java/org/opensearch/plugins/IdentityPlugin.java index b40af14231fb9..b77a4cafd0c6b 100644 --- a/server/src/main/java/org/opensearch/plugins/IdentityPlugin.java +++ b/server/src/main/java/org/opensearch/plugins/IdentityPlugin.java @@ -38,8 +38,8 @@ public interface IdentityPlugin { * Gets a subject corresponding to the passed plugin that can be utilized to perform transport actions * in the plugin system context * - * @param plugin The corresponding plugin + * @param pluginInfo The corresponding pluginInfo * @return Subject corresponding to the plugin */ - PluginSubject getPluginSubject(Plugin plugin); + PluginSubject getPluginSubject(PluginInfo pluginInfo); } diff --git a/server/src/main/java/org/opensearch/plugins/PluginInfo.java b/server/src/main/java/org/opensearch/plugins/PluginInfo.java index 323e061aea567..ee0faeb5ed156 100644 --- a/server/src/main/java/org/opensearch/plugins/PluginInfo.java +++ b/server/src/main/java/org/opensearch/plugins/PluginInfo.java @@ -38,6 +38,8 @@ import org.opensearch.Version; import org.opensearch.common.annotation.PublicApi; import org.opensearch.common.bootstrap.JarHell; +import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.json.JsonXContentParser; import org.opensearch.core.common.Strings; import org.opensearch.core.common.io.stream.StreamInput; @@ -56,6 +58,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -73,11 +76,22 @@ public class PluginInfo implements Writeable, ToXContentObject { public static final String OPENSEARCH_PLUGIN_PROPERTIES = "plugin-descriptor.properties"; public static final String OPENSEARCH_PLUGIN_POLICY = "plugin-security.policy"; + public static final String OPENSEARCH_PLUGIN_ACTIONS = "plugin-permissions.yml"; private static final JsonFactory jsonFactory = new JsonFactory().configure( JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES.mappedFeature(), true ); + public static final Setting> CLUSTER_ACTIONS_SETTING = Setting.listSetting( + "cluster.actions", + Collections.emptyList(), + Function.identity() + ); + + public static final Setting INDEX_ACTIONS_SETTING = Setting.groupSetting("index.actions."); + + public static final Setting DESCRIPTION_SETTING = Setting.simpleString("description"); + private final String name; private final String description; private final String version; @@ -89,6 +103,7 @@ public class PluginInfo implements Writeable, ToXContentObject { // Optional extended plugins are a subset of extendedPlugins that only contains the optional extended plugins private final List optionalExtendedPlugins; private final boolean hasNativeController; + private final Settings requestedActions; /** * Construct plugin info. @@ -112,7 +127,8 @@ public PluginInfo( String classname, String customFolderName, List extendedPlugins, - boolean hasNativeController + boolean hasNativeController, + Settings requestedActions ) { this( name, @@ -123,7 +139,8 @@ public PluginInfo( classname, customFolderName, extendedPlugins, - hasNativeController + hasNativeController, + requestedActions ); } @@ -136,7 +153,8 @@ public PluginInfo( String classname, String customFolderName, List extendedPlugins, - boolean hasNativeController + boolean hasNativeController, + Settings requestedActions ) { this.name = name; this.description = description; @@ -157,6 +175,7 @@ public PluginInfo( .map(s -> s.split(";")[0]) .collect(Collectors.toUnmodifiableList()); this.hasNativeController = hasNativeController; + this.requestedActions = requestedActions; } /** @@ -190,7 +209,8 @@ public PluginInfo( classname, null /* customFolderName */, extendedPlugins, - hasNativeController + hasNativeController, + Settings.EMPTY /* requestedActions */ ); } @@ -220,6 +240,11 @@ public PluginInfo(final StreamInput in) throws IOException { } else { this.optionalExtendedPlugins = new ArrayList<>(); } + if (in.getVersion().onOrAfter(Version.CURRENT)) { + this.requestedActions = Settings.readSettingsFromStream(in); + } else { + this.requestedActions = Settings.EMPTY; + } } static boolean isOptionalExtension(String extendedPlugin) { @@ -253,6 +278,9 @@ This works for currently supported range notations (=,~) if (out.getVersion().onOrAfter(Version.V_2_19_0)) { out.writeStringCollection(optionalExtendedPlugins); } + if (out.getVersion().onOrAfter(Version.CURRENT)) { + Settings.writeSettingsToStream(requestedActions, out); + } } /** @@ -382,6 +410,17 @@ public static PluginInfo readFromProperties(final Path path) throws IOException throw new IllegalArgumentException("Unknown properties in plugin descriptor: " + propsMap.keySet()); } + Settings requestedActions = Settings.EMPTY; + Path actions = path.resolve(PluginInfo.OPENSEARCH_PLUGIN_ACTIONS); + + if (Files.exists(actions)) { + try { + requestedActions = parseRequestedActions(actions); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return new PluginInfo( name, description, @@ -391,7 +430,8 @@ public static PluginInfo readFromProperties(final Path path) throws IOException classname, customFolderName, extendedPlugins, - hasNativeController + hasNativeController, + requestedActions ); } @@ -508,6 +548,41 @@ public String getTargetFolderName() { return (this.customFolderName == null || this.customFolderName.isEmpty()) ? this.name : this.customFolderName; } + /** + * Returns cluster actions requested by this plugin in the plugin-permissions.yml file + * + * @return A list of cluster actions contained within the plugin-permissions.yml file + */ + public List getClusterActions() { + return CLUSTER_ACTIONS_SETTING.get(requestedActions); + } + + /** + * Returns index actions requested by this plugin in the plugin-permissions.yml file + * + * @return A list of index actions contained within the plugin-permissions.yml file. This method returns a map + * of index pattern -> list of actions that apply on the index pattern + */ + public Map> getIndexActions() { + final Map> indexActions = new HashMap<>(); + final Settings requestedIndexActionsGroup = INDEX_ACTIONS_SETTING.get(requestedActions); + if (!requestedIndexActionsGroup.keySet().isEmpty()) { + for (String indexPattern : requestedIndexActionsGroup.keySet()) { + List indexActionsForPattern = requestedIndexActionsGroup.getAsList(indexPattern); + indexActions.put(indexPattern, indexActionsForPattern); + } + } + return indexActions; + } + + /** + * Parses plugin-permissions.yml file. + */ + @SuppressWarnings("removal") + public static Settings parseRequestedActions(Path file) throws IOException { + return Settings.builder().loadFromPath(file).build(); + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -522,6 +597,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field("extended_plugins", extendedPlugins); builder.field("has_native_controller", hasNativeController); builder.field("optional_extended_plugins", optionalExtendedPlugins); + if (!Settings.EMPTY.equals(requestedActions)) { + builder.field("cluster.actions", getClusterActions()); + builder.field("index.actions", getIndexActions()); + } } builder.endObject(); diff --git a/server/src/main/java/org/opensearch/plugins/PluginsService.java b/server/src/main/java/org/opensearch/plugins/PluginsService.java index 0d522311ee649..c624f5e59968d 100644 --- a/server/src/main/java/org/opensearch/plugins/PluginsService.java +++ b/server/src/main/java/org/opensearch/plugins/PluginsService.java @@ -151,7 +151,8 @@ public PluginsService( pluginClass.getName(), null, Collections.emptyList(), - false + false, + Settings.EMPTY ); if (logger.isTraceEnabled()) { logger.trace("plugin loaded from classpath [{}]", pluginInfo); @@ -861,6 +862,10 @@ private String signatureMessage(final Class clazz) { ); } + public List> filterPluginTuples(Class type) { + return plugins.stream().filter(x -> type.isAssignableFrom(x.v2().getClass())).collect(Collectors.toList()); + } + public List filterPlugins(Class type) { return plugins.stream().filter(x -> type.isAssignableFrom(x.v2().getClass())).map(p -> ((T) p.v2())).collect(Collectors.toList()); } diff --git a/server/src/test/java/org/opensearch/identity/noop/NoopPluginSubjectTests.java b/server/src/test/java/org/opensearch/identity/noop/NoopPluginSubjectTests.java index 79c26a7eb790d..0d14a34e24770 100644 --- a/server/src/test/java/org/opensearch/identity/noop/NoopPluginSubjectTests.java +++ b/server/src/test/java/org/opensearch/identity/noop/NoopPluginSubjectTests.java @@ -8,16 +8,20 @@ package org.opensearch.identity.noop; +import org.opensearch.Version; +import org.opensearch.common.collect.Tuple; import org.opensearch.common.settings.Settings; import org.opensearch.identity.IdentityService; import org.opensearch.identity.NamedPrincipal; import org.opensearch.identity.PluginSubject; import org.opensearch.plugins.IdentityAwarePlugin; import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.PluginInfo; import org.opensearch.test.OpenSearchTestCase; import org.opensearch.threadpool.TestThreadPool; import org.opensearch.threadpool.ThreadPool; +import java.util.Collections; import java.util.List; import static org.hamcrest.Matchers.equalTo; @@ -41,7 +45,19 @@ public void testInitializeIdentityAwarePlugin() throws Exception { IdentityService identityService = new IdentityService(Settings.EMPTY, threadPool, List.of()); TestPlugin testPlugin = new TestPlugin(); - identityService.initializeIdentityAwarePlugins(List.of(testPlugin)); + final PluginInfo info = new PluginInfo( + "fake", + "foo", + "x.y.z", + Version.CURRENT, + "1.8", + testPlugin.getClass().getCanonicalName(), + "folder", + Collections.emptyList(), + false, + Settings.EMPTY + ); + identityService.initializeIdentityAwarePlugins(List.of(Tuple.tuple(info, testPlugin))); PluginSubject testPluginSubject = new NoopPluginSubject(threadPool); assertThat(testPlugin.getSubject().getPrincipal().getName(), equalTo(NamedPrincipal.UNAUTHENTICATED.getName())); diff --git a/server/src/test/java/org/opensearch/nodesinfo/NodeInfoStreamingTests.java b/server/src/test/java/org/opensearch/nodesinfo/NodeInfoStreamingTests.java index fba26b0c72e0e..c4f74e7e19ebf 100644 --- a/server/src/test/java/org/opensearch/nodesinfo/NodeInfoStreamingTests.java +++ b/server/src/test/java/org/opensearch/nodesinfo/NodeInfoStreamingTests.java @@ -179,7 +179,8 @@ private static NodeInfo createNodeInfo() { randomAlphaOfLengthBetween(3, 10), name, Collections.emptyList(), - randomBoolean() + randomBoolean(), + Settings.EMPTY ) ); } @@ -197,7 +198,8 @@ private static NodeInfo createNodeInfo() { randomAlphaOfLengthBetween(3, 10), name, Collections.emptyList(), - randomBoolean() + randomBoolean(), + Settings.EMPTY ) ); } diff --git a/server/src/test/java/org/opensearch/plugins/PluginInfoTests.java b/server/src/test/java/org/opensearch/plugins/PluginInfoTests.java index 76294d85c64d4..b070726bca254 100644 --- a/server/src/test/java/org/opensearch/plugins/PluginInfoTests.java +++ b/server/src/test/java/org/opensearch/plugins/PluginInfoTests.java @@ -35,8 +35,11 @@ import com.fasterxml.jackson.core.JsonParseException; import org.opensearch.Version; +import org.opensearch.action.admin.cluster.health.ClusterHealthAction; import org.opensearch.action.admin.cluster.node.info.PluginsAndModules; +import org.opensearch.action.index.IndexAction; import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.json.JsonXContent; import org.opensearch.core.common.io.stream.ByteBufferStreamInput; import org.opensearch.core.xcontent.ToXContent; @@ -48,6 +51,7 @@ import java.nio.ByteBuffer; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -387,7 +391,8 @@ public void testSerialize() throws Exception { "dummyclass", "c", Collections.singletonList("foo"), - randomBoolean() + randomBoolean(), + Settings.EMPTY ); BytesStreamOutput output = new BytesStreamOutput(); info.writeTo(output); @@ -407,10 +412,12 @@ public void testToXContent() throws Exception { "dummyClass", "folder", Collections.emptyList(), - false + false, + Settings.EMPTY ); XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); String prettyPrint = info.toXContent(builder, ToXContent.EMPTY_PARAMS).prettyPrint().toString(); + prettyPrint = Arrays.stream(prettyPrint.split("\n")).map(String::trim).collect(Collectors.joining("")); assertTrue(prettyPrint.contains("\"name\" : \"fake\"")); assertTrue(prettyPrint.contains("\"version\" : \"dummy\"")); assertTrue(prettyPrint.contains("\"opensearch_version\" : \"" + Version.CURRENT)); @@ -422,6 +429,38 @@ public void testToXContent() throws Exception { assertTrue(prettyPrint.contains("\"has_native_controller\" : false")); } + public void testToXContentWithRequestedActions() throws Exception { + PluginInfo info = new PluginInfo( + "fake", + "foo", + "dummy", + Version.CURRENT, + "1.8", + "dummyClass", + "folder", + Collections.emptyList(), + false, + Settings.builder() + .putList("cluster.actions", List.of(ClusterHealthAction.NAME)) + .putList("index.actions.my-index", List.of(IndexAction.NAME)) + .build() + ); + XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); + String prettyPrint = info.toXContent(builder, ToXContent.EMPTY_PARAMS).prettyPrint().toString(); + prettyPrint = Arrays.stream(prettyPrint.split("\n")).map(String::trim).collect(Collectors.joining("")); + assertTrue(prettyPrint.contains("\"name\" : \"fake\"")); + assertTrue(prettyPrint.contains("\"version\" : \"dummy\"")); + assertTrue(prettyPrint.contains("\"opensearch_version\" : \"" + Version.CURRENT)); + assertTrue(prettyPrint.contains("\"java_version\" : \"1.8\"")); + assertTrue(prettyPrint.contains("\"description\" : \"foo\"")); + assertTrue(prettyPrint.contains("\"classname\" : \"dummyClass\"")); + assertTrue(prettyPrint.contains("\"custom_foldername\" : \"folder\"")); + assertTrue(prettyPrint.contains("\"extended_plugins\" : [ ]")); + assertTrue(prettyPrint.contains("\"has_native_controller\" : false")); + assertTrue(prettyPrint.contains("\"cluster.actions\" : [\"" + ClusterHealthAction.NAME + "\"]")); + assertTrue(prettyPrint.contains("\"index.actions\" : {\"my-index\" : [\"" + IndexAction.NAME + "\"]}")); + } + public void testPluginListSorted() { List plugins = new ArrayList<>(); plugins.add(new PluginInfo("c", "foo", "dummy", Version.CURRENT, "1.8", "dummyclass", Collections.emptyList(), randomBoolean())); @@ -645,7 +684,8 @@ public void testhMultipleOpenSearchRangesInConstructor() throws Exception { "dummyclass", null, Collections.emptyList(), - randomBoolean() + randomBoolean(), + Settings.EMPTY ) ); assertThat(e.getMessage(), containsString("Exactly one range is allowed to be specified in dependencies for the plugin")); diff --git a/server/src/test/java/org/opensearch/plugins/PluginsServiceTests.java b/server/src/test/java/org/opensearch/plugins/PluginsServiceTests.java index e9dc2207510e0..57e1e3d692e5d 100644 --- a/server/src/test/java/org/opensearch/plugins/PluginsServiceTests.java +++ b/server/src/test/java/org/opensearch/plugins/PluginsServiceTests.java @@ -757,7 +757,8 @@ public void testCompatibleOpenSearchVersionRange() { "FakePlugin", null, Collections.emptyList(), - false + false, + Settings.EMPTY ); PluginsService.verifyCompatibility(info); } @@ -779,7 +780,8 @@ public void testIncompatibleOpenSearchVersionRange() { "FakePlugin", null, Collections.emptyList(), - false + false, + Settings.EMPTY ); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> PluginsService.verifyCompatibility(info)); assertThat(e.getMessage(), containsString("was built for OpenSearch version ")); @@ -1212,7 +1214,8 @@ private PluginInfo getPluginInfoWithWithSemverRange(String semverRange) { "FakePlugin", null, Collections.emptyList(), - false + false, + Settings.EMPTY ); }