|
| 1 | +/* |
| 2 | + * SPDX-License-Identifier: Apache-2.0 |
| 3 | + * |
| 4 | + * The OpenSearch Contributors require contributions made to |
| 5 | + * this file be licensed under the Apache-2.0 license or a |
| 6 | + * compatible open source license. |
| 7 | + * |
| 8 | + * Modifications Copyright OpenSearch Contributors. See |
| 9 | + * GitHub history for details. |
| 10 | + */ |
| 11 | + |
| 12 | +package org.opensearch.security.privileges.int_tests; |
| 13 | + |
| 14 | +import java.util.ArrayList; |
| 15 | +import java.util.Collection; |
| 16 | +import java.util.List; |
| 17 | + |
| 18 | +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; |
| 19 | +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; |
| 20 | +import com.google.common.collect.ImmutableList; |
| 21 | +import org.apache.logging.log4j.LogManager; |
| 22 | +import org.apache.logging.log4j.Logger; |
| 23 | +import org.junit.After; |
| 24 | +import org.junit.Before; |
| 25 | +import org.junit.Test; |
| 26 | +import org.junit.runner.RunWith; |
| 27 | + |
| 28 | +import org.opensearch.script.mustache.MustacheModulePlugin; |
| 29 | +import org.opensearch.test.framework.TestSecurityConfig; |
| 30 | +import org.opensearch.test.framework.cluster.LocalCluster; |
| 31 | +import org.opensearch.test.framework.cluster.TestRestClient; |
| 32 | +import org.opensearch.test.framework.data.TestIndex; |
| 33 | + |
| 34 | +import static org.hamcrest.MatcherAssert.assertThat; |
| 35 | +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; |
| 36 | +import static org.opensearch.test.framework.matcher.RestIndexMatchers.IndexMatcher; |
| 37 | +import static org.opensearch.test.framework.matcher.RestIndexMatchers.OnUserIndexMatcher.limitedToNone; |
| 38 | +import static org.opensearch.test.framework.matcher.RestMatchers.isForbidden; |
| 39 | +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; |
| 40 | + |
| 41 | +/** |
| 42 | + * Integration tests for index authorization when all indices are closed. |
| 43 | + * Tests operations that should work even when getAllIndicesResolved() returns empty. |
| 44 | + * This test suite verifies that operations like _cat/recovery can still be authorized |
| 45 | + * correctly when all indices are closed, which results in an empty resolved indices set. |
| 46 | + * <p> |
| 47 | + * We need this dedicated test suite because closing indices triggers cluster state changes, |
| 48 | + * affecting some threads and losing access to the RandomizedContext. Therefore, we isolate |
| 49 | + * these tests to avoid interference with other tests. |
| 50 | + */ |
| 51 | +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) |
| 52 | +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) |
| 53 | +public class IndexAuthorizationWithClosedIndicesIntTests { |
| 54 | + |
| 55 | + private static final Logger log = LogManager.getLogger(IndexAuthorizationWithClosedIndicesIntTests.class); |
| 56 | + |
| 57 | + // ------------------------------------------------------------------------------------------------------- |
| 58 | + // Test indices used by this test suite |
| 59 | + // ------------------------------------------------------------------------------------------------------- |
| 60 | + |
| 61 | + static final TestIndex index_a1 = TestIndex.name("index_a1").documentCount(100).seed(1).build(); |
| 62 | + static final TestIndex index_a2 = TestIndex.name("index_a2").documentCount(110).seed(2).build(); |
| 63 | + static final TestIndex index_b1 = TestIndex.name("index_b1").documentCount(51).seed(3).build(); |
| 64 | + static final TestIndex index_b2 = TestIndex.name("index_b2").documentCount(52).seed(4).build(); |
| 65 | + |
| 66 | + /** |
| 67 | + * This key identifies assertion reference data for index search/read permissions of individual users. |
| 68 | + */ |
| 69 | + static final TestSecurityConfig.User.MetadataKey<IndexMatcher> READ = new TestSecurityConfig.User.MetadataKey<>( |
| 70 | + "read", |
| 71 | + IndexMatcher.class |
| 72 | + ); |
| 73 | + |
| 74 | + /** |
| 75 | + * A user with indices:monitor/recovery permission on all indices to verify that it succeeds in case all indices are closed. |
| 76 | + */ |
| 77 | + static final TestSecurityConfig.User LIMITED_USER_RECOVERY = new TestSecurityConfig.User("limited_user_recovery").description( |
| 78 | + "indices:monitor/recovery on *" |
| 79 | + ) |
| 80 | + .roles( |
| 81 | + new TestSecurityConfig.Role("r1").clusterPermissions("cluster_composite_ops_ro", "cluster_monitor") |
| 82 | + .indexPermissions("indices:monitor/recovery") |
| 83 | + .on("*") |
| 84 | + ) |
| 85 | + .reference(READ, limitedToNone()); |
| 86 | + |
| 87 | + /** |
| 88 | + * A user with indices:monitor/recovery permission on specific indices (not wildcard) to test empty resolved indices handling. |
| 89 | + * This user is crucial for testing the empty resolved indices scenario because it doesn't have wildcard privileges, |
| 90 | + * which means checkWildcardIndexPrivilegesOnWellKnownActions will return null, forcing execution to reach |
| 91 | + * the CheckTable.create() call with empty allIndicesResolved. |
| 92 | + */ |
| 93 | + static final TestSecurityConfig.User LIMITED_USER_RECOVERY_SPECIFIC = new TestSecurityConfig.User("limited_user_recovery_specific") |
| 94 | + .description("indices:monitor/recovery on index_a*") |
| 95 | + .roles( |
| 96 | + new TestSecurityConfig.Role("r1").clusterPermissions("cluster_composite_ops_ro", "cluster_monitor") |
| 97 | + .indexPermissions("indices:monitor/recovery") |
| 98 | + .on("index_a*") |
| 99 | + ) |
| 100 | + .reference(READ, limitedToNone()); |
| 101 | + |
| 102 | + /** |
| 103 | + * A user with "*" privileges on "*"; as it is a regular user, they are still subject to system index |
| 104 | + * restrictions and similar things. |
| 105 | + */ |
| 106 | + static final TestSecurityConfig.User UNLIMITED_USER = new TestSecurityConfig.User("unlimited_user")// |
| 107 | + .description("unlimited") |
| 108 | + .roles(new TestSecurityConfig.Role("r1").clusterPermissions("*").indexPermissions("*").on("*")) |
| 109 | + .reference(READ, limitedToNone()); |
| 110 | + |
| 111 | + /** |
| 112 | + * The SUPER_UNLIMITED_USER authenticates with an admin cert, which will cause all access control code to be skipped. |
| 113 | + * This serves as a base for comparison with the default behavior. |
| 114 | + */ |
| 115 | + static final TestSecurityConfig.User SUPER_UNLIMITED_USER = new TestSecurityConfig.User("super_unlimited_user")// |
| 116 | + .description("super unlimited (admin cert)") |
| 117 | + .adminCertUser() |
| 118 | + .reference(READ, limitedToNone()); |
| 119 | + |
| 120 | + /** |
| 121 | + * A user with no index privileges to test that operations are properly denied. |
| 122 | + */ |
| 123 | + static final TestSecurityConfig.User LIMITED_USER_NONE = new TestSecurityConfig.User("limited_user_none").description( |
| 124 | + "no index privileges" |
| 125 | + ) |
| 126 | + .roles(new TestSecurityConfig.Role("r2").clusterPermissions("cluster_composite_ops_ro", "cluster_monitor")) |
| 127 | + .reference(READ, limitedToNone()); |
| 128 | + |
| 129 | + static final List<TestSecurityConfig.User> USERS = ImmutableList.of( |
| 130 | + LIMITED_USER_NONE, |
| 131 | + LIMITED_USER_RECOVERY, |
| 132 | + LIMITED_USER_RECOVERY_SPECIFIC, |
| 133 | + SUPER_UNLIMITED_USER, |
| 134 | + UNLIMITED_USER |
| 135 | + ); |
| 136 | + |
| 137 | + static LocalCluster.Builder clusterBuilder() { |
| 138 | + return new LocalCluster.Builder().singleNode() |
| 139 | + .authc(AUTHC_HTTPBASIC_INTERNAL) |
| 140 | + .users(USERS) |
| 141 | + .indices(index_a1, index_a2, index_b1, index_b2) |
| 142 | + .plugin(MustacheModulePlugin.class); |
| 143 | + } |
| 144 | + |
| 145 | + private final TestSecurityConfig.User user; |
| 146 | + private final LocalCluster cluster; |
| 147 | + private final ClusterConfig clusterConfig; |
| 148 | + |
| 149 | + @ParametersFactory(shuffle = false, argumentFormatting = "%1$s, %3$s") |
| 150 | + public static Collection<Object[]> params() { |
| 151 | + List<Object[]> result = new ArrayList<>(); |
| 152 | + |
| 153 | + for (ClusterConfig clusterConfig : ClusterConfig.values()) { |
| 154 | + for (TestSecurityConfig.User user : USERS) { |
| 155 | + result.add(new Object[] { clusterConfig, user, user.getDescription() }); |
| 156 | + } |
| 157 | + } |
| 158 | + return result; |
| 159 | + } |
| 160 | + |
| 161 | + public IndexAuthorizationWithClosedIndicesIntTests(ClusterConfig clusterConfig, TestSecurityConfig.User user, String description) |
| 162 | + throws Exception { |
| 163 | + this.user = user; |
| 164 | + this.cluster = clusterConfig.cluster(IndexAuthorizationWithClosedIndicesIntTests::clusterBuilder); |
| 165 | + this.clusterConfig = clusterConfig; |
| 166 | + } |
| 167 | + |
| 168 | + @Before |
| 169 | + public void setup() { |
| 170 | + // In order to test index authorization when all indices are closed, we close all indices, including hidden ones. |
| 171 | + try (TestRestClient adminClient = cluster.getAdminCertRestClient()) { |
| 172 | + TestRestClient.HttpResponse hiddenCloseResponse = adminClient.post("_all/_close?expand_wildcards=all"); |
| 173 | + assertThat(hiddenCloseResponse, isOk()); |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + @After |
| 178 | + public void teardown() { |
| 179 | + try (TestRestClient adminClient = cluster.getAdminCertRestClient()) { |
| 180 | + try { |
| 181 | + adminClient.post("_all/_open?expand_wildcards=all"); |
| 182 | + } catch (Exception e) { |
| 183 | + log.warn("Error reopening all indices during teardown", e); |
| 184 | + } |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + /** |
| 189 | + * Tests _cat/recovery operation succeeds when all indices are closed. This verifies that |
| 190 | + * the empty resolved indices check in RuntimeOptimizedActionPrivileges works correctly. |
| 191 | + */ |
| 192 | + @Test |
| 193 | + public void cat_recovery_allIndicesClosed() throws Exception { |
| 194 | + try (TestRestClient restClient = cluster.getRestClient(user)) { |
| 195 | + TestRestClient.HttpResponse httpResponse = restClient.get("_cat/recovery"); |
| 196 | + |
| 197 | + if (user == UNLIMITED_USER || user == SUPER_UNLIMITED_USER) { |
| 198 | + assertThat(httpResponse, isOk()); |
| 199 | + } else if (user == LIMITED_USER_RECOVERY) { |
| 200 | + // This user has indices:monitor/recovery on wildcard "*". |
| 201 | + // checkWildcardIndexPrivilegesOnWellKnownActions() == null should handle this. |
| 202 | + assertThat(httpResponse, isOk()); |
| 203 | + } else if (user == LIMITED_USER_RECOVERY_SPECIFIC) { |
| 204 | + // This user has indices:monitor/recovery on index_a* but not wildcard. |
| 205 | + // When all indices are closed, getAllIndicesResolved() returns empty, |
| 206 | + // then we must ensure that the empty resolved indices case is handled correctly. |
| 207 | + assertThat(httpResponse, isOk()); |
| 208 | + } else if (user == LIMITED_USER_NONE) { |
| 209 | + // This user has no permission at the same time as all indices are closed. |
| 210 | + assertThat(httpResponse, isForbidden("/error/root_cause/0/reason", "no permissions for [indices:monitor/recovery]")); |
| 211 | + } else { |
| 212 | + assertThat(httpResponse, isForbidden("/error/root_cause/0/reason", "no permissions for [indices:monitor/recovery]")); |
| 213 | + } |
| 214 | + } |
| 215 | + } |
| 216 | +} |
0 commit comments