From b9c26e4b4bd0b6d88af83686cae8e74e09a19d60 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 26 Mar 2026 15:55:29 -0400 Subject: [PATCH 1/3] Add application-id to role schema to connect role to OpenSearch Dashboards menu item Signed-off-by: Craig Perkins --- .../src/main/resources/default-roles.yml | 2 + .../api/ApplicationPermissionsInfoTest.java | 117 ++++++++++++++ .../test/framework/TestSecurityConfig.java | 7 + .../security/OpenSearchSecurityPlugin.java | 8 + .../dlic/rest/api/RolesApiAction.java | 1 + .../ApplicationPermissionsInfoAction.java | 147 ++++++++++++++++++ .../security/securityconf/impl/v7/RoleV7.java | 11 ++ .../securityconf/impl/v7/RoleV7Test.java | 20 +++ .../test-role-with-application-id.yml | 3 + 9 files changed, 316 insertions(+) create mode 100644 src/integrationTest/java/org/opensearch/security/api/ApplicationPermissionsInfoTest.java create mode 100644 src/main/java/org/opensearch/security/rest/ApplicationPermissionsInfoAction.java create mode 100644 src/test/resources/test-role-with-application-id.yml 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..eacaf8797b --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/api/ApplicationPermissionsInfoTest.java @@ -0,0 +1,117 @@ +/* + * 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"; + + static final Role ISM_READ_ROLE = new Role("ism_read_access").applicationId("index-management") + .clusterPermissions("cluster:admin/opendistro/ism/get"); + + static final Role ISM_FULL_ROLE = new Role("ism_full_access").applicationId("index-management") + .clusterPermissions("cluster:admin/opendistro/ism/*"); + + static final Role ALERTING_READ_ROLE = new Role("alerting_read_access").applicationId("alerting") + .clusterPermissions("cluster:admin/opendistro/alerting/get"); + + static final Role NO_APP_ROLE = new Role("custom_role_no_app").clusterPermissions("cluster:monitor/health"); + + 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(ISM_USER, MULTI_APP_USER, NO_APP_USER) + .roles(ISM_READ_ROLE, ISM_FULL_ROLE, ALERTING_READ_ROLE, NO_APP_ROLE) + .build(); + + @Test + public void allAccessUserGetsWildcard() throws Exception { + try (TestRestClient client = localCluster.getRestClient(ADMIN_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..93352ddcc3 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -684,6 +684,7 @@ public static class Role implements ToXContentObject { private Boolean hidden; private Boolean reserved; private String description; + private String applicationId; /** * 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 +733,11 @@ public Role reserved(boolean reserved) { return this; } + public Role applicationId(String applicationId) { + this.applicationId = applicationId; + 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. @@ -769,6 +775,7 @@ 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); 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..28f01e7b84 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).privilegesEvaluator(), + Objects.requireNonNull(cr), + Objects.requireNonNull(threadPool) + ) + ); handlers.add( new TenantInfoAction( settings, diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java index 4339a11d96..13c7fedde1 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java @@ -188,6 +188,7 @@ public Map allowedKeys() { .put("tenant_permissions", DataType.ARRAY) .put("index_permissions", DataType.ARRAY) .put("description", DataType.STRING) + .put("application_id", DataType.STRING) .build(); } }); diff --git a/src/main/java/org/opensearch/security/rest/ApplicationPermissionsInfoAction.java b/src/main/java/org/opensearch/security/rest/ApplicationPermissionsInfoAction.java new file mode 100644 index 0000000000..7066bb07f6 --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/ApplicationPermissionsInfoAction.java @@ -0,0 +1,147 @@ +/* + * 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.rest; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestRequest; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.client.node.NodeClient; + +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +/** + * Returns the list of application IDs the current user has access to, based on + * their mapped roles. Dashboards uses this to show/hide menu items. + * + *

Users 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 PrivilegesEvaluator privilegesEvaluator; + private final ConfigurationRepository configurationRepository; + private final ThreadContext threadContext; + + private static final String ALL_ACCESS_ROLE = "all_access"; + + public ApplicationPermissionsInfoAction( + final PrivilegesEvaluator privilegesEvaluator, + final ConfigurationRepository configurationRepository, + final ThreadPool threadPool + ) { + super(); + this.privilegesEvaluator = privilegesEvaluator; + 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 { + 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()) { + if (!mappedRoles.contains(entry.getKey())) { + continue; + } + String appId = entry.getValue().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" From f74759ce6f3aa233d3b379ae1fb5f359dcfae23a Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 26 Mar 2026 16:36:41 -0400 Subject: [PATCH 2/3] Fix tests Signed-off-by: Craig Perkins --- .../security/api/ApplicationPermissionsInfoTest.java | 12 ++++++++---- .../test/framework/TestSecurityConfig.java | 1 + .../security/OpenSearchSecurityPlugin.java | 2 +- .../rest/ApplicationPermissionsInfoAction.java | 9 ++++++--- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/api/ApplicationPermissionsInfoTest.java b/src/integrationTest/java/org/opensearch/security/api/ApplicationPermissionsInfoTest.java index eacaf8797b..efe2fc45b3 100644 --- a/src/integrationTest/java/org/opensearch/security/api/ApplicationPermissionsInfoTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/ApplicationPermissionsInfoTest.java @@ -32,6 +32,9 @@ 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") .clusterPermissions("cluster:admin/opendistro/ism/get"); @@ -43,6 +46,9 @@ public class ApplicationPermissionsInfoTest extends AbstractApiIntegrationTest { 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( @@ -53,13 +59,11 @@ public class ApplicationPermissionsInfoTest extends AbstractApiIntegrationTest { static final TestSecurityConfig.User NO_APP_USER = new TestSecurityConfig.User("no_app_user").roles(NO_APP_ROLE); @ClassRule - public static LocalCluster localCluster = clusterBuilder().users(ISM_USER, MULTI_APP_USER, NO_APP_USER) - .roles(ISM_READ_ROLE, ISM_FULL_ROLE, ALERTING_READ_ROLE, NO_APP_ROLE) - .build(); + 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(ADMIN_USER)) { + try (TestRestClient client = localCluster.getRestClient(ALL_ACCESS_USER)) { TestRestClient.HttpResponse response = client.get(ENDPOINT); assertThat(response, isOk()); diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 93352ddcc3..727bb29c17 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -752,6 +752,7 @@ public Role clone() { role.clusterPermissions.addAll(this.clusterPermissions); role.indexPermissions.addAll(this.indexPermissions); role.tenantPermissions.addAll(this.tenantPermissions); + role.applicationId = this.applicationId; return role; } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 28f01e7b84..13040538a6 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -656,7 +656,7 @@ public List getRestHandlers( ); handlers.add( new ApplicationPermissionsInfoAction( - Objects.requireNonNull(privilegesConfiguration).privilegesEvaluator(), + Objects.requireNonNull(privilegesConfiguration), Objects.requireNonNull(cr), Objects.requireNonNull(threadPool) ) diff --git a/src/main/java/org/opensearch/security/rest/ApplicationPermissionsInfoAction.java b/src/main/java/org/opensearch/security/rest/ApplicationPermissionsInfoAction.java index 7066bb07f6..f2a00d05cd 100644 --- a/src/main/java/org/opensearch/security/rest/ApplicationPermissionsInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/ApplicationPermissionsInfoAction.java @@ -26,10 +26,12 @@ import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -57,19 +59,19 @@ public class ApplicationPermissionsInfoAction extends BaseRestHandler { ); private final Logger log = LogManager.getLogger(this.getClass()); - private final PrivilegesEvaluator privilegesEvaluator; + private final PrivilegesConfiguration privilegesConfiguration; private final ConfigurationRepository configurationRepository; private final ThreadContext threadContext; private static final String ALL_ACCESS_ROLE = "all_access"; public ApplicationPermissionsInfoAction( - final PrivilegesEvaluator privilegesEvaluator, + final PrivilegesConfiguration privilegesConfiguration, final ConfigurationRepository configurationRepository, final ThreadPool threadPool ) { super(); - this.privilegesEvaluator = privilegesEvaluator; + this.privilegesConfiguration = privilegesConfiguration; this.configurationRepository = configurationRepository; this.threadContext = threadPool.getThreadContext(); } @@ -99,6 +101,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli builder.endObject(); response = new BytesRestResponse(RestStatus.FORBIDDEN, builder); } else { + PrivilegesEvaluator privilegesEvaluator = privilegesConfiguration.privilegesEvaluator(); PrivilegesEvaluationContext context = privilegesEvaluator.createContext(user, "dummy:action"); Set mappedRoles = context.getMappedRoles(); From e48419438434874a733ea7607abdd5d5fc2e9a11 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 26 Mar 2026 16:44:10 -0400 Subject: [PATCH 3/3] Enforce static Signed-off-by: Craig Perkins --- .../security/api/ApplicationPermissionsInfoTest.java | 3 +++ .../opensearch/test/framework/TestSecurityConfig.java | 8 ++++++++ .../security/dlic/rest/api/RolesApiAction.java | 1 - .../security/resources/PluginDefaultRolesHelper.java | 3 +++ .../rest/ApplicationPermissionsInfoAction.java | 10 +++++++--- 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/api/ApplicationPermissionsInfoTest.java b/src/integrationTest/java/org/opensearch/security/api/ApplicationPermissionsInfoTest.java index efe2fc45b3..8ccaa03017 100644 --- a/src/integrationTest/java/org/opensearch/security/api/ApplicationPermissionsInfoTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/ApplicationPermissionsInfoTest.java @@ -36,12 +36,15 @@ public class ApplicationPermissionsInfoTest extends AbstractApiIntegrationTest { 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"); diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 727bb29c17..762a85ac0e 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -685,6 +685,7 @@ public static class Role implements ToXContentObject { 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 @@ -738,6 +739,11 @@ public Role applicationId(String 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. @@ -753,6 +759,7 @@ public Role clone() { role.indexPermissions.addAll(this.indexPermissions); role.tenantPermissions.addAll(this.tenantPermissions); role.applicationId = this.applicationId; + role._static = this._static; return role; } @@ -777,6 +784,7 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params } 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/dlic/rest/api/RolesApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java index 13c7fedde1..4339a11d96 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java @@ -188,7 +188,6 @@ public Map allowedKeys() { .put("tenant_permissions", DataType.ARRAY) .put("index_permissions", DataType.ARRAY) .put("description", DataType.STRING) - .put("application_id", DataType.STRING) .build(); } }); 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(Set(); for (Map.Entry entry : rolesConfig.getCEntries().entrySet()) { + RoleV7 role = entry.getValue(); if (!mappedRoles.contains(entry.getKey())) { continue; } - String appId = entry.getValue().getApplication_id(); + // 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); }