diff --git a/sample-resource-plugin/src/main/resources/default-roles.yml b/sample-resource-plugin/src/main/resources/default-roles.yml
index 34b12ac01f..05e9d35338 100644
--- a/sample-resource-plugin/src/main/resources/default-roles.yml
+++ b/sample-resource-plugin/src/main/resources/default-roles.yml
@@ -8,6 +8,7 @@ sample_full_access:
hidden: false
static: true
description: "Provide full access to the sample resource plugin"
+ application_id: "sample-resource-plugin"
cluster_permissions:
- "cluster:admin/sample-resource-plugin/*"
@@ -16,5 +17,6 @@ sample_read_access:
hidden: false
static: true
description: "Provide read access to the sample resource plugin"
+ application_id: "sample-resource-plugin"
cluster_permissions:
- "cluster:admin/sample-resource-plugin/get"
diff --git a/src/integrationTest/java/org/opensearch/security/api/ApplicationPermissionsInfoTest.java b/src/integrationTest/java/org/opensearch/security/api/ApplicationPermissionsInfoTest.java
new file mode 100644
index 0000000000..8ccaa03017
--- /dev/null
+++ b/src/integrationTest/java/org/opensearch/security/api/ApplicationPermissionsInfoTest.java
@@ -0,0 +1,124 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ *
+ * Modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+
+package org.opensearch.security.api;
+
+import java.util.List;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import org.opensearch.test.framework.TestSecurityConfig;
+import org.opensearch.test.framework.TestSecurityConfig.Role;
+import org.opensearch.test.framework.cluster.LocalCluster;
+import org.opensearch.test.framework.cluster.TestRestClient;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.equalTo;
+import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX;
+import static org.opensearch.test.framework.matcher.RestMatchers.isOk;
+
+public class ApplicationPermissionsInfoTest extends AbstractApiIntegrationTest {
+
+ private static final String ENDPOINT = PLUGINS_PREFIX + "/applicationpermissions";
+
+ // Use predefined all_access from static_roles.yml so the role name is exactly "all_access"
+ static final Role ALL_ACCESS = new Role("all_access").isPredefined(true);
+
+ static final Role ISM_READ_ROLE = new Role("ism_read_access").applicationId("index-management")
+ ._static(true)
+ .clusterPermissions("cluster:admin/opendistro/ism/get");
+
+ static final Role ISM_FULL_ROLE = new Role("ism_full_access").applicationId("index-management")
+ ._static(true)
+ .clusterPermissions("cluster:admin/opendistro/ism/*");
+
+ static final Role ALERTING_READ_ROLE = new Role("alerting_read_access").applicationId("alerting")
+ ._static(true)
+ .clusterPermissions("cluster:admin/opendistro/alerting/get");
+
+ static final Role NO_APP_ROLE = new Role("custom_role_no_app").clusterPermissions("cluster:monitor/health");
+
+ // all_access user mapped to the predefined all_access role
+ static final TestSecurityConfig.User ALL_ACCESS_USER = new TestSecurityConfig.User("all_access_user").roles(ALL_ACCESS);
+
+ static final TestSecurityConfig.User ISM_USER = new TestSecurityConfig.User("ism_user").roles(ISM_READ_ROLE);
+
+ static final TestSecurityConfig.User MULTI_APP_USER = new TestSecurityConfig.User("multi_app_user").roles(
+ ISM_FULL_ROLE,
+ ALERTING_READ_ROLE
+ );
+
+ static final TestSecurityConfig.User NO_APP_USER = new TestSecurityConfig.User("no_app_user").roles(NO_APP_ROLE);
+
+ @ClassRule
+ public static LocalCluster localCluster = clusterBuilder().users(ALL_ACCESS_USER, ISM_USER, MULTI_APP_USER, NO_APP_USER).build();
+
+ @Test
+ public void allAccessUserGetsWildcard() throws Exception {
+ try (TestRestClient client = localCluster.getRestClient(ALL_ACCESS_USER)) {
+ TestRestClient.HttpResponse response = client.get(ENDPOINT);
+ assertThat(response, isOk());
+
+ JsonNode appIds = response.bodyAsJsonNode().get("application_ids");
+ assertThat(appIds.size(), equalTo(1));
+ assertThat(appIds.get(0).asText(), equalTo("*"));
+ }
+ }
+
+ @Test
+ public void userWithSingleAppRole() throws Exception {
+ try (TestRestClient client = localCluster.getRestClient(ISM_USER)) {
+ TestRestClient.HttpResponse response = client.get(ENDPOINT);
+ assertThat(response, isOk());
+
+ JsonNode appIds = response.bodyAsJsonNode().get("application_ids");
+ List ids = new java.util.ArrayList<>();
+ appIds.forEach(n -> ids.add(n.asText()));
+ assertThat(ids, containsInAnyOrder("index-management"));
+ }
+ }
+
+ @Test
+ public void userWithMultipleAppRoles() throws Exception {
+ try (TestRestClient client = localCluster.getRestClient(MULTI_APP_USER)) {
+ TestRestClient.HttpResponse response = client.get(ENDPOINT);
+ assertThat(response, isOk());
+
+ JsonNode appIds = response.bodyAsJsonNode().get("application_ids");
+ List ids = new java.util.ArrayList<>();
+ appIds.forEach(n -> ids.add(n.asText()));
+ assertThat(ids, containsInAnyOrder("index-management", "alerting"));
+ }
+ }
+
+ @Test
+ public void userWithNoAppRolesGetsEmptyList() throws Exception {
+ try (TestRestClient client = localCluster.getRestClient(NO_APP_USER)) {
+ TestRestClient.HttpResponse response = client.get(ENDPOINT);
+ assertThat(response, isOk());
+
+ JsonNode appIds = response.bodyAsJsonNode().get("application_ids");
+ assertThat(appIds.size(), equalTo(0));
+ }
+ }
+
+ @Test
+ public void responseIncludesUserName() throws Exception {
+ try (TestRestClient client = localCluster.getRestClient(ISM_USER)) {
+ TestRestClient.HttpResponse response = client.get(ENDPOINT);
+ assertThat(response, isOk());
+ assertThat(response.bodyAsJsonNode().get("user_name").asText(), equalTo("ism_user"));
+ }
+ }
+}
diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java
index 8b8be94fe5..762a85ac0e 100644
--- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java
+++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java
@@ -684,6 +684,8 @@ public static class Role implements ToXContentObject {
private Boolean hidden;
private Boolean reserved;
private String description;
+ private String applicationId;
+ private Boolean _static;
/**
* If this is true, the role is expected to be defined in static_roles.yml. Thus, it is not necessary to include it
@@ -732,6 +734,16 @@ public Role reserved(boolean reserved) {
return this;
}
+ public Role applicationId(String applicationId) {
+ this.applicationId = applicationId;
+ return this;
+ }
+
+ public Role _static(boolean _static) {
+ this._static = _static;
+ return this;
+ }
+
/**
* If this is true, the role is expected to be defined in static_roles.yml. Thus, it is not necessary to include it
* in the written role config.
@@ -746,6 +758,8 @@ public Role clone() {
role.clusterPermissions.addAll(this.clusterPermissions);
role.indexPermissions.addAll(this.indexPermissions);
role.tenantPermissions.addAll(this.tenantPermissions);
+ role.applicationId = this.applicationId;
+ role._static = this._static;
return role;
}
@@ -769,6 +783,8 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params
xContentBuilder.field("reserved", reserved);
}
if (!Strings.isNullOrEmpty(description)) xContentBuilder.field("description", description);
+ if (!Strings.isNullOrEmpty(applicationId)) xContentBuilder.field("application_id", applicationId);
+ if (_static != null) xContentBuilder.field("static", _static);
return xContentBuilder.endObject();
}
diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
index 9e342bcd8f..13040538a6 100644
--- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
+++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
@@ -193,6 +193,7 @@
import org.opensearch.security.resources.api.share.ShareTransportAction;
import org.opensearch.security.resources.settings.ResourceSharingFeatureFlagSetting;
import org.opensearch.security.resources.settings.ResourceSharingProtectedResourcesSetting;
+import org.opensearch.security.rest.ApplicationPermissionsInfoAction;
import org.opensearch.security.rest.DashboardsInfoAction;
import org.opensearch.security.rest.SecurityConfigUpdateAction;
import org.opensearch.security.rest.SecurityHealthAction;
@@ -653,6 +654,13 @@ public List getRestHandlers(
resourceSharingEnabledSetting
)
);
+ handlers.add(
+ new ApplicationPermissionsInfoAction(
+ Objects.requireNonNull(privilegesConfiguration),
+ Objects.requireNonNull(cr),
+ Objects.requireNonNull(threadPool)
+ )
+ );
handlers.add(
new TenantInfoAction(
settings,
diff --git a/src/main/java/org/opensearch/security/resources/PluginDefaultRolesHelper.java b/src/main/java/org/opensearch/security/resources/PluginDefaultRolesHelper.java
index 93f3da00e4..aebc275487 100644
--- a/src/main/java/org/opensearch/security/resources/PluginDefaultRolesHelper.java
+++ b/src/main/java/org/opensearch/security/resources/PluginDefaultRolesHelper.java
@@ -76,6 +76,9 @@ public static SecurityDynamicConfiguration loadDefaultRoles(SetUsers mapped to {@code all_access} receive {@code ["*"]}.
+ */
+public class ApplicationPermissionsInfoAction extends BaseRestHandler {
+
+ private static final List routes = addRoutesPrefix(
+ ImmutableList.of(new Route(GET, "/applicationpermissions")),
+ PLUGIN_ROUTE_PREFIX
+ );
+
+ private final Logger log = LogManager.getLogger(this.getClass());
+ private final PrivilegesConfiguration privilegesConfiguration;
+ private final ConfigurationRepository configurationRepository;
+ private final ThreadContext threadContext;
+
+ private static final String ALL_ACCESS_ROLE = "all_access";
+
+ public ApplicationPermissionsInfoAction(
+ final PrivilegesConfiguration privilegesConfiguration,
+ final ConfigurationRepository configurationRepository,
+ final ThreadPool threadPool
+ ) {
+ super();
+ this.privilegesConfiguration = privilegesConfiguration;
+ this.configurationRepository = configurationRepository;
+ this.threadContext = threadPool.getThreadContext();
+ }
+
+ @Override
+ public List routes() {
+ return routes;
+ }
+
+ @Override
+ public String getName() {
+ return "Application Permissions Info Action";
+ }
+
+ @Override
+ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
+ return channel -> {
+ XContentBuilder builder = channel.newBuilder();
+ BytesRestResponse response = null;
+
+ try {
+ final User user = (User) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER);
+
+ if (user == null) {
+ builder.startObject();
+ builder.field("error", "No user found in thread context");
+ builder.endObject();
+ response = new BytesRestResponse(RestStatus.FORBIDDEN, builder);
+ } else {
+ PrivilegesEvaluator privilegesEvaluator = privilegesConfiguration.privilegesEvaluator();
+ PrivilegesEvaluationContext context = privilegesEvaluator.createContext(user, "dummy:action");
+ Set mappedRoles = context.getMappedRoles();
+
+ Set applicationIds;
+
+ if (mappedRoles.contains(ALL_ACCESS_ROLE)) {
+ applicationIds = Set.of("*");
+ } else {
+ SecurityDynamicConfiguration rolesConfig = configurationRepository.getConfiguration(CType.ROLES);
+ DynamicConfigFactory.addStatics(rolesConfig);
+
+ applicationIds = new HashSet<>();
+ for (Map.Entry entry : rolesConfig.getCEntries().entrySet()) {
+ RoleV7 role = entry.getValue();
+ if (!mappedRoles.contains(entry.getKey())) {
+ continue;
+ }
+ // Only honour application_id on static (default) roles
+ if (!role.isStatic()) {
+ continue;
+ }
+ String appId = role.getApplication_id();
+ if (appId != null && !appId.isEmpty()) {
+ applicationIds.add(appId);
+ }
+ }
+ }
+
+ builder.startObject();
+ builder.field("user_name", user.getName());
+ builder.field("application_ids", applicationIds);
+ builder.endObject();
+ response = new BytesRestResponse(RestStatus.OK, builder);
+ }
+ } catch (final Exception e1) {
+ log.error("Error building application permissions response", e1);
+ builder = channel.newBuilder();
+ builder.startObject();
+ builder.field("error", e1.toString());
+ builder.endObject();
+ response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder);
+ } finally {
+ if (builder != null) {
+ builder.close();
+ }
+ }
+
+ channel.sendResponse(response);
+ };
+ }
+}
diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java
index 8859b4586e..3b57ae5578 100644
--- a/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java
+++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java
@@ -51,6 +51,7 @@ public class RoleV7 implements Hideable, StaticDefinable {
@JsonProperty(value = "static")
private boolean _static;
private String description;
+ private String application_id;
private List cluster_permissions = Collections.emptyList();
private List index_permissions = Collections.emptyList();
private List tenant_permissions = Collections.emptyList();
@@ -231,6 +232,14 @@ public void setDescription(String description) {
this.description = description;
}
+ public String getApplication_id() {
+ return application_id;
+ }
+
+ public void setApplication_id(String application_id) {
+ this.application_id = application_id;
+ }
+
public List getCluster_permissions() {
return cluster_permissions;
}
@@ -283,6 +292,8 @@ public String toString() {
+ _static
+ ", description="
+ description
+ + ", application_id="
+ + application_id
+ ", cluster_permissions="
+ cluster_permissions
+ ", index_permissions="
diff --git a/src/test/java/org/opensearch/security/securityconf/impl/v7/RoleV7Test.java b/src/test/java/org/opensearch/security/securityconf/impl/v7/RoleV7Test.java
index 335b93e4b5..ef8ccd5af0 100644
--- a/src/test/java/org/opensearch/security/securityconf/impl/v7/RoleV7Test.java
+++ b/src/test/java/org/opensearch/security/securityconf/impl/v7/RoleV7Test.java
@@ -116,4 +116,24 @@ public void testFromYmlFileWithMissingIndexPermissions() throws Exception {
assertTrue(role.getCluster_permissions().contains("cluster:monitor/health"));
assertTrue(role.getIndex_permissions().isEmpty());
}
+
+ @Test
+ public void testFromYmlFileWithApplicationId() throws Exception {
+ URL yamlUrl = RoleV7Test.class.getResource("/test-role-with-application-id.yml");
+
+ RoleV7 role = RoleV7.fromPluginPermissionsFile(yamlUrl);
+
+ assertNotNull(role);
+ assertEquals("sample-resource-plugin", role.getApplication_id());
+ assertEquals(1, role.getCluster_permissions().size());
+ assertTrue(role.getCluster_permissions().contains("cluster:admin/sample-resource-plugin/*"));
+ }
+
+ @Test
+ public void testApplicationIdIsNullWhenNotSet() throws Exception {
+ RoleV7 role = RoleV7.fromPluginPermissionsFile(testYamlUrl);
+
+ assertNotNull(role);
+ assertEquals(null, role.getApplication_id());
+ }
}
diff --git a/src/test/resources/test-role-with-application-id.yml b/src/test/resources/test-role-with-application-id.yml
new file mode 100644
index 0000000000..e5599ad243
--- /dev/null
+++ b/src/test/resources/test-role-with-application-id.yml
@@ -0,0 +1,3 @@
+cluster_permissions:
+ - "cluster:admin/sample-resource-plugin/*"
+application_id: "sample-resource-plugin"