diff --git a/src/integrationTest/java/org/opensearch/security/privileges/KibanaServerRoleIssue6103IntegTest.java b/src/integrationTest/java/org/opensearch/security/privileges/KibanaServerRoleIssue6103IntegTest.java new file mode 100644 index 0000000000..0302a64eb0 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/KibanaServerRoleIssue6103IntegTest.java @@ -0,0 +1,91 @@ +/* + * 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. + */ + +package org.opensearch.security.privileges; + +import java.io.IOException; + +import org.junit.ClassRule; +import org.junit.Test; + +import org.opensearch.client.RestHighLevelClient; +import org.opensearch.client.indices.GetIndexRequest; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.opensearch.client.RequestOptions.DEFAULT; +import static org.opensearch.core.rest.RestStatus.FORBIDDEN; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy; +import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.statusException; + +/** + * End-to-end regression test for https://github.com/opensearch-project/security/issues/6103 + */ +public class KibanaServerRoleIssue6103IntegTest { + + private static final TestSecurityConfig.User DASHBOARDS_SERVER_USER = new TestSecurityConfig.User("kibanaserver").password( + "kibanaserver-secret" + ); + + /** + * Mirrors the pre-fix catch-all permissions: alias management only, no {@code indices:admin/get}. + */ + private static final TestSecurityConfig.Role LEGACY_DASHBOARDS_SERVER_ROLE = new TestSecurityConfig.Role("legacy_dashboards_server") + .clusterPermissions( + "cluster_composite_ops", + "cluster_monitor", + "indices:admin/index_template*", + "indices:admin/template*", + "indices:data/read/scroll*", + "manage_point_in_time" + ) + .indexPermissions("indices_all") + .on( + ".kibana", + ".opensearch_dashboards", + ".kibana-6", + ".opensearch_dashboards-6", + ".kibana_*", + ".opensearch_dashboards_*", + ".tasks", + ".management-beats*" + ) + .indexPermissions("indices:admin/aliases*") + .on("*"); + + private static final TestSecurityConfig.User LEGACY_DASHBOARDS_SERVER_USER = new TestSecurityConfig.User("legacy-kibanaserver") + .password("legacy-kibanaserver-secret") + .roles(LEGACY_DASHBOARDS_SERVER_ROLE); + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(DASHBOARDS_SERVER_USER, LEGACY_DASHBOARDS_SERVER_USER) + .rolesMapping(new TestSecurityConfig.RoleMapping("kibana_server").reserved(true).users(DASHBOARDS_SERVER_USER.getName())) + .build(); + + @Test + public void kibanaServerUser_canCheckIfKibanaIndexExistsBeforeBootstrap() throws IOException { + try (RestHighLevelClient client = cluster.getRestHighLevelClient(DASHBOARDS_SERVER_USER)) { + boolean exists = client.indices().exists(new GetIndexRequest(".kibana"), DEFAULT); + + assertThat(exists, is(false)); + } + } + + @Test + public void legacyDashboardsServerRole_cannotCheckIfKibanaIndexExistsBeforeBootstrap() throws IOException { + try (RestHighLevelClient client = cluster.getRestHighLevelClient(LEGACY_DASHBOARDS_SERVER_USER)) { + assertThatThrownBy(() -> client.indices().exists(new GetIndexRequest(".kibana"), DEFAULT), statusException(FORBIDDEN)); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/privileges/KibanaServerRoleIssue6103Test.java b/src/integrationTest/java/org/opensearch/security/privileges/KibanaServerRoleIssue6103Test.java new file mode 100644 index 0000000000..73c9ba4c44 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/KibanaServerRoleIssue6103Test.java @@ -0,0 +1,139 @@ +/* + * 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. + */ + +package org.opensearch.security.privileges; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Set; + +import org.apache.commons.io.IOUtils; +import org.junit.Test; + +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.ResolvedIndices; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; +import org.opensearch.security.privileges.dlsfls.FieldMasking; +import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.securityconf.FlattenedActionGroups; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.RoleV7; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isAllowed; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isForbidden; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.missingPrivileges; +import static org.opensearch.security.util.MockIndexMetadataBuilder.indices; +import static org.opensearch.security.util.MockPrivilegeEvaluationContextBuilder.ctx; + +/** + * Regression test for https://github.com/opensearch-project/security/issues/6103 + */ +public class KibanaServerRoleIssue6103Test { + + @Test + public void kibanaServerRole_hasIndicesAdminGet_onCatchAllIndexPattern() throws Exception { + RoleBasedActionPrivileges actionPrivileges = loadStaticActionPrivileges(); + + PrivilegesEvaluatorResponse response = actionPrivileges.hasIndexPrivilege( + ctx().roles("kibana_server").actionPrivileges(actionPrivileges).get(), + Set.of("indices:admin/get"), + ResolvedIndices.of("some-unrelated-index") + ); + + assertThat(response, isAllowed()); + } + + @Test + public void kibanaServerRole_hasIndicesAdminGet_onKibanaIndex() throws Exception { + RoleBasedActionPrivileges actionPrivileges = loadStaticActionPrivileges(); + + PrivilegesEvaluatorResponse response = actionPrivileges.hasIndexPrivilege( + ctx().roles("kibana_server").actionPrivileges(actionPrivileges).get(), + Set.of("indices:admin/get"), + ResolvedIndices.of(".kibana_1") + ); + + assertThat(response, isAllowed()); + } + + @Test + public void kibanaServerRole_hasIndicesAdminGet_forAnyIndex_whenKibanaNotYetCreated() throws Exception { + RoleBasedActionPrivileges actionPrivileges = loadStaticActionPrivileges(); + ClusterState clusterState = ClusterState.builder(ClusterState.EMPTY_STATE).metadata(indices().build()).build(); + + PrivilegesEvaluatorResponse response = actionPrivileges.hasIndexPrivilegeForAnyIndex( + ctx().roles("kibana_server").actionPrivileges(actionPrivileges).clusterState(clusterState).get(), + Set.of("indices:admin/get") + ); + + assertThat(response, isAllowed()); + } + + @Test + public void kibanaServerRole_hasIndicesAdminGet_onDotKibanaAliasWhenExists() throws Exception { + RoleBasedActionPrivileges actionPrivileges = loadStaticActionPrivileges(); + ClusterState clusterState = ClusterState.builder(ClusterState.EMPTY_STATE) + .metadata(indices().index(".kibana_1").alias(".kibana").of(".kibana_1").build()) + .build(); + + PrivilegesEvaluatorResponse response = actionPrivileges.hasIndexPrivilege( + ctx().roles("kibana_server").actionPrivileges(actionPrivileges).clusterState(clusterState).get(), + Set.of("indices:admin/get"), + ResolvedIndices.of(".kibana") + ); + + assertThat(response, isAllowed()); + } + + @Test + public void kibanaServerRole_cannotWriteUnrelatedIndex() throws Exception { + RoleBasedActionPrivileges actionPrivileges = loadStaticActionPrivileges(); + + PrivilegesEvaluatorResponse response = actionPrivileges.hasIndexPrivilege( + ctx().roles("kibana_server").actionPrivileges(actionPrivileges).get(), + Set.of("indices:data/write/index"), + ResolvedIndices.of("some-unrelated-index") + ); + + assertThat(response, isForbidden()); + assertThat(response, missingPrivileges("indices:data/write/index")); + } + + private static RoleBasedActionPrivileges loadStaticActionPrivileges() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( + testResource("/static_config/static_roles.yml"), + CType.ROLES + ); + FlattenedActionGroups actionGroups = new FlattenedActionGroups( + SecurityDynamicConfiguration.fromYaml(testResource("/static_config/static_action_groups.yml"), CType.ACTIONGROUPS) + ); + + return new RoleBasedActionPrivileges( + new CompiledRoles(roles, actionGroups, NamedXContentRegistry.EMPTY, FieldMasking.Config.DEFAULT, false), + org.opensearch.security.privileges.actionlevel.RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); + } + + private static String testResource(String fileName) throws IOException { + InputStream in = DynamicConfigFactory.class.getResourceAsStream(fileName); + + if (in == null) { + throw new FileNotFoundException("could not find " + fileName); + } + + return IOUtils.toString(in, StandardCharsets.UTF_8); + } +} diff --git a/src/main/resources/static_config/static_roles.yml b/src/main/resources/static_config/static_roles.yml index c7820ab627..82f5ecfed7 100644 --- a/src/main/resources/static_config/static_roles.yml +++ b/src/main/resources/static_config/static_roles.yml @@ -116,6 +116,7 @@ kibana_server: - "*" allowed_actions: - "indices:admin/aliases*" + - "indices:admin/get" logstash: reserved: true