diff --git a/distribution/tools/plugin-cli/src/main/java/org/opensearch/plugins/InstallPluginCommand.java b/distribution/tools/plugin-cli/src/main/java/org/opensearch/plugins/InstallPluginCommand.java index 838d6e22a37bd..b4e568313f7b4 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/opensearch/plugins/InstallPluginCommand.java +++ b/distribution/tools/plugin-cli/src/main/java/org/opensearch/plugins/InstallPluginCommand.java @@ -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; @@ -104,6 +105,9 @@ 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; /** * A command for the plugin cli to install a plugin into opensearch. @@ -880,7 +884,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> 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/test/java/org/opensearch/plugins/InstallPluginCommandTests.java b/distribution/tools/plugin-cli/src/test/java/org/opensearch/plugins/InstallPluginCommandTests.java index c264788df20e8..268beb228ec19 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/opensearch/plugins/InstallPluginCommandTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/opensearch/plugins/InstallPluginCommandTests.java @@ -263,6 +263,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( @@ -286,6 +291,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( @@ -326,11 +356,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); } @@ -1540,43 +1584,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)); } } @@ -1586,7 +1624,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 2da788242a745..1b1290527d4a2 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 @@ -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; @@ -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 e74fca60b0201..3e1ce4a6a7f16 100644 --- a/server/src/main/java/org/opensearch/node/Node.java +++ b/server/src/main/java/org/opensearch/node/Node.java @@ -91,6 +91,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; @@ -211,6 +212,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; @@ -1025,8 +1027,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 b6030f4ded5e5..35ebd3d82f852 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.bootstrap.JarHell; import org.opensearch.common.annotation.PublicApi; +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; @@ -87,6 +101,7 @@ public class PluginInfo implements Writeable, ToXContentObject { private final String customFolderName; private final List extendedPlugins; private final boolean hasNativeController; + private final Settings requestedActions; /** * Construct plugin info. @@ -110,7 +125,8 @@ public PluginInfo( String classname, String customFolderName, List extendedPlugins, - boolean hasNativeController + boolean hasNativeController, + Settings requestedActions ) { this( name, @@ -121,7 +137,8 @@ public PluginInfo( classname, customFolderName, extendedPlugins, - hasNativeController + hasNativeController, + requestedActions ); } @@ -134,7 +151,8 @@ public PluginInfo( String classname, String customFolderName, List extendedPlugins, - boolean hasNativeController + boolean hasNativeController, + Settings requestedActions ) { this.name = name; this.description = description; @@ -151,6 +169,7 @@ public PluginInfo( this.customFolderName = customFolderName; this.extendedPlugins = Collections.unmodifiableList(extendedPlugins); this.hasNativeController = hasNativeController; + this.requestedActions = requestedActions; } /** @@ -184,7 +203,8 @@ public PluginInfo( classname, null /* customFolderName */, extendedPlugins, - hasNativeController + hasNativeController, + Settings.EMPTY /* requestedActions */ ); } @@ -209,6 +229,11 @@ public PluginInfo(final StreamInput in) throws IOException { this.customFolderName = in.readString(); this.extendedPlugins = in.readStringList(); this.hasNativeController = in.readBoolean(); + if (in.getVersion().onOrAfter(Version.CURRENT)) { + this.requestedActions = Settings.readSettingsFromStream(in); + } else { + this.requestedActions = Settings.EMPTY; + } } @Override @@ -234,6 +259,9 @@ This works for currently supported range notations (=,~) } out.writeStringCollection(extendedPlugins); out.writeBoolean(hasNativeController); + if (out.getVersion().onOrAfter(Version.CURRENT)) { + Settings.writeSettingsToStream(requestedActions, out); + } } /** @@ -363,6 +391,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 = PluginSecurity.parseRequestedActions(actions); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return new PluginInfo( name, description, @@ -372,7 +411,8 @@ public static PluginInfo readFromProperties(final Path path) throws IOException classname, customFolderName, extendedPlugins, - hasNativeController + hasNativeController, + requestedActions ); } @@ -480,6 +520,33 @@ 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; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -493,6 +560,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field("custom_foldername", customFolderName); builder.field("extended_plugins", extendedPlugins); builder.field("has_native_controller", hasNativeController); + 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/PluginSecurity.java b/server/src/main/java/org/opensearch/plugins/PluginSecurity.java index 1bf8642d1112f..3420dd5d7f8ed 100644 --- a/server/src/main/java/org/opensearch/plugins/PluginSecurity.java +++ b/server/src/main/java/org/opensearch/plugins/PluginSecurity.java @@ -36,6 +36,7 @@ import org.opensearch.cli.Terminal; import org.opensearch.cli.Terminal.Verbosity; import org.opensearch.cli.UserException; +import org.opensearch.common.settings.Settings; import org.opensearch.common.util.io.IOUtils; import java.io.IOException; @@ -51,6 +52,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 +66,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 +85,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); } } @@ -171,4 +224,12 @@ public static Set parsePermissions(Path file, Path tmpDir) throws IOExce } return Collections.list(actualPermissions.elements()).stream().map(PluginSecurity::formatPermission).collect(Collectors.toSet()); } + + /** + * Parses plugin-permissions.yml file. + */ + @SuppressWarnings("removal") + public static Settings parseRequestedActions(Path file) throws IOException { + return Settings.builder().loadFromPath(file).build(); + } } diff --git a/server/src/main/java/org/opensearch/plugins/PluginsService.java b/server/src/main/java/org/opensearch/plugins/PluginsService.java index f08c9c738f1b4..3833d69792332 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); @@ -817,6 +818,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 12c7dc870c104..192f8eda305a0 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; @@ -47,6 +50,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; @@ -360,7 +364,8 @@ public void testSerialize() throws Exception { "dummyclass", "c", Collections.singletonList("foo"), - randomBoolean() + randomBoolean(), + Settings.EMPTY ); BytesStreamOutput output = new BytesStreamOutput(); info.writeTo(output); @@ -380,10 +385,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)); @@ -395,6 +402,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())); @@ -618,7 +657,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 bd9ee33856f14..2698471f9c635 100644 --- a/server/src/test/java/org/opensearch/plugins/PluginsServiceTests.java +++ b/server/src/test/java/org/opensearch/plugins/PluginsServiceTests.java @@ -730,7 +730,8 @@ public void testCompatibleOpenSearchVersionRange() { "FakePlugin", null, Collections.emptyList(), - false + false, + Settings.EMPTY ); PluginsService.verifyCompatibility(info); } @@ -752,7 +753,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 ")); @@ -1113,7 +1115,8 @@ private PluginInfo getPluginInfoWithWithSemverRange(String semverRange) { "FakePlugin", null, Collections.emptyList(), - false + false, + Settings.EMPTY ); }