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"