diff --git a/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java b/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java index e8e15d1910..c1b3ce8927 100644 --- a/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java +++ b/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java @@ -67,6 +67,7 @@ import org.opensearch.action.search.SearchRequest; import org.opensearch.action.search.SearchResponse; import org.opensearch.action.search.SearchScrollRequest; +import org.opensearch.action.support.IndicesOptions; import org.opensearch.action.update.UpdateRequest; import org.opensearch.action.update.UpdateResponse; import org.opensearch.client.RestHighLevelClient; @@ -2281,7 +2282,11 @@ public void openIndex_negative() throws IOException { .open(new OpenIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), statusException(FORBIDDEN) ); - assertThatThrownBy(() -> restHighLevelClient.indices().open(new OpenIndexRequest("*"), DEFAULT), statusException(FORBIDDEN)); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .open(new OpenIndexRequest("*").indicesOptions(IndicesOptions.LENIENT_EXPAND_OPEN_CLOSED), DEFAULT), + statusException(FORBIDDEN) + ); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java b/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java index e2924bc45d..89d7e0788d 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java @@ -13,18 +13,12 @@ import java.time.ZonedDateTime; import java.time.temporal.ChronoField; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import org.junit.Test; import org.opensearch.cluster.ClusterState; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.metadata.Metadata; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.support.WildcardMatcher; -import org.opensearch.security.user.User; +import org.opensearch.security.util.MockPrivilegeEvaluationContextBuilder; import static org.opensearch.security.util.MockIndexMetadataBuilder.indices; import static org.junit.Assert.assertEquals; @@ -232,19 +226,10 @@ public void equals() { } private static PrivilegesEvaluationContext ctx() { - IndexNameExpressionResolver indexNameExpressionResolver = new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)); - IndexResolverReplacer indexResolverReplacer = new IndexResolverReplacer(indexNameExpressionResolver, () -> CLUSTER_STATE, null); - User user = new User("test_user").withAttributes(ImmutableMap.of("attrs.a11", "a11", "attrs.year", "year")); - return new PrivilegesEvaluationContext( - user, - ImmutableSet.of(), - "indices:action/test", - null, - null, - indexResolverReplacer, - indexNameExpressionResolver, - () -> CLUSTER_STATE, - ActionPrivileges.EMPTY - ); + return MockPrivilegeEvaluationContextBuilder.ctx() + .action("indices:action/test") + .attr("attrs.a11", "a11") + .attr("attrs.year", "year") + .get(); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/IndexRequestModifierTest.java b/src/integrationTest/java/org/opensearch/security/privileges/IndexRequestModifierTest.java new file mode 100644 index 0000000000..76be054389 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndexRequestModifierTest.java @@ -0,0 +1,163 @@ +/* + * 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.privileges; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Suite; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.IndicesRequest; +import org.opensearch.action.OriginalIndices; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.support.IndicesOptions; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.metadata.ResolvedIndices; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.util.MockIndexMetadataBuilder; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ IndexRequestModifierTest.SetLocalIndices.class, IndexRequestModifierTest.SetLocalIndicesToEmpty.class }) +public class IndexRequestModifierTest { + + static final IndexNameExpressionResolver indexNameExpressionResolver = new IndexNameExpressionResolver( + new ThreadContext(Settings.EMPTY) + ); + static final Metadata metadata = MockIndexMetadataBuilder.indices("index", "index1", "index2", "index3").build(); + final static ClusterState clusterState = ClusterState.builder(ClusterState.EMPTY_STATE).metadata(metadata).build(); + static final IndicesRequestModifier subject = new IndicesRequestModifier(); + + public static class SetLocalIndices { + @Test + public void basic() { + ResolvedIndices resolvedIndices = ResolvedIndices.of("index1"); + SearchRequest request = new SearchRequest("index1", "index2", "index3"); + + boolean success = subject.setLocalIndices(request, resolvedIndices, Collections.singletonList("index1")); + assertTrue(success); + assertArrayEquals(new String[] { "index1" }, request.indices()); + } + + @Test + public void withRemote() { + ResolvedIndices resolvedIndices = ResolvedIndices.of("index1") + .withRemoteIndices( + Map.of("remote", new OriginalIndices(new String[] { "index_remote" }, IndicesOptions.LENIENT_EXPAND_OPEN)) + ); + SearchRequest request = new SearchRequest("index1", "index2", "index3", "remote:index_remote"); + + boolean success = subject.setLocalIndices(request, resolvedIndices, Collections.singletonList("index1")); + assertTrue(success); + assertArrayEquals(new String[] { "index1", "remote:index_remote" }, request.indices()); + } + + @Test + public void empty() { + ResolvedIndices resolvedIndices = ResolvedIndices.of("index1"); + SearchRequest request = new SearchRequest("index1", "index2", "index3"); + + boolean success = subject.setLocalIndices(request, resolvedIndices, Collections.emptyList()); + assertTrue(success); + String[] finalResolvedIndices = indexNameExpressionResolver.concreteIndexNames(clusterState, request); + assertArrayEquals(new String[0], finalResolvedIndices); + } + + @Test + public void unsupportedType() { + ResolvedIndices resolvedIndices = ResolvedIndices.of("index1"); + IndexRequest request = new IndexRequest("index1"); + + boolean success = subject.setLocalIndices(request, resolvedIndices, Collections.singletonList("index1")); + assertFalse(success); + } + } + + @RunWith(Parameterized.class) + public static class SetLocalIndicesToEmpty { + + String description; + IndicesRequest request; + + @Test + public void setLocalIndicesToEmpty() { + + ResolvedIndices resolvedIndices = ResolvedIndices.of("index"); + + if (Arrays.asList(request.indices()).contains("remote:index")) { + resolvedIndices = resolvedIndices.withRemoteIndices( + Map.of("remote", new OriginalIndices(new String[] { "index" }, request.indicesOptions())) + ); + } + + boolean success = subject.setLocalIndicesToEmpty((ActionRequest) request, resolvedIndices); + + if (!(request instanceof IndicesRequest.Replaceable)) { + assertFalse(success); + } else if (!request.indicesOptions().allowNoIndices()) { + assertFalse(success); + } else { + assertTrue(success); + + String[] finalResolvedIndices = indexNameExpressionResolver.concreteIndexNames(clusterState, request); + + assertEquals("Resolved to empty indices: " + Arrays.asList(finalResolvedIndices), 0, finalResolvedIndices.length); + } + } + + @Parameterized.Parameters(name = "{0}") + public static Collection params() { + return Arrays.asList( + new Object[] { "lenient expand open", new SearchRequest("index").indicesOptions(IndicesOptions.LENIENT_EXPAND_OPEN) }, + new Object[] { + "lenient expand open/closed", + new SearchRequest("index").indicesOptions(IndicesOptions.LENIENT_EXPAND_OPEN_CLOSED) }, + new Object[] { + "lenient expand open/closed/hidden", + new SearchRequest("index").indicesOptions(IndicesOptions.LENIENT_EXPAND_OPEN_CLOSED_HIDDEN) }, + new Object[] { + "allow no indices", + new SearchRequest("index").indicesOptions(IndicesOptions.fromOptions(false, true, false, false)) }, + new Object[] { + "ignore unavailable", + new SearchRequest("index").indicesOptions(IndicesOptions.fromOptions(true, false, false, false)) }, + new Object[] { + "strict single index", + new SearchRequest("index").indicesOptions(IndicesOptions.STRICT_SINGLE_INDEX_NO_EXPAND_FORBID_CLOSED) }, + new Object[] { + "with remote index", + new SearchRequest("index", "remote:index").indicesOptions(IndicesOptions.LENIENT_EXPAND_OPEN) }, + new Object[] { "not implementing IndicesRequest.Replaceable", new IndexRequest("index") } + ); + + } + + public SetLocalIndicesToEmpty(String description, IndicesRequest request) { + this.description = description; + this.request = request; + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/privileges/IndicesRequestResolverTest.java b/src/integrationTest/java/org/opensearch/security/privileges/IndicesRequestResolverTest.java new file mode 100644 index 0000000000..b1e6af1a1a --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndicesRequestResolverTest.java @@ -0,0 +1,90 @@ +/* + * 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.privileges; + +import java.util.Set; + +import org.junit.Test; + +import org.opensearch.action.admin.cluster.stats.ClusterStatsRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.support.ActionRequestMetadata; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.util.MockIndexMetadataBuilder; +import org.opensearch.security.util.MockPrivilegeEvaluationContextBuilder; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class IndicesRequestResolverTest { + + static final Metadata metadata = MockIndexMetadataBuilder.indices("index_a1", "index_a2", "index_b1", "index_b2").build(); + final static ClusterState clusterState = ClusterState.builder(ClusterState.EMPTY_STATE).metadata(metadata).build(); + static final IndicesRequestResolver subject = new IndicesRequestResolver( + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)) + ); + + @Test + public void resolve_normal() { + SearchRequest request = new SearchRequest("index1"); + ActionRequestMetadata actionRequestMetadata = mock(); + ResolvedIndices resolvedIndices = ResolvedIndices.of("index1"); + when(actionRequestMetadata.resolvedIndices()).thenReturn(resolvedIndices); + + OptionallyResolvedIndices returnedResolvedIndices = subject.resolve(request, actionRequestMetadata, () -> clusterState); + assertEquals(resolvedIndices, returnedResolvedIndices); + } + + @Test + public void resolve_fallback() { + SearchRequest request = new SearchRequest("index1"); + ActionRequestMetadata actionRequestMetadata = mock(); + when(actionRequestMetadata.resolvedIndices()).thenReturn(OptionallyResolvedIndices.unknown()); + + OptionallyResolvedIndices returnedResolvedIndices = subject.resolve(request, actionRequestMetadata, () -> clusterState); + if (returnedResolvedIndices instanceof ResolvedIndices castReturnedResovledIndices) { + assertEquals(Set.of("index1"), castReturnedResovledIndices.local().names()); + } else { + fail("Expected ResolvedIndices, got: " + returnedResolvedIndices); + } + } + + @Test + public void resolve_fallbackUnsupported() { + ClusterStatsRequest request = new ClusterStatsRequest(); + ActionRequestMetadata actionRequestMetadata = mock(); + when(actionRequestMetadata.resolvedIndices()).thenReturn(OptionallyResolvedIndices.unknown()); + + OptionallyResolvedIndices returnedResolvedIndices = subject.resolve(request, actionRequestMetadata, () -> clusterState); + assertFalse(returnedResolvedIndices instanceof ResolvedIndices); + } + + @Test + public void resolve_withPrivilegesEvaluationContext() { + SearchRequest request = new SearchRequest("index_a*"); + ActionRequestMetadata actionRequestMetadata = mock(); + when(actionRequestMetadata.resolvedIndices()).thenReturn(OptionallyResolvedIndices.unknown()); + PrivilegesEvaluationContext context = MockPrivilegeEvaluationContextBuilder.ctx().clusterState(clusterState).get(); + + OptionallyResolvedIndices returnedResolvedIndices = subject.resolve(request, actionRequestMetadata, context); + assertEquals(Set.of("index_a1", "index_a2"), returnedResolvedIndices.local().names(clusterState)); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java index 09c122cc2b..7ad030d2f5 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java @@ -47,6 +47,7 @@ import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.PermissionBuilder; import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; +import org.opensearch.security.privileges.actionlevel.RuntimeOptimizedActionPrivileges; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -117,7 +118,13 @@ static String[] allRestApiPermissions() { final RoleBasedActionPrivileges actionPrivileges; public RestEndpointPermissionTests() throws IOException { - this.actionPrivileges = new RoleBasedActionPrivileges(createRolesConfig(), FlattenedActionGroups.EMPTY, Settings.EMPTY); + this.actionPrivileges = new RoleBasedActionPrivileges( + createRolesConfig(), + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); } @Test diff --git a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/EmptyActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/EmptyActionPrivilegesTest.java index f7278325f2..f43bb9b55b 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/EmptyActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/EmptyActionPrivilegesTest.java @@ -15,9 +15,9 @@ import org.junit.Test; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.security.privileges.ActionPrivileges; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; -import org.opensearch.security.resolver.IndexResolverReplacer; import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isForbidden; @@ -50,7 +50,7 @@ public void hasIndexPrivilege() { PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( ctx().get(), Set.of("indices:data/write/index"), - IndexResolverReplacer.Resolved.ofIndex("any_index") + ResolvedIndices.of("any_index") ); assertThat(result, isForbidden()); } @@ -60,7 +60,7 @@ public void hasExplicitIndexPrivilege() { PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), Set.of("indices:data/write/index"), - IndexResolverReplacer.Resolved.ofIndex("any_index") + ResolvedIndices.of("any_index") ); assertThat(result, isForbidden()); } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java index 765cace0e2..280f7f15de 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java @@ -13,6 +13,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -29,16 +30,17 @@ import org.junit.runners.Parameterized; import org.junit.runners.Suite; +import org.opensearch.action.OriginalIndices; import org.opensearch.action.support.IndicesOptions; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.common.settings.Settings; import org.opensearch.core.common.unit.ByteSizeUnit; import org.opensearch.core.common.unit.ByteSizeValue; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; -import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -80,7 +82,13 @@ public void wellKnown() throws Exception { " cluster_permissions:\n" + // " - cluster:monitor/nodes/stats*", CType.ROLES); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); assertThat(subject.hasClusterPrivilege(ctx().roles("test_role").get(), "cluster:monitor/nodes/stats"), isAllowed()); assertThat( @@ -99,7 +107,13 @@ public void notWellKnown() throws Exception { " cluster_permissions:\n" + // " - cluster:monitor/nodes/stats*", CType.ROLES); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); assertThat( subject.hasClusterPrivilege(ctx().roles("test_role").get(), "cluster:monitor/nodes/stats/somethingnotwellknown"), @@ -121,7 +135,13 @@ public void wildcard() throws Exception { " cluster_permissions:\n" + // " - '*'", CType.ROLES); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); assertThat(subject.hasClusterPrivilege(ctx().roles("test_role").get(), "cluster:whatever"), isAllowed()); assertThat( @@ -144,7 +164,13 @@ public void explicit_wellKnown() throws Exception { CType.ROLES ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); assertThat(subject.hasExplicitClusterPrivilege(ctx().roles("explicit_role").get(), "cluster:monitor/nodes/stats"), isAllowed()); assertThat( @@ -175,7 +201,13 @@ public void explicit_notWellKnown() throws Exception { CType.ROLES ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); assertThat( subject.hasExplicitClusterPrivilege(ctx().roles("explicit_role").get(), "cluster:monitor/nodes/notwellknown"), @@ -201,7 +233,13 @@ public void hasAny_wellKnown() throws Exception { " cluster_permissions:\n" + // " - cluster:monitor/nodes/stats*", CType.ROLES); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); assertThat( subject.hasAnyClusterPrivilege(ctx().roles("test_role").get(), ImmutableSet.of("cluster:monitor/nodes/stats")), @@ -231,7 +269,13 @@ public void hasAny_notWellKnown() throws Exception { " cluster_permissions:\n" + // " - cluster:monitor/nodes/*", CType.ROLES); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); assertThat( subject.hasAnyClusterPrivilege(ctx().roles("test_role").get(), ImmutableSet.of("cluster:monitor/nodes/notwellknown")), @@ -268,7 +312,13 @@ public void hasAny_wildcard() throws Exception { " cluster_permissions:\n" + // " - '*'", CType.ROLES); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); assertThat(subject.hasAnyClusterPrivilege(ctx().roles("test_role").get(), ImmutableSet.of("cluster:whatever")), isAllowed()); @@ -352,13 +402,13 @@ public void positive_partial2() throws Exception { @Test public void positive_noLocal() throws Exception { - IndexResolverReplacer.Resolved resolved = new IndexResolverReplacer.Resolved( - ImmutableSet.of(), - ImmutableSet.of(), - ImmutableSet.of("remote:a"), - ImmutableSet.of("remote:a"), - IndicesOptions.LENIENT_EXPAND_OPEN - ); + ResolvedIndices resolved = ResolvedIndices.of(Collections.emptySet()) + .withRemoteIndices( + Map.of( + "remote", + new OriginalIndices(new String[] { "a" }, IndicesOptions.STRICT_SINGLE_INDEX_NO_EXPAND_FORBID_CLOSED) + ) + ); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( ctx().roles("test_role").indexMetadata(INDEX_METADATA).get(), requiredActions, @@ -462,7 +512,13 @@ public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec, Statefulnes .build(); } - this.subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, settings); + this.subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + settings, + false + ); if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { this.subject.updateStatefulIndexPrivileges(INDEX_METADATA.getIndicesLookup(), 1); @@ -481,14 +537,8 @@ public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec, Statefulnes .of("index_b1", "index_b2")// .build(); - static IndexResolverReplacer.Resolved resolved(String... indices) { - return new IndexResolverReplacer.Resolved( - ImmutableSet.of(), - ImmutableSet.copyOf(indices), - ImmutableSet.copyOf(indices), - ImmutableSet.of(), - IndicesOptions.LENIENT_EXPAND_OPEN - ); + static ResolvedIndices resolved(String... indices) { + return ResolvedIndices.of(indices); } } @@ -501,19 +551,27 @@ public static class DataStreams { final String primaryAction; final ImmutableSet requiredActions; final ImmutableSet otherActions; - final RoleBasedActionPrivileges subject; + final Statefulness statefulness; @Test public void positive_full() throws Exception { PrivilegesEvaluationContext ctx = ctx().roles("test_role").indexMetadata(INDEX_METADATA).get(); - PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); + PrivilegesEvaluatorResponse result = subject(false).hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); if (covers(ctx, "data_stream_a11")) { assertThat(result, isAllowed()); - } else if (covers(ctx, ".ds-data_stream_a11-000001")) { - assertThat( - result, - isPartiallyOk(".ds-data_stream_a11-000001", ".ds-data_stream_a11-000002", ".ds-data_stream_a11-000003") - ); + } else { + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } + } + + @Test + public void positive_full_breakDownAliases() throws Exception { + PrivilegesEvaluationContext ctx = ctx().roles("test_role").indexMetadata(INDEX_METADATA).get(); + PrivilegesEvaluatorResponse result = subject(true).hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); + if (covers(ctx, "data_stream_a11")) { + assertThat(result, isAllowed()); + } else if (covers(ctx, ".ds-data_stream_a11")) { + assertThat(result, isAllowed()); } else { assertThat(result, isForbidden(missingPrivileges(requiredActions))); } @@ -522,7 +580,7 @@ public void positive_full() throws Exception { @Test public void positive_partial() throws Exception { PrivilegesEvaluationContext ctx = ctx().roles("test_role").indexMetadata(INDEX_METADATA).get(); - PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( + PrivilegesEvaluatorResponse result = subject(false).hasIndexPrivilege( ctx, requiredActions, resolved("data_stream_a11", "data_stream_a12") @@ -531,20 +589,27 @@ public void positive_partial() throws Exception { if (covers(ctx, "data_stream_a11", "data_stream_a12")) { assertThat(result, isAllowed()); } else if (covers(ctx, "data_stream_a11")) { - assertThat( - result, - isPartiallyOk( - "data_stream_a11", - ".ds-data_stream_a11-000001", - ".ds-data_stream_a11-000002", - ".ds-data_stream_a11-000003" - ) - ); - } else if (covers(ctx, ".ds-data_stream_a11-000001")) { - assertThat( - result, - isPartiallyOk(".ds-data_stream_a11-000001", ".ds-data_stream_a11-000002", ".ds-data_stream_a11-000003") - ); + assertThat(result, isPartiallyOk("data_stream_a11")); + } else { + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } + } + + @Test + public void positive_partial_breakDownAliases() throws Exception { + PrivilegesEvaluationContext ctx = ctx().roles("test_role").indexMetadata(INDEX_METADATA).get(); + PrivilegesEvaluatorResponse result = subject(true).hasIndexPrivilege( + ctx, + requiredActions, + resolved("data_stream_a11", "data_stream_a12") + ); + + if (covers(ctx, "data_stream_a11", "data_stream_a12")) { + assertThat(result, isAllowed()); + } else if (covers(ctx, "data_stream_a11")) { + assertThat(result, isPartiallyOk("data_stream_a11")); + } else if (covers(ctx, ".ds-data_stream_a11")) { + assertThat(result, isPartiallyOk("data_stream_a11")); } else { assertThat(result, isForbidden(missingPrivileges(requiredActions))); } @@ -553,14 +618,14 @@ public void positive_partial() throws Exception { @Test public void negative_wrongRole() throws Exception { PrivilegesEvaluationContext ctx = ctx().roles("other_role").indexMetadata(INDEX_METADATA).get(); - PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); + PrivilegesEvaluatorResponse result = subject(false).hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); assertThat(result, isForbidden(missingPrivileges(requiredActions))); } @Test public void negative_wrongAction() throws Exception { PrivilegesEvaluationContext ctx = ctx().roles("test_role").indexMetadata(INDEX_METADATA).get(); - PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, otherActions, resolved("data_stream_a11")); + PrivilegesEvaluatorResponse result = subject(false).hasIndexPrivilege(ctx, otherActions, resolved("data_stream_a11")); assertThat(result, isForbidden(missingPrivileges(otherActions))); } @@ -626,7 +691,10 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness stat ? ImmutableSet.of("indices:data/write/update") : ImmutableSet.of("indices:foobar/unknown"); this.indexSpec.indexMetadata = INDEX_METADATA.getIndicesLookup(); + this.statefulness = statefulness; + } + private RoleBasedActionPrivileges subject(boolean breakDownAliases) { Settings settings = Settings.EMPTY; if (statefulness == Statefulness.STATEFUL_LIMITED) { settings = Settings.builder() @@ -637,39 +705,27 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness stat .build(); } - this.subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, settings); + RoleBasedActionPrivileges result = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + settings, + breakDownAliases + ); if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { - this.subject.updateStatefulIndexPrivileges(INDEX_METADATA.getIndicesLookup(), 1); + result.updateStatefulIndexPrivileges(INDEX_METADATA.getIndicesLookup(), 1); } + + return result; } final static Metadata INDEX_METADATA = // dataStreams("data_stream_a11", "data_stream_a12", "data_stream_a21", "data_stream_a22", "data_stream_b1", "data_stream_b2") .build(); - static IndexResolverReplacer.Resolved resolved(String... indices) { - ImmutableSet.Builder allIndices = ImmutableSet.builder(); - - for (String index : indices) { - IndexAbstraction indexAbstraction = INDEX_METADATA.getIndicesLookup().get(index); - - if (indexAbstraction instanceof IndexAbstraction.DataStream) { - allIndices.addAll( - indexAbstraction.getIndices().stream().map(i -> i.getIndex().getName()).collect(Collectors.toList()) - ); - } - - allIndices.add(index); - } - - return new IndexResolverReplacer.Resolved( - ImmutableSet.of(), - allIndices.build(), - ImmutableSet.copyOf(indices), - ImmutableSet.of(), - IndicesOptions.LENIENT_EXPAND_OPEN - ); + static ResolvedIndices resolved(String... indices) { + return ResolvedIndices.of(indices); } } @@ -797,21 +853,6 @@ enum Statefulness { } public static class Misc { - @Test - public void relevantOnly_identity() throws Exception { - Map metadata = // - indices("index_a11", "index_a12", "index_b")// - .alias("alias_a") - .of("index_a11", "index_a12")// - .build() - .getIndicesLookup(); - - assertTrue( - "relevantOnly() returned identical object", - RoleBasedActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata) == metadata - ); - } - @Test public void relevantOnly_closed() throws Exception { Map metadata = indices("index_open_1", "index_open_2")// @@ -822,7 +863,10 @@ public void relevantOnly_closed() throws Exception { assertNotNull("Original metadata contains index_open_1", metadata.get("index_open_1")); assertNotNull("Original metadata contains index_closed", metadata.get("index_closed")); - Map filteredMetadata = RoleBasedActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata); + Map filteredMetadata = RoleBasedActionPrivileges.StatefulIndexPrivileges.relevantOnly( + metadata, + i -> false + ); assertNotNull("Filtered metadata contains index_open_1", filteredMetadata.get("index_open_1")); assertNull("Filtered metadata does not contain index_closed", filteredMetadata.get("index_closed")); @@ -835,7 +879,10 @@ public void relevantOnly_dataStreamBackingIndices() throws Exception { assertNotNull("Original metadata contains backing index", metadata.get(".ds-data_stream_1-000001")); assertNotNull("Original metadata contains data stream", metadata.get("data_stream_1")); - Map filteredMetadata = RoleBasedActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata); + Map filteredMetadata = RoleBasedActionPrivileges.StatefulIndexPrivileges.relevantOnly( + metadata, + i -> false + ); assertNull("Filtered metadata does not contain backing index", filteredMetadata.get(".ds-data_stream_1-000001")); assertNotNull("Filtered metadata contains data stream", filteredMetadata.get("data_stream_1")); @@ -866,19 +913,25 @@ public void hasIndexPrivilege_errors() throws Exception { CType.ROLES ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( ctx().roles("role_with_errors").get(), Set.of("indices:some_action", "indices:data/write/index"), - IndexResolverReplacer.Resolved.ofIndex("any_index") + ResolvedIndices.of("any_index") ); assertThat(result, isForbidden()); assertTrue(result.hasEvaluationExceptions()); assertTrue( "Result mentions role_with_errors: " + result.getEvaluationExceptionInfo(), result.getEvaluationExceptionInfo() - .startsWith("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating") + .contains("Error while evaluating dynamic index pattern: /invalid_regex_with_attr${user.name}\\/") ); } @@ -892,12 +945,18 @@ public void hasExplicitIndexPrivilege_positive() throws Exception { CType.ROLES ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().roles("test_role").get(), Set.of("system:admin/system_index"), - IndexResolverReplacer.Resolved.ofIndex("test_index") + ResolvedIndices.of("test_index") ); assertThat(result, isAllowed()); } @@ -912,12 +971,18 @@ public void hasExplicitIndexPrivilege_positive_wildcard() throws Exception { CType.ROLES ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().roles("test_role").get(), Set.of("system:admin/system_index"), - IndexResolverReplacer.Resolved.ofIndex("test_index") + ResolvedIndices.of("test_index") ); assertThat(result, isAllowed()); } @@ -929,12 +994,18 @@ public void hasExplicitIndexPrivilege_noWildcard() throws Exception { CType.ROLES ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().roles("test_role").get(), Set.of("system:admin/system_index"), - IndexResolverReplacer.Resolved.ofIndex("test_index") + ResolvedIndices.of("test_index") ); assertThat(result, isForbidden()); } @@ -949,12 +1020,18 @@ public void hasExplicitIndexPrivilege_negative_wrongAction() throws Exception { CType.ROLES ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().roles("test_role").get(), Set.of("system:admin/system_foo"), - IndexResolverReplacer.Resolved.ofIndex("test_index") + ResolvedIndices.of("test_index") ); assertThat(result, isForbidden()); } @@ -969,19 +1046,24 @@ public void hasExplicitIndexPrivilege_errors() throws Exception { CType.ROLES ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().roles("role_with_errors").get(), Set.of("system:admin/system_index"), - IndexResolverReplacer.Resolved.ofIndex("any_index") + ResolvedIndices.of("any_index") ); assertThat(result, isForbidden()); assertTrue(result.hasEvaluationExceptions()); assertTrue( "Result mentions role_with_errors: " + result.getEvaluationExceptionInfo(), - result.getEvaluationExceptionInfo() - .startsWith("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating role role_with_errors") + result.getEvaluationExceptionInfo().contains("Error while evaluating role role_with_errors") ); } @@ -998,20 +1080,26 @@ public void aliasesOnDataStreamBackingIndices() throws Exception { + " allowed_actions: ['indices:data/write/index']", CType.ROLES ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); subject.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), 2); PrivilegesEvaluatorResponse resultForIndexCoveredByAlias = subject.hasIndexPrivilege( ctx().roles("role").indexMetadata(metadata).get(), Set.of("indices:data/write/index"), - IndexResolverReplacer.Resolved.ofIndex(".ds-ds_a-000001") + ResolvedIndices.of(".ds-ds_a-000001") ); assertThat(resultForIndexCoveredByAlias, isAllowed()); PrivilegesEvaluatorResponse resultForIndexNotCoveredByAlias = subject.hasIndexPrivilege( ctx().roles("role").indexMetadata(metadata).get(), Set.of("indices:data/write/index"), - IndexResolverReplacer.Resolved.ofIndex(".ds-ds_a-000002") + ResolvedIndices.of(".ds-ds_a-000002") ); assertThat(resultForIndexNotCoveredByAlias, isForbidden()); } @@ -1029,7 +1117,9 @@ public void statefulDisabled() throws Exception { RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( roles, FlattenedActionGroups.EMPTY, - Settings.builder().put(RoleBasedActionPrivileges.PRECOMPUTED_PRIVILEGES_ENABLED.getKey(), false).build() + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.builder().put(RoleBasedActionPrivileges.PRECOMPUTED_PRIVILEGES_ENABLED.getKey(), false).build(), + false ); subject.updateStatefulIndexPrivileges(metadata, 1); assertEquals(0, subject.getEstimatedStatefulIndexByteSize()); @@ -1048,7 +1138,13 @@ public static class StatefulIndexPrivilegesHeapSize { @Test public void estimatedSize() throws Exception { - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); subject.updateStatefulIndexPrivileges(indices, 1); diff --git a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java index 63d630b289..e345ffa2c8 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java @@ -13,6 +13,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -27,12 +28,13 @@ import org.junit.runners.Parameterized; import org.junit.runners.Suite; +import org.opensearch.action.OriginalIndices; import org.opensearch.action.support.IndicesOptions; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; -import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -70,7 +72,12 @@ public void wellKnown() throws Exception { - cluster:monitor/nodes/stats* """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), isAllowed()); } @@ -81,7 +88,12 @@ public void notWellKnown() throws Exception { - cluster:monitor/nodes/stats* """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats/somethingnotwellknown"), isAllowed()); } @@ -92,7 +104,12 @@ public void negative() throws Exception { - cluster:monitor/nodes/stats* """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:monitor/nodes/foo"), isForbidden()); } @@ -103,7 +120,12 @@ public void wildcard() throws Exception { - '*' """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:whatever"), isAllowed()); } @@ -114,7 +136,12 @@ public void explicit_wellKnown() throws Exception { - cluster:monitor/nodes/stats """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); assertThat(subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), isAllowed()); } @@ -125,7 +152,12 @@ public void explicit_notWellKnown() throws Exception { - cluster:monitor/nodes/* """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); assertThat(subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/notwellknown"), isAllowed()); } @@ -136,7 +168,12 @@ public void explicit_notExplicit() throws Exception { - '*' """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); assertThat( subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) @@ -150,7 +187,12 @@ public void hasAny_wellKnown() throws Exception { - cluster:monitor/nodes/stats* """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); assertThat(subject.hasAnyClusterPrivilege(ctx().get(), ImmutableSet.of("cluster:monitor/nodes/stats")), isAllowed()); } @@ -161,7 +203,12 @@ public void hasAny_wildcard() throws Exception { - '*' """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); assertThat(subject.hasAnyClusterPrivilege(ctx().get(), ImmutableSet.of("cluster:monitor/nodes/stats")), isAllowed()); } } @@ -224,13 +271,13 @@ public void positive_partial2() throws Exception { @Test public void positive_noLocal() throws Exception { - IndexResolverReplacer.Resolved resolved = new IndexResolverReplacer.Resolved( - ImmutableSet.of(), - ImmutableSet.of(), - ImmutableSet.of("remote:a"), - ImmutableSet.of("remote:a"), - IndicesOptions.LENIENT_EXPAND_OPEN - ); + ResolvedIndices resolved = ResolvedIndices.of(Collections.emptySet()) + .withRemoteIndices( + Map.of( + "remote", + new OriginalIndices(new String[] { "a" }, IndicesOptions.STRICT_SINGLE_INDEX_NO_EXPAND_FORBID_CLOSED) + ) + ); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( ctx().indexMetadata(INDEX_METADATA).get(), requiredActions, @@ -315,7 +362,12 @@ public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec) throws Exce : ImmutableSet.of("indices:foobar/unknown"); this.indexSpec.indexMetadata = INDEX_METADATA.getIndicesLookup(); - this.subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + this.subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); } final static Metadata INDEX_METADATA = // @@ -330,14 +382,8 @@ public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec) throws Exce .of("index_b1", "index_b2")// .build(); - static IndexResolverReplacer.Resolved resolved(String... indices) { - return new IndexResolverReplacer.Resolved( - ImmutableSet.of(), - ImmutableSet.copyOf(indices), - ImmutableSet.copyOf(indices), - ImmutableSet.of(), - IndicesOptions.LENIENT_EXPAND_OPEN - ); + static ResolvedIndices resolved(String... indices) { + return ResolvedIndices.of(indices); } } @@ -358,11 +404,6 @@ public void positive_full() throws Exception { PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); if (covers(ctx, "data_stream_a11")) { assertThat(result, isAllowed()); - } else if (covers(ctx, ".ds-data_stream_a11-000001")) { - assertThat( - result, - isPartiallyOk(".ds-data_stream_a11-000001", ".ds-data_stream_a11-000002", ".ds-data_stream_a11-000003") - ); } else { assertThat(result, isForbidden(missingPrivileges(requiredActions))); } @@ -380,20 +421,7 @@ public void positive_partial() throws Exception { if (covers(ctx, "data_stream_a11", "data_stream_a12")) { assertThat(result, isAllowed()); } else if (covers(ctx, "data_stream_a11")) { - assertThat( - result, - isPartiallyOk( - "data_stream_a11", - ".ds-data_stream_a11-000001", - ".ds-data_stream_a11-000002", - ".ds-data_stream_a11-000003" - ) - ); - } else if (covers(ctx, ".ds-data_stream_a11-000001")) { - assertThat( - result, - isPartiallyOk(".ds-data_stream_a11-000001", ".ds-data_stream_a11-000002", ".ds-data_stream_a11-000003") - ); + assertThat(result, isPartiallyOk("data_stream_a11")); } else { assertThat(result, isForbidden(missingPrivileges(requiredActions))); } @@ -466,35 +494,20 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec) throws Exception ? ImmutableSet.of("indices:data/write/update") : ImmutableSet.of("indices:foobar/unknown"); this.indexSpec.indexMetadata = INDEX_METADATA.getIndicesLookup(); - this.subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + this.subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false // breakDownAliases = true is already sufficiently checked in RoleBasedActionPrivilegesTest + ); } final static Metadata INDEX_METADATA = // dataStreams("data_stream_a11", "data_stream_a12", "data_stream_a21", "data_stream_a22", "data_stream_b1", "data_stream_b2") .build(); - static IndexResolverReplacer.Resolved resolved(String... indices) { - ImmutableSet.Builder allIndices = ImmutableSet.builder(); - - for (String index : indices) { - IndexAbstraction indexAbstraction = INDEX_METADATA.getIndicesLookup().get(index); - - if (indexAbstraction instanceof IndexAbstraction.DataStream) { - allIndices.addAll( - indexAbstraction.getIndices().stream().map(i -> i.getIndex().getName()).collect(Collectors.toList()) - ); - } - - allIndices.add(index); - } - - return new IndexResolverReplacer.Resolved( - ImmutableSet.of(), - allIndices.build(), - ImmutableSet.copyOf(indices), - ImmutableSet.of(), - IndicesOptions.LENIENT_EXPAND_OPEN - ); + static ResolvedIndices resolved(String... indices) { + return ResolvedIndices.of(indices); } } @@ -625,12 +638,17 @@ public void hasExplicitIndexPrivilege_positive() throws Exception { allowed_actions: ['system:admin/system_index'] """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), Set.of("system:admin/system_index"), - IndexResolverReplacer.Resolved.ofIndex("test_index") + ResolvedIndices.of("test_index") ); assertThat(result, isAllowed()); } @@ -643,12 +661,17 @@ public void hasExplicitIndexPrivilege_positive_pattern() throws Exception { allowed_actions: ['system:admin/system_index*'] """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), Set.of("system:admin/system_index"), - IndexResolverReplacer.Resolved.ofIndex("test_index") + ResolvedIndices.of("test_index") ); assertThat(result, isAllowed()); } @@ -660,12 +683,17 @@ public void hasExplicitIndexPrivilege_noWildcard() throws Exception { - index_patterns: ['test_index'] allowed_actions: ['*'] """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), Set.of("system:admin/system_index"), - IndexResolverReplacer.Resolved.ofIndex("test_index") + ResolvedIndices.of("test_index") ); assertThat(result, isForbidden()); } @@ -677,12 +705,17 @@ public void hasExplicitIndexPrivilege_negative_wrongAction() throws Exception { - index_patterns: ['test_index'] allowed_actions: ['system:admin/system*'] """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), Set.of("system:admin/system_foo"), - IndexResolverReplacer.Resolved.ofIndex("test_index") + ResolvedIndices.of("test_index") ); assertThat(result, isForbidden()); } @@ -694,18 +727,23 @@ public void hasExplicitIndexPrivilege_errors() throws Exception { - index_patterns: ['/invalid_regex${user.name}\\/'] allowed_actions: ['system:admin/system*'] """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), Set.of("system:admin/system_index"), - IndexResolverReplacer.Resolved.ofIndex("test_index") + ResolvedIndices.of("test_index") ); assertThat(result, isForbidden()); assertTrue(result.hasEvaluationExceptions()); assertTrue( "Result contains exception info: " + result.getEvaluationExceptionInfo(), - result.getEvaluationExceptionInfo().startsWith("Exceptions encountered during privilege evaluation:") + result.getEvaluationExceptionInfo().contains("Unescaped trailing backslash near index") ); } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java index 5fe2837d19..8392d9fa1d 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java @@ -15,14 +15,12 @@ import java.util.Map; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import org.junit.Test; import org.opensearch.Version; import org.opensearch.action.admin.cluster.shards.ClusterSearchShardsRequest; import org.opensearch.action.search.SearchRequest; import org.opensearch.cluster.ClusterState; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.CheckedFunction; import org.opensearch.common.settings.Settings; @@ -35,14 +33,13 @@ import org.opensearch.index.query.RangeQueryBuilder; import org.opensearch.index.query.TermQueryBuilder; import org.opensearch.search.internal.ShardSearchRequest; -import org.opensearch.security.privileges.ActionPrivileges; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.Base64Helper; import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.user.User; import org.opensearch.security.util.MockIndexMetadataBuilder; +import org.opensearch.security.util.MockPrivilegeEvaluationContextBuilder; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.transport.Transport; @@ -335,40 +332,21 @@ public void prepare_ccs() throws Exception { ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_TRUSTED_CLUSTER_REQUEST, true); - User user = new User("test_user"); ClusterState clusterState = ClusterState.builder(ClusterState.EMPTY_STATE).metadata(metadata).build(); - PrivilegesEvaluationContext ctx = new PrivilegesEvaluationContext( - user, - ImmutableSet.of("test_role"), - null, - new ClusterSearchShardsRequest(), - null, - null, - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - () -> clusterState, - ActionPrivileges.EMPTY - ); + PrivilegesEvaluationContext ctx = MockPrivilegeEvaluationContextBuilder.ctx() + .roles("test_role") + .request(new ClusterSearchShardsRequest()) + .clusterState(clusterState) + .get(); DlsFlsLegacyHeaders.prepare(threadContext, ctx, dlsFlsProcessedConfig(exampleRolesConfig(), metadata), metadata, false); assertTrue(threadContext.getResponseHeaders().containsKey(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER)); } static PrivilegesEvaluationContext ctx(Metadata metadata, String... roles) { - User user = new User("test_user"); ClusterState clusterState = ClusterState.builder(ClusterState.EMPTY_STATE).metadata(metadata).build(); - - return new PrivilegesEvaluationContext( - user, - ImmutableSet.copyOf(roles), - null, - null, - null, - null, - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - () -> clusterState, - ActionPrivileges.EMPTY - ); + return MockPrivilegeEvaluationContextBuilder.ctx().roles(roles).clusterState(clusterState).get(); } static DlsFlsProcessedConfig dlsFlsProcessedConfig(SecurityDynamicConfiguration rolesConfig, Metadata metadata) { diff --git a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java index 7182b22ed6..68695db59a 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java @@ -32,12 +32,12 @@ import org.junit.runners.Parameterized; import org.junit.runners.Suite; -import org.opensearch.action.IndicesRequest; -import org.opensearch.action.support.IndicesOptions; +import org.opensearch.action.support.ActionRequestMetadata; import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.common.CheckedFunction; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; @@ -56,7 +56,6 @@ import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluationException; import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; -import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.user.User; @@ -531,6 +530,7 @@ public IndicesAndAliases_getRestriction( null, null, null, + null, () -> CLUSTER_STATE, ActionPrivileges.EMPTY ); @@ -567,17 +567,12 @@ public static class IndicesAndAliases_isUnrestricted { final static IndexNameExpressionResolver INDEX_NAME_EXPRESSION_RESOLVER = new IndexNameExpressionResolver( new ThreadContext(Settings.EMPTY) ); - final static IndexResolverReplacer RESOLVER_REPLACER = new IndexResolverReplacer( - INDEX_NAME_EXPRESSION_RESOLVER, - () -> CLUSTER_STATE, - null - ); final Statefulness statefulness; final UserSpec userSpec; final User user; final IndicesSpec indicesSpec; - final IndexResolverReplacer.Resolved resolvedIndices; + final ResolvedIndices resolvedIndices; final PrivilegesEvaluationContext context; final boolean dfmEmptyOverridesAll; @@ -685,7 +680,7 @@ public void alias_static() throws Exception { DocumentPrivileges subject = createSubject(roleConfig); boolean result = subject.isUnrestricted(context, resolvedIndices); - if (resolvedIndices.getAllIndices().contains("index_b1")) { + if (resolvedIndices.local().names().contains("index_b1")) { // index_b1 is not covered by any of the above roles, so there should be always a restriction assertFalse(result); } else if (dfmEmptyOverridesAll && userSpec.roles.contains("non_dls_role")) { @@ -741,7 +736,7 @@ public void alias_wildcard() throws Exception { DocumentPrivileges subject = createSubject(roleConfig); boolean result = subject.isUnrestricted(context, resolvedIndices); - if (resolvedIndices.getAllIndices().contains("index_b1")) { + if (resolvedIndices.local().names().contains("index_b1")) { // index_b1 is not covered by any of the above roles, so there should be always a restriction assertFalse(result); } else if (dfmEmptyOverridesAll && userSpec.roles.contains("non_dls_role")) { @@ -771,7 +766,7 @@ public void alias_template() throws Exception { if (userSpec.attributes.isEmpty()) { // All roles defined above use attributes. If there are no user attributes, we must get a restricted result. assertFalse(result); - } else if (resolvedIndices.getAllIndices().contains("index_b1")) { + } else if (resolvedIndices.local().names().contains("index_b1")) { // index_b1 is not covered by any of the above roles, so there should be always a restriction assertFalse(result); } else if (dfmEmptyOverridesAll && userSpec.roles.contains("non_dls_role")) { @@ -828,31 +823,16 @@ public IndicesAndAliases_isUnrestricted( this.userSpec = userSpec; this.indicesSpec = indicesSpec; this.user = userSpec.buildUser(); - this.resolvedIndices = RESOLVER_REPLACER.resolveRequest(new IndicesRequest.Replaceable() { - - @Override - public String[] indices() { - return indicesSpec.indices.toArray(new String[0]); - } - - @Override - public IndicesOptions indicesOptions() { - return IndicesOptions.LENIENT_EXPAND_OPEN_CLOSED; - } - - @Override - public IndicesRequest indices(String... strings) { - return this; - } - }); + this.resolvedIndices = ResolvedIndices.of(indicesSpec.indices); this.context = new PrivilegesEvaluationContext( this.user, ImmutableSet.copyOf(userSpec.roles), null, null, + ActionRequestMetadata.empty(), null, - RESOLVER_REPLACER, INDEX_NAME_EXPRESSION_RESOLVER, + null, () -> CLUSTER_STATE, ActionPrivileges.EMPTY ); @@ -1151,7 +1131,9 @@ public DataStreams_getRestriction( null, null, null, + null, () -> CLUSTER_STATE, + ActionPrivileges.EMPTY ); this.statefulness = statefulness; diff --git a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java index 75d05e1fae..183bba5080 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java @@ -13,7 +13,6 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; -import com.google.common.collect.ImmutableSet; import org.apache.lucene.util.BytesRef; import org.junit.Test; import org.junit.runner.RunWith; @@ -22,13 +21,12 @@ import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.settings.Settings; -import org.opensearch.security.privileges.ActionPrivileges; import org.opensearch.security.privileges.PrivilegesConfigurationValidationException; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.WildcardMatcher; -import org.opensearch.security.user.User; +import org.opensearch.security.util.MockPrivilegeEvaluationContextBuilder; import org.opensearch.test.framework.TestSecurityConfig; import static org.opensearch.security.privileges.dlsfls.FieldMasking.Config.BLAKE2B_LEGACY_DEFAULT; @@ -117,17 +115,7 @@ static FieldMasking createSubject(SecurityDynamicConfiguration roleConfi } static PrivilegesEvaluationContext ctx(String... roles) { - return new PrivilegesEvaluationContext( - new User("test_user"), - ImmutableSet.copyOf(roles), - null, - null, - null, - null, - null, - () -> CLUSTER_STATE, - ActionPrivileges.EMPTY - ); + return MockPrivilegeEvaluationContextBuilder.ctx().roles(roles).get(); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java index 3b08a9d427..ae1d988f48 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java @@ -13,7 +13,6 @@ import java.util.Arrays; import java.util.Collections; -import com.google.common.collect.ImmutableSet; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -21,13 +20,12 @@ import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.settings.Settings; -import org.opensearch.security.privileges.ActionPrivileges; import org.opensearch.security.privileges.PrivilegesConfigurationValidationException; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.WildcardMatcher; -import org.opensearch.security.user.User; +import org.opensearch.security.util.MockPrivilegeEvaluationContextBuilder; import org.opensearch.test.framework.TestSecurityConfig; import static org.opensearch.security.util.MockIndexMetadataBuilder.indices; @@ -154,17 +152,7 @@ static FieldPrivileges createSubject(SecurityDynamicConfiguration roleCo } static PrivilegesEvaluationContext ctx(String... roles) { - return new PrivilegesEvaluationContext( - new User("test_user"), - ImmutableSet.copyOf(roles), - null, - null, - null, - null, - null, - () -> CLUSTER_STATE, - ActionPrivileges.EMPTY - ); + return MockPrivilegeEvaluationContextBuilder.ctx().roles(roles).get(); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/ClusterConfig.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/ClusterConfig.java index 2d3a1627e0..eabe34951c 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/ClusterConfig.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/ClusterConfig.java @@ -38,7 +38,8 @@ public enum ClusterConfig { true, true, false - ); + ), + NEXT_GEN_PRIVILEGES_EVALUATION("next_gen", c -> c.privilegesEvaluationType("next_gen"), false, true, true); final String name; final Function clusterConfiguration; @@ -48,6 +49,11 @@ public enum ClusterConfig { private LocalCluster cluster; + /** + * Optional: If we need to have a second remote cluster in our tests + */ + private LocalCluster remoteCluster; + ClusterConfig( String name, Function clusterConfiguration, @@ -70,6 +76,14 @@ LocalCluster cluster(Supplier clusterBuilder) { return cluster; } + LocalCluster remoteCluster(Supplier clusterBuilder) { + if (remoteCluster == null) { + remoteCluster = this.clusterConfiguration.apply(clusterBuilder.get()).build(); + remoteCluster.before(); + } + return remoteCluster; + } + void shutdown() { if (cluster != null) { try { @@ -77,6 +91,14 @@ void shutdown() { } catch (Exception e) {} cluster = null; } + if (remoteCluster != null) { + try { + remoteCluster.close(); + } catch (Exception e) { + e.printStackTrace(); + } + remoteCluster = null; + } } @Override diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DashboardMultiTenancyIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DashboardMultiTenancyIntTests.java new file mode 100644 index 0000000000..388d572cee --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DashboardMultiTenancyIntTests.java @@ -0,0 +1,713 @@ +/* + * 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.privileges.int_tests; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.hc.core5.http.message.BasicHeader; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.data.TestAlias; +import org.opensearch.test.framework.data.TestIndex; +import org.opensearch.test.framework.matcher.RestIndexMatchers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.cluster.TestRestClient.json; +import static org.opensearch.test.framework.matcher.RestIndexMatchers.OnResponseIndexMatcher.containsExactly; +import static org.opensearch.test.framework.matcher.RestIndexMatchers.OnUserIndexMatcher.limitedTo; +import static org.opensearch.test.framework.matcher.RestMatchers.isCreated; +import static org.opensearch.test.framework.matcher.RestMatchers.isForbidden; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class DashboardMultiTenancyIntTests { + + // ------------------------------------------------------------------------------------------------------- + // Tenants + // ------------------------------------------------------------------------------------------------------- + + static final TestSecurityConfig.Tenant TENANT_HUMAN_RESOURCES = new TestSecurityConfig.Tenant("human_resources").description( + "Human Resources Department Tenant" + ); + + static final TestSecurityConfig.Tenant TENANT_BUSINESS_INTELLIGENCE = new TestSecurityConfig.Tenant("business_intelligence") + .description("Business Intelligence Department Tenant"); + + // ------------------------------------------------------------------------------------------------------- + // Test indices and aliases + // ------------------------------------------------------------------------------------------------------- + + // Global tenant (default .kibana index) + static final TestIndex dashboards_index_global = TestIndex.name(".kibana_1").documentCount(10).seed(1).build(); + + static final TestAlias dashboards_alias_global = new TestAlias(".kibana").on(dashboards_index_global); + + static final TestIndex dashboards_index_human_resources = TestIndex.name(".kibana_1592542611_humanresources_1") + .documentCount(10) + .seed(2) + .build(); + + static final TestAlias dashboards_alias_human_resources = new TestAlias(".kibana_1592542611_humanresources").on( + dashboards_index_human_resources + ); + + static final TestIndex dashboards_index_business_intelligence = TestIndex.name(".kibana_1592542612_businessintelligence_1") + .documentCount(10) + .seed(3) + .build(); + + static final TestAlias dashboards_alias_business_intelligence = new TestAlias(".kibana_1592542612_businessintelligence").on( + dashboards_index_business_intelligence + ); + + // Private tenant for hr_employee user + static final TestIndex dashboards_index_private_hr = TestIndex.name(".kibana_-1843063229_hremployee_1") + .documentCount(10) + .seed(4) + .build(); + + static final TestAlias dashboards_alias_private_hr = new TestAlias(".kibana_-1843063229_hremployee").on(dashboards_index_private_hr); + + // Private tenant for bi_analyst user + static final TestIndex dashboards_index_private_bi = TestIndex.name(".kibana_-1388717942_bianalyst_1") + .documentCount(10) + .seed(5) + .build(); + + static final TestAlias dashboards_alias_private_bi = new TestAlias(".kibana_-1388717942_bianalyst").on(dashboards_index_private_bi); + + // Private tenant for global_tenant_rw user + static final TestIndex dashboards_index_private_global_tenant_rw = TestIndex.name(".kibana_-2043392244_globaltenantrwuser_1") + .documentCount(10) + .seed(6) + .build(); + + static final TestAlias dashboards_alias_private_global_tenant_rw = new TestAlias(".kibana_-2043392244_globaltenantrwuser").on( + dashboards_index_private_global_tenant_rw + ); + + // Private tenant for global_tenant_rw user + static final TestIndex dashboards_index_private_global_tenant_ro = TestIndex.name(".kibana_2022541844_globaltenantrouser_1") + .documentCount(10) + .seed(7) + .build(); + + static final TestAlias dashboards_alias_private_global_tenant_ro = new TestAlias(".kibana_2022541844_globaltenantrouser").on( + dashboards_index_private_global_tenant_ro + ); + + // Private tenant for no_tenant user + static final TestIndex dashboards_index_private_no_tenant = TestIndex.name(".kibana_-1011980094_notenantuser_1") + .documentCount(10) + .seed(8) + .build(); + + static final TestAlias dashboards_alias_private_no_tenant = new TestAlias(".kibana_-1011980094_notenantuser").on( + dashboards_index_private_no_tenant + ); + + // Private tenant for wildcard_tenant user + static final TestIndex dashboards_index_private_wc_tenant = TestIndex.name(".kibana_-904711333_wildcardtenantuser_1") + .documentCount(10) + .seed(9) + .build(); + + static final TestAlias dashboards_alias_private_wc_tenant = new TestAlias(".kibana_-904711333_wildcardtenantuser").on( + dashboards_index_private_wc_tenant + ); + + static final TestSecurityConfig.User.MetadataKey READ = new TestSecurityConfig.User.MetadataKey<>( + "read", + RestIndexMatchers.IndexMatcher.class + ); + + static final TestSecurityConfig.User.MetadataKey WRITE = new TestSecurityConfig.User.MetadataKey<>( + "write", + RestIndexMatchers.IndexMatcher.class + ); + + // ------------------------------------------------------------------------------------------------------- + // Test users + // ------------------------------------------------------------------------------------------------------- + + /** + * HR Employee with read-write access to human_resources tenant and read-only to business_intelligence tenant. + * Also has access to their private tenant. + */ + static final TestSecurityConfig.User HR_EMPLOYEE = new TestSecurityConfig.User("hr_employee").description( + "r/w to HR tenant, r to BI tenant" + ) + .roles( + new TestSecurityConfig.Role("hr_employee_role").clusterPermissions("cluster_composite_ops") + .tenantPermissions("kibana_all_write") + .on("human_resources") + .tenantPermissions("kibana_all_read") + .on("business_intelligence") + ) + .reference( + READ, + limitedTo( + dashboards_alias_private_hr, + dashboards_index_private_hr, + dashboards_alias_business_intelligence, + dashboards_index_business_intelligence, + dashboards_alias_human_resources, + dashboards_index_human_resources + ) + ) + .reference( + WRITE, + limitedTo( + dashboards_alias_private_hr, + dashboards_index_private_hr, + dashboards_alias_human_resources, + dashboards_index_human_resources + ) + ); + + /** + * BI Analyst with read-write access to business_intelligence tenant only. + */ + static final TestSecurityConfig.User BI_ANALYST = new TestSecurityConfig.User("bi_analyst").description("r/w to BI tenant") + .roles( + new TestSecurityConfig.Role("bi_analyst_role").clusterPermissions("cluster_composite_ops") + .tenantPermissions("kibana_all_write") + .on("business_intelligence") + ) + .reference( + READ, + limitedTo( + dashboards_alias_private_bi, + dashboards_index_private_bi, + dashboards_alias_business_intelligence, + dashboards_index_business_intelligence + ) + ) + .reference( + WRITE, + limitedTo( + dashboards_alias_private_bi, + dashboards_index_private_bi, + dashboards_alias_business_intelligence, + dashboards_index_business_intelligence + ) + ); + + static final TestSecurityConfig.User GLOBAL_TENANT_READ_WRITE_USER = new TestSecurityConfig.User("global_tenant_rw_user").description( + "r/w to global tenant" + ) + .roles( + TestSecurityConfig.Role.KIBANA_USER, + new TestSecurityConfig.Role("global_tenant_role").clusterPermissions("cluster_composite_ops") + .tenantPermissions("kibana_all_write") + .on("global_tenant") + ) + .reference( + READ, + limitedTo( + dashboards_alias_private_global_tenant_rw, + dashboards_index_private_global_tenant_rw, + dashboards_alias_global, + dashboards_index_global + ) + ) + .reference( + WRITE, + limitedTo( + dashboards_alias_private_global_tenant_rw, + dashboards_index_private_global_tenant_rw, + dashboards_alias_global, + dashboards_index_global + ) + ); + + static final TestSecurityConfig.User GLOBAL_TENANT_READ_ONLY_USER = new TestSecurityConfig.User("global_tenant_ro_user").description( + "r/o to global tenant" + ) + .roles( + TestSecurityConfig.Role.KIBANA_USER, + new TestSecurityConfig.Role("global_tenant_role").clusterPermissions("cluster_composite_ops") + .tenantPermissions("kibana_all_read") + .on("global_tenant") + ) + .reference( + READ, + limitedTo( + dashboards_alias_private_global_tenant_ro, + dashboards_index_private_global_tenant_ro, + dashboards_alias_global, + dashboards_index_global + ) + ) + .reference(WRITE, limitedTo(dashboards_alias_private_global_tenant_ro, dashboards_index_private_global_tenant_ro)); + + /** + * User with no tenant access (except the private tenant which every user has by default). + */ + static final TestSecurityConfig.User NO_TENANT_USER = new TestSecurityConfig.User("no_tenant_user").description( + "r/w only to private tenant" + ) + .roles(new TestSecurityConfig.Role("no_tenant_role").clusterPermissions("cluster_composite_ops")) + .reference(READ, limitedTo(dashboards_alias_private_no_tenant, dashboards_index_private_no_tenant)) + .reference(WRITE, limitedTo(dashboards_alias_private_no_tenant, dashboards_index_private_no_tenant)); + + /** + * User with wildcard tenant pattern access - can access any tenant matching the pattern. + * This tests tenant pattern substitution feature. + */ + static final TestSecurityConfig.User WILDCARD_TENANT_USER = new TestSecurityConfig.User("wildcard_tenant_user").description("r/w to *") + .roles( + TestSecurityConfig.Role.KIBANA_USER, + new TestSecurityConfig.Role("wildcard_tenant_role").clusterPermissions("cluster_composite_ops") + .tenantPermissions("kibana_all_write") + .on("*") + ) + .reference( + READ, + limitedTo( + dashboards_alias_private_wc_tenant, + dashboards_index_private_wc_tenant, + dashboards_alias_global, + dashboards_index_global, + dashboards_alias_business_intelligence, + dashboards_index_business_intelligence, + dashboards_alias_human_resources, + dashboards_index_human_resources + ) + ) + .reference( + WRITE, + limitedTo( + dashboards_alias_private_wc_tenant, + dashboards_index_private_wc_tenant, + dashboards_alias_global, + dashboards_index_global, + dashboards_alias_business_intelligence, + dashboards_index_business_intelligence, + dashboards_alias_human_resources, + dashboards_index_human_resources + ) + ); + + static final List USERS = List.of( + HR_EMPLOYEE, + BI_ANALYST, + GLOBAL_TENANT_READ_WRITE_USER, + GLOBAL_TENANT_READ_ONLY_USER, + NO_TENANT_USER, + WILDCARD_TENANT_USER + ); + + static LocalCluster.Builder clusterBuilder() { + return new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USERS) + .tenants(TENANT_HUMAN_RESOURCES, TENANT_BUSINESS_INTELLIGENCE) + .indices( + dashboards_index_global, + dashboards_index_human_resources, + dashboards_index_business_intelligence, + dashboards_index_private_hr, + dashboards_index_private_bi, + dashboards_index_private_global_tenant_rw, + dashboards_index_private_global_tenant_ro, + dashboards_index_private_no_tenant, + dashboards_index_private_wc_tenant + ) + .aliases( + dashboards_alias_global, + dashboards_alias_human_resources, + dashboards_alias_business_intelligence, + dashboards_alias_private_hr, + dashboards_alias_private_bi, + dashboards_alias_private_global_tenant_rw, + dashboards_alias_private_global_tenant_ro, + dashboards_alias_private_no_tenant, + dashboards_alias_private_wc_tenant + ); + } + + @AfterClass + public static void stopClusters() { + for (ClusterConfig clusterConfig : ClusterConfig.values()) { + clusterConfig.shutdown(); + } + } + + final TestSecurityConfig.User user; + final LocalCluster cluster; + final ClusterConfig clusterConfig; + + @ParametersFactory(shuffle = false, argumentFormatting = "%1$s, %3$s") + public static Collection params() { + List result = new ArrayList<>(); + for (ClusterConfig clusterConfig : ClusterConfig.values()) { + for (TestSecurityConfig.User user : USERS) { + result.add(new Object[] { clusterConfig, user, user.getDescription() }); + } + } + return result; + } + + public DashboardMultiTenancyIntTests( + ClusterConfig clusterConfig, + TestSecurityConfig.User user, + @SuppressWarnings("unused") String description + ) { + this.user = user; + this.cluster = clusterConfig.cluster(DashboardMultiTenancyIntTests::clusterBuilder); + this.clusterConfig = clusterConfig; + } + + @Test + public void search_withTenantHeader_humanResources() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse response = restClient.get( + ".kibana/_search/?pretty", + new BasicHeader("securitytenant", "human_resources") + ); + + assertThat( + response, + containsExactly(dashboards_index_human_resources).at("hits.hits[*]._index").butForbiddenIfIncomplete(user.reference(READ)) + ); + } + } + + /** + * This should access the user's private tenant. + */ + @Test + public void search_withTenantHeader_private() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse response = restClient.get(".kibana/_search/?pretty", new BasicHeader("securitytenant", "__user__")); + + assertThat( + response, + containsExactly( + dashboards_index_private_hr, + dashboards_index_private_bi, + dashboards_index_private_global_tenant_rw, + dashboards_index_private_global_tenant_ro, + dashboards_index_private_no_tenant, + dashboards_index_private_wc_tenant + ).at("hits.hits[*]._index").reducedBy(user.reference(READ)) + ); + } + } + + /** + * If the tenant header is absent, the global tenant should be used. + */ + @Test + public void search_withoutTenantHeader() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + + TestRestClient.HttpResponse response = restClient.get(".kibana/_search/?pretty"); + + assertThat( + response, + containsExactly(dashboards_index_global).at("hits.hits[*]._index").butForbiddenIfIncomplete(user.reference(READ)) + ); + } + } + + @Test + public void search_nonExistingTenant() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse res = restClient.get( + ".kibana/_search/?pretty", + new BasicHeader("securitytenant", "nonexistent_tenant") + ); + + assertThat(res, isForbidden()); + } + } + + /** + * This is a search request that goes directly against the alias for the tenant, bypassing the tenant resolution. + * Still, the user's tenant permissions must be checked and enforced. + */ + @Test + public void search_withTenantHeader_direct_alias() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse response = restClient.get( + ".kibana_1592542611_humanresources/_search/?pretty", + new BasicHeader("securitytenant", "human_resources") + ); + + assertThat( + response, + containsExactly(dashboards_index_human_resources).at("hits.hits[*]._index").butForbiddenIfIncomplete(user.reference(READ)) + ); + } + } + + /** + * This is a search request that goes directly against the index for the tenant, bypassing the tenant resolution. + * Still, the user's tenant permissions must be checked and enforced. + */ + @Test + public void search_withTenantHeader_direct_index() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse response = restClient.get( + ".kibana_1592542611_humanresources_1/_search/?pretty", + new BasicHeader("securitytenant", "human_resources") + ); + + assertThat( + response, + containsExactly(dashboards_index_human_resources).at("hits.hits[*]._index").butForbiddenIfIncomplete(user.reference(READ)) + ); + } + } + + /** + * This is a search request that goes directly against the index for a tenant, that is different from the one specified in the tenant header. + * Thus, these requests must be always forbidden. + */ + @Test + public void search_withTenantHeader_direct_wrongIndex() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse response = restClient.get( + ".kibana_1592542612_businessintelligence/_search/?pretty", + new BasicHeader("securitytenant", "human_resources") + ); + + assertThat(response, isForbidden()); + } + } + + @Test + public void msearch_withTenantHeader_humanResources() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse response = restClient.postJson("_msearch/?pretty", """ + {"index":".kibana", "ignore_unavailable": false} + {"size":10, "query":{"bool":{"must":{"match_all":{}}}}} + """, new BasicHeader("securitytenant", "human_resources")); + + assertThat( + response, + containsExactly(dashboards_index_human_resources).at("responses[*].hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(isOk()) + ); + } + } + + @Test + public void get_withTenantHeader_humanResources() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + String docId = dashboards_index_human_resources.anyDocument().id(); + + TestRestClient.HttpResponse response = restClient.get( + ".kibana/_doc/" + docId + "?pretty", + new BasicHeader("securitytenant", "human_resources") + ); + + assertThat( + response, + containsExactly(dashboards_index_human_resources).at("_index").reducedBy(user.reference(READ)).whenEmpty(isForbidden()) + ); + } + } + + @Test + public void mget_withTenantHeader_humanResources() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + String docId = dashboards_index_human_resources.anyDocument().id(); + + TestRestClient.HttpResponse response = restClient.postJson("_mget/?pretty", """ + { + "docs": [ + { + "_index": ".kibana", + "_id": "%s" + } + ] + } + """.formatted(docId), new BasicHeader("securitytenant", "human_resources")); + + assertThat( + response, + containsExactly(dashboards_index_human_resources).at("docs[?(@.found == true)]._index") + .butForbiddenIfIncomplete(user.reference(READ)) + ); + } + } + + @Test + public void index_withTenantHeader_humanResources() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + String indexDoc = """ + { + "foo": "bar" + } + """; + + TestRestClient.HttpResponse response = restClient.putJson( + ".kibana/_doc/test_mt_write_1?pretty", + indexDoc, + new BasicHeader("securitytenant", "human_resources") + ); + + if (user.reference(WRITE).covers(dashboards_index_human_resources)) { + assertThat(response, isCreated()); + } else { + assertThat(response, isForbidden()); + } + } finally { + delete(dashboards_index_human_resources.name() + "/_doc/test_mt_write_1"); + } + } + + @Test + public void bulk_withTenantHeader_humanResources() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + String bulkBody = """ + { "index" : { "_index" : ".kibana", "_id" : "mt_bulk_doc_1" } } + { "type": "config", "config": { "buildNum": 12345 } } + { "index" : { "_index" : ".kibana", "_id" : "mt_bulk_doc_2" } } + { "type": "index-pattern", "index-pattern": { "title": "logs*" } } + """; + + TestRestClient.HttpResponse response = restClient.postJson( + "_bulk?pretty", + bulkBody, + new BasicHeader("securitytenant", "human_resources") + ); + + assertThat( + response, + containsExactly(dashboards_index_human_resources).at("items[*].index[?(@.result == 'created')]._index") + .butForbiddenIfIncomplete(user.reference(WRITE)) + ); + } finally { + delete( + dashboards_index_human_resources.name() + "/_doc/mt_bulk_doc_1", + dashboards_index_human_resources.name() + "/_doc/mt_bulk_doc_2" + ); + } + } + + @Test + public void bulk_withTenantHeader_direct_humanResources_alias() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + String bulkBody = """ + { "index" : { "_index" : ".kibana_1592542611_humanresources", "_id" : "mt_bulk_doc_1" } } + { "type": "config", "config": { "buildNum": 12345 } } + { "index" : { "_index" : ".kibana_1592542611_humanresources", "_id" : "mt_bulk_doc_2" } } + { "type": "index-pattern", "index-pattern": { "title": "logs*" } } + """; + + TestRestClient.HttpResponse response = restClient.postJson( + "_bulk?pretty", + bulkBody, + new BasicHeader("securitytenant", "human_resources") + ); + + assertThat( + response, + containsExactly(dashboards_index_human_resources).at("items[*].index[?(@.result == 'created')]._index") + .reducedBy(user.reference(WRITE)) + .whenEmpty(isOk()) + ); + } finally { + delete( + dashboards_index_human_resources.name() + "/_doc/mt_bulk_doc_1", + dashboards_index_human_resources.name() + "/_doc/mt_bulk_doc_2" + ); + } + } + + @Test + public void bulk_withTenantHeader_direct_global_index() { + try (TestRestClient restClient = cluster.getRestClient(user)) { + String bulkBody = """ + { "index" : { "_index" : ".kibana_1", "_id" : "mt_bulk_doc_1" } } + { "type": "config", "config": { "buildNum": 12345 } } + { "index" : { "_index" : ".kibana_1", "_id" : "mt_bulk_doc_2" } } + { "type": "index-pattern", "index-pattern": { "title": "logs*" } } + """; + + TestRestClient.HttpResponse response = restClient.postJson( + "_bulk?pretty", + bulkBody, + new BasicHeader("securitytenant", "human_resources") + ); + + assertThat( + response, + containsExactly(dashboards_index_global).at("items[*].index[?(@.result == 'created')]._index") + .reducedBy(user.reference(WRITE)) + .whenEmpty(isOk()) + ); + } finally { + delete(dashboards_index_global.name() + "/_doc/mt_bulk_doc_1", dashboards_index_global.name() + "/_doc/mt_bulk_doc_2"); + } + } + + @Test + public void delete_withTenantHeader_humanResources() { + String testDocId = "test_delete_doc_" + UUID.randomUUID(); + + try (TestRestClient restClient = cluster.getRestClient(user); TestRestClient adminRestClient = cluster.getAdminCertRestClient()) { + + TestRestClient.HttpResponse response = adminRestClient.put( + dashboards_index_human_resources.name() + "/_doc/" + testDocId + "?pretty", + json("foo", "bar"), + new BasicHeader("securitytenant", "human_resources") + ); + + assertThat(response, isCreated()); + + TestRestClient.HttpResponse deleteRes = restClient.delete( + ".kibana/_doc/" + testDocId + "?pretty", + new BasicHeader("securitytenant", "human_resources") + ); + + if (user.reference(WRITE).covers(dashboards_index_human_resources)) { + assertThat(deleteRes, isOk()); + assertThat(deleteRes.getBody(), containsString("\"result\" : \"deleted\"")); + } else { + assertThat(deleteRes, isForbidden()); + } + } finally { + delete(dashboards_index_human_resources.name() + "/_doc/" + testDocId); + } + } + + private void delete(String... paths) { + try (TestRestClient adminRestClient = cluster.getAdminCertRestClient()) { + for (String path : paths) { + TestRestClient.HttpResponse response = adminRestClient.delete(path); + if (response.getStatusCode() != 200 && response.getStatusCode() != 404) { + throw new RuntimeException("Error while deleting " + path + "\n" + response.getBody()); + } + } + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadOnlyIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadOnlyIntTests.java index 10a107d057..724755a4c3 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadOnlyIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadOnlyIntTests.java @@ -261,14 +261,7 @@ public void search_noPattern_noWildcards() throws Exception { assertThat(httpResponse, isOk()); assertThat(httpResponse, containsExactly().at("hits.hits[*]._index")); } else { - // The dnfof implementation has the effect that the expand_wildcards=none option is disregarded - // Additionally, the dnfof implementation has the effect that hidden indices might be included even though not requested - assertThat( - httpResponse, - containsExactly(ALL_INDICES).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + assertThat(httpResponse, isForbidden()); } } } @@ -277,13 +270,17 @@ public void search_noPattern_noWildcards() throws Exception { public void search_noPattern_allowNoIndicesFalse() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_search?size=1000&allow_no_indices=false"); - - assertThat( - httpResponse, - containsExactly(ALL_INDICES_EXCEPT_SYSTEM_INDICES).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isNotFound() : isForbidden()) - ); + if (user != LIMITED_USER_OTHER_PRIVILEGES) { + assertThat( + httpResponse, + containsExactly(ALL_INDICES_EXCEPT_SYSTEM_INDICES).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isNotFound() : isForbidden()) + ); + } else { + // Due to allow_no_indices=false, we cannot reduce to the empty set for the user without any privileges. Thus we get a 403 + assertThat(httpResponse, isForbidden()); + } } } @@ -308,14 +305,7 @@ public void search_all_noWildcards() throws Exception { assertThat(httpResponse, isOk()); assertThat(httpResponse, containsExactly().at("hits.hits[*]._index")); } else { - // The dnfof implementation has the effect that the expand_wildcards=none option is disregarded - // Additionally, the dnfof implementation has the effect that hidden indices might be included even though not requested - assertThat( - httpResponse, - containsExactly(ALL_INDICES).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + assertThat(httpResponse, isForbidden()); } } } @@ -338,11 +328,19 @@ public void search_wildcard() throws Exception { public void search_staticNames_noIgnoreUnavailable() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("ds_a1,ds_a2,ds_b1/_search?size=1000"); - // With dnfof data streams with incomplete privileges will be replaced by their member indices - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_b1).at("hits.hits[*]._index").reducedBy(user.reference(READ)).whenEmpty(isForbidden()) - ); + if (clusterConfig.legacyPrivilegeEvaluation) { + // In the old privilege evaluation, data streams with incomplete privileges will be replaced by their member indices + assertThat( + httpResponse, + containsExactly(ds_a1, ds_a2, ds_b1).at("hits.hits[*]._index").reducedBy(user.reference(READ)).whenEmpty(isForbidden()) + ); + } else { + // In the new privilege evaluation, data streams with incomplete privileges will lead to a 403 error + assertThat( + httpResponse, + containsExactly(ds_a1, ds_a2, ds_b1).at("hits.hits[*]._index").butForbiddenIfIncomplete(user.reference(READ)) + ); + } } } @@ -389,38 +387,14 @@ public void search_indexPattern() throws Exception { public void search_indexPattern_minus() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("ds_a*,ds_b*,-ds_b2,-ds_b3/_search?size=1000"); - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { - // does not handle the expression ds_a*,ds_b*,-ds_b2,-ds_b3 in a way that excludes the data streams. See - // search_indexPattern_minus_backingIndices for an alternative. - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3, ds_b1, ds_b2, ds_b3).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); - } else { - // The IndexResolverReplacer fails to interpret the minus patterns and falls back to interpreting the given index names - // literally - // In the logs, this then looks like this: - // | indices:data/read/search | - // -ds_b2| MISSING | - // -ds_b3| MISSING | - // ds_b* | MISSING | - // ds_a* | MISSING | - // This has the effect that granted privileges using wildcards might work, but granted privileges without wildcards won't - // work - if (user == LIMITED_USER_B1) { - // No wildcard in the index pattern - assertThat(httpResponse, isForbidden()); - } else { - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3, ds_b1, ds_b2, ds_b3).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); - } - } + // OpenSearch does not handle the expression ds_a*,ds_b*,-ds_b2,-ds_b3 in a way that excludes the data streams. See + // search_indexPattern_minus_backingIndices for an alternative. + assertThat( + httpResponse, + containsExactly(ds_a1, ds_a2, ds_a3, ds_b1, ds_b2, ds_b3).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); } } @@ -428,23 +402,12 @@ public void search_indexPattern_minus() throws Exception { public void search_indexPattern_minus_backingIndices() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("ds_a*,ds_b*,-.ds-ds_b2*,-.ds-ds_b3*/_search?size=1000"); - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3, ds_b1).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); - } else { - - // dnfof has the effect that the index expression is interpreted differently and that ds_b2 and ds_b3 get included - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3, ds_b1, ds_b2, ds_b3).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); - } + assertThat( + httpResponse, + containsExactly(ds_a1, ds_a2, ds_a3, ds_b1).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); } } @@ -455,19 +418,13 @@ public void search_indexPattern_nonExistingIndex_ignoreUnavailable() throws Exce "ds_a*,ds_b*,xxx_non_existing/_search?size=1000&ignore_unavailable=true" ); - // The presence of a non existing index has the effect that the other patterns are not resolved by IndexResolverReplacer - // This causes a few more 403 errors where the granted index patterns do not use wildcards + assertThat( + httpResponse, + containsExactly(ds_a1, ds_a2, ds_a3, ds_b1, ds_b2, ds_b3).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); - if (user == LIMITED_USER_B1) { - assertThat(httpResponse, isForbidden()); - } else { - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3, ds_b1, ds_b2, ds_b3).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); - } } } @@ -477,17 +434,11 @@ public void search_indexPattern_noWildcards() throws Exception { TestRestClient.HttpResponse httpResponse = restClient.get( "ds_a*,ds_b*/_search?size=1000&expand_wildcards=none&ignore_unavailable=true" ); - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { + if (clusterConfig.legacyPrivilegeEvaluation && (user == LIMITED_USER_B1 || user == LIMITED_USER_OTHER_PRIVILEGES)) { + assertThat(httpResponse, isForbidden("/error/root_cause/0/reason", "no permissions for [indices:data/read/search]")); + } else { assertThat(httpResponse, isOk()); assertThat(httpResponse, containsExactly().at("hits.hits[*]._index")); - } else { - // dnfof makes the expand_wildcards=none option ineffective - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3, ds_b1, ds_b2, ds_b3).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); } } } @@ -531,19 +482,13 @@ public void search_termsAggregation_index() throws Exception { } }"""); - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { - assertThat( - httpResponse, - containsExactly(ALL_INDICES_EXCEPT_SYSTEM_INDICES).at("aggregations.indices.buckets[*].key") - .reducedBy(user.reference(READ)) - .whenEmpty(isOk()) - ); - } else { - // Users without full privileges will not see hidden indices here; thus on a cluster with only data streams, the result is - // often just empty - assertThat(httpResponse, isOk()); - assertThat(httpResponse, containsExactly().at("aggregations.indices.buckets[*].key")); - } + assertThat( + httpResponse, + containsExactly(ALL_INDICES_EXCEPT_SYSTEM_INDICES).at("aggregations.indices.buckets[*].key") + .reducedBy(user.reference(READ)) + .whenEmpty(isOk()) + ); + } } @@ -595,11 +540,18 @@ public void index_stats_pattern() throws Exception { public void getDataStream_all() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_data_stream"); - // The legacy mode does not support dnfof for indices:admin/data_stream/get - assertThat( - httpResponse, - containsExactly(ALL_DATA_STREAMS).at("$.data_streams[*].name").butForbiddenIfIncomplete(user.reference(READ)) - ); + if (clusterConfig.legacyPrivilegeEvaluation) { + // The legacy mode does not support dnfof for indices:admin/data_stream/get + assertThat( + httpResponse, + containsExactly(ALL_DATA_STREAMS).at("$.data_streams[*].name").butForbiddenIfIncomplete(user.reference(READ)) + ); + } else { + assertThat( + httpResponse, + containsExactly(ALL_DATA_STREAMS).at("$.data_streams[*].name").reducedBy(user.reference(READ)).whenEmpty(isOk()) + ); + } } } @@ -607,11 +559,18 @@ public void getDataStream_all() throws Exception { public void getDataStream_wildcard() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_data_stream/*"); - // The legacy mode does not support dnfof for indices:admin/data_stream/get - assertThat( - httpResponse, - containsExactly(ALL_DATA_STREAMS).at("$.data_streams[*].name").butForbiddenIfIncomplete(user.reference(READ)) - ); + if (clusterConfig.legacyPrivilegeEvaluation) { + // The legacy mode does not support dnfof for indices:admin/data_stream/get + assertThat( + httpResponse, + containsExactly(ALL_DATA_STREAMS).at("$.data_streams[*].name").butForbiddenIfIncomplete(user.reference(READ)) + ); + } else { + assertThat( + httpResponse, + containsExactly(ALL_DATA_STREAMS).at("$.data_streams[*].name").reducedBy(user.reference(READ)).whenEmpty(isOk()) + ); + } } } @@ -619,11 +578,18 @@ public void getDataStream_wildcard() throws Exception { public void getDataStream_pattern() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_data_stream/ds_a*"); - // The legacy mode does not support dnfof for indices:admin/data_stream/get - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3).at("$.data_streams[*].name").butForbiddenIfIncomplete(user.reference(READ)) - ); + if (clusterConfig.legacyPrivilegeEvaluation) { + // The legacy mode does not support dnfof for indices:admin/data_stream/get + assertThat( + httpResponse, + containsExactly(ds_a1, ds_a2, ds_a3).at("$.data_streams[*].name").butForbiddenIfIncomplete(user.reference(READ)) + ); + } else { + assertThat( + httpResponse, + containsExactly(ds_a1, ds_a2, ds_a3).at("$.data_streams[*].name").reducedBy(user.reference(READ)).whenEmpty(isOk()) + ); + } } } @@ -631,11 +597,18 @@ public void getDataStream_pattern() throws Exception { public void getDataStream_pattern_negation() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_data_stream/ds_*,-ds_b*"); - // The legacy mode does not support dnfof for indices:admin/data_stream/get - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3).at("$.data_streams[*].name").butForbiddenIfIncomplete(user.reference(READ)) - ); + if (clusterConfig.legacyPrivilegeEvaluation) { + // The legacy mode does not support dnfof for indices:admin/data_stream/get + assertThat( + httpResponse, + containsExactly(ds_a1, ds_a2, ds_a3).at("$.data_streams[*].name").butForbiddenIfIncomplete(user.reference(READ)) + ); + } else { + assertThat( + httpResponse, + containsExactly(ds_a1, ds_a2, ds_a3).at("$.data_streams[*].name").reducedBy(user.reference(READ)).whenEmpty(isOk()) + ); + } } } @@ -654,11 +627,18 @@ public void getDataStream_static() throws Exception { public void getDataStreamStats_all() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_data_stream/_stats"); - // The legacy mode does not support dnfof for indices:monitor/data_stream/stats - assertThat( - httpResponse, - containsExactly(ALL_DATA_STREAMS).at("$.data_streams[*].data_stream").butForbiddenIfIncomplete(user.reference(READ)) - ); + if (clusterConfig.legacyPrivilegeEvaluation) { + // The legacy mode does not support dnfof for indices:monitor/data_stream/stats + assertThat( + httpResponse, + containsExactly(ALL_DATA_STREAMS).at("$.data_streams[*].data_stream").butForbiddenIfIncomplete(user.reference(READ)) + ); + } else { + assertThat( + httpResponse, + containsExactly(ALL_DATA_STREAMS).at("$.data_streams[*].data_stream").reducedBy(user.reference(READ)).whenEmpty(isOk()) + ); + } } } @@ -666,11 +646,18 @@ public void getDataStreamStats_all() throws Exception { public void getDataStreamStats_wildcard() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_data_stream/*/_stats"); - // The legacy mode does not support dnfof for indices:monitor/data_stream/stats - assertThat( - httpResponse, - containsExactly(ALL_DATA_STREAMS).at("$.data_streams[*].data_stream").butForbiddenIfIncomplete(user.reference(READ)) - ); + if (clusterConfig.legacyPrivilegeEvaluation) { + // The legacy mode does not support dnfof for indices:monitor/data_stream/stats + assertThat( + httpResponse, + containsExactly(ALL_DATA_STREAMS).at("$.data_streams[*].data_stream").butForbiddenIfIncomplete(user.reference(READ)) + ); + } else { + assertThat( + httpResponse, + containsExactly(ALL_DATA_STREAMS).at("$.data_streams[*].data_stream").reducedBy(user.reference(READ)).whenEmpty(isOk()) + ); + } } } @@ -678,11 +665,20 @@ public void getDataStreamStats_wildcard() throws Exception { public void getDataStreamStats_pattern() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_data_stream/ds_a*/_stats"); - // The legacy mode does not support dnfof for indices:monitor/data_stream/stats - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3).at("$.data_streams[*].data_stream").butForbiddenIfIncomplete(user.reference(READ)) - ); + if (clusterConfig.legacyPrivilegeEvaluation) { + // The legacy mode does not support dnfof for indices:monitor/data_stream/stats + assertThat( + httpResponse, + containsExactly(ds_a1, ds_a2, ds_a3).at("$.data_streams[*].data_stream").butForbiddenIfIncomplete(user.reference(READ)) + ); + } else { + assertThat( + httpResponse, + containsExactly(ds_a1, ds_a2, ds_a3).at("$.data_streams[*].data_stream") + .reducedBy(user.reference(READ)) + .whenEmpty(isOk()) + ); + } } } @@ -753,11 +749,14 @@ public void field_caps_indexPattern() throws Exception { public void field_caps_staticIndices_noIgnoreUnavailable() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("ds_a1,ds_a2,ds_b1/_field_caps?fields=*"); - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_b1).at("indices").reducedBy(user.reference(READ)).whenEmpty(isForbidden()) - ); - + if (clusterConfig.legacyPrivilegeEvaluation) { + assertThat( + httpResponse, + containsExactly(ds_a1, ds_a2, ds_b1).at("indices").reducedBy(user.reference(READ)).whenEmpty(isForbidden()) + ); + } else { + assertThat(httpResponse, containsExactly(ds_a1, ds_a2, ds_b1).at("indices").butForbiddenIfIncomplete(user.reference(READ))); + } } } @@ -800,28 +799,14 @@ public void field_caps_nonExisting_indexPattern() throws Exception { public void field_caps_indexPattern_minus() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("ds_a*,ds_b*,-ds_b2,-ds_b3/_field_caps?fields=*"); - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { - // OpenSearch does not handle the expression ds_a*,ds_b*,-ds_b2,-ds_b3 in a way that excludes the data streams. See - // field_caps_indexPattern_minus_backingIndices for an alternative. - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3, ds_b1, ds_b2, ds_b3).at("indices") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); - } else { - if (user == LIMITED_USER_B1) { - // No wildcard in the index pattern - assertThat(httpResponse, isForbidden()); - } else { - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3, ds_b1, ds_b2, ds_b3).at("indices") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); - } - } + // does not handle the expression ds_a*,ds_b*,-ds_b2,-ds_b3 in a way that excludes the data streams. See + // field_caps_indexPattern_minus_backingIndices for an alternative. + assertThat( + httpResponse, + containsExactly(ds_a1, ds_a2, ds_a3, ds_b1, ds_b2, ds_b3).at("indices") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); } } @@ -829,22 +814,12 @@ public void field_caps_indexPattern_minus() throws Exception { public void field_caps_indexPattern_minus_backingIndices() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("ds_a*,ds_b*,-.ds-ds_b2*,-.ds-ds_b3*/_field_caps?fields=*"); - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3, ds_b1).at("indices") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); - } else { - // dnfof has the effect that the index expression is interpreted differently and that ds_b2 and ds_b3 get included - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3, ds_b1, ds_b2, ds_b3).at("indices") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); - } + assertThat( + httpResponse, + containsExactly(ds_a1, ds_a2, ds_a3, ds_b1).at("indices") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadWriteIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadWriteIntTests.java index 11a341f726..48a47746a8 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadWriteIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DataStreamAuthorizationReadWriteIntTests.java @@ -510,13 +510,7 @@ public void putDocument_bulk() throws Exception { { "b": 1, "test": "putDocument_bulk", "@timestamp": "2025-09-15T12:00:01Z" } """); - if (user == LIMITED_USER_PERMISSIONS_ON_BACKING_INDICES) { - // IndexResolverReplacer won't resolve data stream names to member index names, because it does not - // specify the includeDataStream option and thus just stumbles over an IndexNotFoundException - // Thus, in contrast to aliases, privileges on backing index names won't work - assertThat(httpResponse, isOk()); - assertThat(httpResponse, containsExactly().at("items[*].create[?(@.result == 'created')]._index")); - } else if (user != LIMITED_USER_NONE) { + if (user != LIMITED_USER_NONE) { assertThat( httpResponse, containsExactly(ds_aw1, ds_bw1).at("items[*].create[?(@.result == 'created')]._index") diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadOnlyIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadOnlyIntTests.java index 5182f6fb53..a20fa04a0f 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadOnlyIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadOnlyIntTests.java @@ -16,6 +16,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; @@ -40,7 +41,6 @@ import org.opensearch.test.framework.matcher.RestIndexMatchers; import static java.util.stream.Collectors.joining; -import static org.apache.commons.lang3.StringEscapeUtils.escapeJson; import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.cluster.TestRestClient.json; @@ -194,6 +194,7 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_a*") )// .reference(READ, limitedTo(index_a1, index_a2, index_a3, index_ax))// + .reference(READ_NEXT_GEN, limitedTo(index_a1, index_a2, index_a3, index_ax))// .reference(GET_ALIAS, limitedToNone()); /** @@ -208,6 +209,7 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_b*") )// .reference(READ, limitedTo(index_b1, index_b2, index_b3))// + .reference(READ_NEXT_GEN, limitedTo(index_b1, index_b2, index_b3))// .reference(GET_ALIAS, limitedToNone()); /** @@ -222,6 +224,7 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_b1") )// .reference(READ, limitedTo(index_b1))// + .reference(READ_NEXT_GEN, limitedTo(index_b1))// .reference(GET_ALIAS, limitedToNone()); /** @@ -236,6 +239,7 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_c*") )// .reference(READ, limitedTo(index_c1, alias_c1))// + .reference(READ_NEXT_GEN, limitedTo(index_c1))// .reference(GET_ALIAS, limitedToNone()); /** @@ -251,6 +255,7 @@ public class IndexAuthorizationReadOnlyIntTests { .on("alias_ab1*") )// .reference(READ, limitedTo(index_a1, index_a2, index_a3, index_b1, alias_ab1))// + .reference(READ_NEXT_GEN, limitedTo(index_a1, index_a2, index_a3, index_b1, alias_ab1))// .reference(GET_ALIAS, limitedTo(index_a1, index_a2, index_a3, index_b1, alias_ab1)); /** @@ -266,6 +271,7 @@ public class IndexAuthorizationReadOnlyIntTests { .on("alias_c1") )// .reference(READ, limitedTo(index_c1, alias_c1))// + .reference(READ_NEXT_GEN, limitedTo(index_c1, alias_c1))// .reference(GET_ALIAS, limitedTo(index_c1, alias_c1)); /** * Same as LIMITED_USER_A with the addition of read privileges for index_hidden* and .index_hidden* @@ -279,6 +285,7 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_a*", "index_hidden*", ".index_hidden*") )// .reference(READ, limitedTo(index_a1, index_a2, index_a3, index_ax, index_hidden, index_hidden_dot))// + .reference(READ_NEXT_GEN, limitedTo(index_a1, index_a2, index_a3, index_ax, index_hidden, index_hidden_dot))// .reference(GET_ALIAS, limitedToNone()); /** @@ -304,8 +311,8 @@ public class IndexAuthorizationReadOnlyIntTests { .on(".system_index_plugin") )// .reference(READ, limitedTo(index_c1, alias_c1, system_index_plugin, alias_with_system_index))// + .reference(READ_NEXT_GEN, limitedTo(index_c1, alias_c1, system_index_plugin))// .reference(GET_ALIAS, limitedTo(index_c1, alias_c1, system_index_plugin, alias_with_system_index)); - /** * This user has no privileges for indices that are used in this test. But they have privileges for other indices. * This allows them to use actions like _search and receive empty result sets. @@ -321,6 +328,7 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_does_not_exist_*") )// .reference(READ, limitedToNone())// + .reference(READ_NEXT_GEN, limitedToNone())// .reference(GET_ALIAS, limitedToNone()); /** @@ -333,8 +341,8 @@ public class IndexAuthorizationReadOnlyIntTests { .clusterPermissions("cluster_composite_ops_ro", "cluster_monitor") )// .reference(READ, limitedToNone())// + .reference(READ_NEXT_GEN, limitedToNone())// .reference(GET_ALIAS, limitedToNone()); - /** * A user with "*" privileges on "*"; as it is a regular user, they are still subject to system index * restrictions and similar things. @@ -349,6 +357,7 @@ public class IndexAuthorizationReadOnlyIntTests { )// .reference(READ, limitedTo(ALL_INDICES_AND_ALIASES_EXCEPT_SYSTEM_INDICES).and(index_ax))// + .reference(READ_NEXT_GEN, limitedTo(ALL_INDICES_AND_ALIASES_EXCEPT_SYSTEM_INDICES).and(index_ax))// .reference(GET_ALIAS, limitedTo(ALL_INDICES_AND_ALIASES_EXCEPT_SYSTEM_INDICES).and(index_ax)); /** @@ -359,6 +368,7 @@ public class IndexAuthorizationReadOnlyIntTests { .description("super unlimited (admin cert)")// .adminCertUser()// .reference(READ, unlimitedIncludingOpenSearchSecurityIndex())// + .reference(READ_NEXT_GEN, unlimitedIncludingOpenSearchSecurityIndex())// .reference(GET_ALIAS, unlimitedIncludingOpenSearchSecurityIndex()); static final List USERS = ImmutableList.of( @@ -411,8 +421,9 @@ public static void stopClusters() { public void search_noPattern() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_search?size=1000"); - - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { + if (user != LIMITED_USER_NONE) { + // The main case: the requested indices are reduced according to privileges; depending on config even to an empty set of + // indices assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at("hits.hits[*]._index") @@ -420,13 +431,8 @@ public void search_noPattern() throws Exception { .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { - // The dnfof implementation has the effect that hidden indices might be included even though not requested - assertThat( - httpResponse, - containsExactly(clusterConfig.systemIndexPrivilegeEnabled ? ALL_INDICES : ALL_INDICES_EXCEPT_SYSTEM_INDICES).at( - "hits.hits[*]._index" - ).reducedBy(user.reference(READ)).whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + // If a user has no search privileges at all, they will get a 403 error + assertThat(httpResponse, isForbidden()); } } } @@ -435,20 +441,12 @@ public void search_noPattern() throws Exception { public void search_noPattern_noWildcards() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_search?size=1000&expand_wildcards=none"); - - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { + if (user == UNLIMITED_USER || user == SUPER_UNLIMITED_USER) { // Users with full privileges get an empty result, like expected due to the expand_wildcards=none option assertThat(httpResponse, isOk()); assertThat(httpResponse, containsExactly().at("hits.hits[*]._index")); } else { - // The dnfof implementation has the effect that the expand_wildcards=none option is disregarded - // Additionally, the dnfof implementation has the effect that hidden indices might be included even though not requested - assertThat( - httpResponse, - containsExactly(clusterConfig.systemIndexPrivilegeEnabled ? ALL_INDICES : ALL_INDICES_EXCEPT_SYSTEM_INDICES).at( - "hits.hits[*]._index" - ).reducedBy(user.reference(READ)).whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + assertThat(httpResponse, isForbidden()); } } } @@ -456,24 +454,13 @@ public void search_noPattern_noWildcards() throws Exception { @Test public void search_noPattern_allowNoIndicesFalse() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("_search?size=1000&allow_no_indices=false"); - - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { - assertThat( - httpResponse, - containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(isForbidden()) - ); - } else { - // The dnfof implementation has the effect that hidden indices might be included even though not requested - assertThat( - httpResponse, - containsExactly(clusterConfig.systemIndexPrivilegeEnabled ? ALL_INDICES : ALL_INDICES_EXCEPT_SYSTEM_INDICES).at( - "hits.hits[*]._index" - ).reducedBy(user.reference(READ)).whenEmpty(isForbidden()) - ); - } + TestRestClient.HttpResponse httpResponse = restClient.get("/_search?size=1000&allow_no_indices=false"); + assertThat( + httpResponse, + containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(isForbidden()) + ); } } @@ -482,7 +469,9 @@ public void search_all() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_all/_search?size=1000"); - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { + if (user != LIMITED_USER_NONE) { + // The main case: the requested indices are reduced according to privileges; depending on config even to an empty set of + // indices assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at("hits.hits[*]._index") @@ -490,13 +479,8 @@ public void search_all() throws Exception { .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { - // The dnfof implementation has the effect that hidden indices might be included even though not requested - assertThat( - httpResponse, - containsExactly(clusterConfig.systemIndexPrivilegeEnabled ? ALL_INDICES : ALL_INDICES_EXCEPT_SYSTEM_INDICES).at( - "hits.hits[*]._index" - ).reducedBy(user.reference(READ)).whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + // If a user has no search privileges at all, they will get a 403 error + assertThat(httpResponse, isForbidden()); } } } @@ -511,14 +495,7 @@ public void search_all_noWildcards() throws Exception { assertThat(httpResponse, isOk()); assertThat(httpResponse, containsExactly().at("hits.hits[*]._index")); } else { - // The dnfof implementation has the effect that the expand_wildcards=none option is disregarded - // Additionally, the dnfof implementation has the effect that hidden indices might be included even though not requested - assertThat( - httpResponse, - containsExactly(clusterConfig.systemIndexPrivilegeEnabled ? ALL_INDICES : ALL_INDICES_EXCEPT_SYSTEM_INDICES).at( - "hits.hits[*]._index" - ).reducedBy(user.reference(READ)).whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + assertThat(httpResponse, isForbidden()); } } } @@ -528,16 +505,28 @@ public void search_all_includeHidden() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_all/_search?size=1000&expand_wildcards=all"); - assertThat( - httpResponse, - containsExactly( - clusterConfig.systemIndexPrivilegeEnabled || user == SUPER_UNLIMITED_USER - ? ALL_INDICES - : ALL_INDICES_EXCEPT_SYSTEM_INDICES - ).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + if (clusterConfig.legacyPrivilegeEvaluation) { + assertThat( + httpResponse, + containsExactly( + clusterConfig.systemIndexPrivilegeEnabled || user == SUPER_UNLIMITED_USER + ? ALL_INDICES + : ALL_INDICES_EXCEPT_SYSTEM_INDICES + ).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { // next gen privilege evaluation + if (user != LIMITED_USER_NONE) { + // In the new privilege evaluation, the system index privilege is observed and contributes to dnfof. + assertThat( + httpResponse, + containsExactly(ALL_INDICES).at("hits.hits[*]._index").reducedBy(user.reference(READ)).whenEmpty(isOk()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } + } } } @@ -546,7 +535,9 @@ public void search_wildcard() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("*/_search?size=1000"); - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { + if (user != LIMITED_USER_NONE) { + // The main case: the requested indices are reduced according to privileges; depending on config even to an empty set of + // indices assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at("hits.hits[*]._index") @@ -554,13 +545,8 @@ public void search_wildcard() throws Exception { .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { - // The dnfof implementation has the effect that hidden indices might be included even though not requested - assertThat( - httpResponse, - containsExactly(clusterConfig.systemIndexPrivilegeEnabled ? ALL_INDICES : ALL_INDICES_EXCEPT_SYSTEM_INDICES).at( - "hits.hits[*]._index" - ).reducedBy(user.reference(READ)).whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + // If a user has no search privileges at all, they will get a 403 error + assertThat(httpResponse, isForbidden()); } } } @@ -575,14 +561,7 @@ public void search_wildcard_noWildcards() throws Exception { assertThat(httpResponse, isOk()); assertThat(httpResponse, containsExactly().at("hits.hits[*]._index")); } else { - // The dnfof implementation has the effect that the expand_wildcards=none option is disregarded - // Additionally, the dnfof implementation has the effect that hidden indices might be included even though not requested - assertThat( - httpResponse, - containsExactly(clusterConfig.systemIndexPrivilegeEnabled ? ALL_INDICES : ALL_INDICES_EXCEPT_SYSTEM_INDICES).at( - "hits.hits[*]._index" - ).reducedBy(user.reference(READ)).whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + assertThat(httpResponse, isForbidden()); } } } @@ -592,16 +571,21 @@ public void search_wildcard_includeHidden() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("*/_search?size=1000&expand_wildcards=all"); - assertThat( - httpResponse, - containsExactly( - clusterConfig.systemIndexPrivilegeEnabled || user == SUPER_UNLIMITED_USER - ? ALL_INDICES - : ALL_INDICES_EXCEPT_SYSTEM_INDICES - ).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + if (user != LIMITED_USER_NONE) { + assertThat( + httpResponse, + containsExactly( + clusterConfig.systemIndexPrivilegeEnabled || user == SUPER_UNLIMITED_USER + ? ALL_INDICES + : ALL_INDICES_EXCEPT_SYSTEM_INDICES + ).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + // If a user has no search privileges at all, they will get a 403 error + assertThat(httpResponse, isForbidden()); + } } } @@ -621,12 +605,17 @@ public void search_staticIndices_ignoreUnavailable() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("index_a1,index_b1/_search?size=1000&ignore_unavailable=true"); - assertThat( - httpResponse, - containsExactly(index_a1, index_b1).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + if (user != LIMITED_USER_NONE) { + assertThat( + httpResponse, + containsExactly(index_a1, index_b1).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + // If a user has no search privileges at all, they will get a 403 error + assertThat(httpResponse, isForbidden()); + } } } @@ -695,12 +684,21 @@ public void search_staticIndices_systemIndex_alias() throws Exception { assertThat(httpResponse, isForbidden()); } } else if (clusterConfig == ClusterConfig.LEGACY_PRIVILEGES_EVALUATION_SYSTEM_INDEX_PERMISSION) { - assertThat( - httpResponse, - containsExactly(system_index_plugin).at("hits.hits[*]._index").reducedBy(user.reference(READ)).whenEmpty(isForbidden()) - ); + if (user == UNLIMITED_USER) { + // The legacy evaluation grants access in SystemIndexAccessPrivilegesEvaluator for users with * privileges, + // but withholds documents on the DLS level + assertThat(httpResponse, isOk()); + assertThat(httpResponse, containsExactly().at("hits.hits[*]._index")); + } else { + assertThat( + httpResponse, + containsExactly(system_index_plugin).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(isForbidden()) + ); + } } else { - if (user.reference(READ).covers(alias_with_system_index)) { + if (user.reference(READ_NEXT_GEN).covers(alias_with_system_index)) { assertThat(httpResponse, isOk()); assertThat(httpResponse, containsExactly(system_index_plugin).at("hits.hits[*]._index")); } else { @@ -715,12 +713,17 @@ public void search_indexPattern() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("index_a*,index_b*/_search?size=1000"); - assertThat( - httpResponse, - containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + if (user != LIMITED_USER_NONE) { + assertThat( + httpResponse, + containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + // If a user has no search privileges at all, they will get a 403 error + assertThat(httpResponse, isForbidden()); + } } } @@ -729,12 +732,17 @@ public void search_indexPattern_minus() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("index_a*,index_b*,-index_b2,-index_b3/_search?size=1000"); - assertThat( - httpResponse, - containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + if (user != LIMITED_USER_NONE) { + assertThat( + httpResponse, + containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + // If a user has no search privileges at all, they will get a 403 error + assertThat(httpResponse, isForbidden()); + } } } @@ -744,19 +752,16 @@ public void search_indexPattern_nonExistingIndex_ignoreUnavailable() throws Exce TestRestClient.HttpResponse httpResponse = restClient.get( "index_a*,index_b*,xxx_non_existing/_search?size=1000&ignore_unavailable=true" ); - - // The presence of a non existing index has the effect that the other patterns are not resolved by IndexResolverReplacer - // This causes a few more 403 errors where the granted index patterns do not use wildcards - - if (user == LIMITED_USER_B1 || user == LIMITED_USER_ALIAS_AB1) { - assertThat(httpResponse, isForbidden()); - } else { + if (user != LIMITED_USER_NONE) { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3).at("hits.hits[*]._index") .reducedBy(user.reference(READ)) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); + } else { + // If a user has no search privileges at all, they will get a 403 error + assertThat(httpResponse, isForbidden()); } } } @@ -765,21 +770,26 @@ public void search_indexPattern_nonExistingIndex_ignoreUnavailable() throws Exce public void search_indexPattern_noWildcards() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("index_a*,index_b*/_search?size=1000&expand_wildcards=none"); - // We have to specify the users here explicitly because here we need to check privileges for the - // non-existing (and invalidly named) indices "index_a*" and "index_b*". - // However: Again, dnfof gets the indices options wrong and ignores the expand_wildcards=none flag when getting active - if (user == UNLIMITED_USER || user == SUPER_UNLIMITED_USER) { - // Only these users "get through". Because the indices does not exist, they get a 404 - assertThat(httpResponse, isNotFound()); + if (clusterConfig.legacyPrivilegeEvaluation) { + // We have to specify the users here explicitly because here we need to check privileges for the + // non-existing (and invalidly named) indices "index_a*" and "index_b*". Only users with privileges for "index_a*" + // and "index_b*" will get a ok response. + if (user == UNLIMITED_USER || user == SUPER_UNLIMITED_USER) { + assertThat(httpResponse, isNotFound()); + } else if (user == LIMITED_USER_A || user == LIMITED_USER_B || user == LIMITED_USER_A_HIDDEN) { + assertThat(httpResponse, isOk()); + assertThat(httpResponse, containsExactly().at("hits.hits[*]._index")); + } else { + assertThat(httpResponse, isForbidden("/error/root_cause/0/reason", "no permissions for [indices:data/read/search]")); + } } else { - assertThat( - httpResponse, - containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + if (user == UNLIMITED_USER || user == SUPER_UNLIMITED_USER) { + // Only these users "get through". Because the indices does not exist, they get a 404 + assertThat(httpResponse, isNotFound()); + } else { + assertThat(httpResponse, isForbidden("/error/root_cause/0/reason", "no permissions for [indices:data/read/search]")); + } } - } } @@ -788,13 +798,16 @@ public void search_indexPatternAndStatic_negation() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { // If there is a wildcard, negation will also affect indices specified without a wildcard TestRestClient.HttpResponse httpResponse = restClient.get("index_a*,index_b1,index_b2,-index_b2/_search?size=1000"); - - assertThat( - httpResponse, - containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + if (user != LIMITED_USER_NONE) { + assertThat( + httpResponse, + containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } @@ -831,7 +844,7 @@ public void search_indexPattern_includeHidden() throws Exception { .reducedBy(user.reference(READ)) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); - } else { + } else if (clusterConfig == ClusterConfig.LEGACY_PRIVILEGES_EVALUATION_SYSTEM_INDEX_PERMISSION) { // Things get buggy here; basically all requests fail with a 403 if (user == LIMITED_USER_C_WITH_SYSTEM_INDICES) { // This user is supposed to have the system index privilege for the index .system_index_plugin @@ -847,6 +860,29 @@ public void search_indexPattern_includeHidden() throws Exception { // See also https://github.com/opensearch-project/security/issues/5546 assertThat(httpResponse, isForbidden()); } + } else { + if (user != LIMITED_USER_NONE) { + // Without system index privileges, the system_index_plugin will be included if we have the privilege + assertThat( + httpResponse, + containsExactly( + index_a1, + index_a2, + index_a3, + index_b1, + index_b2, + index_b3, + index_c1, + index_hidden, + index_hidden_dot, + system_index_plugin + ).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } } @@ -863,6 +899,13 @@ public void search_alias() throws Exception { .reducedBy(user.reference(READ)) .whenEmpty(isForbidden()) ); + } else { + // The new privilege evaluation never replaces aliases + if (user.reference(READ).covers(alias_ab1)) { + assertThat(httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index")); + } else { + assertThat(httpResponse, isForbidden("/error/root_cause/0/reason", "no permissions for [indices:data/read/search]")); + } } } } @@ -872,12 +915,16 @@ public void search_alias_pattern() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("alias_ab1*/_search?size=1000"); - assertThat( - httpResponse, - containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + if (user != LIMITED_USER_NONE) { + assertThat( + httpResponse, + containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } @@ -885,14 +932,28 @@ public void search_alias_pattern() throws Exception { public void search_alias_pattern_negation() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("alias_*,-alias_ab1/_search?size=1000"); - // Another interesting effect: The negation on alias names does actually have no effect. - // This is this time a bug in core. TODO: File issue - assertThat( - httpResponse, - containsExactly(index_a1, index_a2, index_a3, index_b1, index_c1).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + + if (user != LIMITED_USER_NONE) { + if (clusterConfig.systemIndexPrivilegeEnabled) { + // If the system index privilege is enabled, we might also see the system_index_plugin index (being included via the + // alias) + assertThat( + httpResponse, + containsExactly(index_a1, index_a2, index_a3, index_b1, index_c1).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + assertThat( + httpResponse, + containsExactly(index_a1, index_a2, index_a3, index_b1, index_c1).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } + } else { + assertThat(httpResponse, isForbidden()); + } } } @@ -946,7 +1007,19 @@ public void search_aliasAndIndex_ignoreUnavailable() throws Exception { .reducedBy(user.reference(READ)) .whenEmpty(isForbidden()) ); - + } else { + // The new privilege evaluation never replaces aliases + if (user == LIMITED_USER_NONE) { + assertThat(httpResponse, isForbidden("/error/root_cause/0/reason", "no permissions for [indices:data/read/search]")); + } else if (user.reference(READ).covers(alias_ab1)) { + assertThat(httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index")); + } else if (user.reference(READ).covers(index_b1)) { + // Due to the "ignore_unavailable" request param, the alias_ab1 will be just silently ignored if we do not have + // privileges for it + assertThat(httpResponse, containsExactly(index_b1).at("hits.hits[*]._index")); + } else { + assertThat(httpResponse, containsExactly().at("hits.hits[*]._index")); + } } } } @@ -968,7 +1041,11 @@ public void search_nonExisting_indexPattern() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("x_does_not_exist*/_search?size=1000"); - assertThat(httpResponse, containsExactly().at("hits.hits[*]._index").whenEmpty(isOk())); + if (user != LIMITED_USER_NONE) { + assertThat(httpResponse, containsExactly().at("hits.hits[*]._index").whenEmpty(isOk())); + } else { + assertThat(httpResponse, isForbidden("/error/root_cause/0/reason", "no permissions for [indices:data/read/search]")); + } } } @@ -988,20 +1065,36 @@ public void search_termsAggregation_index() throws Exception { } }"""); - assertThat( - httpResponse, - containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at( - "aggregations.indices.buckets[*].key" - ).reducedBy(user.reference(READ)).whenEmpty(isOk()) - ); - } - } - - @Test - public void search_pit() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.post("index_a*,index_b*/_search/point_in_time?keep_alive=1m"); - + if (clusterConfig == ClusterConfig.NEXT_GEN_PRIVILEGES_EVALUATION) { + if (user == LIMITED_USER_NONE) { + assertThat(httpResponse, isForbidden("/error/root_cause/0/reason", "no permissions for [indices:data/read/search]")); + } else if (user == LIMITED_USER_OTHER_PRIVILEGES) { + assertThat(httpResponse, isOk()); + assertTrue(httpResponse.getBody(), httpResponse.bodyAsMap().get("aggregations") == null); + } else { + assertThat( + httpResponse, + containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at( + "aggregations.indices.buckets[*].key" + ).reducedBy(user.reference(READ)).whenEmpty(isOk()) + ); + } + } else { + assertThat( + httpResponse, + containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at( + "aggregations.indices.buckets[*].key" + ).reducedBy(user.reference(READ)).whenEmpty(isOk()) + ); + } + } + } + + @Test + public void search_pit() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.post("index_a*,index_b*/_search/point_in_time?keep_alive=1m"); + RestIndexMatchers.OnResponseIndexMatcher indexMatcher = containsExactly( index_a1, index_a2, @@ -1037,23 +1130,15 @@ public void search_pit_all() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.post("_all/_search/point_in_time?keep_alive=1m"); - RestIndexMatchers.OnResponseIndexMatcher indexMatcher; - - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { - indexMatcher = containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1); - } else { - indexMatcher = containsExactly( - index_a1, - index_a2, - index_a3, - index_b1, - index_b2, - index_b3, - index_c1, - index_hidden, - index_hidden_dot - ); - } + RestIndexMatchers.OnResponseIndexMatcher indexMatcher = containsExactly( + index_a1, + index_a2, + index_a3, + index_b1, + index_b2, + index_b3, + index_c1 + ); if (indexMatcher.reducedBy(user.reference(READ)).isEmpty()) { assertThat( @@ -1070,16 +1155,8 @@ public void search_pit_all() throws Exception { } } """, pitId)); - if (clusterConfig.systemIndexPrivilegeEnabled && user == LIMITED_USER_C_WITH_SYSTEM_INDICES) { - // The current request mixes access to a normal index and a system index. - // The current system index permission implementation has the issue that it also - // expects the system index permission for the normal issue then. - // As this is not present, the request https://github.com/opensearch-project/security/issues/5508 - assertThat(httpResponse, isForbidden()); - } else { - assertThat(httpResponse, isOk()); - assertThat(httpResponse, indexMatcher.at("hits.hits[*]._index").reducedBy(user.reference(READ))); - } + assertThat(httpResponse, isOk()); + assertThat(httpResponse, indexMatcher.at("hits.hits[*]._index").reducedBy(user.reference(READ))); } } } @@ -1144,10 +1221,6 @@ public void search_pit_wrongIndex() throws Exception { @Test public void search_template_staticIndices() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { - String params = """ - { - "department": [%s] - }""".formatted(TestData.DEPARTMENTS.stream().map(s -> '"' + s + '"').collect(joining(","))); String query = """ { "query": { @@ -1162,11 +1235,10 @@ public void search_template_staticIndices() throws Exception { } """; - TestRestClient.HttpResponse httpResponse = restClient.getWithJsonBody("index_a1/_search/template?size=1000", """ - { - "params": %s, - "source": "%s" - }""".formatted(params, escapeJson(query))); + TestRestClient.HttpResponse httpResponse = restClient.get( + "index_a1/_search/template?size=1000", + json("params", Map.of("department", TestData.DEPARTMENTS), "source", query) + ); assertThat( httpResponse, @@ -1295,30 +1367,16 @@ public void cat_indices_all() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_cat/indices?format=json"); - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { + if (user != LIMITED_USER_NONE) { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at("$[*].index") .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { - // Also here, dnfof might introduce hidden indices even though they were not requested - assertThat( - httpResponse, - containsExactly( - index_a1, - index_a2, - index_a3, - index_b1, - index_b2, - index_b3, - index_c1, - index_hidden_dot, - index_hidden, - system_index_plugin - ).at("$[*].index").reducedBy(user.reference(READ)).whenEmpty(isForbidden()) - ); + assertThat(httpResponse, isForbidden()); } } } @@ -1328,12 +1386,17 @@ public void cat_indices_pattern() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_cat/indices/index_a*?format=json"); - assertThat( - httpResponse, - containsExactly(index_a1, index_a2, index_a3).at("$[*].index") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + if (user != LIMITED_USER_NONE) { + assertThat( + httpResponse, + containsExactly(index_a1, index_a2, index_a3).at("$[*].index") + + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } @@ -1341,13 +1404,27 @@ public void cat_indices_pattern() throws Exception { public void cat_indices_all_includeHidden() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_cat/indices?format=json&expand_wildcards=all"); - if (user == UNLIMITED_USER) { - assertThat(httpResponse, containsExactly(ALL_INDICES).at("$[*].index")); + + if (clusterConfig.legacyPrivilegeEvaluation) { + if (user == UNLIMITED_USER) { + assertThat(httpResponse, containsExactly(ALL_INDICES).at("$[*].index")); + } else { + assertThat( + httpResponse, + containsExactly(ALL_INDICES).at("$[*].index").reducedBy(user.reference(READ)).whenEmpty(isForbidden()) + ); + } } else { - assertThat( - httpResponse, - containsExactly(ALL_INDICES).at("$[*].index").reducedBy(user.reference(READ)).whenEmpty(isForbidden()) - ); + if (user != LIMITED_USER_NONE) { + assertThat( + httpResponse, + containsExactly(ALL_INDICES).at("$[*].index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } } @@ -1379,13 +1456,24 @@ public void cat_aliases_pattern() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_cat/aliases/alias_a*?format=json"); - if (!user.reference(GET_ALIAS).isEmpty()) { - assertThat( - httpResponse, - containsExactly(alias_ab1).at("$[*].alias").reducedBy(user.reference(GET_ALIAS)).whenEmpty(isOk()) - ); + if (clusterConfig.legacyPrivilegeEvaluation) { + if (!user.reference(GET_ALIAS).isEmpty()) { + assertThat( + httpResponse, + containsExactly(alias_ab1).at("$[*].alias").reducedBy(user.reference(GET_ALIAS)).whenEmpty(isOk()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } else { - assertThat(httpResponse, isForbidden()); + if (!user.reference(GET_ALIAS).isEmpty()) { + assertThat( + httpResponse, + containsExactly(alias_ab1).at("$[*].alias").reducedBy(user.reference(GET_ALIAS)).whenEmpty(isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } } @@ -1395,7 +1483,7 @@ public void index_stats_all() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("/_stats"); - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { + if (user != LIMITED_USER_NONE) { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at("indices.keys()") @@ -1403,24 +1491,7 @@ public void index_stats_all() throws Exception { .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { - // Also here, dnfof can introduce hidden indices even though they were not requested - assertThat( - httpResponse, - containsExactly( - index_a1, - index_a2, - index_a3, - index_b1, - index_b2, - index_b3, - index_c1, - index_hidden, - index_hidden_dot, - system_index_plugin - ).at("indices.keys()") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + assertThat(httpResponse, isForbidden()); } } } @@ -1430,12 +1501,16 @@ public void index_stats_pattern() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("index_b*/_stats"); - assertThat( - httpResponse, - containsExactly(index_b1, index_b2, index_b3).at("indices.keys()") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + if (user != LIMITED_USER_NONE) { + assertThat( + httpResponse, + containsExactly(index_b1, index_b2, index_b3).at("indices.keys()") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } @@ -1443,23 +1518,28 @@ public void index_stats_pattern() throws Exception { public void getAlias_all() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_alias"); - if (user == UNLIMITED_USER) { + + if (clusterConfig.legacyPrivilegeEvaluation && user == UNLIMITED_USER) { // The legacy privilege evaluation also allows regular users access to metadata of the security index // This is not a security issue, as the metadata are not really security relevant assertThat(httpResponse, containsExactly(ALL_INDICES).at("$.keys()")); } else { - assertThat( - httpResponse, - containsExactly(alias_ab1, alias_c1, alias_with_system_index).at("$.*.aliases.keys()") - .reducedBy(user.reference(GET_ALIAS)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); - assertThat( - httpResponse, - containsExactly(ALL_INDICES).at("$.keys()") - .reducedBy(user.reference(GET_ALIAS)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + if (!user.reference(GET_ALIAS).isEmpty()) { + assertThat( + httpResponse, + containsExactly(alias_ab1, alias_c1, alias_with_system_index).at("$.*.aliases.keys()") + .reducedBy(user.reference(GET_ALIAS)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + assertThat( + httpResponse, + containsExactly(ALL_INDICES).at("$.keys()") + .reducedBy(user.reference(GET_ALIAS)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } } @@ -1494,17 +1574,27 @@ public void getAlias_aliasPattern() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_alias/alias_ab*"); - if (user == LIMITED_USER_ALIAS_AB1 || user == UNLIMITED_USER || user == SUPER_UNLIMITED_USER) { - assertThat(httpResponse, isOk()); - assertThat(httpResponse, containsExactly(alias_ab1).at("$.*.aliases.keys()").reducedBy(user.reference(GET_ALIAS))); - assertThat(httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("$.keys()")); - } else if (user == LIMITED_USER_ALIAS_C1 || user == LIMITED_USER_C_WITH_SYSTEM_INDICES) { - // This is also a kind of anomaly in the legacy privilege evaluation: Even though we do not have permissions - // we get a 200 response with an empty result - assertThat(httpResponse, isOk()); - assertTrue(httpResponse.getBody(), httpResponse.bodyAsMap().isEmpty()); + if (clusterConfig.legacyPrivilegeEvaluation) { + if (user == LIMITED_USER_ALIAS_AB1 || user == UNLIMITED_USER || user == SUPER_UNLIMITED_USER) { + assertThat(httpResponse, isOk()); + assertThat(httpResponse, containsExactly(alias_ab1).at("$.*.aliases.keys()").reducedBy(user.reference(GET_ALIAS))); + assertThat(httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("$.keys()")); + } else if (user == LIMITED_USER_ALIAS_C1 || user == LIMITED_USER_C_WITH_SYSTEM_INDICES) { + // This is also a kind of anomaly in the legacy privilege evaluation: Even though we do not have permissions + // we get a 200 response with an empty result + assertThat(httpResponse, isOk()); + assertTrue(httpResponse.getBody(), httpResponse.bodyAsMap().isEmpty()); + } else { + assertThat(httpResponse, isForbidden("/error/root_cause/0/reason", "no permissions for [indices:admin/aliases/get]")); + } } else { - assertThat(httpResponse, isForbidden("/error/root_cause/0/reason", "no permissions for [indices:admin/aliases/get]")); + if (user.reference(GET_ALIAS).covers(alias_ab1)) { + assertThat(httpResponse, isOk()); + assertThat(httpResponse, containsExactly(alias_ab1).at("$.*.aliases.keys()")); + assertThat(httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("$.keys()")); + } else { + assertThat(httpResponse, isForbidden("/error/root_cause/0/reason", "no permissions for [indices:admin/aliases/get]")); + } } } } @@ -1532,27 +1622,45 @@ public void getAlias_indexPattern_includeHidden() throws Exception { system_index_plugin ).at("$.keys()") ); - } else if (!clusterConfig.systemIndexPrivilegeEnabled) { + } else if (clusterConfig != ClusterConfig.LEGACY_PRIVILEGES_EVALUATION_SYSTEM_INDEX_PERMISSION) { if (user == UNLIMITED_USER) { - assertThat( - httpResponse, - containsExactly( - index_a1, - index_a2, - index_a3, - index_b1, - index_b2, - index_b3, - index_c1, - index_hidden, - index_hidden_dot, - system_index_plugin - ).at("$.keys()") - ); - } else { + if (clusterConfig == ClusterConfig.LEGACY_PRIVILEGES_EVALUATION) { + assertThat( + httpResponse, + containsExactly( + index_a1, + index_a2, + index_a3, + index_b1, + index_b2, + index_b3, + index_c1, + index_hidden, + index_hidden_dot, + system_index_plugin + ).at("$.keys()") + ); + } else { + assertThat( + httpResponse, + containsExactly( + index_a1, + index_a2, + index_a3, + index_b1, + index_b2, + index_b3, + index_c1, + index_hidden, + index_hidden_dot + ).at("$.keys()") + ); + } + } else if (!user.reference(GET_ALIAS).isEmpty()) { assertThat( httpResponse, containsExactly(alias_ab1, alias_c1, alias_with_system_index).at("$.*.aliases.keys()") + .reducedBy(user.reference(GET_ALIAS)) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); @@ -1562,10 +1670,12 @@ public void getAlias_indexPattern_includeHidden() throws Exception { .reducedBy(user.reference(GET_ALIAS)) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); + } else { + assertThat(httpResponse, isForbidden()); } - } else { - // If the system index privilege is enabled, we only get 403 errors, as SystemIndexPrivilegeEvaluator - // is not aware of dnfof; see https://github.com/opensearch-project/security/issues/5546 + } else { // clusterConfig == ClusterConfig.LEGACY_PRIVILEGES_EVALUATION_SYSTEM_INDEX_PERMISSION + // If the system index privilege is enabled, we only get 403 errors, as + // the legacy SystemIndexPrivilegeEvaluator is not aware of dnfof assertThat(httpResponse, isForbidden()); } } @@ -1576,14 +1686,11 @@ public void analyze_noIndex() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.postJson("_analyze", "{\"text\": \"sample text\"}"); - // _analyze without index is different from most other operations: - // Usually, the absence of an index means "all indices". For analyze, however, it means: "no index". - // However, the IndexResolverReplacer does not get this right; it assumes that all indices are requested. - // Thus, we get only through to this operation with full privileges for all indices - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { + if (user != LIMITED_USER_NONE) { assertThat(httpResponse, isOk()); } else { - assertThat(httpResponse, isForbidden("/error/root_cause/0/reason", "no permissions for [indices:admin/analyze]")); + // This is only forbidden if the user has no index privileges at all for indices:admin/analyze + assertThat(httpResponse, isForbidden()); } } } @@ -1607,31 +1714,17 @@ public void resolve_wildcard() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_resolve/index/*"); - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { + if (user != LIMITED_USER_NONE) { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1, alias_ab1, alias_c1).at( "$.*[*].name" - ).reducedBy(user.reference(READ)).whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); - } else { - assertThat( - httpResponse, - containsExactly( - index_a1, - index_a2, - index_a3, - index_b1, - index_b2, - index_b3, - index_c1, - index_hidden, - index_hidden_dot, - system_index_plugin - ).at("$.*[*].name") - .reducedBy(user.reference(READ)) + ) + .reducedBy(user.reference(clusterConfig.legacyPrivilegeEvaluation ? READ : READ_NEXT_GEN)) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); + } else { + assertThat(httpResponse, isForbidden()); } } @@ -1642,15 +1735,19 @@ public void resolve_wildcard_includeHidden() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_resolve/index/*?expand_wildcards=all"); - if (user == UNLIMITED_USER || user == SUPER_UNLIMITED_USER) { + if (clusterConfig.legacyPrivilegeEvaluation && (user == UNLIMITED_USER || user == SUPER_UNLIMITED_USER)) { // The legacy privilege evaluation also allows regular users access to metadata of the security index // This is not a security issue, as the metadata are not really security relevant assertThat(httpResponse, containsExactly(ALL_INDICES_AND_ALIASES).at("$.*[*].name")); - } else { + } else if (user != LIMITED_USER_NONE) { assertThat( httpResponse, - containsExactly(ALL_INDICES).at("$.*[*].name").reducedBy(user.reference(READ)).whenEmpty(isForbidden()) + containsExactly(ALL_INDICES_AND_ALIASES).at("$.*[*].name") + .reducedBy(user.reference(clusterConfig.legacyPrivilegeEvaluation ? READ : READ_NEXT_GEN)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); + } else { + assertThat(httpResponse, isForbidden()); } } } @@ -1659,21 +1756,25 @@ public void resolve_wildcard_includeHidden() throws Exception { public void resolve_indexPattern() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_resolve/index/index_a*,index_b*"); - assertThat( - httpResponse, - containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3).at("$.*[*].name") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + if (user != LIMITED_USER_NONE) { + assertThat( + httpResponse, + containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3).at("$.*[*].name") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } @Test public void field_caps_all() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { - if (user == SUPER_UNLIMITED_USER || user == UNLIMITED_USER) { - TestRestClient.HttpResponse httpResponse = restClient.get("_field_caps?fields=*"); + TestRestClient.HttpResponse httpResponse = restClient.get("_field_caps?fields=*"); + if (user != LIMITED_USER_NONE) { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at("indices") @@ -1682,14 +1783,7 @@ public void field_caps_all() throws Exception { ); } else { - TestRestClient.HttpResponse httpResponse = restClient.get("_field_caps?fields=*"); - assertThat( - httpResponse, - containsExactly(ALL_INDICES).at("indices") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); - + assertThat(httpResponse, isForbidden()); } } @@ -1699,11 +1793,16 @@ public void field_caps_all() throws Exception { public void field_caps_indexPattern() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("index_b*/_field_caps?fields=*"); - - assertThat( - httpResponse, - containsExactly(index_b1, index_b2, index_b3).at("indices").reducedBy(user.reference(READ)).whenEmpty(isForbidden()) - ); + if (user != LIMITED_USER_NONE) { + assertThat( + httpResponse, + containsExactly(index_b1, index_b2, index_b3).at("indices") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } @@ -1734,6 +1833,13 @@ public void field_caps_alias() throws Exception { .reducedBy(user.reference(READ)) .whenEmpty(isForbidden()) ); + } else { + if (user.reference(READ).covers(alias_ab1)) { + assertThat(httpResponse, isOk()); + assertThat(httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("indices")); + } else { + assertThat(httpResponse, isForbidden()); + } } } } @@ -1742,13 +1848,17 @@ public void field_caps_alias() throws Exception { public void field_caps_aliasPattern() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("alias_ab*/_field_caps?fields=*"); - assertThat( - httpResponse, - containsExactly(index_a1, index_a2, index_a3, index_b1).at("indices") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + if (user != LIMITED_USER_NONE) { + assertThat( + httpResponse, + containsExactly(index_a1, index_a2, index_a3, index_b1).at("indices") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } @@ -1770,8 +1880,11 @@ public void field_caps_nonExisting_indexPattern() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("x_does_not_exist*/_field_caps?fields=*"); - // As this resolves to an empty set of indices, we are always allowed - assertThat(httpResponse, containsExactly().at("indices").whenEmpty(isOk())); + if (user != LIMITED_USER_NONE) { + assertThat(httpResponse, containsExactly().at("indices").whenEmpty(isOk())); + } else { + assertThat(httpResponse, isForbidden()); + } } } @@ -1779,14 +1892,16 @@ public void field_caps_nonExisting_indexPattern() throws Exception { public void field_caps_indexPattern_minus() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("index_a*,index_b*,-index_b2,-index_b3/_field_caps?fields=*"); + if (user != LIMITED_USER_NONE) { + assertThat( + httpResponse, + containsExactly(index_a1, index_a2, index_a3, index_b1).at("indices") - assertThat( - httpResponse, - containsExactly(index_a1, index_a2, index_a3, index_b1).at("indices") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } } } @@ -1797,15 +1912,21 @@ public void pit_list_all() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_search/point_in_time/_all"); - // At the moment, it is sufficient to have any privileges for any existing index to use the _all API - // This is clearly a bug; yet, not a severe issue, as we do not have really sensitive things available here. - // This is caused by the following line which makes PrivilegesEvaluator believe it could reduce the indices - // to authorized indices, even though it actually could not: - // https://github.com/opensearch-project/security/blob/aee54a8ca2a6cc596cb1e490be1e9fa240286246/src/main/java/org/opensearch/security/resolver/IndexResolverReplacer.java#L824-L825 - if (user != LIMITED_USER_NONE && user != LIMITED_USER_OTHER_PRIVILEGES) { - assertThat(httpResponse, isOk()); + if (clusterConfig.legacyPrivilegeEvaluation) { + // At the moment, it is sufficient to have any privileges for any existing index to use the _all API + // This is clearly a bug; yet, not a severe issue, as we do not have really sensitive things available here + if (user != LIMITED_USER_NONE) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } } else { - assertThat(httpResponse, isForbidden()); + // New privilege evaluation: this is now a cluster privilege, the users below are the users with full cluster privileges + if (user == UNLIMITED_USER || user == SUPER_UNLIMITED_USER) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } } } finally { deletePit(indexA1pitId); diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadWriteIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadWriteIntTests.java index fe685bc2a0..afdd2fd6bf 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadWriteIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadWriteIntTests.java @@ -52,7 +52,9 @@ import static org.opensearch.test.framework.matcher.RestMatchers.isForbidden; import static org.opensearch.test.framework.matcher.RestMatchers.isNotFound; import static org.opensearch.test.framework.matcher.RestMatchers.isOk; +import static org.junit.Assert.assertEquals; +/** /** * This class defines a huge test matrix for index related access controls. This class is especially for read/write operations on indices and aliases. * It uses the following dimensions: @@ -666,6 +668,14 @@ public void deleteByQuery_indexPattern() throws Exception { } else { assertThat(httpResponse, isForbidden()); } + } else { + if (user != LIMITED_USER_NONE && user != LIMITED_READ_ONLY_ALL && user != LIMITED_READ_ONLY_A) { + assertThat(httpResponse, isOk()); + int expectedDeleteCount = containsExactly(index_aw1, index_bw1).at("_index").reducedBy(user.reference(WRITE)).size(); + assertEquals(httpResponse.getBody(), expectedDeleteCount, httpResponse.bodyAsMap().get("deleted")); + } else { + assertThat(httpResponse, isForbidden()); + } } } finally { @@ -716,6 +726,12 @@ public void putDocument_alias() throws Exception { } else { assertThat(httpResponse, isForbidden()); } + } else { + if (user.reference(WRITE).coversAll(alias_ab1w)) { + assertThat(httpResponse, isCreated()); + } else { + assertThat(httpResponse, isForbidden()); + } } } finally { delete("alias_ab1w/_doc/put_doc_alias_test_1"); @@ -743,12 +759,7 @@ public void putDocument_bulk_alias() throws Exception { {"a": 1} """); - if (user == LIMITED_USER_A || user == LIMITED_USER_AB1_ALIAS_READ_ONLY) { - // Theoretically, a user with privileges for index_aw* could write into alias_ab2w, as the write index is index_aw1 - // However, the index resolution code is not aware that this is a write operation; thus it resolves - // to all alias members which contain also index_bw1, for which we do not have permissions - assertThat(httpResponse, containsExactly().at("items[*].index[?(@.result == 'created')]._index")); - } else if (user != LIMITED_USER_NONE) { + if (user != LIMITED_USER_NONE) { assertThat( httpResponse, containsExactly(index_aw1).at("items[*].index[?(@.result == 'created')]._index") @@ -758,6 +769,7 @@ public void putDocument_bulk_alias() throws Exception { } else { assertThat(httpResponse, isForbidden()); } + } finally { delete("index_aw1/_doc/put_doc_alias_bulk_test_1"); } @@ -794,13 +806,15 @@ public void createIndex_systemIndex() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { HttpResponse httpResponse = restClient.putJson(".system_index_plugin_not_existing", "{}"); - if (user.reference(CREATE_INDEX).covers(system_index_plugin_not_existing)) { - assertThat(httpResponse, isOk()); - } else if (user == SUPER_UNLIMITED_USER || (user == UNLIMITED_USER && !clusterConfig.systemIndexPrivilegeEnabled)) { + if (clusterConfig.systemIndexPrivilegeEnabled && user.reference(CREATE_INDEX).covers(system_index_plugin_not_existing)) { assertThat(httpResponse, isOk()); - } else { - assertThat(httpResponse, isForbidden()); - } + } else if (user == SUPER_UNLIMITED_USER + || (clusterConfig == ClusterConfig.LEGACY_PRIVILEGES_EVALUATION + && (user == UNLIMITED_USER || user == LIMITED_USER_B_SYSTEM_INDEX_MANAGE))) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } } finally { delete(system_index_plugin_not_existing); } @@ -857,6 +871,16 @@ public void createIndex_withAlias() throws Exception { } else { assertThat(httpResponse, isForbidden()); } + } else { + if (user.reference(MANAGE_ALIAS).coversAll(alias_bwx, index_bwx1)) { + assertThat(httpResponse, isOk()); + assertThat( + httpResponse, + containsExactly(index_bwx1).at("index").reducedBy(user.reference(CREATE_INDEX)).whenEmpty(isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } finally { delete(index_bwx1); @@ -876,7 +900,14 @@ public void deleteAlias_staticIndex() throws Exception { } else { assertThat(httpResponse, isForbidden()); } + } else { + if (user.reference(MANAGE_ALIAS).covers(alias_bwx)) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } } + } finally { delete(alias_bwx); } @@ -889,12 +920,19 @@ public void deleteAlias_wildcard() throws Exception { HttpResponse httpResponse = restClient.delete("*/_aliases/alias_bwx"); - if (user == SUPER_UNLIMITED_USER) { - assertThat(httpResponse, isOk()); + if (clusterConfig.legacyPrivilegeEvaluation) { + // This is only allowed if we have privileges for all indices, even if not all indices are member of alias_bwx + if (user.reference(MANAGE_ALIAS).coversAll(ALL_NON_HIDDEN_INDICES)) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } } else { - // For all non super admin users, this will be rejected by SystemIndexAccessEvaluator: - // WARN SystemIndexAccessEvaluator:361 - indices:admin/aliases for '_all' indices is not allowed for a regular user - assertThat(httpResponse, isForbidden()); + if (user.reference(MANAGE_ALIAS).coversAll(alias_bwx)) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } } } finally { delete(alias_bwx); @@ -918,6 +956,12 @@ public void aliases_createAlias() throws Exception { } else { assertThat(httpResponse, isForbidden()); } + } else { + if (user.reference(MANAGE_ALIAS).coversAll(alias_bwx, index_bw1)) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } } } finally { @@ -940,6 +984,12 @@ public void aliases_createAlias_indexPattern() throws Exception { } else { assertThat(httpResponse, isForbidden()); } + } else { + if (user.reference(MANAGE_ALIAS).coversAll(alias_bwx, index_bw1, index_bw2)) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } } } finally { delete(alias_bwx); @@ -964,6 +1014,12 @@ public void aliases_deleteAlias_staticIndex() throws Exception { } else { assertThat(httpResponse, isForbidden()); } + } else { + if (user.reference(MANAGE_ALIAS).covers(alias_bwx)) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } } } finally { delete(alias_bwx); @@ -982,13 +1038,21 @@ public void aliases_deleteAlias_wildcard() throws Exception { ] }"""); - if (user == SUPER_UNLIMITED_USER) { - assertThat(httpResponse, isOk()); + if (clusterConfig.legacyPrivilegeEvaluation) { + // This is only allowed if we have privileges for all indices, even if not all indices are member of alias_bwx + if (user.reference(MANAGE_ALIAS).coversAll(ALL_NON_HIDDEN_INDICES)) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } } else { - // For all non super admin users, this will be rejected by SystemIndexAccessEvaluator: - // WARN SystemIndexAccessEvaluator:361 - indices:admin/aliases for '_all' indices is not allowed for a regular user - assertThat(httpResponse, isForbidden()); + if (user.reference(MANAGE_ALIAS).coversAll(alias_bwx)) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } } + } finally { delete(alias_bwx); } @@ -1082,12 +1146,24 @@ public void closeIndex() throws Exception { public void closeIndex_wildcard() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { HttpResponse httpResponse = restClient.post("*/_close"); - if (user == SUPER_UNLIMITED_USER) { - assertThat(httpResponse, isOk()); + + if (clusterConfig.legacyPrivilegeEvaluation) { + if (user.reference(MANAGE_INDEX).coversAll(ALL_NON_HIDDEN_INDICES)) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } } else { - // For all non super admin users, this will be rejected by SystemIndexAccessEvaluator: - // WARN SystemIndexAccessEvaluator:361 - indices:admin/close for '_all' indices is not allowed for a regular user - assertThat(httpResponse, isForbidden()); + if (!user.reference(MANAGE_INDEX).isEmpty()) { + assertThat( + httpResponse, + containsExactly(ALL_NON_HIDDEN_INDICES).at("indices.keys()") + .reducedBy(user.reference(MANAGE_INDEX)) + .whenEmpty(isOk()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } finally { cluster.getInternalNodeClient().admin().indices().open(new OpenIndexRequest("*")).actionGet(); @@ -1132,6 +1208,12 @@ public void rollover_explicitTargetIndex() throws Exception { } else { assertThat(httpResponse, isForbidden()); } + } else { + if (user.reference(MANAGE_ALIAS).covers(alias_bwx) && user.reference(MANAGE_INDEX).covers(index_bw2)) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } } } finally { delete(alias_bwx, index_bwx1); diff --git a/src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java similarity index 64% rename from src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java rename to src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java index 94a10233e0..e3bf7702fe 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java @@ -9,7 +9,7 @@ * GitHub history for details. */ -package org.opensearch.security.privileges; +package org.opensearch.security.privileges.int_tests; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; @@ -30,21 +30,16 @@ import static org.hamcrest.Matchers.equalTo; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; -/** -* This is a port for the test -* org.opensearch.security.privileges.PrivilegesEvaluatorTest to the new test -* framework for direct comparison -*/ @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) -public class PrivilegesEvaluatorTest { +public class MiscPrivilegesIntTests { protected final static TestSecurityConfig.User NEGATIVE_LOOKAHEAD = new TestSecurityConfig.User("negative_lookahead_user").roles( new Role("negative_lookahead_role").indexPermissions("read").on("/^(?!t.*).*/").clusterPermissions("cluster_composite_ops") ); protected final static TestSecurityConfig.User NEGATED_REGEX = new TestSecurityConfig.User("negated_regex_user").roles( - new Role("negated_regex_role").indexPermissions("read").on("/^[a-z].*/").clusterPermissions("cluster_composite_ops") + new Role("negated_regex_role").indexPermissions("read").on("/^[a-r].*/").clusterPermissions("cluster_composite_ops") ); protected final static TestSecurityConfig.User SEARCH_TEMPLATE = new TestSecurityConfig.User("search_template_user").roles( @@ -58,13 +53,9 @@ public class PrivilegesEvaluatorTest { .clusterPermissions(RenderSearchTemplateAction.NAME) ); - private String TEST_QUERY = - "{\"source\":{\"query\":{\"match\":{\"service\":\"{{service_name}}\"}}},\"params\":{\"service_name\":\"Oracle\"}}"; - - private String TEST_DOC = "{\"source\": {\"title\": \"Spirited Away\"}}"; - private String TEST_RENDER_SEARCH_TEMPLATE_QUERY = - "{\"params\":{\"status\":[\"pending\",\"published\"]},\"source\":\"{\\\"query\\\": {\\\"terms\\\": {\\\"status\\\": [\\\"{{#status}}\\\",\\\"{{.}}\\\",\\\"{{/status}}\\\"]}}}\"}"; + """ + {"params":{"status":["pending","published"]},"source":"{\\"query\\": {\\"terms\\": {\\"status\\": [\\"{{#status}}\\",\\"{{.}}\\",\\"{{/status}}\\"]}}}"}"""; final static TestIndex R = TestIndex.name("r").build(); /** @@ -99,45 +90,6 @@ public void testRegexPattern() throws Exception { } - @Test - public void testSearchTemplateRequestSuccess() { - // Insert doc into services index with admin user - try (TestRestClient client = cluster.getRestClient(TestSecurityConfig.User.USER_ADMIN)) { - TestRestClient.HttpResponse response = client.postJson("services/_doc", TEST_DOC); - assertThat(response.getStatusCode(), equalTo(HttpStatus.SC_CREATED)); - } - - try (TestRestClient client = cluster.getRestClient(SEARCH_TEMPLATE)) { - final String searchTemplateOnServicesIndex = "services/_search/template"; - final TestRestClient.HttpResponse searchTemplateOnAuthorizedIndexResponse = client.getWithJsonBody( - searchTemplateOnServicesIndex, - TEST_QUERY - ); - assertThat(searchTemplateOnAuthorizedIndexResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); - } - } - - @Test - public void testSearchTemplateRequestUnauthorizedIndex() { - try (TestRestClient client = cluster.getRestClient(SEARCH_TEMPLATE)) { - final String searchTemplateOnMoviesIndex = "movies/_search/template"; - final TestRestClient.HttpResponse searchTemplateOnUnauthorizedIndexResponse = client.getWithJsonBody( - searchTemplateOnMoviesIndex, - TEST_QUERY - ); - assertThat(searchTemplateOnUnauthorizedIndexResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); - } - } - - @Test - public void testSearchTemplateRequestUnauthorizedAllIndices() { - try (TestRestClient client = cluster.getRestClient(SEARCH_TEMPLATE)) { - final String searchTemplateOnAllIndices = "_search/template"; - final TestRestClient.HttpResponse searchOnAllIndicesResponse = client.getWithJsonBody(searchTemplateOnAllIndices, TEST_QUERY); - assertThat(searchOnAllIndicesResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); - } - } - @Test public void testRenderSearchTemplateRequestFailure() { try (TestRestClient client = cluster.getRestClient(SEARCH_TEMPLATE)) { diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/SnapshotAuthorizationIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/SnapshotAuthorizationIntTests.java index 65ec63c271..1c7bedb8ee 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/SnapshotAuthorizationIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/SnapshotAuthorizationIntTests.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import javax.annotation.concurrent.NotThreadSafe; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; @@ -47,6 +48,7 @@ */ @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) +@NotThreadSafe public class SnapshotAuthorizationIntTests { static final TestIndex index_a1 = TestIndex.name("index_ar1").documentCount(10).seed(1).build(); static final TestIndex index_a2 = TestIndex.name("index_ar2").documentCount(11).seed(2).build(); @@ -156,7 +158,6 @@ public class SnapshotAuthorizationIntTests { .clusterPermissions("cluster_composite_ops_ro", "cluster_monitor", "manage_snapshots") .indexPermissions("*") .on("*")// - )// .reference( READ, diff --git a/src/integrationTest/java/org/opensearch/security/systemindex/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/systemindex/SystemIndexTests.java index e8fdd9d7d4..440cf222b2 100644 --- a/src/integrationTest/java/org/opensearch/security/systemindex/SystemIndexTests.java +++ b/src/integrationTest/java/org/opensearch/security/systemindex/SystemIndexTests.java @@ -114,7 +114,7 @@ public void testPluginShouldNotBeAbleToIndexDocumentIntoSystemIndexRegisteredByO response, RestMatchers.isForbidden( "/error/root_cause/0/reason", - "no permissions for [] and User [name=plugin:org.opensearch.security.systemindex.sampleplugin.SystemIndexPlugin1" + "no permissions for [indices:admin/create] and User [name=plugin:org.opensearch.security.systemindex.sampleplugin.SystemIndexPlugin1" ) ); } @@ -125,7 +125,10 @@ public void testPluginShouldBeAbleToCreateSystemIndexButUserShouldNotBeAbleToInd try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { HttpResponse response = client.put("try-create-and-index/" + SYSTEM_INDEX_1 + "?runAs=user"); - assertThat(response, RestMatchers.isForbidden("/error/root_cause/0/reason", "no permissions for [] and User [name=admin")); + assertThat( + response, + RestMatchers.isForbidden("/error/root_cause/0/reason", "no permissions for [indices:data/write/index] and User [name=admin") + ); } } @@ -284,7 +287,7 @@ public void testPluginShouldNotBeAbleToBulkIndexDocumentIntoMixOfSystemIndexWher assertThat( response.getBody(), containsString( - "no permissions for [] and User [name=plugin:org.opensearch.security.systemindex.sampleplugin.SystemIndexPlugin1" + "no permissions for [indices:data/write/bulk[s]] and User [name=plugin:org.opensearch.security.systemindex.sampleplugin.SystemIndexPlugin1" ) ); } diff --git a/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java b/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java index a2312f0f7f..0e3816ba1d 100644 --- a/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java +++ b/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java @@ -20,14 +20,16 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.support.ActionRequestMetadata; import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.privileges.ActionPrivileges; +import org.opensearch.security.privileges.IndicesRequestResolver; import org.opensearch.security.privileges.PrivilegesEvaluationContext; -import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.user.User; /** @@ -47,6 +49,8 @@ public static MockPrivilegeEvaluationContextBuilder ctx() { private Set roles = new HashSet<>(); private ClusterState clusterState = EMPTY_CLUSTER_STATE; private ActionPrivileges actionPrivileges = ActionPrivileges.EMPTY; + private String action; + private ActionRequest request; public MockPrivilegeEvaluationContextBuilder attr(String key, String value) { this.attributes.put(key, value); @@ -72,6 +76,16 @@ public MockPrivilegeEvaluationContextBuilder actionPrivileges(ActionPrivileges a return this; } + public MockPrivilegeEvaluationContextBuilder action(String action) { + this.action = action; + return this; + } + + public MockPrivilegeEvaluationContextBuilder request(ActionRequest request) { + this.request = request; + return this; + } + public PrivilegesEvaluationContext get() { IndexNameExpressionResolver indexNameExpressionResolver = new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)); @@ -79,11 +93,12 @@ public PrivilegesEvaluationContext get() { return new PrivilegesEvaluationContext( user, ImmutableSet.copyOf(roles), + action, + request, + ActionRequestMetadata.empty(), null, - null, - null, - new IndexResolverReplacer(indexNameExpressionResolver, () -> clusterState, null), indexNameExpressionResolver, + new IndicesRequestResolver(indexNameExpressionResolver), () -> clusterState, this.actionPrivileges ); diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 4f94af9796..b18b8d3f97 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -44,6 +44,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import com.google.common.collect.Streams; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; @@ -58,6 +59,7 @@ import org.opensearch.core.common.Strings; import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.core.xcontent.MediaTypeRegistry; +import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.index.query.QueryBuilder; @@ -76,6 +78,7 @@ import org.opensearch.test.framework.matcher.RestIndexMatchers; import org.opensearch.transport.client.Client; +import static java.util.Arrays.asList; import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; @@ -105,8 +108,8 @@ public class TestSecurityConfig { private Map roles = new LinkedHashMap<>(); private AuditConfiguration auditConfiguration; private Map rolesMapping = new LinkedHashMap<>(); - private Map actionGroups = new LinkedHashMap<>(); + private Map tenants = new LinkedHashMap<>(); /** * A map from document id to a string containing config JSON. @@ -141,6 +144,11 @@ public TestSecurityConfig doNotFailOnForbidden(boolean doNotFailOnForbidden) { return this; } + public TestSecurityConfig privilegesEvaluationType(String privilegesEvaluationType) { + config.privilegesEvaluationType(privilegesEvaluationType); + return this; + } + public TestSecurityConfig xff(XffConfig xffConfig) { config.xffConfig(xffConfig); return this; @@ -163,11 +171,11 @@ public TestSecurityConfig authz(AuthzDomain authzDomain) { public TestSecurityConfig user(User user) { this.internalUsers.put(user.name, user); - for (Role role : user.roles) { - this.roles.put(role.name, role); + if (!role.isPredefined) { + this.roles.put(role.name, role); + } } - return this; } @@ -240,6 +248,17 @@ public List actionGroups() { return List.copyOf(actionGroups.values()); } + public TestSecurityConfig tenants(Tenant... tenants) { + for (Tenant tenant : tenants) { + if (this.tenants.containsKey(tenant.name)) { + throw new IllegalArgumentException("Tenant " + tenant.name + " already exists"); + } + this.tenants.put(tenant.name, tenant); + } + + return this; + } + /** * Specifies raw document content for the configuration index as YAML document. If this method is used, * then ONLY the raw documents will be written to the configuration index. Any other configuration specified @@ -265,6 +284,7 @@ public static class Config implements ToXContentObject { private boolean anonymousAuth; private Boolean doNotFailOnForbidden; + private String privilegesEvaluationType; private XffConfig xffConfig; private OnBehalfOfConfig onBehalfOfConfig; private Map authcDomainMap = new LinkedHashMap<>(); @@ -282,6 +302,11 @@ public Config doNotFailOnForbidden(Boolean doNotFailOnForbidden) { return this; } + public Config privilegesEvaluationType(String privilegesEvaluationType) { + this.privilegesEvaluationType = privilegesEvaluationType; + return this; + } + public Config xffConfig(XffConfig xffConfig) { this.xffConfig = xffConfig; return this; @@ -327,6 +352,9 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params if (doNotFailOnForbidden != null) { xContentBuilder.field("do_not_fail_on_forbidden", doNotFailOnForbidden); } + if (privilegesEvaluationType != null) { + xContentBuilder.field("privileges_evaluation_type", privilegesEvaluationType); + } xContentBuilder.field("authc", authcDomainMap); if (authzDomainMap.isEmpty() == false) { xContentBuilder.field("authz", authzDomainMap); @@ -456,6 +484,7 @@ public static final class User implements UserCredentialsHolder, ToXContentObjec String name; private String password; List roles = new ArrayList<>(); + List referencedRoles = new ArrayList<>(); List backendRoles = new ArrayList<>(); String requestedTenant; private Map attributes = new HashMap<>(); @@ -486,12 +515,36 @@ public User password(String password) { return this; } + /** + * Adds a user-specific role to this user. Internally, the role name will be scoped with the user name + * to avoid accidental collisions between roles of different users. + */ public User roles(Role... roles) { // We scope the role names by user to keep tests free of potential side effects String roleNamePrefix = "user_" + this.getName() + "__"; - this.roles.addAll( - Arrays.asList(roles).stream().map((r) -> r.clone().name(roleNamePrefix + r.getName())).collect(Collectors.toSet()) - ); + + for (Role role : roles) { + if (!role.isPredefined) { + Role copy = role.clone(); + if (!copy.name.startsWith(roleNamePrefix)) { + copy.name = roleNamePrefix + role.name; + } + this.roles.add(copy); + } else { + // Add the unscoped role for predefined roles + this.roles.add(role); + } + } + + return this; + } + + /** + * Adds references to roles which are already defined for the top-level SecurityTestConfig object. + * This allows tests to share roles between users. + */ + public User referencedRoles(Role... roles) { + this.referencedRoles.addAll(Arrays.asList(roles)); return this; } @@ -538,7 +591,7 @@ public String getPassword() { } public Set getRoleNames() { - return roles.stream().map(Role::getName).collect(Collectors.toSet()); + return Streams.concat(roles.stream(), referencedRoles.stream()).map(Role::getName).collect(Collectors.toSet()); } public String getDescription() { @@ -645,18 +698,23 @@ public MetadataKey(String name, Class type) { public static class Role implements ToXContentObject { public static Role ALL_ACCESS = new Role("all_access").clusterPermissions("*").indexPermissions("*").on("*"); + public static Role KIBANA_USER = new Role("kibana_user").isPredefined(true); private String name; private List clusterPermissions = new ArrayList<>(); - private List indexPermissions = new ArrayList<>(); + private List tenantPermissions = new ArrayList<>(); private Boolean hidden; - private Boolean reserved; - private String description; + /** + * 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. + */ + private boolean isPredefined = false; + public Role(String name) { this(name, null); } @@ -675,6 +733,10 @@ public IndexPermission indexPermissions(String... indexPermissions) { return new IndexPermission(this, indexPermissions); } + public TenantPermission tenantPermissions(String... tenantPermissions) { + return new TenantPermission(this, tenantPermissions); + } + public Role name(String name) { this.name = name; return this; @@ -694,10 +756,20 @@ public Role reserved(boolean reserved) { 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. + */ + public Role isPredefined(boolean isPredefined) { + this.isPredefined = isPredefined; + return this; + } + public Role clone() { Role role = new Role(this.name); role.clusterPermissions.addAll(this.clusterPermissions); role.indexPermissions.addAll(this.indexPermissions); + role.tenantPermissions.addAll(this.tenantPermissions); return role; } @@ -708,10 +780,12 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params if (!clusterPermissions.isEmpty()) { xContentBuilder.field("cluster_permissions", clusterPermissions); } - if (!indexPermissions.isEmpty()) { xContentBuilder.field("index_permissions", indexPermissions); } + if (!tenantPermissions.isEmpty()) { + xContentBuilder.field("tenant_permissions", tenantPermissions); + } if (hidden != null) { xContentBuilder.field("hidden", hidden); } @@ -919,6 +993,58 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params } } + public static class TenantPermission implements ToXContentObject { + private List tenantPatterns; + private final List allowedActions; + private Role role; + + TenantPermission(Role role, String... allowedActions) { + this.allowedActions = asList(allowedActions); + this.role = role; + } + + public Role on(String... tenantPatterns) { + this.tenantPatterns = asList(tenantPatterns); + this.role.tenantPermissions.add(this); + return this.role; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + xContentBuilder.field("tenant_patterns", tenantPatterns); + xContentBuilder.field("allowed_actions", allowedActions); + xContentBuilder.endObject(); + return xContentBuilder; + } + } + + public static class Tenant implements ToXContentObject { + private String name; + private String description; + + public Tenant(String name) { + this.name = Objects.requireNonNull(name, "Name is required"); + } + + public Tenant description(String description) { + this.description = description; + return this; + } + + public String getName() { + return name; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, ToXContent.Params params) throws IOException { + xContentBuilder.startObject(); + xContentBuilder.field("description", description); + xContentBuilder.endObject(); + return xContentBuilder; + } + } + public static class AuthcDomain implements ToXContentObject { private static String PUBLIC_KEY = @@ -1091,6 +1217,8 @@ public void initIndex(Client client) { client.admin().indices().create(new CreateIndexRequest(indexName).settings(settings)).actionGet(); if (rawConfigurationDocuments == null) { + checkReferencedRoles(); + writeSingleEntryConfigToIndex(client, CType.CONFIG, config); if (auditConfiguration != null) { writeSingleEntryConfigToIndex(client, CType.AUDIT, "config", auditConfiguration); @@ -1099,7 +1227,7 @@ public void initIndex(Client client) { writeConfigToIndex(client, CType.INTERNALUSERS, internalUsers); writeConfigToIndex(client, CType.ROLESMAPPING, rolesMapping); writeConfigToIndex(client, CType.ACTIONGROUPS, actionGroups); - writeEmptyConfigToIndex(client, CType.TENANTS); + writeConfigToIndex(client, CType.TENANTS, tenants); } else { // Write raw configuration alternatively to the normal configuration @@ -1107,7 +1235,26 @@ public void initIndex(Client client) { writeConfigToIndex(client, entry.getKey(), entry.getValue()); } } + } + /** + * Does a sanity check on the user's referenced roles; these must actually match the globally defined roles. + */ + private void checkReferencedRoles() { + for (User user : this.internalUsers.values()) { + for (Role role : user.referencedRoles) { + if (this.roles.containsKey(role.name) && !this.roles.get(role.name).equals(role)) { + throw new RuntimeException( + "Referenced role '" + + role.name + + "' in user '" + + user.name + + "' does not match the definition in TestSecurityConfig: " + + this.roles.get(role.name) + ); + } + } + } } public void updateInternalUsersConfiguration(Client client, List users) { diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 5aef9f29b4..2b86d21549 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -603,6 +603,11 @@ public Builder rolesMapping(TestSecurityConfig.RoleMapping... mappings) { return this; } + public Builder tenants(TestSecurityConfig.Tenant... tenants) { + testSecurityConfig.tenants(tenants); + return this; + } + public Builder authc(TestSecurityConfig.AuthcDomain authc) { testSecurityConfig.authc(authc); return this; @@ -658,6 +663,11 @@ public Builder doNotFailOnForbidden(boolean doNotFailOnForbidden) { return this; } + public Builder privilegesEvaluationType(String privilegesEvaluationType) { + testSecurityConfig.privilegesEvaluationType(privilegesEvaluationType); + return this; + } + public Builder defaultConfigurationInitDirectory(String defaultConfigurationInitDirectory) { this.defaultConfigurationInitDirectory = defaultConfigurationInitDirectory; return this; diff --git a/src/integrationTest/java/org/opensearch/test/framework/matcher/RestIndexMatchers.java b/src/integrationTest/java/org/opensearch/test/framework/matcher/RestIndexMatchers.java index ed1d9e8901..74ef63491d 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/matcher/RestIndexMatchers.java +++ b/src/integrationTest/java/org/opensearch/test/framework/matcher/RestIndexMatchers.java @@ -105,11 +105,11 @@ default IndexMatcher butForbiddenIfIncomplete(IndexMatcher other) { * This method has the special feature that you can also specify data streams; it will then assert that * the backing indices of the data streams will be present in the result set. */ - public static OnResponseIndexMatcher containsExactly(TestIndexOrAliasOrDatastream... testIndices) { + public static ContainsExactlyMatcher containsExactly(TestIndexOrAliasOrDatastream... testIndices) { return containsExactly(Arrays.asList(testIndices)); } - public static OnResponseIndexMatcher containsExactly(Collection testIndices) { + public static ContainsExactlyMatcher containsExactly(Collection testIndices) { Map indexNameMap = new HashMap<>(); boolean containsOpenSearchSecurityIndex = false; @@ -401,7 +401,7 @@ protected static String formatResponse(TestRestClient.HttpResponse response) { * This asserts that the item we assert on contains a set of indices that exactly corresponds to the expected * indices (i.e., not fewer and not more indices). This is usually used to match against REST responses. */ - static class ContainsExactlyMatcher extends AbstractIndexMatcher implements OnResponseIndexMatcher { + public static class ContainsExactlyMatcher extends AbstractIndexMatcher implements OnResponseIndexMatcher { private static final Pattern DS_BACKING_INDEX_PATTERN = Pattern.compile("\\.ds-(.+)-[0-9]+"); ContainsExactlyMatcher(Map indexNameMap, boolean containsOpenSearchSecurityIndex) { @@ -564,6 +564,16 @@ public OnResponseIndexMatcher butFailIfIncomplete(IndexMatcher other, RestMatche return this.reducedBy(other); } } + + public OnResponseIndexMatcher andFromRemote(String prefix, TestIndexOrAliasOrDatastream... remoteTestIndices) { + Map indexNameMap = new HashMap<>(this.expectedIndices); + + for (TestIndexOrAliasOrDatastream testIndex : remoteTestIndices) { + indexNameMap.put(prefix + ":" + testIndex.name(), testIndex); + } + + return new ContainsExactlyMatcher(indexNameMap, this.containsOpenSearchSecurityIndex); + } } /** diff --git a/src/integrationTest/resources/log4j2-test.properties b/src/integrationTest/resources/log4j2-test.properties index d0bb23fa3f..21eb271446 100644 --- a/src/integrationTest/resources/log4j2-test.properties +++ b/src/integrationTest/resources/log4j2-test.properties @@ -10,6 +10,8 @@ appender.console.filter.prerelease.type=RegexFilter appender.console.filter.prerelease.regex=.+\\Qis a pre-release version of OpenSearch and is not suitable for production\\E appender.console.filter.prerelease.onMatch=DENY appender.console.filter.prerelease.onMismatch=NEUTRAL +appender.console.follow = true +appender.console.immediateFlush = true appender.capturing.type = LogCapturingAppender appender.capturing.name = logCapturingAppender @@ -53,3 +55,8 @@ logger.securenetty4transport.name = org.opensearch.transport.netty4.ssl.SecureNe logger.securenetty4transport.level = error logger.securenetty4transport.appenderRef.capturing.ref = logCapturingAppender +logger.p.name=org.opensearch.security.privileges +logger.p.level=DEBUG + +logger.pi.name=org.opensearch.security.configuration.PrivilegesInterceptorImpl +logger.pi.level=TRACE \ No newline at end of file diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 24bfd79932..694d5aff0c 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -141,6 +141,7 @@ import org.opensearch.security.auditlog.config.AuditConfig.Filter.FilterEntries; import org.opensearch.security.auditlog.impl.AuditLogImpl; import org.opensearch.security.auth.BackendRegistry; +import org.opensearch.security.auth.RolesInjector; import org.opensearch.security.compliance.ComplianceIndexingOperationListener; import org.opensearch.security.compliance.ComplianceIndexingOperationListenerImpl; import org.opensearch.security.configuration.AdminDNs; @@ -149,7 +150,6 @@ import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.configuration.DlsFlsRequestValve; import org.opensearch.security.configuration.DlsFlsValveImpl; -import org.opensearch.security.configuration.PrivilegesInterceptorImpl; import org.opensearch.security.configuration.SecurityConfigVersionHandler; import org.opensearch.security.configuration.SecurityFlsDlsIndexSearcherWrapper; import org.opensearch.security.dlic.rest.api.Endpoint; @@ -165,14 +165,14 @@ import org.opensearch.security.http.XFFResolver; import org.opensearch.security.identity.SecurePluginSubject; import org.opensearch.security.identity.SecurityTokenManager; +import org.opensearch.security.privileges.ConfigurableRoleMapper; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationException; -import org.opensearch.security.privileges.PrivilegesEvaluator; -import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.privileges.ResourceAccessEvaluator; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext; -import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resources.ResourceAccessControlClient; import org.opensearch.security.resources.ResourceAccessHandler; import org.opensearch.security.resources.ResourceActionGroupsHelper; @@ -268,7 +268,8 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private boolean sslCertReloadEnabled; private volatile SecurityInterceptor si; - private volatile PrivilegesEvaluator evaluator; + private volatile PrivilegesConfiguration privilegesConfiguration; + private volatile RoleMapper roleMapper; private volatile UserService userService; private volatile RestLayerPrivilegesEvaluator restLayerEvaluator; private volatile ConfigurationRepository cr; @@ -284,7 +285,6 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile DynamicConfigFactory dcf; private final List demoCertHashes = new ArrayList(3); private volatile SecurityFilter sf; - private volatile IndexResolverReplacer irr; private final AtomicReference namedXContentRegistry = new AtomicReference<>(NamedXContentRegistry.EMPTY);; private volatile DlsFlsRequestValve dlsFlsValve = null; private final OpensearchDynamicSetting transportPassiveAuthSetting; @@ -621,19 +621,27 @@ public List getRestHandlers( // FGAC enabled == not sslOnly if (!SSLConfig.isSslOnlyMode()) { handlers.add( - new SecurityInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool)) + new SecurityInfoAction( + settings, + restController, + Objects.requireNonNull(privilegesConfiguration), + Objects.requireNonNull(threadPool) + ) ); handlers.add( new SecurityHealthAction( settings, restController, Objects.requireNonNull(backendRegistry), - Objects.requireNonNull(evaluator) + Objects.requireNonNull(privilegesConfiguration) ) ); handlers.add( new DashboardsInfoAction( - Objects.requireNonNull(evaluator), + settings, + restController, + Objects.requireNonNull(privilegesConfiguration), + Objects.requireNonNull(cr), Objects.requireNonNull(threadPool), resourceSharingEnabledSetting ) @@ -642,7 +650,7 @@ public List getRestHandlers( new TenantInfoAction( settings, restController, - Objects.requireNonNull(evaluator), + Objects.requireNonNull(privilegesConfiguration), Objects.requireNonNull(threadPool), Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), @@ -680,7 +688,8 @@ public List getRestHandlers( cr, cs, principalExtractor, - evaluator, + roleMapper, + privilegesConfiguration, threadPool, Objects.requireNonNull(auditLog), sslSettingsManager, @@ -751,7 +760,8 @@ public void onIndexModule(IndexModule indexModule) { cs, auditLog, ciol, - evaluator, + privilegesConfiguration, + roleMapper, dlsFlsValve::getCurrentConfig, dlsFlsBaseContext ) @@ -1121,7 +1131,6 @@ public Collection createComponents( this.cs.addListener(cih); final IndexNameExpressionResolver resolver = new IndexNameExpressionResolver(threadPool.getThreadContext()); - irr = new IndexResolverReplacer(resolver, clusterService::state, cih); final String DEFAULT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = DefaultInterClusterRequestEvaluator.class.getName(); InterClusterRequestEvaluator interClusterRequestEvaluator = new DefaultInterClusterRequestEvaluator(settings); @@ -1137,15 +1146,11 @@ public Collection createComponents( UserFactory userFactory = new UserFactory.Caching(settings); - final PrivilegesInterceptor privilegesInterceptor; - namedXContentRegistry.set(xContentRegistry); if (SSLConfig.isSslOnlyMode()) { auditLog = new NullAuditLog(); - privilegesInterceptor = new PrivilegesInterceptor(resolver, clusterService, localClient, threadPool); } else { auditLog = new AuditLogImpl(settings, configPath, localClient, threadPool, resolver, clusterService, environment, userFactory); - privilegesInterceptor = new PrivilegesInterceptorImpl(resolver, clusterService, localClient, threadPool); } sslExceptionHandler = new AuditLogSslExceptionHandler(auditLog); @@ -1162,26 +1167,31 @@ public Collection createComponents( backendRegistry = new BackendRegistry(settings, adminDns, xffResolver, auditLog, threadPool, cih); backendRegistry.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); cr.subscribeOnChange(configMap -> { backendRegistry.invalidateCache(); }); - tokenManager = new SecurityTokenManager(cs, threadPool, userService); + RoleMapper roleMapper = new RolesInjector.InjectedRoleMapper( + new ConfigurableRoleMapper(cr, settings), + threadPool.getThreadContext() + ); + this.roleMapper = roleMapper; + tokenManager = new SecurityTokenManager(cs, threadPool, userService, roleMapper); final CompatConfig compatConfig = new CompatConfig(environment, transportPassiveAuthSetting); - rsIndexHandler = new ResourceSharingIndexHandler(localClient, threadPool, resourcePluginInfo); - evaluator = new PrivilegesEvaluator( + + PrivilegesConfiguration privilegesConfiguration = new PrivilegesConfiguration( + cr, clusterService, clusterService::state, + localClient, + roleMapper, threadPool, - threadPool.getThreadContext(), - cr, resolver, auditLog, settings, - privilegesInterceptor, - cih, - irr + cih::getReasonForUnavailability ); + this.privilegesConfiguration = privilegesConfiguration; - dlsFlsBaseContext = new DlsFlsBaseContext(evaluator, threadPool.getThreadContext(), adminDns); + dlsFlsBaseContext = new DlsFlsBaseContext(privilegesConfiguration, threadPool.getThreadContext(), adminDns); if (SSLConfig.isSslOnlyMode()) { dlsFlsValve = new DlsFlsRequestValve.NoopDlsFlsRequestValve(); @@ -1201,7 +1211,13 @@ public Collection createComponents( cr.subscribeOnChange(configMap -> { ((DlsFlsValveImpl) dlsFlsValve).updateConfiguration(cr.getConfiguration(CType.ROLES)); }); } - resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns, evaluator, resourcePluginInfo); + resourceAccessHandler = new ResourceAccessHandler( + threadPool, + rsIndexHandler, + adminDns, + privilegesConfiguration, + resourcePluginInfo + ); // Assign resource sharing client to each extension // Using the non-gated client (i.e. no additional permissions required) @@ -1226,7 +1242,8 @@ public Collection createComponents( sf = new SecurityFilter( settings, - evaluator, + privilegesConfiguration, + roleMapper, adminDns, dlsFlsValve, auditLog, @@ -1234,7 +1251,6 @@ public Collection createComponents( cs, cih, compatConfig, - irr, xffResolver, resourceAccessEvaluator ); @@ -1247,7 +1263,7 @@ public Collection createComponents( principalExtractor = ReflectionHelper.instantiatePrincipalExtractor(principalExtractorClass); } - restLayerEvaluator = new RestLayerPrivilegesEvaluator(evaluator); + restLayerEvaluator = new RestLayerPrivilegesEvaluator(privilegesConfiguration); securityRestHandler = new SecurityRestFilter( backendRegistry, @@ -1262,9 +1278,7 @@ public Collection createComponents( dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih, passwordHasher); dcf.registerDCFListener(backendRegistry); dcf.registerDCFListener(compatConfig); - dcf.registerDCFListener(irr); dcf.registerDCFListener(xffResolver); - dcf.registerDCFListener(evaluator); dcf.registerDCFListener(securityRestHandler); dcf.registerDCFListener(tokenManager); if (!(auditLog instanceof NullAuditLog)) { @@ -1304,7 +1318,7 @@ public Collection createComponents( components.add(cr); components.add(xffResolver); components.add(backendRegistry); - components.add(evaluator); + components.add(privilegesConfiguration); components.add(restLayerEvaluator); components.add(si); components.add(dcf); @@ -2402,7 +2416,7 @@ public PluginSubject getPluginSubject(Plugin plugin) { } } pluginPermissions.getCluster_permissions().add(BulkAction.NAME); - evaluator.updatePluginToActionPrivileges(pluginPrincipal, pluginPermissions); + privilegesConfiguration.updatePluginToActionPrivileges(pluginPrincipal, pluginPermissions); } return subject; } diff --git a/src/main/java/org/opensearch/security/auth/RolesInjector.java b/src/main/java/org/opensearch/security/auth/RolesInjector.java index 42afc77ad2..2b6ea82dca 100644 --- a/src/main/java/org/opensearch/security/auth/RolesInjector.java +++ b/src/main/java/org/opensearch/security/auth/RolesInjector.java @@ -15,6 +15,8 @@ package org.opensearch.security.auth; +import java.util.Arrays; +import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -23,8 +25,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.OpenSearchSecurityException; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; @@ -92,4 +98,51 @@ private void addUser(final User user, final ThreadPool threadPool) { } } + + /** + * For users injected by this class, no role mapping shall be performed. This RoleMapper checks whether there + * is an injected user (by header) and skips default role mapping (realized by the delegate) if so. + */ + public static class InjectedRoleMapper implements RoleMapper { + + private final ThreadContext threadContext; + private final RoleMapper defaultRoleMapper; + + public InjectedRoleMapper(RoleMapper defaultRoleMapper, ThreadContext threadContext) { + this.threadContext = threadContext; + this.defaultRoleMapper = defaultRoleMapper; + } + + @Override + public ImmutableSet map(User user, TransportAddress caller) { + ImmutableSet mappedRoles; + + if (threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES) != null) { + // Just return the security roles, like they were initialized in the injectUserAndRoles() method above + mappedRoles = user.getSecurityRoles(); + } else { + // No injected user => use default role mapping + mappedRoles = defaultRoleMapper.map(user, caller); + } + + String injectedRolesValidationString = threadContext.getTransient( + ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION + ); + if (injectedRolesValidationString != null) { + // Moved from + // https://github.com/opensearch-project/security/blob/d29095f26dba1a26308c69b608dc926bd40c0f52/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java#L406 + // See also https://github.com/opensearch-project/security/pull/1367 + HashSet injectedRolesValidationSet = new HashSet<>(Arrays.asList(injectedRolesValidationString.split(","))); + if (!mappedRoles.containsAll(injectedRolesValidationSet)) { + throw new OpenSearchSecurityException( + String.format("No mapping for %s on roles %s", user, injectedRolesValidationSet), + RestStatus.FORBIDDEN + ); + } + mappedRoles = ImmutableSet.copyOf(injectedRolesValidationSet); + } + + return mappedRoles; + } + } } diff --git a/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java b/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java index 7ccbeb6d14..e4ccb026f4 100644 --- a/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java +++ b/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java @@ -101,4 +101,12 @@ public Boolean hasClusterManager() { } return false; } + + public String getReasonForUnavailability() { + if (!hasClusterManager()) { + return CLUSTER_MANAGER_NOT_PRESENT; + } else { + return null; + } + } } diff --git a/src/main/java/org/opensearch/security/configuration/DlsFilterLevelActionHandler.java b/src/main/java/org/opensearch/security/configuration/DlsFilterLevelActionHandler.java index 1e179b1243..d9ba6cb316 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFilterLevelActionHandler.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFilterLevelActionHandler.java @@ -39,6 +39,8 @@ import org.opensearch.action.search.SearchScrollAction; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.document.DocumentField; import org.opensearch.common.util.concurrent.ThreadContext; @@ -63,7 +65,6 @@ import org.opensearch.security.privileges.dlsfls.DocumentPrivileges; import org.opensearch.security.privileges.dlsfls.IndexToRuleMap; import org.opensearch.security.queries.QueryBuilderTraverser; -import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.ReflectiveAttributeAccessors; import org.opensearch.security.util.ParentChildrenQueryDetector; @@ -131,7 +132,7 @@ public static boolean handle( private final ActionRequest request; private final ActionListener listener; private final IndexToRuleMap dlsRestrictionMap; - private final Resolved resolved; + private final OptionallyResolvedIndices resolved; private final boolean requiresIndexScoping; private final Client nodeClient; private final ClusterService clusterService; @@ -162,7 +163,9 @@ public static boolean handle( this.threadContext = threadContext; this.resolver = resolver; - this.requiresIndexScoping = resolved.isLocalAll() || resolved.getAllIndicesResolved(clusterService, resolver).size() != 1; + this.requiresIndexScoping = resolved instanceof ResolvedIndices resolvedIndices + ? resolvedIndices.local().names().size() != 1 + : true; } private boolean handle() { @@ -474,7 +477,7 @@ private boolean modifyQuery(String localClusterAlias) throws IOException { int queryCount = 0; - Set indices = resolved.getAllIndicesResolved(clusterService, resolver); + Set indices = resolved.local().names(clusterService.state()); for (String index : indices) { String prefixedIndex; diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index f22a7d6ddd..b2bcbe50c9 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -33,12 +33,15 @@ import org.opensearch.action.ActionRequest; import org.opensearch.action.RealtimeRequest; import org.opensearch.action.admin.indices.shrink.ResizeRequest; +import org.opensearch.action.bulk.BulkAction; import org.opensearch.action.bulk.BulkItemRequest; import org.opensearch.action.bulk.BulkShardRequest; -import org.opensearch.action.get.MultiGetAction; +import org.opensearch.action.search.MultiSearchAction; import org.opensearch.action.search.SearchRequest; import org.opensearch.action.update.UpdateRequest; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; @@ -48,6 +51,8 @@ import org.opensearch.core.xcontent.MediaTypeRegistry; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.index.query.ParsedQuery; +import org.opensearch.index.reindex.ReindexAction; +import org.opensearch.script.mustache.RenderSearchTemplateAction; import org.opensearch.search.DocValueFormat; import org.opensearch.search.aggregations.AggregationBuilder; import org.opensearch.search.aggregations.AggregatorFactories; @@ -76,7 +81,6 @@ import org.opensearch.security.privileges.dlsfls.DlsRestriction; import org.opensearch.security.privileges.dlsfls.FieldMasking; import org.opensearch.security.privileges.dlsfls.IndexToRuleMap; -import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resources.ResourcePluginInfo; import org.opensearch.security.resources.ResourceSharingDlsUtils; import org.opensearch.security.securityconf.DynamicConfigFactory; @@ -89,8 +93,6 @@ import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; -import static org.opensearch.security.privileges.PrivilegesEvaluator.isClusterPerm; - public class DlsFlsValveImpl implements DlsFlsRequestValve { private static final String MAP_EXECUTION_HINT = "map"; @@ -152,23 +154,27 @@ public DlsFlsValveImpl( */ @Override public boolean invoke(PrivilegesEvaluationContext context, final ActionListener listener) { - UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER); - if (isClusterPerm(context.getAction()) && !MultiGetAction.NAME.equals(context.getAction())) { + if (!isApplicable(context.getAction())) { return true; } + + UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER); if (userSubject != null && adminDNs.isAdmin(userSubject.getUser())) { return true; } + OptionallyResolvedIndices resolved = context.getResolvedRequest(); ActionRequest request = context.getRequest(); if (HeaderHelper.isInternalOrPluginRequest(threadContext)) { - if (resourceSharingEnabledSetting.getDynamicSettingValue() && request instanceof SearchRequest) { - IndexResolverReplacer.Resolved resolved = context.getResolvedRequest(); + if (resourceSharingEnabledSetting.getDynamicSettingValue() + && request instanceof SearchRequest + && resolved instanceof ResolvedIndices resolvedIndices) { Set protectedIndices = resourcePluginInfo.getResourceIndicesForProtectedTypes(); WildcardMatcher resourceIndicesMatcher = WildcardMatcher.from(protectedIndices); - if (resourceIndicesMatcher.matchAll(resolved.getAllIndices())) { + Set resolvedIndexNames = resolvedIndices.local().namesOfIndices(context.clusterState()); + if (resourceIndicesMatcher.matchAll(resolvedIndexNames)) { IndexToRuleMap sharedResourceMap = ResourceSharingDlsUtils.resourceRestrictions( namedXContentRegistry, - resolved, + resolvedIndexNames, userSubject.getUser() ); @@ -187,7 +193,6 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< return true; } DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get(); - IndexResolverReplacer.Resolved resolved = context.getResolvedRequest(); try { boolean hasDlsRestrictions = !config.getDocumentPrivileges().isUnrestricted(context, resolved); @@ -214,14 +219,12 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< if (mode == Mode.FILTER_LEVEL) { doFilterLevelDls = true; - dlsRestrictionMap = config.getDocumentPrivileges() - .getRestrictions(context, resolved.getAllIndicesResolved(clusterService, context.getIndexNameExpressionResolver())); + dlsRestrictionMap = config.getDocumentPrivileges().getRestrictions(context, resolved.local().names(context.clusterState())); } else if (mode == Mode.LUCENE_LEVEL) { doFilterLevelDls = false; } else { // mode == Mode.ADAPTIVE Mode modeByHeader = getDlsModeHeader(); - dlsRestrictionMap = config.getDocumentPrivileges() - .getRestrictions(context, resolved.getAllIndicesResolved(clusterService, context.getIndexNameExpressionResolver())); + dlsRestrictionMap = config.getDocumentPrivileges().getRestrictions(context, resolved.local().names(context.clusterState())); if (modeByHeader == Mode.FILTER_LEVEL) { doFilterLevelDls = true; @@ -539,6 +542,34 @@ public boolean isFieldAllowed(String index, String field) throws PrivilegesEvalu return config.getFieldPrivileges().getRestriction(privilegesEvaluationContext, index).isAllowedRecursive(field); } + private static boolean isApplicable(String action) { + if (action.startsWith("cluster:")) { + // Cluster actions are generally not applicable + return false; + } + if (action.startsWith("indices:admin/template/") || action.startsWith("indices:admin/index_template/")) { + // Template related actions can be safely executed without DLS/FLS applied + return false; + } + if (action.equals(BulkAction.NAME)) { + // We do not need to consider top-level bulk actions; we check the shard level later + return false; + } + if (action.equals(MultiSearchAction.NAME)) { + // We do not need to consider top-level multi search actions; we check the search actions that are executed later + return false; + } + if (action.equals(RenderSearchTemplateAction.NAME)) { + // Template related actions trigger further sub actions which we check later + return false; + } + if (action.equals(ReindexAction.NAME)) { + // Reindex actions break apart in search and bulk actions; we will handle on these levels + return false; + } + return true; + } + private static InternalAggregation aggregateBuckets(InternalAggregation aggregation) { if (aggregation instanceof StringTerms) { StringTerms stringTerms = (StringTerms) aggregation; diff --git a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java index 6a64eada3e..e6dc0f1bae 100644 --- a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java +++ b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java @@ -13,6 +13,7 @@ import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -44,13 +45,14 @@ import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.security.privileges.DashboardsMultiTenancyConfiguration; import org.opensearch.security.privileges.DocumentAllowList; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.privileges.TenantPrivileges; -import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; -import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; @@ -77,13 +79,20 @@ public class PrivilegesInterceptorImpl extends PrivilegesInterceptor { protected final Logger log = LogManager.getLogger(this.getClass()); + private final Supplier tenantPrivilegesSupplier; + private final Supplier multiTenancyConfigurationSupplier; + public PrivilegesInterceptorImpl( IndexNameExpressionResolver resolver, ClusterService clusterService, Client client, - ThreadPool threadPool + ThreadPool threadPool, + Supplier tenantPrivilegesSupplier, + Supplier multiTenancyConfigurationSupplier ) { super(resolver, clusterService, client, threadPool); + this.tenantPrivilegesSupplier = tenantPrivilegesSupplier; + this.multiTenancyConfigurationSupplier = multiTenancyConfigurationSupplier; } /** @@ -97,25 +106,31 @@ public ReplaceResult replaceDashboardsIndex( final ActionRequest request, final String action, final User user, - final DynamicConfigModel config, - final Resolved requestedResolved, - final PrivilegesEvaluationContext context, - final TenantPrivileges tenantPrivileges + final PrivilegesEvaluationContext context ) { + DashboardsMultiTenancyConfiguration config = this.multiTenancyConfigurationSupplier.get(); - final boolean enabled = config.isDashboardsMultitenancyEnabled();// config.dynamic.kibana.multitenancy_enabled; + final boolean enabled = config.multitenancyEnabled();// config.dynamic.kibana.multitenancy_enabled; if (!enabled) { return CONTINUE_EVALUATION_REPLACE_RESULT; } + OptionallyResolvedIndices optionallyResolvedIndices = context.getResolvedRequest(); + TenantPrivileges tenantPrivileges = this.tenantPrivilegesSupplier.get(); + + if (!(optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices)) { + // If we have no information about the indices, it is safe to skip multi tenancy handling + return CONTINUE_EVALUATION_REPLACE_RESULT; + } + // next two lines needs to be retrieved from configuration - final String dashboardsServerUsername = config.getDashboardsServerUsername();// config.dynamic.kibana.server_username; - final String dashboardsIndexName = config.getDashboardsIndexname();// config.dynamic.kibana.index; + final String dashboardsServerUsername = config.dashboardsServerUsername();// config.dynamic.kibana.server_username; + final String dashboardsIndexName = config.dashboardsIndex();// config.dynamic.kibana.index; String requestedTenant = user.getRequestedTenant(); if (USER_TENANT.equals(requestedTenant)) { - final boolean private_tenant_enabled = config.isDashboardsPrivateTenantEnabled(); + final boolean private_tenant_enabled = config.privateTenantEnabled(); if (!private_tenant_enabled) { return ACCESS_DENIED_REPLACE_RESULT; } @@ -129,7 +144,7 @@ public ReplaceResult replaceDashboardsIndex( // intercept when requests are not made by the kibana server and if the kibana index/alias (.kibana) is the only index/alias // involved final boolean dashboardsIndexOnly = !user.getName().equals(dashboardsServerUsername) - && resolveToDashboardsIndexOrAlias(requestedResolved, dashboardsIndexName); + && resolveToDashboardsIndexOrAlias(resolvedIndices, dashboardsIndexName); final boolean isTraceEnabled = log.isTraceEnabled(); TenantPrivileges.ActionType actionType = getActionTypeForAction(action); @@ -157,12 +172,12 @@ public ReplaceResult replaceDashboardsIndex( if (isDebugEnabled && !user.getName().equals(dashboardsServerUsername)) { // log statements only here - log.debug("requestedResolved: " + requestedResolved); + log.debug("requestedResolved: {}", resolvedIndices); } // request not made by the kibana server and user index is the only index/alias involved - if (!user.getName().equals(dashboardsServerUsername) && !requestedResolved.isLocalAll()) { - final Set indices = requestedResolved.getAllIndices(); + if (!user.getName().equals(dashboardsServerUsername) && resolvedIndices.local().names().size() == 1) { + final Set indices = resolvedIndices.local().namesOfIndices(context.clusterState()); final String tenantIndexName = toUserIndexName(dashboardsIndexName, requestedTenant); if (indices.size() == 1 && indices.iterator().next().startsWith(tenantIndexName) @@ -201,7 +216,7 @@ public ReplaceResult replaceDashboardsIndex( if (isTraceEnabled) { log.trace("not a request to only the .kibana index"); log.trace(user.getName() + "/" + dashboardsServerUsername); - log.trace(requestedResolved + " does not contain only " + dashboardsIndexName); + log.trace(resolvedIndices + " does not contain only " + dashboardsIndexName); } } @@ -393,15 +408,7 @@ private String toUserIndexName(final String originalDashboardsIndex, final Strin return originalDashboardsIndex + "_" + tenant.hashCode() + "_" + tenant.toLowerCase().replaceAll("[^a-z0-9]+", EMPTY_STRING); } - private static boolean resolveToDashboardsIndexOrAlias(final Resolved requestedResolved, final String dashboardsIndexName) { - if (requestedResolved.isLocalAll()) { - return false; - } - final Set allIndices = requestedResolved.getAllIndices(); - if (allIndices.size() == 1 && allIndices.iterator().next().equals(dashboardsIndexName)) { - return true; - } - final Set aliases = requestedResolved.getAliases(); - return (aliases.size() == 1 && aliases.iterator().next().equals(dashboardsIndexName)); + private static boolean resolveToDashboardsIndexOrAlias(final ResolvedIndices requestedResolved, final String dashboardsIndexName) { + return requestedResolved.local().names().size() == 1 && requestedResolved.local().names().contains(dashboardsIndexName); } } diff --git a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java index 2cabfbd1a4..96c1616183 100644 --- a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java @@ -37,9 +37,10 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.compliance.ComplianceIndexingOperationListener; import org.opensearch.security.privileges.DocumentAllowList; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluationException; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext; import org.opensearch.security.privileges.dlsfls.DlsFlsProcessedConfig; import org.opensearch.security.privileges.dlsfls.DlsRestriction; @@ -69,11 +70,12 @@ public SecurityFlsDlsIndexSearcherWrapper( final ClusterService clusterService, final AuditLog auditlog, final ComplianceIndexingOperationListener ciol, - final PrivilegesEvaluator evaluator, + final PrivilegesConfiguration privilegesConfiguration, + final RoleMapper roleMapper, final Supplier dlsFlsProcessedConfigSupplier, final DlsFlsBaseContext dlsFlsBaseContext ) { - super(indexService, settings, adminDNs, evaluator); + super(indexService, settings, adminDNs, privilegesConfiguration, roleMapper); Set metadataFieldsCopy; if (indexService.getMetadata().getState() == IndexMetadata.State.CLOSE) { if (log.isDebugEnabled()) { diff --git a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java index 447f134877..e7f12be80c 100644 --- a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java @@ -33,6 +33,7 @@ import org.apache.logging.log4j.Logger; import org.apache.lucene.index.DirectoryReader; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.common.CheckedFunction; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; @@ -40,18 +41,15 @@ import org.opensearch.core.index.Index; import org.opensearch.index.IndexService; import org.opensearch.indices.SystemIndexRegistry; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; -import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; -import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; -import org.greenrobot.eventbus.Subscribe; - public class SystemIndexSearcherWrapper implements CheckedFunction { protected final Logger log = LogManager.getLogger(this.getClass()); @@ -59,8 +57,8 @@ public class SystemIndexSearcherWrapper implements CheckedFunction securityRoles = evaluator.mapRoles(user, caller); + final Set securityRoles = roleMapper.map(user, caller); if (allowedRolesMatcher.matchAny(securityRoles)) { return true; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java index c6a01ecad9..a74983733e 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java @@ -128,7 +128,9 @@ private void userAccount( final TransportAddress remoteAddress, final SecurityDynamicConfiguration configuration ) { - PrivilegesEvaluationContext context = securityApiDependencies.privilegesEvaluator().createContext(user, null); + PrivilegesEvaluationContext context = securityApiDependencies.privilegesConfiguration() + .privilegesEvaluator() + .createContext(user, null); ok( channel, (builder, params) -> builder.startObject() @@ -139,7 +141,7 @@ private void userAccount( .field("user_requested_tenant", user.getRequestedTenant()) .field("backend_roles", user.getRoles()) .field("custom_attribute_names", user.getCustomAttributesMap().keySet()) - .field("tenants", securityApiDependencies.privilegesEvaluator().tenantPrivileges().tenantMap(context)) + .field("tenants", securityApiDependencies.privilegesConfiguration().tenantPrivileges().tenantMap(context)) .field("roles", context.getMappedRoles()) .endObject() ); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/PermissionsInfoAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/PermissionsInfoAction.java index db67e9b979..06f407f715 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/PermissionsInfoAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/PermissionsInfoAction.java @@ -35,7 +35,7 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -60,7 +60,7 @@ public class PermissionsInfoAction extends BaseRestHandler { private final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator; private final ThreadPool threadPool; - private final PrivilegesEvaluator privilegesEvaluator; + private final RoleMapper roleMapper; private final ConfigurationRepository configurationRepository; protected PermissionsInfoAction( @@ -72,17 +72,17 @@ protected PermissionsInfoAction( final ConfigurationRepository configurationRepository, final ClusterService cs, final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator privilegesEvaluator, + final RoleMapper roleMapper, ThreadPool threadPool, AuditLog auditLog ) { super(); this.threadPool = threadPool; - this.privilegesEvaluator = privilegesEvaluator; + this.roleMapper = roleMapper; this.restApiPrivilegesEvaluator = new RestApiPrivilegesEvaluator( settings, adminDNs, - privilegesEvaluator, + roleMapper, principalExtractor, configPath, threadPool @@ -129,7 +129,7 @@ public void accept(RestChannel channel) throws Exception { final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); final TransportAddress remoteAddress = threadPool.getThreadContext() .getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - Set userRoles = privilegesEvaluator.mapRoles(user, remoteAddress); + Set userRoles = roleMapper.map(user, remoteAddress); Boolean hasApiAccess = restApiPrivilegesEvaluator.currentUserHasRestApiAccess(userRoles); Map> disabledEndpoints = restApiPrivilegesEvaluator.getDisabledEndpointsForCurrentUser( user.getName(), diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java index 2f66797076..bcff258fa6 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java @@ -23,7 +23,8 @@ import org.opensearch.core.common.transport.TransportAddress; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.dlic.rest.support.Utils; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.PrivilegesConfiguration; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.WildcardMatcher; @@ -77,7 +78,7 @@ default String build() { private final ThreadContext threadContext; - private final PrivilegesEvaluator privilegesEvaluator; + private final PrivilegesConfiguration privilegesConfiguration; private final AdminDNs adminDNs; @@ -85,12 +86,12 @@ default String build() { public RestApiAdminPrivilegesEvaluator( final ThreadContext threadContext, - final PrivilegesEvaluator privilegesEvaluator, + final PrivilegesConfiguration privilegesConfiguration, final AdminDNs adminDNs, final boolean restapiAdminEnabled ) { this.threadContext = threadContext; - this.privilegesEvaluator = privilegesEvaluator; + this.privilegesConfiguration = privilegesConfiguration; this.adminDNs = adminDNs; this.restapiAdminEnabled = restapiAdminEnabled; } @@ -108,11 +109,10 @@ public boolean isCurrentUserAdminFor(final Endpoint endpoint, final String actio return false; } final String permission = ENDPOINTS_WITH_PERMISSIONS.get(endpoint).build(action); - final boolean hasAccess = privilegesEvaluator.hasRestAdminPermissions( - userAndRemoteAddress.getLeft(), - userAndRemoteAddress.getRight(), - permission - ); + PrivilegesEvaluationContext context = privilegesConfiguration.privilegesEvaluator() + .createContext(userAndRemoteAddress.getLeft(), permission); + final boolean hasAccess = context.getActionPrivileges().hasExplicitClusterPrivilege(context, permission).isAllowed(); + if (logger.isDebugEnabled()) { logger.debug( "User {} with permission {} {} access to endpoint {}", diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java index f1a336986b..5a3b66a561 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java @@ -37,7 +37,7 @@ import org.opensearch.security.dlic.rest.support.Utils; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.filter.SecurityRequestFactory; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLRequestHelper; import org.opensearch.security.support.ConfigConstants; @@ -50,7 +50,7 @@ public class RestApiPrivilegesEvaluator { protected final Logger logger = LogManager.getLogger(this.getClass()); private final AdminDNs adminDNs; - private final PrivilegesEvaluator privilegesEvaluator; + private final RoleMapper roleMapper; private final PrincipalExtractor principalExtractor; private final Path configPath; private final ThreadPool threadPool; @@ -77,14 +77,14 @@ public class RestApiPrivilegesEvaluator { public RestApiPrivilegesEvaluator( final Settings settings, final AdminDNs adminDNs, - final PrivilegesEvaluator privilegesEvaluator, + final RoleMapper roleMapper, final PrincipalExtractor principalExtractor, final Path configPath, ThreadPool threadPool ) { this.adminDNs = adminDNs; - this.privilegesEvaluator = privilegesEvaluator; + this.roleMapper = roleMapper; this.principalExtractor = principalExtractor; this.configPath = configPath; this.threadPool = threadPool; @@ -376,7 +376,7 @@ private String checkRoleBasedAccessPermissions(RestRequest request, Endpoint end final TransportAddress remoteAddress = userAndRemoteAddress.getRight(); // map the users Security roles - Set userRoles = privilegesEvaluator.mapRoles(user, remoteAddress); + Set userRoles = roleMapper.map(user, remoteAddress); // check if user has any role that grants access if (currentUserHasRestApiAccess(userRoles)) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java index 498230423f..cb985899b1 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java @@ -15,7 +15,7 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.support.ConfigConstants; public class SecurityApiDependencies { @@ -26,12 +26,12 @@ public class SecurityApiDependencies { private final AuditLog auditLog; private final Settings settings; - private final PrivilegesEvaluator privilegesEvaluator; + private final PrivilegesConfiguration privilegesConfiguration; public SecurityApiDependencies( final AdminDNs adminDNs, final ConfigurationRepository configurationRepository, - final PrivilegesEvaluator privilegesEvaluator, + final PrivilegesConfiguration privilegesConfiguration, final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator, final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator, final AuditLog auditLog, @@ -39,7 +39,7 @@ public SecurityApiDependencies( ) { this.adminDNs = adminDNs; this.configurationRepository = configurationRepository; - this.privilegesEvaluator = privilegesEvaluator; + this.privilegesConfiguration = privilegesConfiguration; this.restApiPrivilegesEvaluator = restApiPrivilegesEvaluator; this.restApiAdminPrivilegesEvaluator = restApiAdminPrivilegesEvaluator; this.auditLog = auditLog; @@ -50,8 +50,8 @@ public AdminDNs adminDNs() { return adminDNs; } - public PrivilegesEvaluator privilegesEvaluator() { - return privilegesEvaluator; + public PrivilegesConfiguration privilegesConfiguration() { + return privilegesConfiguration; } public ConfigurationRepository configurationRepository() { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index 957e693bc3..9a1314b837 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -26,7 +26,8 @@ import org.opensearch.security.configuration.SecurityConfigVersionHandler; import org.opensearch.security.configuration.SecurityConfigVersionsLoader; import org.opensearch.security.hasher.PasswordHasher; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.PrivilegesConfiguration; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.resources.ResourcePluginInfo; import org.opensearch.security.resources.ResourceSharingIndexHandler; import org.opensearch.security.resources.api.migrate.MigrateResourceSharingInfoApiAction; @@ -49,7 +50,8 @@ public static Collection getHandler( final ConfigurationRepository configurationRepository, final ClusterService clusterService, final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, + final RoleMapper roleMapper, + final PrivilegesConfiguration privilegesConfiguration, final ThreadPool threadPool, final AuditLog auditLog, final SslSettingsManager sslSettingsManager, @@ -62,11 +64,11 @@ public static Collection getHandler( final var securityApiDependencies = new SecurityApiDependencies( adminDns, configurationRepository, - evaluator, - new RestApiPrivilegesEvaluator(settings, adminDns, evaluator, principalExtractor, configPath, threadPool), + privilegesConfiguration, + new RestApiPrivilegesEvaluator(settings, adminDns, roleMapper, principalExtractor, configPath, threadPool), new RestApiAdminPrivilegesEvaluator( threadPool.getThreadContext(), - evaluator, + privilegesConfiguration, adminDns, settings.getAsBoolean(SECURITY_RESTAPI_ADMIN_ENABLED, false) ), @@ -91,7 +93,7 @@ public static Collection getHandler( configurationRepository, clusterService, principalExtractor, - evaluator, + roleMapper, threadPool, auditLog ), diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index a23db341fd..eee48f798b 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -63,6 +63,8 @@ import org.opensearch.action.support.ActionFilterChain; import org.opensearch.action.support.ActionRequestMetadata; import org.opensearch.action.update.UpdateRequest; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; @@ -86,16 +88,18 @@ import org.opensearch.security.configuration.CompatConfig; import org.opensearch.security.configuration.DlsFlsRequestValve; import org.opensearch.security.http.XFFResolver; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.privileges.ResourceAccessEvaluator; -import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.support.Base64Helper; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; import org.opensearch.security.support.SourceFieldsContext; import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.ThreadContextUserInfo; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; import org.opensearch.threadpool.ThreadPool; @@ -107,7 +111,8 @@ public class SecurityFilter implements ActionFilter { protected final Logger log = LogManager.getLogger(this.getClass()); - private final PrivilegesEvaluator evalp; + private final PrivilegesConfiguration privilegesConfiguration; + private final RoleMapper roleMapper; private final AdminDNs adminDns; private final DlsFlsRequestValve dlsFlsValve; private final AuditLog auditLog; @@ -115,15 +120,17 @@ public class SecurityFilter implements ActionFilter { private final ClusterService cs; private final ClusterInfoHolder clusterInfoHolder; private final CompatConfig compatConfig; - private final IndexResolverReplacer indexResolverReplacer; + private final XFFResolver xffResolver; private final WildcardMatcher immutableIndicesMatcher; private final RolesInjector rolesInjector; private final UserInjector userInjector; private final ResourceAccessEvaluator resourceAccessEvaluator; + private final ThreadContextUserInfo threadContextUserInfo; public SecurityFilter( final Settings settings, - final PrivilegesEvaluator evalp, + PrivilegesConfiguration privilegesConfiguration, + RoleMapper roleMapper, final AdminDNs adminDns, DlsFlsRequestValve dlsFlsValve, AuditLog auditLog, @@ -131,11 +138,11 @@ public SecurityFilter( ClusterService cs, final ClusterInfoHolder clusterInfoHolder, final CompatConfig compatConfig, - final IndexResolverReplacer indexResolverReplacer, final XFFResolver xffResolver, ResourceAccessEvaluator resourceAccessEvaluator ) { - this.evalp = evalp; + this.privilegesConfiguration = privilegesConfiguration; + this.roleMapper = roleMapper; this.adminDns = adminDns; this.dlsFlsValve = dlsFlsValve; this.auditLog = auditLog; @@ -143,13 +150,14 @@ public SecurityFilter( this.cs = cs; this.clusterInfoHolder = clusterInfoHolder; this.compatConfig = compatConfig; - this.indexResolverReplacer = indexResolverReplacer; + this.xffResolver = xffResolver; this.immutableIndicesMatcher = WildcardMatcher.from( settings.getAsList(ConfigConstants.SECURITY_COMPLIANCE_IMMUTABLE_INDICES, Collections.emptyList()) ); this.rolesInjector = new RolesInjector(auditLog); this.userInjector = new UserInjector(settings, threadPool, auditLog, xffResolver); this.resourceAccessEvaluator = resourceAccessEvaluator; + this.threadContextUserInfo = new ThreadContextUserInfo(threadPool.getThreadContext(), privilegesConfiguration, settings); log.info("{} indices are made immutable.", immutableIndicesMatcher); } @@ -174,7 +182,7 @@ public void app ) { try (StoredContext ctx = threadPool.getThreadContext().newStoredContext(true)) { org.apache.logging.log4j.ThreadContext.clearAll(); - apply0(task, action, request, listener, chain); + apply0(task, action, request, actionRequestMetadata, listener, chain); } } @@ -186,6 +194,7 @@ private void ap Task task, final String action, Request request, + ActionRequestMetadata actionRequestMetadata, ActionListener listener, ActionFilterChain chain ) { @@ -199,7 +208,7 @@ private void ap if (complianceConfig != null && complianceConfig.isEnabled()) { attachSourceFieldContext(request); } - final Set injectedRoles = rolesInjector.injectUserAndRoles(threadPool); + rolesInjector.injectUserAndRoles(threadPool); User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); if (user == null) { UserInjector.Result injectedUser = userInjector.getInjectedUser(); @@ -310,13 +319,13 @@ private void ap if (request instanceof BulkShardRequest) { for (BulkItemRequest bsr : ((BulkShardRequest) request).items()) { - isImmutable = checkImmutableIndices(bsr.request(), listener); + isImmutable = checkImmutableIndices(bsr.request(), actionRequestMetadata, listener); if (isImmutable) { break; } } } else { - isImmutable = checkImmutableIndices(request, listener); + isImmutable = checkImmutableIndices(request, actionRequestMetadata, listener); } if (isImmutable) { @@ -327,7 +336,6 @@ private void ap if (Origin.LOCAL.toString().equals(threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)) && (interClusterRequest || HeaderHelper.isDirectRequest(threadContext)) - && (injectedRoles == null) && (user == null)) { chain.proceed(task, action, request, listener); @@ -379,25 +387,14 @@ private void ap } } - final PrivilegesEvaluator eval = evalp; - - if (!eval.isInitialized()) { - StringBuilder error = new StringBuilder("OpenSearch Security not initialized for "); - error.append(action); - if (!clusterInfoHolder.hasClusterManager()) { - error.append(String.format(". %s", ClusterInfoHolder.CLUSTER_MANAGER_NOT_PRESENT)); - } - - log.error(error.toString()); - listener.onFailure(new OpenSearchSecurityException(error.toString(), RestStatus.SERVICE_UNAVAILABLE)); - return; - } + final PrivilegesEvaluator eval = this.privilegesConfiguration.privilegesEvaluator(); if (log.isTraceEnabled()) { log.trace("Evaluate permissions for user: {}", user.getName()); } - PrivilegesEvaluationContext context = eval.createContext(user, action, request, task, injectedRoles); + PrivilegesEvaluationContext context = eval.createContext(user, action, request, actionRequestMetadata, task); + this.threadContextUserInfo.setUserInfoInThreadContext(context); User finalUser = user; Consumer handleUnauthorized = response -> { auditLog.logMissingPrivileges(action, request, task); @@ -405,13 +402,7 @@ private void ap if (!response.getMissingSecurityRoles().isEmpty()) { err = String.format("No mapping for %s on roles %s", finalUser, response.getMissingSecurityRoles()); } else { - err = (injectedRoles != null) - ? String.format( - "no permissions for %s and associated roles %s", - response.getMissingPrivileges(), - context.getMappedRoles() - ) - : String.format("no permissions for %s and %s", response.getMissingPrivileges(), finalUser); + err = String.format("no permissions for %s and %s", response.getMissingPrivileges(), finalUser); } log.debug(err); listener.onFailure(new OpenSearchSecurityException(err, RestStatus.FORBIDDEN)); @@ -567,7 +558,7 @@ private boolean handlePermissionCheckRequest( } @SuppressWarnings("rawtypes") - private boolean checkImmutableIndices(Object request, ActionListener listener) { + private boolean checkImmutableIndices(Object request, ActionRequestMetadata actionRequestMetadata, ActionListener listener) { final boolean isModifyIndexRequest = request instanceof DeleteRequest || request instanceof UpdateRequest || request instanceof UpdateByQueryRequest @@ -577,24 +568,24 @@ private boolean checkImmutableIndices(Object request, ActionListener listener) { || request instanceof CloseIndexRequest || request instanceof IndicesAliasesRequest; - if (isModifyIndexRequest && isRequestIndexImmutable(request)) { + if (isModifyIndexRequest && isRequestIndexImmutable(request, actionRequestMetadata)) { listener.onFailure(new OpenSearchSecurityException("Index is immutable", RestStatus.FORBIDDEN)); return true; } - if ((request instanceof IndexRequest) && isRequestIndexImmutable(request)) { + if ((request instanceof IndexRequest) && isRequestIndexImmutable(request, actionRequestMetadata)) { ((IndexRequest) request).opType(OpType.CREATE); } return false; } - private boolean isRequestIndexImmutable(Object request) { - final IndexResolverReplacer.Resolved resolved = indexResolverReplacer.resolveRequest(request); - if (resolved.isLocalAll()) { + private boolean isRequestIndexImmutable(Object request, ActionRequestMetadata actionRequestMetadata) { + OptionallyResolvedIndices optionalResolvedIndices = actionRequestMetadata.resolvedIndices(); + if (optionalResolvedIndices instanceof ResolvedIndices resolvedIndices) { + return immutableIndicesMatcher.matchAny(resolvedIndices.local().namesOfIndices(cs.state())); + } else { return true; } - final Set allIndices = resolved.getAllIndices(); - return immutableIndicesMatcher.matchAny(allIndices); } } diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index 9e686c33d6..59678293fc 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -264,14 +264,13 @@ void authorizeRequest(RestHandler original, SecurityRequestChannel request, User .findFirst(); final boolean routeSupportsRestAuthorization = handler.isPresent() && handler.get() instanceof NamedRoute; if (routeSupportsRestAuthorization) { - PrivilegesEvaluatorResponse pres = new PrivilegesEvaluatorResponse(); NamedRoute route = ((NamedRoute) handler.get()); // Check both route.actionNames() and route.name(). The presence of either is sufficient. Set actionNames = ImmutableSet.builder() .addAll(route.actionNames() != null ? route.actionNames() : Collections.emptySet()) .add(route.name()) .build(); - pres = evaluator.evaluate(user, route.name(), actionNames); + PrivilegesEvaluatorResponse pres = evaluator.evaluate(user, route.name(), actionNames); if (log.isDebugEnabled()) { log.debug(pres.toString()); diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index c9e8f4a78a..339c08fd75 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -32,7 +32,7 @@ import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.authtoken.jwt.claims.OBOJwtClaimsBuilder; -import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -53,21 +53,22 @@ public class SecurityTokenManager implements TokenManager { private final ClusterService cs; private final ThreadPool threadPool; private final UserService userService; + private final RoleMapper roleMapper; private Settings oboSettings = null; - private ConfigModel configModel = null; private final LongSupplier timeProvider = System::currentTimeMillis; private static final Integer OBO_MAX_EXPIRY_SECONDS = 600; - public SecurityTokenManager(final ClusterService cs, final ThreadPool threadPool, final UserService userService) { + public SecurityTokenManager( + final ClusterService cs, + final ThreadPool threadPool, + final UserService userService, + RoleMapper roleMapper + ) { this.cs = cs; this.threadPool = threadPool; this.userService = userService; - } - - @Subscribe - public void onConfigModelChanged(final ConfigModel configModel) { - this.configModel = configModel; + this.roleMapper = roleMapper; } @Subscribe @@ -90,7 +91,7 @@ JwtVendor createJwtVendor(final Settings settings) { } public boolean issueOnBehalfOfTokenAllowed() { - return oboSettings != null && configModel != null; + return oboSettings != null; } @Override @@ -117,7 +118,7 @@ public ExpiringBearerAuthToken issueOnBehalfOfToken(final Subject subject, final } final TransportAddress callerAddress = null; /* OBO tokens must not roles based on location from network address */ - final Set mappedRoles = configModel.mapSecurityRoles(user, callerAddress); + final Set mappedRoles = roleMapper.map(user, callerAddress); final long currentTimeMs = timeProvider.getAsLong(); final Date now = new Date(currentTimeMs); diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 9ee104246f..d47d9be4c1 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -13,7 +13,7 @@ import java.util.Set; -import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; /** * Defines the general interface for evaluating privileges on actions. References to ActionPrivileges instances @@ -77,9 +77,18 @@ public interface ActionPrivileges { PrivilegesEvaluatorResponse hasIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices + OptionallyResolvedIndices resolvedIndices ); + /** + * Checks whether this instance provides privileges for the provided actions on any possible index. + *

+ * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + *

+ * If no privileges are available, this method will return PrivilegeEvaluatorResponse.insufficient() + */ + PrivilegesEvaluatorResponse hasIndexPrivilegeForAnyIndex(PrivilegesEvaluationContext context, Set actions); + /** * Checks whether this instance provides explicit privileges for the combination of the provided action, * the provided indices and the provided roles. @@ -90,7 +99,7 @@ PrivilegesEvaluatorResponse hasIndexPrivilege( PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices + OptionallyResolvedIndices resolvedIndices ); ActionPrivileges EMPTY = new ActionPrivileges() { @@ -113,16 +122,21 @@ public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluat public PrivilegesEvaluatorResponse hasIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices + OptionallyResolvedIndices resolvedIndices ) { return PrivilegesEvaluatorResponse.insufficient("all of " + actions).reason("User has no privileges"); } + @Override + public PrivilegesEvaluatorResponse hasIndexPrivilegeForAnyIndex(PrivilegesEvaluationContext context, Set actions) { + return PrivilegesEvaluatorResponse.insufficient("all of " + actions).reason("User has no privileges"); + } + @Override public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices + OptionallyResolvedIndices resolvedIndices ) { return PrivilegesEvaluatorResponse.insufficient("all of " + actions).reason("User has no privileges"); } diff --git a/src/main/java/org/opensearch/security/privileges/ConfigurableRoleMapper.java b/src/main/java/org/opensearch/security/privileges/ConfigurableRoleMapper.java new file mode 100644 index 0000000000..14ab7ffa42 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/ConfigurableRoleMapper.java @@ -0,0 +1,249 @@ +/* + * 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.privileges; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ListMultimap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; +import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.HostResolverMode; +import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.User; + +/** + * A RoleMapper implementation that automatically picks up changes from the role mapping configuration in the configuration index. + */ +public class ConfigurableRoleMapper implements RoleMapper { + private final static Logger log = LogManager.getLogger(ConfigurableRoleMapper.class); + + private final AtomicReference activeConfiguration = new AtomicReference<>(); + + public ConfigurableRoleMapper(ConfigurationRepository configurationRepository, ResolutionMode resolutionMode) { + if (configurationRepository != null) { + configurationRepository.subscribeOnChange(configMap -> { + HostResolverMode hostResolverMode = getHostResolverMode(configurationRepository.getConfiguration(CType.CONFIG)); + SecurityDynamicConfiguration rawRoleMappingConfiguration = configurationRepository.getConfiguration( + CType.ROLESMAPPING + ); + if (rawRoleMappingConfiguration == null) { + rawRoleMappingConfiguration = SecurityDynamicConfiguration.empty(CType.ROLESMAPPING); + } + + this.activeConfiguration.set(new CompiledConfiguration(rawRoleMappingConfiguration, hostResolverMode, resolutionMode)); + }); + } + } + + public ConfigurableRoleMapper(ConfigurationRepository configurationRepository, Settings settings) { + this(configurationRepository, ResolutionMode.fromSettings(settings)); + } + + @Override + public ImmutableSet map(User user, TransportAddress caller) { + CompiledConfiguration activeConfiguration = this.activeConfiguration.get(); + + if (activeConfiguration != null) { + return activeConfiguration.map(user, caller); + } else { + return ImmutableSet.of(); + } + } + + /** + * Determines which roles are used in the final set of effective roles returned by the map() method. + * + * The setting is sourced from the plugins.secutiry.roles_mapping_resolution setting. + */ + enum ResolutionMode { + /** + * Include only the target roles from the role mapping configuration. + */ + MAPPING_ONLY, + + /** + * Include only the backend roles. This effectively disables the role mapping process. + */ + BACKENDROLES_ONLY, + + /** + * Include the union of the target roles and the source backend roles. + */ + BOTH; + + static ResolutionMode fromSettings(Settings settings) { + + try { + return ResolutionMode.valueOf( + settings.get(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, ResolutionMode.MAPPING_ONLY.toString()).toUpperCase() + ); + } catch (Exception e) { + log.error("Cannot apply roles mapping resolution", e); + return ResolutionMode.MAPPING_ONLY; + } + } + } + + private static HostResolverMode getHostResolverMode(SecurityDynamicConfiguration configConfig) { + final HostResolverMode defaultValue = HostResolverMode.IP_HOSTNAME; + + if (configConfig == null) { + return defaultValue; + } + + ConfigV7 config = configConfig.getCEntry(CType.CONFIG.name()); + if (config == null || config.dynamic == null) { + return defaultValue; + } + return HostResolverMode.fromConfig(config.dynamic.hosts_resolver_mode); + } + + /** + * Moved from https://github.com/opensearch-project/security/blob/d29095f26dba1a26308c69b608dc926bd40c0f52/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java + */ + static class CompiledConfiguration implements RoleMapper { + + private final ResolutionMode resolutionMode; + private final HostResolverMode hostResolverMode; + + private ListMultimap users; + private ListMultimap, String> abars; + private ListMultimap bars; + private ListMultimap hosts; + + private List userMatchers; + private List barMatchers; + private List hostMatchers; + + private CompiledConfiguration( + SecurityDynamicConfiguration rolemappings, + HostResolverMode hostResolverMode, + ResolutionMode resolutionMode + ) { + + this.hostResolverMode = hostResolverMode; + this.resolutionMode = resolutionMode; + + users = ArrayListMultimap.create(); + abars = ArrayListMultimap.create(); + bars = ArrayListMultimap.create(); + hosts = ArrayListMultimap.create(); + + for (final Map.Entry roleMap : rolemappings.getCEntries().entrySet()) { + final String roleMapKey = roleMap.getKey(); + final RoleMappingsV7 roleMapValue = roleMap.getValue(); + + for (String u : roleMapValue.getUsers()) { + users.put(u, roleMapKey); + } + + final Set abar = new HashSet<>(roleMapValue.getAnd_backend_roles()); + + if (!abar.isEmpty()) { + abars.put(WildcardMatcher.matchers(abar), roleMapKey); + } + + for (String bar : roleMapValue.getBackend_roles()) { + bars.put(bar, roleMapKey); + } + + for (String host : roleMapValue.getHosts()) { + hosts.put(host, roleMapKey); + } + } + + userMatchers = WildcardMatcher.matchers(users.keySet()); + barMatchers = WildcardMatcher.matchers(bars.keySet()); + hostMatchers = WildcardMatcher.matchers(hosts.keySet()); + + } + + @Override + public ImmutableSet map(final User user, final TransportAddress caller) { + + if (user == null) { + return ImmutableSet.of(); + } + + ImmutableSet.Builder result = ImmutableSet.builderWithExpectedSize( + user.getSecurityRoles().size() + user.getRoles().size() + ); + + result.addAll(user.getSecurityRoles()); + + if (resolutionMode == ResolutionMode.BOTH || resolutionMode == ResolutionMode.BACKENDROLES_ONLY) { + result.addAll(user.getRoles()); + } + + if (((resolutionMode == ResolutionMode.BOTH || resolutionMode == ResolutionMode.MAPPING_ONLY))) { + + for (String p : WildcardMatcher.getAllMatchingPatterns(userMatchers, user.getName())) { + result.addAll(users.get(p)); + } + for (String p : WildcardMatcher.getAllMatchingPatterns(barMatchers, user.getRoles())) { + result.addAll(bars.get(p)); + } + + for (List patterns : abars.keySet()) { + if (patterns.stream().allMatch(p -> p.matchAny(user.getRoles()))) { + result.addAll(abars.get(patterns)); + } + } + + if (caller != null) { + // IPV4 or IPv6 (compressed and without scope identifiers) + final String ipAddress = caller.getAddress(); + + for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, ipAddress)) { + result.addAll(hosts.get(p)); + } + + if (caller.address() != null + && (hostResolverMode == HostResolverMode.IP_HOSTNAME || hostResolverMode == HostResolverMode.IP_HOSTNAME_LOOKUP)) { + final String hostName = caller.address().getHostString(); + + for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, hostName)) { + result.addAll(hosts.get(p)); + } + } + + if (caller.address() != null && hostResolverMode == HostResolverMode.IP_HOSTNAME_LOOKUP) { + + final String resolvedHostName = caller.address().getHostName(); + + for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, resolvedHostName)) { + result.addAll(hosts.get(p)); + } + } + } + } + + return result.build(); + } + } + +} diff --git a/src/main/java/org/opensearch/security/privileges/DashboardsMultiTenancyConfiguration.java b/src/main/java/org/opensearch/security/privileges/DashboardsMultiTenancyConfiguration.java new file mode 100644 index 0000000000..ebd3306478 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/DashboardsMultiTenancyConfiguration.java @@ -0,0 +1,72 @@ +/* + * 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.privileges; + +import org.opensearch.security.securityconf.impl.v7.ConfigV7; + +public class DashboardsMultiTenancyConfiguration { + public static final DashboardsMultiTenancyConfiguration DEFAULT = new DashboardsMultiTenancyConfiguration(new ConfigV7.Kibana()); + + private final boolean multitenancyEnabled; + private final boolean privateTenantEnabled; + private final String defaultTenant; + private final String index; + private final String serverUsername; + private final String role; + + public DashboardsMultiTenancyConfiguration(ConfigV7.Kibana dashboardsConfig) { + this.multitenancyEnabled = dashboardsConfig.multitenancy_enabled; + this.privateTenantEnabled = dashboardsConfig.private_tenant_enabled; + this.defaultTenant = dashboardsConfig.default_tenant; + this.index = dashboardsConfig.index; + this.serverUsername = dashboardsConfig.server_username; + this.role = dashboardsConfig.opendistro_role; + } + + public DashboardsMultiTenancyConfiguration(ConfigV7 generalConfig) { + this(dashboardsConfig(generalConfig)); + } + + public boolean multitenancyEnabled() { + return multitenancyEnabled; + } + + public boolean privateTenantEnabled() { + return privateTenantEnabled; + } + + public String dashboardsDefaultTenant() { + return defaultTenant; + } + + public String dashboardsIndex() { + return index; + } + + public String dashboardsServerUsername() { + return serverUsername; + } + + public String dashboardsOpenSearchRole() { + return role; + } + + private static ConfigV7.Kibana dashboardsConfig(ConfigV7 generalConfig) { + if (generalConfig != null && generalConfig.dynamic != null && generalConfig.dynamic.kibana != null) { + return generalConfig.dynamic.kibana; + } else { + // Fallback to defaults + return new ConfigV7.Kibana(); + } + } + +} diff --git a/src/main/java/org/opensearch/security/privileges/DocumentAllowList.java b/src/main/java/org/opensearch/security/privileges/DocumentAllowList.java index 6e41857737..e5518eb63d 100644 --- a/src/main/java/org/opensearch/security/privileges/DocumentAllowList.java +++ b/src/main/java/org/opensearch/security/privileges/DocumentAllowList.java @@ -17,6 +17,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.get.GetRequest; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.support.ConfigConstants; @@ -45,6 +47,37 @@ public static DocumentAllowList get(ThreadContext threadContext) { } } + public static boolean isAllowed(ActionRequest request, ThreadContext threadContext) { + String docAllowListHeader = threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER); + + if (docAllowListHeader == null) { + return false; + } + + if (!(request instanceof GetRequest)) { + return false; + } + + try { + DocumentAllowList documentAllowList = DocumentAllowList.parse(docAllowListHeader); + GetRequest getRequest = (GetRequest) request; + + if (documentAllowList.isAllowed(getRequest.index(), getRequest.id())) { + if (log.isDebugEnabled()) { + log.debug("Request " + request + " is allowed by " + documentAllowList); + } + + return true; + } else { + return false; + } + + } catch (Exception e) { + log.error("Error while handling document allow list: " + docAllowListHeader, e); + return false; + } + } + private static final DocumentAllowList EMPTY = new DocumentAllowList(); private final Set entries = new HashSet<>(); diff --git a/src/main/java/org/opensearch/security/privileges/IndexPattern.java b/src/main/java/org/opensearch/security/privileges/IndexPattern.java index 014af99e54..df296df0ea 100644 --- a/src/main/java/org/opensearch/security/privileges/IndexPattern.java +++ b/src/main/java/org/opensearch/security/privileges/IndexPattern.java @@ -21,6 +21,7 @@ import org.apache.logging.log4j.Logger; import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.security.support.WildcardMatcher; @@ -35,7 +36,7 @@ public class IndexPattern { /** * An IndexPattern which does not match any index. */ - public static final IndexPattern EMPTY = new IndexPattern(WildcardMatcher.NONE, ImmutableList.of(), ImmutableList.of()); + public static final IndexPattern EMPTY = new IndexPattern(WildcardMatcher.NONE, ImmutableList.of(), ImmutableList.of(), false); /** * Plain index patterns without any dynamic expressions like user attributes and date math. @@ -52,18 +53,78 @@ public class IndexPattern { * Index patterns which contain date math (like ) */ private final ImmutableList dateMathExpressions; + + /** + * If this is true, this pattern will also match an alias or data stream if it actually matches ALL child indices of + * of the alias or data stream. + */ + private final boolean memberIndexPrivilegesYieldAliasPrivileges; + private final int hashCode; - private IndexPattern(WildcardMatcher staticPattern, ImmutableList patternTemplates, ImmutableList dateMathExpressions) { + private IndexPattern( + WildcardMatcher staticPattern, + ImmutableList patternTemplates, + ImmutableList dateMathExpressions, + boolean memberIndexPrivilegesYieldAliasPrivileges + ) { this.staticPattern = staticPattern; this.patternTemplates = patternTemplates; this.dateMathExpressions = dateMathExpressions; this.hashCode = staticPattern.hashCode() + patternTemplates.hashCode() + dateMathExpressions.hashCode(); + this.memberIndexPrivilegesYieldAliasPrivileges = memberIndexPrivilegesYieldAliasPrivileges; + } + + public boolean matches( + String indexOrAliasOrDatastream, + PrivilegesEvaluationContext context, + Map indexMetadata + ) throws PrivilegesEvaluationException { + + if (matchesDirectly(indexOrAliasOrDatastream, context)) { + return true; + } + + IndexAbstraction indexAbstraction = indexMetadata.get(indexOrAliasOrDatastream); + + if (indexAbstraction instanceof IndexAbstraction.Index) { + // Check for the privilege for aliases or data streams containing this index + + if (indexAbstraction.getParentDataStream() != null) { + if (matchesDirectly(indexAbstraction.getParentDataStream().getName(), context)) { + return true; + } + } + + // Retrieve aliases: The use of getWriteIndex() is a bit messy, but it is the only way to access + // alias metadata from here. + for (String alias : indexAbstraction.getWriteIndex().getAliases().keySet()) { + if (matchesDirectly(alias, context)) { + return true; + } + } + + return false; + } else if (this.memberIndexPrivilegesYieldAliasPrivileges + && (indexAbstraction instanceof IndexAbstraction.Alias || indexAbstraction instanceof IndexAbstraction.DataStream)) { + // We have a data stream or alias: If we have no match so far, let's also check whether we have privileges for all members. + + for (IndexMetadata memberIndex : indexAbstraction.getIndices()) { + if (!matchesDirectly(memberIndex.getIndex().getName(), context)) { + return false; + } + } + + // If we could match all members, we have a match + return true; + } else { + return false; + } } - public boolean matches(String index, PrivilegesEvaluationContext context, Map indexMetadata) + private boolean matchesDirectly(String indexOrAliasOrDatastream, PrivilegesEvaluationContext context) throws PrivilegesEvaluationException { - if (staticPattern != WildcardMatcher.NONE && staticPattern.test(index)) { + if (staticPattern != WildcardMatcher.NONE && staticPattern.test(indexOrAliasOrDatastream)) { return true; } @@ -72,7 +133,7 @@ public boolean matches(String index, PrivilegesEvaluationContext context, Map constantPatterns = new ArrayList<>(); private List patternTemplates = new ArrayList<>(); private List dateMathExpressions = new ArrayList<>(); + private boolean memberIndexPrivilegesYieldAliasPrivileges; + + public Builder(boolean memberIndexPrivilegesYieldAliasPrivileges) { + this.memberIndexPrivilegesYieldAliasPrivileges = memberIndexPrivilegesYieldAliasPrivileges; + } public void add(List source) { for (int i = 0; i < source.size(); i++) { @@ -236,18 +287,22 @@ public IndexPattern build() { return new IndexPattern( constantPatterns.size() != 0 ? WildcardMatcher.from(constantPatterns) : WildcardMatcher.NONE, ImmutableList.copyOf(patternTemplates), - ImmutableList.copyOf(dateMathExpressions) + ImmutableList.copyOf(dateMathExpressions), + this.memberIndexPrivilegesYieldAliasPrivileges ); } } - public static IndexPattern from(List source) { - Builder builder = new Builder(); + public static IndexPattern from(List source, boolean memberIndexPrivilegesYieldAliasPrivileges) { + Builder builder = new Builder(memberIndexPrivilegesYieldAliasPrivileges); builder.add(source); return builder.build(); } - public static IndexPattern from(String... source) { - return from(Arrays.asList(source)); + /** + * Only for testing. + */ + static IndexPattern from(String... source) { + return from(Arrays.asList(source), true); } } diff --git a/src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java b/src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java new file mode 100644 index 0000000000..1b6ac780fd --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java @@ -0,0 +1,80 @@ +/* + * 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.privileges; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.IndicesRequest; +import org.opensearch.action.admin.indices.segments.PitSegmentsRequest; +import org.opensearch.cluster.metadata.ResolvedIndices; + +/** + * Provides methods to modify the local indices of an IndicesRequest. All methods use the ResolvedIndices metadata object + * to make sure that remote indices are properly retained. + *

+ * We need the distinction between local indices and remote indices because authorization on remote indices is performed + * on the remote cluster - thus, we can leave them here just as they are. + */ +public class IndicesRequestModifier { + + public boolean setLocalIndices(ActionRequest targetRequest, ResolvedIndices resolvedIndices, Collection newIndices) { + if (newIndices.isEmpty()) { + return setLocalIndicesToEmpty(targetRequest, resolvedIndices); + } + + if (targetRequest instanceof PitSegmentsRequest) { + // PitSegmentsRequest implements IndicesRequest.Replaceable, but ignores all specified indices + return false; + } else if (targetRequest instanceof IndicesRequest.Replaceable) { + ((IndicesRequest.Replaceable) targetRequest).indices(concat(newIndices, resolvedIndices.remote().asRawExpressions())); + return true; + } else { + return false; + } + } + + public boolean setLocalIndicesToEmpty(ActionRequest targetRequest, ResolvedIndices resolvedIndices) { + if (targetRequest instanceof PitSegmentsRequest) { + // PitSegmentsRequest implements IndicesRequest.Replaceable, but ignores all specified indices + return false; + } else if (targetRequest instanceof IndicesRequest.Replaceable replaceable) { + if (resolvedIndices.remote().isEmpty()) { + if (replaceable.indicesOptions().expandWildcardsOpen() || replaceable.indicesOptions().expandWildcardsClosed()) { + // If the request expands wildcards, we use an index expression which resolves to no indices + replaceable.indices(".none*,-*"); + return true; + } else if (replaceable.indicesOptions().allowNoIndices()) { + // If the request does not expand wildcards, we use a index name that cannot exist. + replaceable.indices("-.none*"); + return true; + } else { + // In this case, we cannot perform replacement. But it also won't be necessary due to the + // semantics of the feature + return false; + } + } else { + // If we have remote indices, things get much easier + replaceable.indices(resolvedIndices.remote().asRawExpressionsArray()); + return true; + } + } else { + return false; + } + } + + private String[] concat(Collection local, List remote) { + return Stream.concat(local.stream(), remote.stream()).toArray(String[]::new); + } + +} diff --git a/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java b/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java new file mode 100644 index 0000000000..971b9b7a8c --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java @@ -0,0 +1,63 @@ +/* + * 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.privileges; + +import java.util.function.Supplier; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.IndicesRequest; +import org.opensearch.action.support.ActionRequestMetadata; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; + +/** + * Provides a thin wrapper around ActionRequestMetadata.resolveIndices(), adding a fallback mechanism in case the + * particular action does not support it. + */ +public class IndicesRequestResolver { + protected final IndexNameExpressionResolver indexNameExpressionResolver; + + public IndicesRequestResolver(IndexNameExpressionResolver indexNameExpressionResolver) { + this.indexNameExpressionResolver = indexNameExpressionResolver; + } + + public OptionallyResolvedIndices resolve( + ActionRequest request, + ActionRequestMetadata actionRequestMetadata, + Supplier clusterStateSupplier + ) { + OptionallyResolvedIndices providedIndices = actionRequestMetadata.resolvedIndices(); + if (providedIndices instanceof ResolvedIndices) { + return providedIndices; + } else { + // The action does not implement the resolution mechanism; we have to do it by ourselves + return resolveFallback(request, clusterStateSupplier.get()); + } + } + + public OptionallyResolvedIndices resolve( + ActionRequest request, + ActionRequestMetadata actionRequestMetadata, + PrivilegesEvaluationContext context + ) { + return resolve(request, actionRequestMetadata, context::clusterState); + } + + private OptionallyResolvedIndices resolveFallback(ActionRequest request, ClusterState clusterState) { + if (request instanceof IndicesRequest indicesRequest) { + return ResolvedIndices.of(this.indexNameExpressionResolver.concreteResolvedIndices(clusterState, indicesRequest)); + } else { + return ResolvedIndices.unknown(); + } + } +} diff --git a/src/main/java/org/opensearch/security/privileges/PitPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PitPrivilegesEvaluator.java deleted file mode 100644 index 4fd4141b08..0000000000 --- a/src/main/java/org/opensearch/security/privileges/PitPrivilegesEvaluator.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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.privileges; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import com.google.common.collect.ImmutableSet; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.admin.indices.segments.PitSegmentsRequest; -import org.opensearch.action.search.CreatePitRequest; -import org.opensearch.action.search.DeletePitRequest; -import org.opensearch.common.unit.TimeValue; -import org.opensearch.security.OpenSearchSecurityPlugin; -import org.opensearch.security.resolver.IndexResolverReplacer; - -/** - * This class evaluates privileges for point in time (Delete and List all) operations. - * For aliases - users must have either alias permission or backing index permissions - * For data streams - users must have access to backing indices permission + data streams permission. - */ -public class PitPrivilegesEvaluator { - - public PrivilegesEvaluatorResponse evaluate( - final ActionRequest request, - final PrivilegesEvaluationContext context, - final ActionPrivileges actionPrivileges, - final String action, - final PrivilegesEvaluatorResponse presponse, - final IndexResolverReplacer irr - ) { - - if (!(request instanceof DeletePitRequest || request instanceof PitSegmentsRequest)) { - return presponse; - } - List pitIds = new ArrayList<>(); - - if (request instanceof DeletePitRequest) { - DeletePitRequest deletePitRequest = (DeletePitRequest) request; - pitIds = deletePitRequest.getPitIds(); - } else if (request instanceof PitSegmentsRequest) { - PitSegmentsRequest pitSegmentsRequest = (PitSegmentsRequest) request; - pitIds = pitSegmentsRequest.getPitIds(); - } - // if request is for all PIT IDs, skip custom pit ids evaluation - if (pitIds.size() == 1 && "_all".equals(pitIds.get(0))) { - return presponse; - } else { - return handlePitsAccess(pitIds, context, actionPrivileges, action, presponse, irr); - } - } - - /** - * Handle access for delete operation / pit segments operation where PIT IDs are explicitly passed - */ - private PrivilegesEvaluatorResponse handlePitsAccess( - List pitIds, - PrivilegesEvaluationContext context, - ActionPrivileges actionPrivileges, - final String action, - PrivilegesEvaluatorResponse presponse, - final IndexResolverReplacer irr - ) { - Map pitToIndicesMap = OpenSearchSecurityPlugin.GuiceHolder.getPitService().getIndicesForPits(pitIds); - Set pitIndices = new HashSet<>(); - // add indices across all PITs to a set and evaluate if user has access to all indices - for (String[] indices : pitToIndicesMap.values()) { - pitIndices.addAll(Arrays.asList(indices)); - } - String[] indicesArr = new String[pitIndices.size()]; - CreatePitRequest req = new CreatePitRequest(new TimeValue(1, TimeUnit.DAYS), true, pitIndices.toArray(indicesArr)); - final IndexResolverReplacer.Resolved pitResolved = irr.resolveRequest(req); - PrivilegesEvaluatorResponse subResponse = actionPrivileges.hasIndexPrivilege(context, ImmutableSet.of(action), pitResolved); - // Only if user has access to all PIT's indices, allow operation, otherwise continue evaluation in PrivilegesEvaluator. - if (subResponse.isAllowed()) { - presponse.allowed = true; - presponse.markComplete(); - } - - return presponse; - } -} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java b/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java new file mode 100644 index 0000000000..be47961efd --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java @@ -0,0 +1,284 @@ +/* + * 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.privileges; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.configuration.PrivilegesInterceptorImpl; +import org.opensearch.security.privileges.actionlevel.RuntimeOptimizedActionPrivileges; +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.ActionGroupsV7; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; +import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.securityconf.impl.v7.TenantV7; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.client.Client; + +/** + * This class manages and gives access to various additional classes which are derived from privileges related configuration in + * the security plugin. + *

+ * This is especially: + *

    + *
  • The current PrivilegesEvaluator instance
  • + *
  • The current Dashboards multi tenancy configuration
  • + *
  • The current action groups configuration
  • + *
+ * This class also manages updates to the different configuration objects. + *

+ * Historically, most of this information has been located directly in PrivilegesEvaluator instances. To concentrate + * the purpose of PrivilegesEvaluator to just action based privilege evaluation, the information was distributed amongst + * several classes. + */ +public class PrivilegesConfiguration { + private final static Logger log = LogManager.getLogger(PrivilegesConfiguration.class); + + private final AtomicReference tenantPrivileges = new AtomicReference<>(TenantPrivileges.EMPTY); + private final AtomicReference privilegesEvaluator; + private final AtomicReference actionGroups = new AtomicReference<>(FlattenedActionGroups.EMPTY); + private final Map pluginIdToRolePrivileges = new HashMap<>(); + private final AtomicReference multiTenancyConfiguration = new AtomicReference<>( + DashboardsMultiTenancyConfiguration.DEFAULT + ); + private final PrivilegesInterceptorImpl privilegesInterceptor; + private final SpecialIndices specialIndices; + + /** + * The pure static action groups should be ONLY used by action privileges for plugins; only those cannot and should + * not have knowledge of any action groups defined in the dynamic configuration. All other functionality should + * use the action groups derived from the dynamic configuration (which is always computed on the fly on + * configuration updates). + */ + private final FlattenedActionGroups staticActionGroups; + + public PrivilegesConfiguration( + ConfigurationRepository configurationRepository, + ClusterService clusterService, + Supplier clusterStateSupplier, + Client client, + RoleMapper roleMapper, + ThreadPool threadPool, + IndexNameExpressionResolver resolver, + AuditLog auditLog, + Settings settings, + Supplier unavailablityReasonSupplier + ) { + + this.privilegesEvaluator = new AtomicReference<>(new PrivilegesEvaluator.NotInitialized(unavailablityReasonSupplier)); + this.privilegesInterceptor = new PrivilegesInterceptorImpl( + resolver, + clusterService, + client, + threadPool, + this.tenantPrivileges::get, + this.multiTenancyConfiguration::get + ); + this.staticActionGroups = buildStaticActionGroups(); + this.specialIndices = new SpecialIndices(settings); + + if (configurationRepository != null) { + configurationRepository.subscribeOnChange(configMap -> { + SecurityDynamicConfiguration actionGroupsConfiguration = configurationRepository.getConfiguration( + CType.ACTIONGROUPS + ); + SecurityDynamicConfiguration rolesConfiguration = configurationRepository.getConfiguration(CType.ROLES) + .withStaticConfig(); + SecurityDynamicConfiguration tenantConfiguration = configurationRepository.getConfiguration(CType.TENANTS) + .withStaticConfig(); + ConfigV7 generalConfiguration = configurationRepository.getConfiguration(CType.CONFIG).getCEntry(CType.CONFIG.name()); + + FlattenedActionGroups flattenedActionGroups = new FlattenedActionGroups(actionGroupsConfiguration.withStaticConfig()); + this.actionGroups.set(flattenedActionGroups); + + PrivilegesEvaluator currentPrivilegesEvaluator = privilegesEvaluator.get(); + PrivilegesEvaluationType privilegesEvaluationType = PrivilegesEvaluationType.getFrom( + configurationRepository.getConfiguration(CType.CONFIG) + ); + PrivilegesEvaluationType currentEvaluationType = PrivilegesEvaluationType.typeOf(currentPrivilegesEvaluator); + + if (privilegesEvaluationType != currentEvaluationType) { + if (privilegesEvaluationType == PrivilegesEvaluationType.LEGACY) { + PrivilegesEvaluator oldInstance = privilegesEvaluator.getAndSet( + new org.opensearch.security.privileges.actionlevel.legacy.PrivilegesEvaluator( + clusterService, + clusterStateSupplier, + roleMapper, + threadPool, + threadPool.getThreadContext(), + resolver, + auditLog, + settings, + privilegesInterceptor, + flattenedActionGroups, + staticActionGroups, + rolesConfiguration, + generalConfiguration, + pluginIdToRolePrivileges + ) + ); + if (oldInstance != null) { + oldInstance.shutdown(); + } + } else { + PrivilegesEvaluator oldInstance = privilegesEvaluator.getAndSet( + new org.opensearch.security.privileges.actionlevel.nextgen.PrivilegesEvaluator( + clusterStateSupplier, + roleMapper, + threadPool, + threadPool.getThreadContext(), + resolver, + settings, + privilegesInterceptor, + flattenedActionGroups, + staticActionGroups, + rolesConfiguration, + generalConfiguration, + pluginIdToRolePrivileges, + new RuntimeOptimizedActionPrivileges.SpecialIndexProtection( + this.specialIndices::isUniversallyDeniedIndex, + this.specialIndices::isSystemIndex + ) + ) + ); + if (oldInstance != null) { + oldInstance.shutdown(); + } + } + } else { + privilegesEvaluator.get().updateConfiguration(flattenedActionGroups, rolesConfiguration, generalConfiguration); + } + + try { + this.multiTenancyConfiguration.set(new DashboardsMultiTenancyConfiguration(generalConfiguration)); + } catch (Exception e) { + log.error("Error while updating DashboardsMultiTenancyConfiguration", e); + } + + try { + this.tenantPrivileges.set(new TenantPrivileges(rolesConfiguration, tenantConfiguration, flattenedActionGroups)); + } catch (Exception e) { + log.error("Error while updating TenantPrivileges", e); + } + }); + } + + if (clusterService != null) { + clusterService.addListener(event -> { this.privilegesEvaluator.get().updateClusterStateMetadata(clusterService); }); + } + } + + /** + * For testing only: Creates a passive PrivilegesConfiguration object with the given PrivilegesEvaluator implementation and otherwise + * just defaults. + */ + public PrivilegesConfiguration(PrivilegesEvaluator privilegesEvaluator) { + this.privilegesEvaluator = new AtomicReference<>(privilegesEvaluator); + this.privilegesInterceptor = null; + this.staticActionGroups = buildStaticActionGroups(); + this.specialIndices = new SpecialIndices(Settings.EMPTY); + } + + /** + * Returns the current tenant privileges object. Important: Do not store the references to the instances returned here; these will change + * after configuration updates. + */ + public TenantPrivileges tenantPrivileges() { + return this.tenantPrivileges.get(); + } + + /** + * Returns the current PrivilegesEvaluator implementation. Important: Do not store the references to the instances returned here; these will change + * after configuration updates. + */ + public PrivilegesEvaluator privilegesEvaluator() { + return this.privilegesEvaluator.get(); + } + + /** + * Returns the current action groups configuration. Important: Do not store the references to the instances returned here; these will change + * after configuration updates. + */ + public FlattenedActionGroups actionGroups() { + return this.actionGroups.get(); + } + + /** + * Returns the current Dashboards multi tenancy configuration. Important: Do not store the references to the instances returned here; these will change + * after configuration updates. + */ + public DashboardsMultiTenancyConfiguration multiTenancyConfiguration() { + return this.multiTenancyConfiguration.get(); + } + + public void updatePluginToActionPrivileges(String pluginIdentifier, RoleV7 pluginPermissions) { + pluginIdToRolePrivileges.put(pluginIdentifier, pluginPermissions); + } + + public boolean isInitialized() { + return this.privilegesEvaluator().isInitialized(); + } + + /** + * TODO: Think about better names + */ + enum PrivilegesEvaluationType { + LEGACY, + NEXT_GEN; + + static PrivilegesEvaluationType getFrom(SecurityDynamicConfiguration configConfig) { + final PrivilegesEvaluationType defaultValue = PrivilegesEvaluationType.LEGACY; + + if (configConfig == null) { + return defaultValue; + } + + ConfigV7 config = configConfig.getCEntry(CType.CONFIG.name()); + if (config == null || config.dynamic == null) { + return defaultValue; + } + if (NEXT_GEN.name().equalsIgnoreCase(config.dynamic.privilegesEvaluationType)) { + return NEXT_GEN; + } else { + return LEGACY; + } + } + + static PrivilegesEvaluationType typeOf(PrivilegesEvaluator privilegesEvaluator) { + if (privilegesEvaluator instanceof org.opensearch.security.privileges.actionlevel.legacy.PrivilegesEvaluator) { + return PrivilegesEvaluationType.LEGACY; + } else if (privilegesEvaluator instanceof org.opensearch.security.privileges.actionlevel.nextgen.PrivilegesEvaluator) { + return PrivilegesEvaluationType.NEXT_GEN; + } else { + return null; + } + } + } + + private static FlattenedActionGroups buildStaticActionGroups() { + return new FlattenedActionGroups(DynamicConfigFactory.addStatics(SecurityDynamicConfiguration.empty(CType.ACTIONGROUPS))); + } + +} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index b4cc2fe805..a475a26f4c 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -17,10 +17,11 @@ import com.google.common.collect.ImmutableSet; import org.opensearch.action.ActionRequest; +import org.opensearch.action.support.ActionRequestMetadata; import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; @@ -38,13 +39,14 @@ public class PrivilegesEvaluationContext { private final User user; private final String action; private final ActionRequest request; - private IndexResolverReplacer.Resolved resolvedRequest; + private OptionallyResolvedIndices resolvedIndices; private Map indicesLookup; private final Task task; private ImmutableSet mappedRoles; - private final IndexResolverReplacer indexResolverReplacer; private final IndexNameExpressionResolver indexNameExpressionResolver; + private final IndicesRequestResolver indicesRequestResolver; private final Supplier clusterStateSupplier; + private final ActionRequestMetadata actionRequestMetadata; /** * Stores the ActionPrivileges instance to be used for this request. Plugin system users or users created from @@ -64,9 +66,10 @@ public PrivilegesEvaluationContext( ImmutableSet mappedRoles, String action, ActionRequest request, + ActionRequestMetadata actionRequestMetadata, Task task, - IndexResolverReplacer indexResolverReplacer, IndexNameExpressionResolver indexNameExpressionResolver, + IndicesRequestResolver indicesRequestResolver, Supplier clusterStateSupplier, ActionPrivileges actionPrivileges ) { @@ -75,9 +78,10 @@ public PrivilegesEvaluationContext( this.action = action; this.request = request; this.clusterStateSupplier = clusterStateSupplier; - this.indexResolverReplacer = indexResolverReplacer; this.indexNameExpressionResolver = indexNameExpressionResolver; + this.indicesRequestResolver = indicesRequestResolver; this.task = task; + this.actionRequestMetadata = actionRequestMetadata; this.actionPrivileges = actionPrivileges; } @@ -118,12 +122,14 @@ public ActionRequest getRequest() { return request; } - public IndexResolverReplacer.Resolved getResolvedRequest() { - IndexResolverReplacer.Resolved result = this.resolvedRequest; - + public OptionallyResolvedIndices getResolvedRequest() { + OptionallyResolvedIndices result = this.resolvedIndices; if (result == null) { - result = indexResolverReplacer.resolveRequest(request); - this.resolvedRequest = result; + this.resolvedIndices = result = this.indicesRequestResolver.resolve( + this.request, + this.actionRequestMetadata, + this.clusterStateSupplier + ); } return result; @@ -137,20 +143,8 @@ public ImmutableSet getMappedRoles() { return mappedRoles; } - /** - * Note: Ideally, mappedRoles would be an unmodifiable attribute. PrivilegesEvaluator however contains logic - * related to OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION which first validates roles and afterwards modifies - * them again. Thus, we need to be able to set this attribute. - * - * However, this method should be only used for this one particular phase. Normally, all roles should be determined - * upfront and stay constant during the whole privilege evaluation process. - */ - void setMappedRoles(ImmutableSet mappedRoles) { - this.mappedRoles = mappedRoles; - } - - public Supplier getClusterStateSupplier() { - return clusterStateSupplier; + public ClusterState clusterState() { + return clusterStateSupplier.get(); } public Map getIndicesLookup() { @@ -182,8 +176,8 @@ public String toString() { + '\'' + ", request=" + request - + ", resolvedRequest=" - + resolvedRequest + + ", resolvedIndices=" + + resolvedIndices + ", mappedRoles=" + mappedRoles + '}'; diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 5747e3bb4f..8b1f5568f9 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -26,911 +26,128 @@ package org.opensearch.security.privileges; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.StringJoiner; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import org.opensearch.OpenSearchSecurityException; import org.opensearch.action.ActionRequest; -import org.opensearch.action.IndicesRequest; -import org.opensearch.action.admin.cluster.shards.ClusterSearchShardsRequest; -import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; -import org.opensearch.action.admin.indices.alias.IndicesAliasesAction; -import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; -import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; -import org.opensearch.action.admin.indices.create.AutoCreateAction; -import org.opensearch.action.admin.indices.create.CreateIndexAction; -import org.opensearch.action.admin.indices.create.CreateIndexRequest; -import org.opensearch.action.admin.indices.delete.DeleteIndexAction; -import org.opensearch.action.admin.indices.mapping.get.GetFieldMappingsRequest; -import org.opensearch.action.admin.indices.mapping.put.AutoPutMappingAction; -import org.opensearch.action.admin.indices.mapping.put.PutMappingAction; -import org.opensearch.action.bulk.BulkAction; -import org.opensearch.action.bulk.BulkItemRequest; -import org.opensearch.action.bulk.BulkRequest; -import org.opensearch.action.bulk.BulkShardRequest; -import org.opensearch.action.delete.DeleteAction; -import org.opensearch.action.get.GetRequest; -import org.opensearch.action.get.MultiGetAction; -import org.opensearch.action.index.IndexAction; -import org.opensearch.action.search.MultiSearchAction; -import org.opensearch.action.search.SearchAction; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchScrollAction; -import org.opensearch.action.support.IndicesOptions; -import org.opensearch.action.termvectors.MultiTermVectorsAction; -import org.opensearch.action.update.UpdateAction; -import org.opensearch.cluster.ClusterState; -import org.opensearch.cluster.metadata.AliasMetadata; -import org.opensearch.cluster.metadata.IndexMetadata; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.action.support.ActionRequestMetadata; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.settings.ClusterSettings; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.core.common.Strings; -import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.index.reindex.ReindexAction; -import org.opensearch.script.mustache.RenderSearchTemplateAction; -import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.configuration.ClusterInfoHolder; -import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; -import org.opensearch.security.privileges.actionlevel.SubjectBasedActionPrivileges; -import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; -import org.opensearch.security.securityconf.ConfigModel; -import org.opensearch.security.securityconf.DynamicConfigFactory; -import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.core.rest.RestStatus; import org.opensearch.security.securityconf.FlattenedActionGroups; -import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.security.securityconf.impl.DashboardSignInOption; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; -import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; -import org.opensearch.security.securityconf.impl.v7.TenantV7; -import org.opensearch.security.support.Base64Helper; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.support.SecuritySettings; -import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; -import org.opensearch.threadpool.ThreadPool; - -import org.greenrobot.eventbus.Subscribe; -import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; -import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; -import static org.opensearch.security.support.ConfigConstants.USER_ATTRIBUTE_SERIALIZATION_ENABLED; -import static org.opensearch.security.support.ConfigConstants.USER_ATTRIBUTE_SERIALIZATION_ENABLED_DEFAULT; -import static org.opensearch.security.support.SecurityUtils.escapePipe; +public interface PrivilegesEvaluator { -public class PrivilegesEvaluator { - - private static final String USER_TENANT = "__user__"; - private static final String GLOBAL_TENANT = "global_tenant"; - private static final String READ_ACCESS = "READ"; - private static final String WRITE_ACCESS = "WRITE"; - private static final String NO_ACCESS = "NONE"; + default PrivilegesEvaluationContext createContext(User user, String action) { + return createContext(user, action, null, ActionRequestMetadata.empty(), null); + } - static final WildcardMatcher DNFOF_MATCHER = WildcardMatcher.from( - ImmutableList.of( - "indices:data/read/*", - "indices:admin/mappings/fields/get*", - "indices:admin/shards/search_shards", - "indices:admin/resolve/index", - "indices:monitor/settings/get", - "indices:monitor/stats", - "indices:admin/aliases/get" - ) + PrivilegesEvaluationContext createContext( + User user, + String action, + ActionRequest actionRequest, + ActionRequestMetadata actionRequestMetadata, + Task task ); - private static final WildcardMatcher ACTION_MATCHER = WildcardMatcher.from("indices:data/read/*search*"); - - private static final IndicesOptions ALLOW_EMPTY = IndicesOptions.fromOptions(true, true, false, false); - - protected final Logger log = LogManager.getLogger(this.getClass()); - private final Supplier clusterStateSupplier; - - private final IndexNameExpressionResolver resolver; - - private final AuditLog auditLog; - private ThreadContext threadContext; - - private PrivilegesInterceptor privilegesInterceptor; - - private final boolean checkSnapshotRestoreWritePrivileges; - private boolean isUserAttributeSerializationEnabled; - - private final ClusterInfoHolder clusterInfoHolder; - private final ConfigurationRepository configurationRepository; - private ConfigModel configModel; - private final IndexResolverReplacer irr; - private final SnapshotRestoreEvaluator snapshotRestoreEvaluator; - private final SystemIndexAccessEvaluator systemIndexAccessEvaluator; - private final ProtectedIndexAccessEvaluator protectedIndexAccessEvaluator; - private final TermsAggregationEvaluator termsAggregationEvaluator; - private final PitPrivilegesEvaluator pitPrivilegesEvaluator; - private DynamicConfigModel dcm; - private final Settings settings; - private final AtomicReference actionPrivileges = new AtomicReference<>(); - private final AtomicReference tenantPrivileges = new AtomicReference<>(); - private final Map pluginIdToActionPrivileges = new HashMap<>(); - - /** - * The pure static action groups should be ONLY used by action privileges for plugins; only those cannot and should - * not have knowledge of any action groups defined in the dynamic configuration. All other functionality should - * use the action groups derived from the dynamic configuration (which is always computed on the fly on - * configuration updates). - */ - private final FlattenedActionGroups staticActionGroups; - - public PrivilegesEvaluator( - final ClusterService clusterService, - Supplier clusterStateSupplier, - ThreadPool threadPool, - final ThreadContext threadContext, - final ConfigurationRepository configurationRepository, - final IndexNameExpressionResolver resolver, - AuditLog auditLog, - final Settings settings, - final PrivilegesInterceptor privilegesInterceptor, - final ClusterInfoHolder clusterInfoHolder, - final IndexResolverReplacer irr - ) { - - super(); - this.resolver = resolver; - this.auditLog = auditLog; - - this.threadContext = threadContext; - this.privilegesInterceptor = privilegesInterceptor; - this.clusterStateSupplier = clusterStateSupplier; - this.settings = settings; - - this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean( - ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, - ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES - ); - this.isUserAttributeSerializationEnabled = settings.getAsBoolean( - USER_ATTRIBUTE_SERIALIZATION_ENABLED, - USER_ATTRIBUTE_SERIALIZATION_ENABLED_DEFAULT - ); - - this.clusterInfoHolder = clusterInfoHolder; - this.irr = irr; - snapshotRestoreEvaluator = new SnapshotRestoreEvaluator(settings, auditLog); - systemIndexAccessEvaluator = new SystemIndexAccessEvaluator(settings, auditLog, irr); - protectedIndexAccessEvaluator = new ProtectedIndexAccessEvaluator(settings, auditLog); - termsAggregationEvaluator = new TermsAggregationEvaluator(); - pitPrivilegesEvaluator = new PitPrivilegesEvaluator(); - this.configurationRepository = configurationRepository; - this.staticActionGroups = new FlattenedActionGroups( - DynamicConfigFactory.addStatics(SecurityDynamicConfiguration.empty(CType.ACTIONGROUPS)) - ); - - if (configurationRepository != null) { - configurationRepository.subscribeOnChange(configMap -> { - SecurityDynamicConfiguration actionGroupsConfiguration = configurationRepository.getConfiguration( - CType.ACTIONGROUPS - ); - SecurityDynamicConfiguration rolesConfiguration = configurationRepository.getConfiguration(CType.ROLES); - SecurityDynamicConfiguration tenantConfiguration = configurationRepository.getConfiguration(CType.TENANTS); + PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context); - this.updateConfiguration(actionGroupsConfiguration, rolesConfiguration, tenantConfiguration); - }); - } - - if (clusterService != null) { - clusterService.addListener(event -> { - RoleBasedActionPrivileges actionPrivileges = PrivilegesEvaluator.this.actionPrivileges.get(); - if (actionPrivileges != null) { - actionPrivileges.clusterStateMetadataDependentPrivileges().updateClusterStateMetadataAsync(clusterService, threadPool); - } - }); - - this.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); - } - } + boolean isClusterPermission(String action); void updateConfiguration( - SecurityDynamicConfiguration actionGroupsConfiguration, + FlattenedActionGroups actionGroups, SecurityDynamicConfiguration rolesConfiguration, - SecurityDynamicConfiguration tenantConfiguration - ) { - FlattenedActionGroups flattenedActionGroups = new FlattenedActionGroups(actionGroupsConfiguration.withStaticConfig()); - rolesConfiguration = rolesConfiguration.withStaticConfig(); - tenantConfiguration = tenantConfiguration.withStaticConfig(); - try { - RoleBasedActionPrivileges actionPrivileges = new RoleBasedActionPrivileges(rolesConfiguration, flattenedActionGroups, settings); - Metadata metadata = clusterStateSupplier.get().metadata(); - actionPrivileges.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); - RoleBasedActionPrivileges oldInstance = this.actionPrivileges.getAndSet(actionPrivileges); - - if (oldInstance != null) { - oldInstance.clusterStateMetadataDependentPrivileges().shutdown(); - } - } catch (Exception e) { - log.error("Error while updating ActionPrivileges", e); - } - - try { - this.tenantPrivileges.set(new TenantPrivileges(rolesConfiguration, tenantConfiguration, flattenedActionGroups)); - } catch (Exception e) { - log.error("Error while updating TenantPrivileges", e); - } - } - - @Subscribe - public void onConfigModelChanged(ConfigModel configModel) { - this.configModel = configModel; - } - - @Subscribe - public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { - this.dcm = dcm; - } - - public boolean hasRestAdminPermissions(final User user, final TransportAddress remoteAddress, final String permission) { - PrivilegesEvaluationContext context = createContext(user, permission); - return context.getActionPrivileges().hasExplicitClusterPrivilege(context, permission).isAllowed(); - } - - public boolean isInitialized() { - return configModel != null && dcm != null && actionPrivileges.get() != null; - } - - public void registerClusterSettingsChangeListener(final ClusterSettings clusterSettings) { - clusterSettings.addSettingsUpdateConsumer( - SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING, - newIsUserAttributeSerializationEnabled -> { - isUserAttributeSerializationEnabled = newIsUserAttributeSerializationEnabled; - } - ); - } - - private boolean isUserAttributeSerializationEnabled() { - return isUserAttributeSerializationEnabled; - } - - private void setUserInfoInThreadContext(PrivilegesEvaluationContext context) { - if (threadContext.getTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT) == null) { - StringJoiner joiner = new StringJoiner("|"); - // Escape any pipe characters in the values before joining - joiner.add(escapePipe(context.getUser().getName())); - joiner.add(escapePipe(String.join(",", context.getUser().getRoles()))); - joiner.add(escapePipe(String.join(",", context.getMappedRoles()))); - - String requestedTenant = context.getUser().getRequestedTenant(); - joiner.add(requestedTenant); - - String tenantAccessToCheck = getTenancyAccess(context); - joiner.add(tenantAccessToCheck); - log.debug(joiner); - - if (this.isUserAttributeSerializationEnabled()) { - joiner.add(Base64Helper.serializeObject(new HashMap<>(context.getUser().getCustomAttributesMap()))); - } - - threadContext.putTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT, joiner.toString()); - } - } - - public PrivilegesEvaluationContext createContext(User user, String action) { - return createContext(user, action, null, null, null); - } - - private String getTenancyAccess(PrivilegesEvaluationContext context) { - String requestedTenant = context.getUser().getRequestedTenant(); - final String tenant = Strings.isNullOrEmpty(requestedTenant) ? GLOBAL_TENANT : requestedTenant; - if (tenantPrivileges.get().hasTenantPrivilege(context, tenant, TenantPrivileges.ActionType.WRITE)) { - return WRITE_ACCESS; - } else if (tenantPrivileges.get().hasTenantPrivilege(context, tenant, TenantPrivileges.ActionType.READ)) { - return READ_ACCESS; - } else { - return NO_ACCESS; - } - } - - public PrivilegesEvaluationContext createContext( - User user, - String action0, - ActionRequest request, - Task task, - Set injectedRoles - ) { - if (!isInitialized()) { - StringBuilder error = new StringBuilder("OpenSearch Security is not initialized."); - if (!clusterInfoHolder.hasClusterManager()) { - error.append(String.format(" %s", ClusterInfoHolder.CLUSTER_MANAGER_NOT_PRESENT)); - } - throw new OpenSearchSecurityException(error.toString()); - } - - TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - - ActionPrivileges actionPrivileges; - ImmutableSet mappedRoles; - - if (user.isPluginUser()) { - mappedRoles = ImmutableSet.of(); - actionPrivileges = this.pluginIdToActionPrivileges.get(user.getName()); - if (actionPrivileges == null) { - actionPrivileges = ActionPrivileges.EMPTY; - } - } else { - mappedRoles = ImmutableSet.copyOf((injectedRoles == null) ? mapRoles(user, caller) : injectedRoles); - actionPrivileges = this.actionPrivileges.get(); - } - - return new PrivilegesEvaluationContext( - user, - mappedRoles, - action0, - request, - task, - irr, - resolver, - clusterStateSupplier, - actionPrivileges - ); - } - - public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { - - if (!isInitialized()) { - StringBuilder error = new StringBuilder("OpenSearch Security is not initialized."); - if (!clusterInfoHolder.hasClusterManager()) { - error.append(String.format(" %s", ClusterInfoHolder.CLUSTER_MANAGER_NOT_PRESENT)); - } - throw new OpenSearchSecurityException(error.toString()); - } - - String action0 = context.getAction(); - ImmutableSet mappedRoles = context.getMappedRoles(); - User user = context.getUser(); - ActionRequest request = context.getRequest(); - Task task = context.getTask(); - - if (action0.startsWith("internal:indices/admin/upgrade")) { - action0 = "indices:admin/upgrade"; - } - - if (AutoCreateAction.NAME.equals(action0)) { - action0 = CreateIndexAction.NAME; - } - - if (AutoPutMappingAction.NAME.equals(action0)) { - action0 = PutMappingAction.NAME; - } - - PrivilegesEvaluatorResponse presponse = new PrivilegesEvaluatorResponse(); - - final String injectedRolesValidationString = threadContext.getTransient( - ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION - ); - if (injectedRolesValidationString != null) { - HashSet injectedRolesValidationSet = new HashSet<>(Arrays.asList(injectedRolesValidationString.split(","))); - if (!mappedRoles.containsAll(injectedRolesValidationSet)) { - presponse.allowed = false; - presponse.missingSecurityRoles.addAll(injectedRolesValidationSet); - log.info("Roles {} are not mapped to the user {}", injectedRolesValidationSet, user); - return presponse; - } - mappedRoles = ImmutableSet.copyOf(injectedRolesValidationSet); - context.setMappedRoles(mappedRoles); - } - - setUserInfoInThreadContext(context); - - final boolean isDebugEnabled = log.isDebugEnabled(); - if (isDebugEnabled) { - log.debug("Evaluate permissions for {}", user); - log.debug("Action: {} ({})", action0, request.getClass().getSimpleName()); - log.debug("Mapped roles: {}", mappedRoles.toString()); - } - - ActionPrivileges actionPrivileges = context.getActionPrivileges(); - if (actionPrivileges == null) { - throw new OpenSearchSecurityException("OpenSearch Security is not initialized: roles configuration is missing"); - } - - if (request instanceof BulkRequest && (Strings.isNullOrEmpty(user.getRequestedTenant()))) { - // Shortcut for bulk actions. The details are checked on the lower level of the BulkShardRequests (Action - // indices:data/write/bulk[s]). - // This shortcut is only possible if the default tenant is selected, as we might need to rewrite the request for non-default - // tenants. - // No further access check for the default tenant is necessary, as access will be also checked on the TransportShardBulkAction - // level. - - presponse = actionPrivileges.hasClusterPrivilege(context, action0); - - if (!presponse.allowed) { - log.info( - "No cluster-level perm match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", - user, - action0, - mappedRoles, - presponse.getMissingPrivileges() - ); - } - return presponse; - } - - final Resolved requestedResolved = context.getResolvedRequest(); - - if (isDebugEnabled) { - log.debug("RequestedResolved : {}", requestedResolved); - } - - // check snapshot/restore requests - // NOTE: Has to go first as restore request could be for protected and/or system indices and the request may - // fail with 403 if system index or protected index evaluators are triggered first - if (snapshotRestoreEvaluator.evaluate(request, task, action0, clusterInfoHolder, presponse).isComplete()) { - return presponse; - } - - // System index access - if (systemIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, context, actionPrivileges, user) - .isComplete()) { - return presponse; - } - - // Protected index access - if (protectedIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, mappedRoles).isComplete()) { - return presponse; - } - - // check access for point in time requests - if (pitPrivilegesEvaluator.evaluate(request, context, actionPrivileges, action0, presponse, irr).isComplete()) { - return presponse; - } - - final boolean dnfofEnabled = dcm.isDnfofEnabled(); - - final boolean isTraceEnabled = log.isTraceEnabled(); - if (isTraceEnabled) { - log.trace("dnfof enabled? {}", dnfofEnabled); - } - - final boolean serviceAccountUser = user.isServiceAccount(); - if (isClusterPerm(action0)) { - if (serviceAccountUser) { - log.info("{} is a service account which doesn't have access to cluster level permission: {}", user, action0); - return PrivilegesEvaluatorResponse.insufficient(action0); - } - - presponse = actionPrivileges.hasClusterPrivilege(context, action0); - - if (!presponse.allowed) { - log.info( - "No cluster-level perm match for {} {} [Action [{}]] [RolesChecked {}]. No permissions for {}", - user, - requestedResolved, - action0, - mappedRoles, - presponse.getMissingPrivileges() - ); - return presponse; - } else { - - if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { - if (isDebugEnabled) { - log.debug("Normally allowed but we need to apply some extra checks for a restore request."); - } - } else { - if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { - - final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( - request, - action0, - user, - dcm, - requestedResolved, - context, - this.tenantPrivileges.get() - ); - - if (isDebugEnabled) { - log.debug("Result from privileges interceptor for cluster perm: {}", replaceResult); - } - - if (!replaceResult.continueEvaluation) { - if (replaceResult.accessDenied) { - auditLog.logMissingPrivileges(action0, request, task); - } else { - presponse.allowed = true; - presponse.createIndexRequestBuilder = replaceResult.createIndexRequestBuilder; - } - return presponse; - } - } - - if (isDebugEnabled) { - log.debug("Allowed because we have cluster permissions for {}", action0); - } - presponse.allowed = true; - return presponse; - } - } - } - - if (checkDocAllowListHeader(user, action0, request)) { - presponse.allowed = true; - return presponse; - } - - // term aggregations - if (termsAggregationEvaluator.evaluate(requestedResolved, request, context, actionPrivileges, presponse).isComplete()) { - return presponse; - } - - ImmutableSet allIndexPermsRequired = evaluateAdditionalIndexPermissions(request, action0); - - if (isDebugEnabled) { - log.debug( - "Requested {} from {}", - allIndexPermsRequired, - threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS) - ); - } - - if (isDebugEnabled) { - log.debug("Requested resolved index types: {}", requestedResolved); - log.debug("Security roles: {}", mappedRoles); - } - - // TODO exclude Security index - - if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { - - final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( - request, - action0, - user, - dcm, - requestedResolved, - context, - this.tenantPrivileges.get() - ); - - if (isDebugEnabled) { - log.debug("Result from privileges interceptor: {}", replaceResult); - } - - if (!replaceResult.continueEvaluation) { - if (replaceResult.accessDenied) { - auditLog.logMissingPrivileges(action0, request, task); - return PrivilegesEvaluatorResponse.insufficient(action0); - } else { - presponse.allowed = true; - presponse.createIndexRequestBuilder = replaceResult.createIndexRequestBuilder; - return presponse; - } - } - } - - boolean dnfofPossible = dnfofEnabled && DNFOF_MATCHER.test(action0); - - presponse = actionPrivileges.hasIndexPrivilege(context, allIndexPermsRequired, requestedResolved); - - if (presponse.isPartiallyOk()) { - if (dnfofPossible) { - if (irr.replace(request, true, presponse.getAvailableIndices())) { - return PrivilegesEvaluatorResponse.ok(); - } - } - } else if (!presponse.isAllowed()) { - if (dnfofPossible && dcm.isDnfofForEmptyResultsEnabled() && request instanceof IndicesRequest.Replaceable) { - ((IndicesRequest.Replaceable) request).indices(new String[0]); - - if (request instanceof SearchRequest) { - ((SearchRequest) request).indicesOptions(ALLOW_EMPTY); - } else if (request instanceof ClusterSearchShardsRequest) { - ((ClusterSearchShardsRequest) request).indicesOptions(ALLOW_EMPTY); - } else if (request instanceof GetFieldMappingsRequest) { - ((GetFieldMappingsRequest) request).indicesOptions(ALLOW_EMPTY); - } - - return PrivilegesEvaluatorResponse.ok(); - } - } - - if (presponse.isAllowed()) { - if (checkFilteredAliases(requestedResolved, action0, isDebugEnabled)) { - presponse.allowed = false; - return presponse; - } - - if (isDebugEnabled) { - log.debug("Allowed because we have all indices permissions for {}", action0); - } - } else { - log.info( - "No {}-level perm match for {} {}: {} [Action [{}]] [RolesChecked {}]", - "index", - user, - requestedResolved, - presponse.getReason(), - action0, - mappedRoles - ); - log.info("Index to privilege matrix:\n{}", presponse.getPrivilegeMatrix()); - if (presponse.hasEvaluationExceptions()) { - log.info("Evaluation errors:\n{}", presponse.getEvaluationExceptionInfo()); - } - } - - return presponse; - } - - public Set mapRoles(final User user, final TransportAddress caller) { - return this.configModel.mapSecurityRoles(user, caller); - } - - public TenantPrivileges tenantPrivileges() { - return this.tenantPrivileges.get(); - } - - public boolean multitenancyEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsMultitenancyEnabled(); - } - - public boolean privateTenantEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsPrivateTenantEnabled(); - } - - public String dashboardsDefaultTenant() { - return dcm.getDashboardsDefaultTenant(); - } - - public boolean notFailOnForbiddenEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDnfofEnabled(); - } - - public String dashboardsIndex() { - return dcm.getDashboardsIndexname(); - } + ConfigV7 generalConfiguration + ); - public String dashboardsServerUsername() { - return dcm.getDashboardsServerUsername(); - } + void updateClusterStateMetadata(ClusterService clusterService); - public String dashboardsOpenSearchRole() { - return dcm.getDashboardsOpenSearchRole(); - } + /** + * Shuts down any background processes or other resources that need an explicit shut down + */ + void shutdown(); - public List getSignInOptions() { - return dcm.getSignInOptions(); - } + boolean notFailOnForbiddenEnabled(); - private ImmutableSet evaluateAdditionalIndexPermissions(final ActionRequest request, final String originalAction) { - ImmutableSet.Builder additionalPermissionsRequired = ImmutableSet.builder(); + boolean isInitialized(); - if (!isClusterPerm(originalAction)) { - additionalPermissionsRequired.add(originalAction); - } + /** + * A PrivilegesEvaluator implementation that just throws "not initialized" exceptions. + * Used initially by PrivilegesConfiguration. + */ + class NotInitialized implements PrivilegesEvaluator { + private final Supplier unavailablityReasonSupplier; - if (request instanceof ClusterSearchShardsRequest) { - additionalPermissionsRequired.add(SearchAction.NAME); + NotInitialized(Supplier unavailablityReasonSupplier) { + this.unavailablityReasonSupplier = unavailablityReasonSupplier; } - if (request instanceof BulkShardRequest) { - BulkShardRequest bsr = (BulkShardRequest) request; - for (BulkItemRequest bir : bsr.items()) { - switch (bir.request().opType()) { - case CREATE: - additionalPermissionsRequired.add(IndexAction.NAME); - break; - case INDEX: - additionalPermissionsRequired.add(IndexAction.NAME); - break; - case DELETE: - additionalPermissionsRequired.add(DeleteAction.NAME); - break; - case UPDATE: - additionalPermissionsRequired.add(UpdateAction.NAME); - break; - } - } + @Override + public PrivilegesEvaluationContext createContext( + User user, + String action, + ActionRequest actionRequest, + ActionRequestMetadata actionRequestMetadata, + Task task + ) { + throw exception(); } - if (request instanceof IndicesAliasesRequest) { - IndicesAliasesRequest bsr = (IndicesAliasesRequest) request; - for (AliasActions bir : bsr.getAliasActions()) { - switch (bir.actionType()) { - case REMOVE_INDEX: - additionalPermissionsRequired.add(DeleteIndexAction.NAME); - break; - default: - break; - } - } + @Override + public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { + throw exception(); } - if (request instanceof CreateIndexRequest) { - CreateIndexRequest cir = (CreateIndexRequest) request; - if (cir.aliases() != null && !cir.aliases().isEmpty()) { - additionalPermissionsRequired.add(IndicesAliasesAction.NAME); - } - } - - if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { - additionalPermissionsRequired.addAll(ConfigConstants.SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES); + @Override + public boolean isClusterPermission(String action) { + return false; } - ImmutableSet result = additionalPermissionsRequired.build(); + @Override + public void updateConfiguration( + FlattenedActionGroups actionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration + ) { - if (result.size() > 1) { - traceAction("Additional permissions required: {}", result); } - if (log.isDebugEnabled() && result.size() > 1) { - log.debug("Additional permissions required: {}", result); - } + @Override + public void updateClusterStateMetadata(ClusterService clusterService) { - return result; - } - - public static boolean isClusterPerm(String action0) { - return (action0.startsWith("cluster:") - || action0.startsWith("indices:admin/template/") - || action0.startsWith("indices:admin/index_template/") - || action0.startsWith(SearchScrollAction.NAME) - || (action0.equals(BulkAction.NAME)) - || (action0.equals(MultiGetAction.NAME)) - || (action0.startsWith(MultiSearchAction.NAME)) - || (action0.equals(MultiTermVectorsAction.NAME)) - || (action0.equals(ReindexAction.NAME)) - || (action0.equals(RenderSearchTemplateAction.NAME))); - } - - @SuppressWarnings("unchecked") - private boolean checkFilteredAliases(Resolved requestedResolved, String action, boolean isDebugEnabled) { - final String faMode = dcm.getFilteredAliasMode();// getConfigSettings().dynamic.filtered_alias_mode; - - if (!"disallow".equals(faMode)) { - return false; - } - - if (!ACTION_MATCHER.test(action)) { - return false; } - Iterable indexMetaDataCollection; + @Override + public void shutdown() { - if (requestedResolved.isLocalAll()) { - indexMetaDataCollection = new Iterable() { - @Override - public Iterator iterator() { - return clusterStateSupplier.get().getMetadata().getIndices().values().iterator(); - } - }; - } else { - Set indexMetaDataSet = new HashSet<>(requestedResolved.getAllIndices().size()); - - for (String requestAliasOrIndex : requestedResolved.getAllIndices()) { - IndexMetadata indexMetaData = clusterStateSupplier.get().getMetadata().getIndices().get(requestAliasOrIndex); - if (indexMetaData == null) { - if (isDebugEnabled) { - log.debug("{} does not exist in cluster metadata", requestAliasOrIndex); - } - continue; - } - - indexMetaDataSet.add(indexMetaData); - } - - indexMetaDataCollection = indexMetaDataSet; } - // check filtered aliases - for (IndexMetadata indexMetaData : indexMetaDataCollection) { - - final List filteredAliases = new ArrayList(); - - final Map aliases = indexMetaData.getAliases(); - - if (aliases != null && aliases.size() > 0) { - if (isDebugEnabled) { - log.debug("Aliases for {}: {}", indexMetaData.getIndex().getName(), aliases); - } - - final Iterator it = aliases.keySet().iterator(); - while (it.hasNext()) { - final String alias = it.next(); - final AliasMetadata aliasMetadata = aliases.get(alias); - - if (aliasMetadata != null && aliasMetadata.filteringRequired()) { - filteredAliases.add(aliasMetadata); - if (isDebugEnabled) { - log.debug("{} is a filtered alias {}", alias, aliasMetadata.getFilter()); - } - } else { - if (isDebugEnabled) { - log.debug("{} is not an alias or does not have a filter", alias); - } - } - } - } - - if (filteredAliases.size() > 1 && ACTION_MATCHER.test(action)) { - // TODO add queries as dls queries (works only if dls module is installed) - log.error( - "More than one ({}) filtered alias found for same index ({}). This is currently not supported. Aliases: {}", - filteredAliases.size(), - indexMetaData.getIndex().getName(), - toString(filteredAliases) - ); - return true; - } - } // end-for - - return false; - } - private boolean checkDocAllowListHeader(User user, String action, ActionRequest request) { - String docAllowListHeader = threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER); - - if (docAllowListHeader == null) { + @Override + public boolean notFailOnForbiddenEnabled() { return false; } - if (!(request instanceof GetRequest)) { + @Override + public boolean isInitialized() { return false; } - try { - DocumentAllowList documentAllowList = DocumentAllowList.parse(docAllowListHeader); - GetRequest getRequest = (GetRequest) request; - - if (documentAllowList.isAllowed(getRequest.index(), getRequest.id())) { - if (log.isDebugEnabled()) { - log.debug("Request " + request + " is allowed by " + documentAllowList); - } + private OpenSearchSecurityException exception() { + StringBuilder error = new StringBuilder("OpenSearch Security not initialized"); + String reason = this.unavailablityReasonSupplier.get(); - return true; + if (reason != null) { + error.append(": ").append(reason); } else { - return false; + error.append("."); } - } catch (Exception e) { - log.error("Error while handling document allow list: " + docAllowListHeader, e); - return false; + return new OpenSearchSecurityException(error.toString(), RestStatus.SERVICE_UNAVAILABLE); } - } - - private List toString(List aliases) { - if (aliases == null || aliases.size() == 0) { - return Collections.emptyList(); - } - - final List ret = new ArrayList<>(aliases.size()); + }; - for (final AliasMetadata amd : aliases) { - if (amd != null) { - ret.add(amd.alias()); - } - } - - return Collections.unmodifiableList(ret); - } - - public void updatePluginToActionPrivileges(String pluginIdentifier, RoleV7 pluginPermissions) { - pluginIdToActionPrivileges.put(pluginIdentifier, new SubjectBasedActionPrivileges(pluginPermissions, staticActionGroups)); - } } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java index d072ec301c..d069a7caf0 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java @@ -31,8 +31,10 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import org.opensearch.action.admin.indices.create.CreateIndexRequestBuilder; @@ -42,12 +44,13 @@ public class PrivilegesEvaluatorResponse { boolean allowed = false; Set missingSecurityRoles = new HashSet<>(); - PrivilegesEvaluatorResponseState state = PrivilegesEvaluatorResponseState.PENDING; CreateIndexRequestBuilder createIndexRequestBuilder; private Set onlyAllowedForIndices = ImmutableSet.of(); private CheckTable indexToActionCheckTable; + private ImmutableList subResults = ImmutableList.of(); private String privilegeMatrix; private String reason; + private PrivilegesEvaluatorResponse originalResult; /** * Contains issues that were encountered during privilege evaluation. Can be used for logging. @@ -101,7 +104,13 @@ public PrivilegesEvaluatorResponse reason(String reason) { * Returns a diagnostic string that contains issues that were encountered during privilege evaluation. Can be used for logging. */ public String getEvaluationExceptionInfo() { - StringBuilder result = new StringBuilder("Exceptions encountered during privilege evaluation:\n"); + if (this.evaluationExceptions.isEmpty()) { + return "No errors"; + } + + StringBuilder result = new StringBuilder( + this.evaluationExceptions.size() == 1 ? "One error:\n" : this.evaluationExceptions.size() + " errors:\n" + ); for (PrivilegesEvaluationException evaluationException : this.evaluationExceptions) { result.append(evaluationException.getNestedMessages()).append("\n"); @@ -127,7 +136,24 @@ public String getPrivilegeMatrix() { String result = this.privilegeMatrix; if (result == null) { - result = this.indexToActionCheckTable.toTableString("ok", "MISSING"); + String topLevelMatrix; + + if (this.indexToActionCheckTable != null) { + topLevelMatrix = this.indexToActionCheckTable.toTableString("ok", "MISSING"); + } else { + topLevelMatrix = "n/a"; + } + + if (subResults.isEmpty()) { + result = topLevelMatrix; + } else { + StringBuilder resultBuilder = new StringBuilder(topLevelMatrix); + for (PrivilegesEvaluatorResponse subResult : subResults) { + resultBuilder.append("\n"); + resultBuilder.append(subResult.getPrivilegeMatrix()); + } + result = resultBuilder.toString(); + } this.privilegeMatrix = result; } return result; @@ -141,22 +167,41 @@ public CreateIndexRequestBuilder getCreateIndexRequestBuilder() { return createIndexRequestBuilder; } - public PrivilegesEvaluatorResponse markComplete() { - this.state = PrivilegesEvaluatorResponseState.COMPLETE; - return this; + public PrivilegesEvaluatorResponse originalResult() { + return this.originalResult; } - public PrivilegesEvaluatorResponse markPending() { - this.state = PrivilegesEvaluatorResponseState.PENDING; - return this; + public boolean privilegesAreComplete() { + if (originalResult != null && !originalResult.privilegesAreComplete()) { + return false; + } else if (indexToActionCheckTable != null && !indexToActionCheckTable.isComplete()) { + return false; + } else if (!subResults.isEmpty() && subResults.stream().anyMatch(subResult -> !subResult.privilegesAreComplete())) { + return false; + } else { + return this.allowed; + } } - public boolean isComplete() { - return this.state == PrivilegesEvaluatorResponseState.COMPLETE; + public PrivilegesEvaluatorResponse insufficient(List subResults) { + String reason = this.reason; + if (reason == null) { + reason = subResults.stream().map(result -> result.reason).filter(Objects::nonNull).findFirst().orElse(null); + } + PrivilegesEvaluatorResponse result = new PrivilegesEvaluatorResponse(); + result.allowed = false; + result.indexToActionCheckTable = this.indexToActionCheckTable; + result.subResults = ImmutableList.copyOf(subResults); + result.reason = reason; + return result; } - public boolean isPending() { - return this.state == PrivilegesEvaluatorResponseState.PENDING; + public PrivilegesEvaluatorResponse originalResult(PrivilegesEvaluatorResponse originalResult) { + if (originalResult != null && !originalResult.evaluationExceptions.isEmpty()) { + this.originalResult = originalResult; + this.evaluationExceptions.addAll(originalResult.evaluationExceptions); + } + return this; } @Override @@ -176,6 +221,20 @@ public static PrivilegesEvaluatorResponse ok() { return response; } + public static PrivilegesEvaluatorResponse ok(CheckTable indexToActionCheckTable) { + PrivilegesEvaluatorResponse response = new PrivilegesEvaluatorResponse(); + response.indexToActionCheckTable = indexToActionCheckTable; + response.allowed = true; + return response; + } + + public static PrivilegesEvaluatorResponse ok(CreateIndexRequestBuilder createIndexRequestBuilder) { + PrivilegesEvaluatorResponse response = new PrivilegesEvaluatorResponse(); + response.allowed = true; + response.createIndexRequestBuilder = createIndexRequestBuilder; + return response; + } + public static PrivilegesEvaluatorResponse partiallyOk( Set availableIndices, CheckTable indexToActionCheckTable diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java index 0ae809bc9d..9486fdff3d 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java @@ -31,8 +31,6 @@ import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; -import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; @@ -40,15 +38,27 @@ public class PrivilegesInterceptor { public static class ReplaceResult { - final boolean continueEvaluation; - final boolean accessDenied; - final CreateIndexRequestBuilder createIndexRequestBuilder; + public final boolean continueEvaluation; + public final boolean accessDenied; + public final CreateIndexRequestBuilder createIndexRequestBuilder; private ReplaceResult(boolean continueEvaluation, boolean accessDenied, CreateIndexRequestBuilder createIndexRequestBuilder) { this.continueEvaluation = continueEvaluation; this.accessDenied = accessDenied; this.createIndexRequestBuilder = createIndexRequestBuilder; } + + @Override + public String toString() { + return "ReplaceResult{" + + "continueEvaluation=" + + continueEvaluation + + ", accessDenied=" + + accessDenied + + ", createIndexRequestBuilder=" + + createIndexRequestBuilder + + '}'; + } } public static final ReplaceResult CONTINUE_EVALUATION_REPLACE_RESULT = new ReplaceResult(true, false, null); @@ -80,10 +90,7 @@ public ReplaceResult replaceDashboardsIndex( final ActionRequest request, final String action, final User user, - final DynamicConfigModel config, - final Resolved requestedResolved, - final PrivilegesEvaluationContext context, - final TenantPrivileges tenantPrivileges + final PrivilegesEvaluationContext context ) { throw new RuntimeException("not implemented"); } diff --git a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java index b83e174600..4d3e2eb4c4 100644 --- a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java @@ -78,21 +78,22 @@ public void evaluateAsync( final String action, final ActionListener pResponseListener ) { - PrivilegesEvaluatorResponse pResponse = new PrivilegesEvaluatorResponse(); - log.debug("Evaluating resource access"); // if it reached this evaluator, it is safe to assume that the request if of DocRequest type DocRequest req = (DocRequest) request; - resourceAccessHandler.hasPermission(req.id(), req.type(), action, ActionListener.wrap(hasAccess -> { - if (hasAccess) { - pResponse.allowed = true; - pResponseListener.onResponse(pResponse.markComplete()); - return; - } - pResponseListener.onResponse(PrivilegesEvaluatorResponse.insufficient(action).markComplete()); - }, e -> { pResponseListener.onResponse(pResponse.markComplete()); })); + resourceAccessHandler.hasPermission( + req.id(), + req.type(), + action, + ActionListener.wrap( + hasAccess -> pResponseListener.onResponse( + hasAccess ? PrivilegesEvaluatorResponse.ok() : PrivilegesEvaluatorResponse.insufficient(action) + ), + pResponseListener::onFailure + ) + ); } /** diff --git a/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java index 5274ad3456..4890dd5a89 100644 --- a/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java @@ -20,14 +20,14 @@ public class RestLayerPrivilegesEvaluator { protected final Logger log = LogManager.getLogger(this.getClass()); - private final PrivilegesEvaluator privilegesEvaluator; + private final PrivilegesConfiguration privilegesConfiguration; - public RestLayerPrivilegesEvaluator(PrivilegesEvaluator privilegesEvaluator) { - this.privilegesEvaluator = privilegesEvaluator; + public RestLayerPrivilegesEvaluator(PrivilegesConfiguration privilegesConfiguration) { + this.privilegesConfiguration = privilegesConfiguration; } public PrivilegesEvaluatorResponse evaluate(final User user, final String routeName, final Set actions) { - PrivilegesEvaluationContext context = privilegesEvaluator.createContext(user, routeName); + PrivilegesEvaluationContext context = privilegesConfiguration.privilegesEvaluator().createContext(user, routeName); final boolean isDebugEnabled = log.isDebugEnabled(); if (isDebugEnabled) { diff --git a/src/main/java/org/opensearch/security/privileges/RoleMapper.java b/src/main/java/org/opensearch/security/privileges/RoleMapper.java new file mode 100644 index 0000000000..5a5011968f --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/RoleMapper.java @@ -0,0 +1,25 @@ +/* + * 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.privileges; + +import com.google.common.collect.ImmutableSet; + +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.security.user.User; + +/** + * A general interface for components that map users to their effective roles. + */ +@FunctionalInterface +public interface RoleMapper { + ImmutableSet map(User user, TransportAddress caller); +} diff --git a/src/main/java/org/opensearch/security/privileges/SpecialIndices.java b/src/main/java/org/opensearch/security/privileges/SpecialIndices.java new file mode 100644 index 0000000000..2cc0480978 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/SpecialIndices.java @@ -0,0 +1,43 @@ +/* + * 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.privileges; + +import org.opensearch.common.settings.Settings; +import org.opensearch.indices.SystemIndexRegistry; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.WildcardMatcher; + +/** + * Contains information regarding system indices and other specially handled indices + */ +public class SpecialIndices { + private final String securityIndex; + private final WildcardMatcher manuallyConfiguredSystemIndexMatcher; + + public SpecialIndices(Settings settings) { + this.securityIndex = settings.get( + ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX + ); + this.manuallyConfiguredSystemIndexMatcher = WildcardMatcher.from( + settings.getAsList(ConfigConstants.SECURITY_SYSTEM_INDICES_KEY, ConfigConstants.SECURITY_SYSTEM_INDICES_DEFAULT) + ); + } + + public boolean isUniversallyDeniedIndex(String index) { + return index.equals(securityIndex); + } + + public boolean isSystemIndex(String index) { + return this.manuallyConfiguredSystemIndexMatcher.test(index) || SystemIndexRegistry.matchesSystemIndexPattern(index); + } + +} diff --git a/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java b/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java deleted file mode 100644 index a2cd1c16a7..0000000000 --- a/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * 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.privileges; - -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.fieldcaps.FieldCapabilitiesAction; -import org.opensearch.action.get.GetAction; -import org.opensearch.action.get.MultiGetAction; -import org.opensearch.action.search.MultiSearchAction; -import org.opensearch.action.search.SearchAction; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.index.query.MatchNoneQueryBuilder; -import org.opensearch.index.query.QueryBuilder; -import org.opensearch.index.query.TermsQueryBuilder; -import org.opensearch.search.aggregations.AggregationBuilder; -import org.opensearch.search.aggregations.bucket.terms.TermsAggregationBuilder; -import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; - -public class TermsAggregationEvaluator { - - protected final Logger log = LogManager.getLogger(this.getClass()); - - private static final ImmutableSet READ_ACTIONS = ImmutableSet.of( - MultiSearchAction.NAME, - MultiGetAction.NAME, - GetAction.NAME, - SearchAction.NAME, - FieldCapabilitiesAction.NAME - ); - - private static final QueryBuilder NONE_QUERY = new MatchNoneQueryBuilder(); - - public TermsAggregationEvaluator() {} - - public PrivilegesEvaluatorResponse evaluate( - final Resolved resolved, - final ActionRequest request, - PrivilegesEvaluationContext context, - ActionPrivileges actionPrivileges, - PrivilegesEvaluatorResponse presponse - ) { - try { - if (request instanceof SearchRequest) { - SearchRequest sr = (SearchRequest) request; - - if (sr.source() != null - && sr.source().query() == null - && sr.source().aggregations() != null - && sr.source().aggregations().getAggregatorFactories() != null - && sr.source().aggregations().getAggregatorFactories().size() == 1 - && sr.source().size() == 0) { - AggregationBuilder ab = sr.source().aggregations().getAggregatorFactories().iterator().next(); - if (ab instanceof TermsAggregationBuilder && "terms".equals(ab.getType()) && "indices".equals(ab.getName())) { - if ("_index".equals(((TermsAggregationBuilder) ab).field()) - && ab.getPipelineAggregations().isEmpty() - && ab.getSubAggregations().isEmpty()) { - - PrivilegesEvaluatorResponse subResponse = actionPrivileges.hasIndexPrivilege( - context, - READ_ACTIONS, - Resolved._LOCAL_ALL - ); - - if (subResponse.isPartiallyOk()) { - sr.source() - .query( - new TermsQueryBuilder( - "_index", - Sets.union(subResponse.getAvailableIndices(), resolved.getRemoteIndices()) - ) - ); - } else if (!subResponse.isAllowed()) { - sr.source().query(NONE_QUERY); - } - - presponse.allowed = true; - return presponse.markComplete(); - } - } - } - } - } catch (Exception e) { - log.warn("Unable to evaluate terms aggregation", e); - return presponse; - } - - return presponse; - } -} diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java index 3734f340ab..590fb08f12 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java @@ -18,6 +18,8 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import com.google.common.collect.ImmutableMap; @@ -39,7 +41,6 @@ import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluationException; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; -import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; @@ -97,8 +98,29 @@ public class RoleBasedActionPrivileges extends RuntimeOptimizedActionPrivileges private final AtomicReference statefulIndex = new AtomicReference<>(); - public RoleBasedActionPrivileges(SecurityDynamicConfiguration roles, FlattenedActionGroups actionGroups, Settings settings) { - super(new ClusterPrivileges(roles, actionGroups), new IndexPrivileges(roles, actionGroups)); + /** + * Creates a new RoleBasedActionPrivileges instance based on the given parameters. + * + * @param roles the roles form the basis for the privilege configuration + * @param actionGroups the action groups will be used to expand the "allowed_actions" attributes in the roles config + * @param specialIndexProtection configuration that identifies indices for which additional protections should be applied + * @param settings Other settings for this instance. The settings PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE and PRECOMPUTED_PRIVILEGES_ENABLED + * will be read from this. + * @param breakDownAliases if true, this class is allowed to break down aliases into member indices to see whether a subset of the member indices have the necessary privileges. This is used for the legacy privilege evaluation. + * It has the issue that filtered aliases are lost in the process. Thus, the new privilege evaluation does not use this any more. + */ + public RoleBasedActionPrivileges( + SecurityDynamicConfiguration roles, + FlattenedActionGroups actionGroups, + SpecialIndexProtection specialIndexProtection, + Settings settings, + boolean breakDownAliases + ) { + super( + new ClusterPrivileges(roles, actionGroups), + new IndexPrivileges(roles, actionGroups, specialIndexProtection, breakDownAliases), + breakDownAliases + ); this.roles = roles; this.actionGroups = actionGroups; this.statefulIndexMaxHeapSize = PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.get(settings); @@ -121,7 +143,7 @@ public void updateStatefulIndexPrivileges(Map indices, StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); - indices = StatefulIndexPrivileges.relevantOnly(indices); + indices = StatefulIndexPrivileges.relevantOnly(indices, this.index.universallyDeniedIndices); if (statefulIndex == null || !statefulIndex.indices.equals(indices)) { long start = System.currentTimeMillis(); @@ -348,7 +370,17 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde * just results in fewer available privileges. However, having a proper error reporting mechanism would be * kind of nice. */ - IndexPrivileges(SecurityDynamicConfiguration roles, FlattenedActionGroups actionGroups) { + IndexPrivileges( + SecurityDynamicConfiguration roles, + FlattenedActionGroups actionGroups, + SpecialIndexProtection specialIndexProtection, + boolean memberIndexPrivilegesYieldAliasPrivileges + ) { + super(specialIndexProtection); + + Function indexPatternBuilder = k -> new IndexPattern.Builder( + memberIndexPrivilegesYieldAliasPrivileges + ); Map> rolesToActionToIndexPattern = new HashMap<>(); Map> rolesToActionPatternToIndexPattern = new HashMap<>(); @@ -379,12 +411,12 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde if (WildcardMatcher.isExact(permission)) { rolesToActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) - .computeIfAbsent(permission, k -> new IndexPattern.Builder()) + .computeIfAbsent(permission, indexPatternBuilder) .add(indexPermissions.getIndex_patterns()); if (WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS.contains(permission)) { rolesToExplicitActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) - .computeIfAbsent(permission, k -> new IndexPattern.Builder()) + .computeIfAbsent(permission, indexPatternBuilder) .add(indexPermissions.getIndex_patterns()); } @@ -399,7 +431,7 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde for (String action : actionMatcher.iterateMatching(WellKnownActions.INDEX_ACTIONS)) { rolesToActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) - .computeIfAbsent(action, k -> new IndexPattern.Builder()) + .computeIfAbsent(action, indexPatternBuilder) .add(indexPermissions.getIndex_patterns()); if (indexPermissions.getIndex_patterns().contains("*")) { @@ -411,7 +443,7 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde } rolesToActionPatternToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) - .computeIfAbsent(actionMatcher, k -> new IndexPattern.Builder()) + .computeIfAbsent(actionMatcher, indexPatternBuilder) .add(indexPermissions.getIndex_patterns()); if (actionMatcher != WildcardMatcher.ANY) { @@ -419,7 +451,7 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS )) { rolesToExplicitActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) - .computeIfAbsent(action, k -> new IndexPattern.Builder()) + .computeIfAbsent(action, indexPatternBuilder) .add(indexPermissions.getIndex_patterns()); } } @@ -492,10 +524,9 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde * checkTable instance as checked. */ @Override - protected PrivilegesEvaluatorResponse providesPrivilege( + protected IntermediateResult providesPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices, CheckTable checkTable ) { List exceptions = new ArrayList<>(); @@ -505,7 +536,7 @@ protected PrivilegesEvaluatorResponse providesPrivilege( if (actionToIndexPattern != null) { checkPrivilegeWithIndexPatternOnWellKnownActions(context, actions, checkTable, actionToIndexPattern, exceptions); if (checkTable.isComplete()) { - return PrivilegesEvaluatorResponse.ok(); + return new IntermediateResult(checkTable).evaluationExceptions(exceptions); } } } @@ -523,36 +554,94 @@ protected PrivilegesEvaluatorResponse providesPrivilege( if (actionPatternToIndexPattern != null) { checkPrivilegesForNonWellKnownActions(context, actions, checkTable, actionPatternToIndexPattern, exceptions); if (checkTable.isComplete()) { - return PrivilegesEvaluatorResponse.ok(); + return new IntermediateResult(checkTable).evaluationExceptions(exceptions); + } + } + } + } + + return new IntermediateResult(checkTable).evaluationExceptions(exceptions); + } + + @Override + protected PrivilegesEvaluatorResponse providesPrivilegeOnAnyIndex( + PrivilegesEvaluationContext context, + Set actions, + CheckTable checkTable + ) { + for (String role : context.getMappedRoles()) { + ImmutableMap actionToIndexPattern = this.rolesToActionToIndexPattern.get(role); + if (actionToIndexPattern != null) { + checkTable.checkIf( + checkTable.getRows(), + action -> !actionToIndexPattern.getOrDefault(action, IndexPattern.EMPTY).isEmpty() + ); + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(checkTable); + } + } + } + + // If all actions are well-known, the index.rolesToActionToIndexPattern data structure that was evaluated above, + // would have contained all the actions if privileges are provided. If there are non-well-known actions among the + // actions, we also have to evaluate action patterns to check the authorization + + if (!checkTable.isComplete() && !allWellKnownIndexActions(actions)) { + for (String role : context.getMappedRoles()) { + ImmutableMap actionPatternToIndexPattern = this.rolesToActionPatternToIndexPattern.get( + role + ); + if (actionPatternToIndexPattern != null) { + for (String action : actions) { + for (Map.Entry entry : actionPatternToIndexPattern.entrySet()) { + WildcardMatcher actionMatcher = entry.getKey(); + IndexPattern indexPattern = entry.getValue(); + + if (actionMatcher.test(action) && !indexPattern.isEmpty()) { + checkTable.getRows().forEach(index -> checkTable.check(index, action)); + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(checkTable); + } + } + } } } } } - return responseForIncompletePrivileges(context, resolvedIndices, checkTable, exceptions); + return PrivilegesEvaluatorResponse.insufficient(checkTable) + .reason("The user does not have any index privileges for the requested action"); } /** - * Returns PrivilegesEvaluatorResponse.ok() if the user identified in the context object has privileges for all + * Returns IntermediateResult.ok() if the user identified in the context object has privileges for all * indices (using *) for the given actions. Returns null otherwise. Then, further checks must be done to check * the user's privileges. + *

+ * As a side-effect, this method will mark the available index/action combinations in the provided + * checkTable instance as checked. */ @Override - protected PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownActions( + protected IntermediateResult checkWildcardIndexPrivilegesOnWellKnownActions( PrivilegesEvaluationContext context, - Set actions + Set actions, + CheckTable checkTable ) { ImmutableSet effectiveRoles = context.getMappedRoles(); for (String action : actions) { ImmutableCompactSubSet rolesWithWildcardIndexPrivileges = this.actionToRolesWithWildcardIndexPrivileges.get(action); - if (rolesWithWildcardIndexPrivileges == null || !rolesWithWildcardIndexPrivileges.containsAny(effectiveRoles)) { - return null; + if (rolesWithWildcardIndexPrivileges != null && rolesWithWildcardIndexPrivileges.containsAny(effectiveRoles)) { + checkTable.checkIf(index -> true, action); } } - return PrivilegesEvaluatorResponse.ok(); + if (checkTable.isComplete()) { + return new IntermediateResult(checkTable); + } else { + return null; + } } /** @@ -582,7 +671,7 @@ protected PrivilegesEvaluatorResponse providesExplicitPrivilege( for (String index : checkTable.iterateUncheckedRows(action)) { try { if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { - return PrivilegesEvaluatorResponse.ok(); + return PrivilegesEvaluatorResponse.ok(checkTable); } } catch (PrivilegesEvaluationException e) { // We can ignore these errors, as this max leads to fewer privileges than available @@ -599,6 +688,40 @@ protected PrivilegesEvaluatorResponse providesExplicitPrivilege( .reason("No explicit privileges have been provided for the referenced indices.") .evaluationExceptions(exceptions); } + + @Override + protected boolean providesExplicitPrivilege( + PrivilegesEvaluationContext context, + String index, + String action, + List exceptions + ) { + Map indexMetadata = context.getIndicesLookup(); + + for (String role : context.getMappedRoles()) { + ImmutableMap actionToIndexPattern = this.rolesToExplicitActionToIndexPattern.get(role); + + if (actionToIndexPattern != null) { + IndexPattern indexPattern = actionToIndexPattern.get(action); + + if (indexPattern != null) { + try { + if (indexPattern.matches(index, context, indexMetadata)) { + return true; + } + } catch (PrivilegesEvaluationException e) { + // We can ignore these errors, as this max leads to fewer privileges than available + log.error("Error while evaluating index pattern of role {}. Ignoring entry", role, e); + exceptions.add(new PrivilegesEvaluationException("Error while evaluating role " + role, e)); + } + + } + } + + } + + return false; + } } /** @@ -688,7 +811,7 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St continue; } - WildcardMatcher indexMatcher = IndexPattern.from(indexPermissions.getIndex_patterns()).getStaticPattern(); + WildcardMatcher indexMatcher = IndexPattern.from(indexPermissions.getIndex_patterns(), false).getStaticPattern(); if (indexMatcher == WildcardMatcher.NONE) { // The pattern is likely blank because there are only templated patterns. @@ -798,26 +921,25 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St * is returned, because then the remaining logic needs only to check for the unchecked cases. * * @param actions the actions the user needs to have privileges for - * @param resolvedIndices the index the user needs to have privileges for * @param context context information like user, resolved roles, etc. * @param checkTable An action/index matrix. This method will modify the table as a side effect and check the cells where privileges are present. * @return PrivilegesEvaluatorResponse.ok() or null. */ @Override - protected PrivilegesEvaluatorResponse providesPrivilege( + protected IntermediateResult providesPrivilege( Set actions, - IndexResolverReplacer.Resolved resolvedIndices, PrivilegesEvaluationContext context, CheckTable checkTable ) { Map indexMetadata = context.getIndicesLookup(); ImmutableSet effectiveRoles = context.getMappedRoles(); + Set indices = checkTable.getRows(); for (String action : actions) { Map> indexToRoles = actionToIndexToRoles.get(action); if (indexToRoles != null) { - for (String index : resolvedIndices.getAllIndices()) { + for (String index : indices) { String lookupIndex = index; if (index.startsWith(DataStream.BACKING_INDEX_PREFIX)) { @@ -831,7 +953,7 @@ protected PrivilegesEvaluatorResponse providesPrivilege( if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(effectiveRoles)) { if (checkTable.check(index, action)) { - return PrivilegesEvaluatorResponse.ok(); + return new IntermediateResult(checkTable); } } } @@ -869,31 +991,20 @@ static String backingIndexToDataStream(String index, MapIndices which are not matched by includeIndices + *

  • Indices which are universally denied * */ - static Map relevantOnly(Map indices) { - // First pass: Check if we need to filter at all - boolean doFilter = false; + static Map relevantOnly( + Map indices, + Predicate universallyDeniedIndices + ) { + ImmutableMap.Builder builder = ImmutableMap.builder(); for (IndexAbstraction indexAbstraction : indices.values()) { - if (indexAbstraction instanceof IndexAbstraction.Index) { - if (indexAbstraction.getParentDataStream() != null - || indexAbstraction.getWriteIndex().getState() == IndexMetadata.State.CLOSE) { - doFilter = true; - break; - } + if (universallyDeniedIndices != null && universallyDeniedIndices.test(indexAbstraction.getName())) { + continue; } - } - if (!doFilter) { - return indices; - } - - // Second pass: Only if we actually need filtering, we will do it - ImmutableMap.Builder builder = ImmutableMap.builder(); - - for (IndexAbstraction indexAbstraction : indices.values()) { if (indexAbstraction instanceof IndexAbstraction.Index) { if (indexAbstraction.getParentDataStream() == null && indexAbstraction.getWriteIndex().getState() != IndexMetadata.State.CLOSE) { diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java index 1ab6a11fbb..ac4ab665eb 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java @@ -11,10 +11,15 @@ package org.opensearch.security.privileges.actionlevel; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.apache.commons.collections4.CollectionUtils; @@ -22,12 +27,15 @@ import org.apache.logging.log4j.Logger; import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.security.privileges.ActionPrivileges; import org.opensearch.security.privileges.IndexPattern; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluationException; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; -import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import com.selectivem.collections.CheckTable; @@ -50,9 +58,18 @@ public abstract class RuntimeOptimizedActionPrivileges implements ActionPrivileg protected final ClusterPrivileges cluster; protected final StaticIndexPrivileges index; - RuntimeOptimizedActionPrivileges(ClusterPrivileges cluster, StaticIndexPrivileges index) { + /** + * If true, aliases or data streams which do not have direct privileges, will be broken down into individual indices + * to check whether there are privileges for the individual indices. This is mainly for backwards compatibility. The + * new privilege evaluation mode won't use this, as this does not work well with filtered aliases (the filters would + * just disappear). + */ + protected final boolean breakDownAliases; + + RuntimeOptimizedActionPrivileges(ClusterPrivileges cluster, StaticIndexPrivileges index, boolean breakDownAliases) { this.cluster = cluster; this.index = index; + this.breakDownAliases = breakDownAliases; } @Override @@ -84,14 +101,9 @@ public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluat public PrivilegesEvaluatorResponse hasIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices + OptionallyResolvedIndices resolvedIndices ) { - PrivilegesEvaluatorResponse response = this.index.checkWildcardIndexPrivilegesOnWellKnownActions(context, actions); - if (response != null) { - return response; - } - - if (!resolvedIndices.isLocalAll() && resolvedIndices.getAllIndices().isEmpty()) { + if (resolvedIndices.local().isEmpty()) { // This is necessary for requests which operate on remote indices. // Access control for the remote indices will be performed on the remote cluster. log.debug("No local indices; grant the request"); @@ -100,20 +112,21 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( // TODO one might want to consider to create a semantic wrapper for action in order to be better tell apart // what's the action and what's the index in the generic parameters of CheckTable. - CheckTable checkTable = CheckTable.create( - resolvedIndices.getAllIndicesResolved(context.getClusterStateSupplier(), context.getIndexNameExpressionResolver()), - actions - ); + CheckTable checkTable = CheckTable.create(resolvedIndices.local().names(context.clusterState()), actions); + + IntermediateResult result = this.index.checkWildcardIndexPrivilegesOnWellKnownActions(context, actions, checkTable); + if (result != null) { + return this.index.finalizeResult(context, result); + } StatefulIndexPrivileges statefulIndex = this.currentStatefulIndexPrivileges(); - PrivilegesEvaluatorResponse resultFromStatefulIndex = null; if (statefulIndex != null) { - resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, resolvedIndices, context, checkTable); + IntermediateResult resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, context, checkTable); if (resultFromStatefulIndex != null) { // If we get a result from statefulIndex, we are done. - return resultFromStatefulIndex; + return this.index.finalizeResult(context, resultFromStatefulIndex); } // Otherwise, we need to carry on checking privileges using the non-stateful object. @@ -121,7 +134,29 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( // We can carry on using this as an intermediate result and further complete checkTable below. } - return this.index.providesPrivilege(context, actions, resolvedIndices, checkTable); + IntermediateResult resultFromStaticIndex = this.index.providesPrivilege(context, actions, checkTable); + + if (this.breakDownAliases + && !checkTable.isComplete() + && resolvedIndices instanceof ResolvedIndices + && containsAliasesOrDataStreams(context, checkTable.getIncompleteRows())) { + // If we could not gather privileges for all aliases, try to break down aliases and data streams into individual members + // This is for backwards compatibility only + return this.breakDownAliases(context, actions, checkTable); + } + + return this.index.finalizeResult(context, resultFromStaticIndex); + } + + @Override + public PrivilegesEvaluatorResponse hasIndexPrivilegeForAnyIndex(PrivilegesEvaluationContext context, Set actions) { + CheckTable checkTable = CheckTable.create(Set.of("_any_index"), actions); + IntermediateResult result = this.index.checkWildcardIndexPrivilegesOnWellKnownActions(context, actions, checkTable); + if (result != null) { + return PrivilegesEvaluatorResponse.ok(checkTable); + } + + return this.index.providesPrivilegeOnAnyIndex(context, actions, checkTable); } /** @@ -135,13 +170,13 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices + OptionallyResolvedIndices resolvedIndices ) { if (!CollectionUtils.containsAny(actions, WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS)) { return PrivilegesEvaluatorResponse.insufficient(CheckTable.create(ImmutableSet.of("_"), actions)); } - CheckTable checkTable = CheckTable.create(resolvedIndices.getAllIndices(), actions); + CheckTable checkTable = CheckTable.create(resolvedIndices.local().names(context.clusterState()), actions); return this.index.providesExplicitPrivilege(context, actions, checkTable); } @@ -151,6 +186,56 @@ public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( */ protected abstract StatefulIndexPrivileges currentStatefulIndexPrivileges(); + protected PrivilegesEvaluatorResponse breakDownAliases( + PrivilegesEvaluationContext context, + Set actions, + CheckTable checkTable + ) { + Map indicesLookup = context.getIndicesLookup(); + Set newIndices = new HashSet<>(); + + for (String index : checkTable.getRows()) { + if (checkTable.isRowComplete(index)) { + newIndices.add(index); + } else { + IndexAbstraction indexAbstraction = indicesLookup.get(index); + if (indexAbstraction instanceof IndexAbstraction.Alias || indexAbstraction instanceof IndexAbstraction.DataStream) { + indexAbstraction.getIndices().forEach(i -> newIndices.add(i.getIndex().getName())); + } else { + newIndices.add(index); + } + } + } + + CheckTable newCheckTable = CheckTable.create(newIndices, actions); + for (String action : actions) { + newCheckTable.checkIf(index -> checkTable.getRows().contains(index) && checkTable.isChecked(index, action), action); + } + + StatefulIndexPrivileges statefulIndex = this.currentStatefulIndexPrivileges(); + if (statefulIndex != null) { + IntermediateResult resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, context, newCheckTable); + if (resultFromStatefulIndex != null) { + // If we get a result from statefulIndex, we are done. + return this.index.finalizeResult(context, resultFromStatefulIndex); + } + } + + IntermediateResult resultFromStaticIndex = this.index.providesPrivilege(context, actions, newCheckTable); + return this.index.finalizeResult(context, resultFromStaticIndex); + } + + private boolean containsAliasesOrDataStreams(PrivilegesEvaluationContext context, Collection names) { + Map indicesLookup = context.getIndicesLookup(); + for (String name : names) { + IndexAbstraction indexAbstraction = indicesLookup.get(name); + if (indexAbstraction instanceof IndexAbstraction.Alias || indexAbstraction instanceof IndexAbstraction.DataStream) { + return true; + } + } + return false; + } + /** * Base class for evaluating cluster privileges. */ @@ -260,34 +345,46 @@ PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext con * This is the slowest way to check for a privilege. */ protected abstract boolean checkPrivilegeViaActionMatcher(PrivilegesEvaluationContext context, String action); - } /** * Base class for evaluating index permissions which evaluates index patterns at privilege evaluation time. */ protected abstract static class StaticIndexPrivileges { + protected final Predicate universallyDeniedIndices; + protected final Predicate indicesNeedingSystemIndexPrivileges; + + protected StaticIndexPrivileges(SpecialIndexProtection specialIndexProtection) { + this.universallyDeniedIndices = specialIndexProtection.universallyDeniedIndices; + this.indicesNeedingSystemIndexPrivileges = specialIndexProtection.indicesNeedingSystemIndexPrivileges; + } /** * Checks whether this instance provides privileges for the combination of the provided action, * the provided indices and the provided roles. *

    - * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. - *

    - * If privileges are only available for a sub-set of indices, isPartiallyOk() will return true - * and the indices for which privileges are available are returned by getAvailableIndices(). This allows the - * do_not_fail_on_forbidden behaviour. - *

    * This method will only verify privileges for the index/action combinations which are un-checked in * the checkTable instance provided to this method. Checked index/action combinations are considered to be * "already fulfilled by other means" - usually that comes from the stateful data structure. * As a side-effect, this method will further mark the available index/action combinations in the provided * checkTable instance as checked. */ - protected abstract PrivilegesEvaluatorResponse providesPrivilege( + protected abstract IntermediateResult providesPrivilege( + PrivilegesEvaluationContext context, + Set actions, + CheckTable checkTable + ); + + /** + * Checks whether this instance provides privileges for the provided action on any possible index. + *

    + * As a side-effect, this method will mark the available actions in the provided checkTable instance as checked. + * This method should not try to interpret the index names in the check table; as we are interested in any + * index, the index names will be arbitrary. + */ + protected abstract PrivilegesEvaluatorResponse providesPrivilegeOnAnyIndex( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices, CheckTable checkTable ); @@ -304,6 +401,13 @@ protected abstract PrivilegesEvaluatorResponse providesExplicitPrivilege( CheckTable checkTable ); + protected abstract boolean providesExplicitPrivilege( + PrivilegesEvaluationContext context, + String index, + String action, + List exceptions + ); + /** * Tests whether the current user (according to the context data) has wildcard index privileges for the given well known index actions. * Returns false if no privileges are given or if the given actions are not well known actions. @@ -311,9 +415,10 @@ protected abstract PrivilegesEvaluatorResponse providesExplicitPrivilege( * Implementations of this class may interpret the context data differently; they can check the mapped roles * or just the subject. */ - protected abstract PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownActions( + protected abstract IntermediateResult checkWildcardIndexPrivilegesOnWellKnownActions( PrivilegesEvaluationContext context, - Set actions + Set actions, + CheckTable checkTable ); /** @@ -347,7 +452,7 @@ protected void checkPrivilegeWithIndexPatternOnWellKnownActions( } catch (PrivilegesEvaluationException e) { // We can ignore these errors, as this max leads to fewer privileges than available log.error("Error while evaluating index pattern of {}. Ignoring entry", this, e); - exceptions.add(new PrivilegesEvaluationException("Error while evaluating " + this, e)); + exceptions.add(new PrivilegesEvaluationException("Error while evaluating index pattern " + indexPattern, e)); } } } @@ -401,33 +506,71 @@ protected void checkPrivilegesForNonWellKnownActions( } /** - * Creates a PrivilegesEvaluationResponse in the case we find that the user does not have full privileges. - * This result is built based on the state of the given check table: - *

      - *
    • If the check table is empty, a result with the state "insufficient" will be returned
    • - *
    • If the check table is not empty, a result with the state "partially ok" will be returned. The response - * object will carry a list of the indices for which we have privileges. This can be used for the DNFOF mode.
    • - *
    + * When we have finished the "normal" privilege evaluation, which is based on index_permissions in roles.yml, + * we have to pass the CheckTable with the available privileges through this method in order to have specially + * protected indices and actions removed again from the CheckTable. */ - protected PrivilegesEvaluatorResponse responseForIncompletePrivileges( - PrivilegesEvaluationContext context, - IndexResolverReplacer.Resolved resolvedIndices, - CheckTable checkTable, - List exceptions - ) { + protected PrivilegesEvaluatorResponse finalizeResult(PrivilegesEvaluationContext context, IntermediateResult intermediateResult) { + CheckTable checkTable = intermediateResult.indexToActionCheckTable; + List exceptions = new ArrayList<>(intermediateResult.exceptions); + if (this.universallyDeniedIndices != null) { + checkTable.uncheckIf(this.universallyDeniedIndices, checkTable.getColumns()); + } + if (this.indicesNeedingSystemIndexPrivileges != null) { + checkTable.uncheckIf(index -> this.isUnauthorizedSystemIndex(context, index, exceptions), checkTable.getColumns()); + } + + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(checkTable); + } + Set availableIndices = checkTable.getCompleteRows(); if (!availableIndices.isEmpty()) { return PrivilegesEvaluatorResponse.partiallyOk(availableIndices, checkTable).evaluationExceptions(exceptions); } - return PrivilegesEvaluatorResponse.insufficient(checkTable) - .reason( - resolvedIndices.getAllIndices().size() == 1 - ? "Insufficient permissions for the referenced index" - : "None of " + resolvedIndices.getAllIndices().size() + " referenced indices has sufficient permissions" - ) - .evaluationExceptions(exceptions); + Set allIndices = checkTable.getRows(); + + String reason; + if (allIndices.size() != 1) { + reason = "None of the referenced indices has sufficient permissions"; + } else { + reason = "Insufficient permissions for the referenced index"; + } + + return PrivilegesEvaluatorResponse.insufficient(checkTable).reason(reason).evaluationExceptions(exceptions); + + } + + /** + * Returns true if the given indexOrAlias is a system index or an alias containing a system index AND if + * the current user does not have the necessary explicit privilege to access this system index. + */ + private boolean isUnauthorizedSystemIndex( + PrivilegesEvaluationContext context, + String indexOrAlias, + List exceptions + ) { + if (this.indicesNeedingSystemIndexPrivileges.test(indexOrAlias)) { + return !providesExplicitPrivilege(context, indexOrAlias, ConfigConstants.SYSTEM_INDEX_PERMISSION, exceptions); + } + + IndexAbstraction indexAbstraction = context.getIndicesLookup().get(indexOrAlias); + if (indexAbstraction instanceof IndexAbstraction.Alias alias) { + for (IndexMetadata index : alias.getIndices()) { + if (this.indicesNeedingSystemIndexPrivileges.test(index.getIndex().getName())) { + return !providesExplicitPrivilege( + context, + index.getIndex().getName(), + ConfigConstants.SYSTEM_INDEX_PERMISSION, + exceptions + ); + } + } + } + + return false; } } @@ -450,17 +593,64 @@ protected abstract static class StatefulIndexPrivileges { * is returned, because then the remaining logic needs only to check for the unchecked cases. * * @param actions the actions the user needs to have privileges for - * @param resolvedIndices the index the user needs to have privileges for * @param context context information like user, resolved roles, etc. * @param checkTable An action/index matrix. This method will modify the table as a side effect and check the cells where privileges are present. * @return PrivilegesEvaluatorResponse.ok() or null. */ - protected abstract PrivilegesEvaluatorResponse providesPrivilege( + protected abstract IntermediateResult providesPrivilege( Set actions, - IndexResolverReplacer.Resolved resolvedIndices, PrivilegesEvaluationContext context, CheckTable checkTable ); } + public static class SpecialIndexProtection { + public static final SpecialIndexProtection NONE = new SpecialIndexProtection(null, null); + + protected final Predicate universallyDeniedIndices; + protected final Predicate indicesNeedingSystemIndexPrivileges; + + public SpecialIndexProtection(Predicate universallyDeniedIndices, Predicate indicesNeedingSystemIndexPrivileges) { + this.universallyDeniedIndices = universallyDeniedIndices; + this.indicesNeedingSystemIndexPrivileges = indicesNeedingSystemIndexPrivileges; + } + } + + protected static class IntermediateResult { + + protected final CheckTable indexToActionCheckTable; + protected final String reason; + protected final ImmutableList exceptions; + + protected IntermediateResult(CheckTable indexToActionCheckTable) { + this.indexToActionCheckTable = indexToActionCheckTable; + this.reason = null; + this.exceptions = ImmutableList.of(); + } + + IntermediateResult( + CheckTable indexToActionCheckTable, + String reason, + ImmutableList exceptions + ) { + this.indexToActionCheckTable = indexToActionCheckTable; + this.reason = reason; + this.exceptions = exceptions; + } + + protected IntermediateResult reason(String reason) { + return new IntermediateResult(this.indexToActionCheckTable, reason, this.exceptions); + } + + protected IntermediateResult evaluationExceptions(List exceptions) { + if (exceptions.isEmpty()) { + return this; + } else { + ImmutableList.Builder newExceptions = ImmutableList.builder(); + newExceptions.addAll(this.exceptions); + newExceptions.addAll(exceptions); + return new IntermediateResult(this.indexToActionCheckTable, reason, newExceptions.build()); + } + } + } } diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java index ee04f61105..f437300860 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import com.google.common.collect.ImmutableMap; @@ -25,11 +26,11 @@ import org.apache.logging.log4j.Logger; import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.security.privileges.ActionPrivileges; import org.opensearch.security.privileges.IndexPattern; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluationException; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; -import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.WildcardMatcher; @@ -48,6 +49,24 @@ * This class is useful for plugin users and API tokens. */ public class SubjectBasedActionPrivileges extends RuntimeOptimizedActionPrivileges { + + public static ImmutableMap buildFromMap( + Map pluginIdToRolePrivileges, + FlattenedActionGroups staticActionGroups, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection specialIndexProtection + ) { + Map result = new HashMap<>(pluginIdToRolePrivileges.size()); + + for (Map.Entry entry : pluginIdToRolePrivileges.entrySet()) { + result.put( + entry.getKey(), + new SubjectBasedActionPrivileges(entry.getValue(), staticActionGroups, specialIndexProtection, false) + ); + } + + return ImmutableMap.copyOf(result); + } + private static final Logger log = LogManager.getLogger(SubjectBasedActionPrivileges.class); /** @@ -59,8 +78,17 @@ public class SubjectBasedActionPrivileges extends RuntimeOptimizedActionPrivileg * @param actionGroups The FlattenedActionGroups instance that shall be used to resolve the action groups * specified in the roles configuration. */ - public SubjectBasedActionPrivileges(RoleV7 role, FlattenedActionGroups actionGroups) { - super(new ClusterPrivileges(actionGroups.resolve(role.getCluster_permissions())), new IndexPrivileges(role, actionGroups)); + public SubjectBasedActionPrivileges( + RoleV7 role, + FlattenedActionGroups actionGroups, + SpecialIndexProtection specialIndexProtection, + boolean breakDownAliases + ) { + super( + new ClusterPrivileges(actionGroups.resolve(role.getCluster_permissions())), + new IndexPrivileges(role, actionGroups, specialIndexProtection, breakDownAliases), + breakDownAliases + ); } /** @@ -215,7 +243,17 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde /** * Creates pre-computed index privileges based on the given parameters. */ - IndexPrivileges(RoleV7 role, FlattenedActionGroups actionGroups) { + IndexPrivileges( + RoleV7 role, + FlattenedActionGroups actionGroups, + SpecialIndexProtection specialIndexProtection, + boolean memberIndexPrivilegesYieldALiasPrivileges + ) { + super(specialIndexProtection); + + Function indexPatternBuilder = k -> new IndexPattern.Builder( + memberIndexPrivilegesYieldALiasPrivileges + ); Map actionToIndexPattern = new HashMap<>(); Map actionPatternToIndexPattern = new HashMap<>(); @@ -234,11 +272,10 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde // as a last resort later. if (WildcardMatcher.isExact(permission)) { - actionToIndexPattern.computeIfAbsent(permission, k -> new IndexPattern.Builder()) - .add(indexPermissions.getIndex_patterns()); + actionToIndexPattern.computeIfAbsent(permission, indexPatternBuilder).add(indexPermissions.getIndex_patterns()); if (WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS.contains(permission)) { - explicitActionToIndexPattern.computeIfAbsent(permission, k -> new IndexPattern.Builder()) + explicitActionToIndexPattern.computeIfAbsent(permission, indexPatternBuilder) .add(indexPermissions.getIndex_patterns()); } @@ -249,20 +286,19 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde WildcardMatcher actionMatcher = WildcardMatcher.from(permission); for (String action : actionMatcher.iterateMatching(WellKnownActions.INDEX_ACTIONS)) { - actionToIndexPattern.computeIfAbsent(action, k -> new IndexPattern.Builder()) - .add(indexPermissions.getIndex_patterns()); + actionToIndexPattern.computeIfAbsent(action, indexPatternBuilder).add(indexPermissions.getIndex_patterns()); if (indexPermissions.getIndex_patterns().contains("*")) { actionWithWildcardIndexPrivileges.add(permission); } } - actionPatternToIndexPattern.computeIfAbsent(actionMatcher, k -> new IndexPattern.Builder()) + actionPatternToIndexPattern.computeIfAbsent(actionMatcher, indexPatternBuilder) .add(indexPermissions.getIndex_patterns()); if (actionMatcher != WildcardMatcher.ANY) { for (String action : actionMatcher.iterateMatching(WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS)) { - explicitActionToIndexPattern.computeIfAbsent(action, k -> new IndexPattern.Builder()) + explicitActionToIndexPattern.computeIfAbsent(action, indexPatternBuilder) .add(indexPermissions.getIndex_patterns()); } } @@ -302,17 +338,16 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde * checkTable instance as checked. */ @Override - protected PrivilegesEvaluatorResponse providesPrivilege( + protected IntermediateResult providesPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices, CheckTable checkTable ) { List exceptions = new ArrayList<>(); checkPrivilegeWithIndexPatternOnWellKnownActions(context, actions, checkTable, actionToIndexPattern, exceptions); if (checkTable.isComplete()) { - return PrivilegesEvaluatorResponse.ok(); + return new IntermediateResult(checkTable).evaluationExceptions(exceptions); } // If all actions are well-known, the index.actionToIndexPattern data structure that was evaluated above, @@ -322,11 +357,50 @@ protected PrivilegesEvaluatorResponse providesPrivilege( if (!allWellKnownIndexActions(actions)) { checkPrivilegesForNonWellKnownActions(context, actions, checkTable, this.actionPatternToIndexPattern, exceptions); if (checkTable.isComplete()) { - return PrivilegesEvaluatorResponse.ok(); + return new IntermediateResult(checkTable).evaluationExceptions(exceptions); + } + } + + return new IntermediateResult(checkTable).evaluationExceptions(exceptions); + } + + @Override + protected PrivilegesEvaluatorResponse providesPrivilegeOnAnyIndex( + PrivilegesEvaluationContext context, + Set actions, + CheckTable checkTable + ) { + checkTable.checkIf( + checkTable.getRows(), + action -> !this.actionToIndexPattern.getOrDefault(action, IndexPattern.EMPTY).isEmpty() + ); + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(checkTable); + } + + // If all actions are well-known, the index.rolesToActionToIndexPattern data structure that was evaluated above, + // would have contained all the actions if privileges are provided. If there are non-well-known actions among the + // actions, we also have to evaluate action patterns to check the authorization + + if (!allWellKnownIndexActions(actions)) { + for (String action : actions) { + for (Map.Entry entry : this.actionPatternToIndexPattern.entrySet()) { + WildcardMatcher actionMatcher = entry.getKey(); + IndexPattern indexPattern = entry.getValue(); + + if (actionMatcher.test(action) && !indexPattern.isEmpty()) { + checkTable.getRows().forEach(index -> checkTable.check(index, action)); + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(checkTable); + } + } + } } + } - return responseForIncompletePrivileges(context, resolvedIndices, checkTable, exceptions); + return PrivilegesEvaluatorResponse.insufficient(checkTable) + .reason("The user does not have any index privileges for the requested action"); } /** @@ -335,17 +409,22 @@ protected PrivilegesEvaluatorResponse providesPrivilege( * the user's privileges. */ @Override - protected PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownActions( + protected IntermediateResult checkWildcardIndexPrivilegesOnWellKnownActions( PrivilegesEvaluationContext context, - Set actions + Set actions, + CheckTable checkTable ) { for (String action : actions) { - if (!this.actionsWithWildcardIndexPrivileges.contains(action)) { - return null; + if (this.actionsWithWildcardIndexPrivileges.contains(action)) { + checkTable.checkIf(index -> true, action); } } - return PrivilegesEvaluatorResponse.ok(); + if (checkTable.isComplete()) { + return new IntermediateResult(checkTable); + } else { + return null; + } } /** @@ -386,6 +465,33 @@ protected PrivilegesEvaluatorResponse providesExplicitPrivilege( .reason("No explicit privileges have been provided for the referenced indices.") .evaluationExceptions(exceptions); } + + @Override + protected boolean providesExplicitPrivilege( + PrivilegesEvaluationContext context, + String index, + String action, + List exceptions + ) { + Map indexMetadata = context.getIndicesLookup(); + + IndexPattern indexPattern = this.explicitActionToIndexPattern.get(action); + + if (indexPattern != null) { + try { + if (indexPattern.matches(index, context, indexMetadata)) { + return true; + } + } catch (PrivilegesEvaluationException e) { + // We can ignore these errors, as this max leads to fewer privileges than available + log.error("Error while evaluating {}. Ignoring entry", indexPattern, e); + exceptions.add(new PrivilegesEvaluationException("Error while evaluating " + indexPattern, e)); + } + + } + + return false; + } } } diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/LegacyIndicesRequestResolver.java b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/LegacyIndicesRequestResolver.java new file mode 100644 index 0000000000..5058d599c4 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/LegacyIndicesRequestResolver.java @@ -0,0 +1,109 @@ +/* + * 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.privileges.actionlevel.legacy; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.opensearch.action.admin.indices.alias.get.GetAliasesRequest; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.support.ActionRequestMetadata; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; +import org.opensearch.security.privileges.IndicesRequestResolver; +import org.opensearch.security.support.SnapshotRestoreHelper; + +/** + * A modified IndicesRequestResolver which keeps the default index resolution behavior of OpenSearch 3.2.0 and earlier + */ +public class LegacyIndicesRequestResolver extends IndicesRequestResolver { + + private final Supplier isNodeElectedMaster; + private static final Logger log = LogManager.getLogger(LegacyIndicesRequestResolver.class); + + public LegacyIndicesRequestResolver(IndexNameExpressionResolver indexNameExpressionResolver, Supplier isNodeElectedMaster) { + super(indexNameExpressionResolver); + this.isNodeElectedMaster = isNodeElectedMaster; + } + + @Override + public OptionallyResolvedIndices resolve( + ActionRequest request, + ActionRequestMetadata actionRequestMetadata, + Supplier clusterStateSupplier + ) { + // For the legacy mode, we still need a couple of special cases to stay backwards compatible + if (request instanceof IndicesAliasesRequest indicesAliasesRequest) { + List indices = new ArrayList<>(); + ClusterState clusterState = clusterStateSupplier.get(); + for (IndicesAliasesRequest.AliasActions aliasActions : indicesAliasesRequest.getAliasActions()) { + indices.addAll( + indexNameExpressionResolver.concreteResolvedIndices(clusterState, aliasActions).namesOfIndices(clusterState) + ); + } + return ResolvedIndices.of(indices); + } else if (request instanceof GetAliasesRequest getAliasesRequest) { + ClusterState clusterState = clusterStateSupplier.get(); + return ResolvedIndices.of( + indexNameExpressionResolver.concreteResolvedIndices(clusterState, getAliasesRequest).namesOfIndices(clusterState) + ); + } else if (request instanceof CreateIndexRequest createIndexRequest) { + return ResolvedIndices.of(indexNameExpressionResolver.resolveDateMathExpression(createIndexRequest.index())); + } else if (request instanceof RestoreSnapshotRequest restoreSnapshotRequest) { + try { + // TODO possibly we need to change the result when we are not master + + if (this.isNodeElectedMaster.get()) { + return SnapshotRestoreHelper.resolveTargetIndices(restoreSnapshotRequest); + } else { + return ResolvedIndices.unknown(); + } + } catch (Exception e) { + log.error("Error while resolving RestoreSnapshotRequest {}", restoreSnapshotRequest, e); + return ResolvedIndices.unknown(); + } + } else { + return flatten(super.resolve(request, actionRequestMetadata, clusterStateSupplier)); + } + } + + /** + * This copies all names from subActions stored in the ResolvedIndices object into the root object. + * This is necessary because the legacy privileges evaluator is not aware of sub actions. + */ + OptionallyResolvedIndices flatten(OptionallyResolvedIndices optionallyResolvedIndices) { + if (!(optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices)) { + return optionallyResolvedIndices; + } + + if (resolvedIndices.local().subActions().isEmpty()) { + return resolvedIndices; + } + + Set names = new HashSet<>(resolvedIndices.local().names()); + for (ResolvedIndices.Local subAction : resolvedIndices.local().subActions().values()) { + names.addAll(subAction.names()); + } + + return ResolvedIndices.of(names).withRemoteIndices(resolvedIndices.remote().asClusterToOriginalIndicesMap()); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java new file mode 100644 index 0000000000..336e4698ea --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java @@ -0,0 +1,808 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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.privileges.actionlevel.legacy; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.IndicesRequest; +import org.opensearch.action.admin.cluster.shards.ClusterSearchShardsRequest; +import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.opensearch.action.admin.indices.alias.IndicesAliasesAction; +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; +import org.opensearch.action.admin.indices.analyze.AnalyzeAction; +import org.opensearch.action.admin.indices.create.AutoCreateAction; +import org.opensearch.action.admin.indices.create.CreateIndexAction; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.delete.DeleteIndexAction; +import org.opensearch.action.admin.indices.mapping.get.GetFieldMappingsRequest; +import org.opensearch.action.admin.indices.mapping.put.AutoPutMappingAction; +import org.opensearch.action.admin.indices.mapping.put.PutMappingAction; +import org.opensearch.action.bulk.BulkAction; +import org.opensearch.action.bulk.BulkItemRequest; +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.bulk.BulkShardRequest; +import org.opensearch.action.delete.DeleteAction; +import org.opensearch.action.get.MultiGetAction; +import org.opensearch.action.index.IndexAction; +import org.opensearch.action.search.GetAllPitsAction; +import org.opensearch.action.search.MultiSearchAction; +import org.opensearch.action.search.SearchAction; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchScrollAction; +import org.opensearch.action.support.ActionRequestMetadata; +import org.opensearch.action.support.IndicesOptions; +import org.opensearch.action.termvectors.MultiTermVectorsAction; +import org.opensearch.action.update.UpdateAction; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.AliasMetadata; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.common.Strings; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.index.reindex.ReindexAction; +import org.opensearch.script.mustache.RenderSearchTemplateAction; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.privileges.ActionPrivileges; +import org.opensearch.security.privileges.DocumentAllowList; +import org.opensearch.security.privileges.IndicesRequestModifier; +import org.opensearch.security.privileges.IndicesRequestResolver; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.privileges.PrivilegesInterceptor; +import org.opensearch.security.privileges.RoleMapper; +import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; +import org.opensearch.security.privileges.actionlevel.RuntimeOptimizedActionPrivileges; +import org.opensearch.security.privileges.actionlevel.SubjectBasedActionPrivileges; +import org.opensearch.security.securityconf.FlattenedActionGroups; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; +import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.User; +import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; + +import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; + +public class PrivilegesEvaluator implements org.opensearch.security.privileges.PrivilegesEvaluator { + + private static final String USER_TENANT = "__user__"; + + static final WildcardMatcher DNFOF_MATCHER = WildcardMatcher.from( + ImmutableList.of( + "indices:data/read/*", + "indices:admin/mappings/fields/get*", + "indices:admin/shards/search_shards", + "indices:admin/resolve/index", + "indices:monitor/settings/get", + "indices:monitor/stats", + "indices:admin/aliases/get" + ) + ); + + private static final WildcardMatcher ACTION_MATCHER = WildcardMatcher.from("indices:data/read/*search*"); + + private static final IndicesOptions ALLOW_EMPTY = IndicesOptions.fromOptions(true, true, false, false); + + protected final Logger log = LogManager.getLogger(this.getClass()); + private final Supplier clusterStateSupplier; + + private final IndexNameExpressionResolver resolver; + + private final AuditLog auditLog; + private ThreadContext threadContext; + + private final PrivilegesInterceptor privilegesInterceptor; + + private final boolean checkSnapshotRestoreWritePrivileges; + + private final SnapshotRestoreEvaluator snapshotRestoreEvaluator; + private final SystemIndexAccessEvaluator systemIndexAccessEvaluator; + private final ProtectedIndexAccessEvaluator protectedIndexAccessEvaluator; + private final TermsAggregationEvaluator termsAggregationEvaluator; + private final Settings settings; + private final AtomicReference actionPrivileges = new AtomicReference<>(); + private final Map pluginIdToActionPrivileges = new HashMap<>(); + private final IndicesRequestResolver indicesRequestResolver; + private final IndicesRequestModifier indicesRequestModifier = new IndicesRequestModifier(); + private final RoleMapper roleMapper; + private final ThreadPool threadPool; + + private volatile boolean dnfofEnabled = false; + private volatile boolean dnfofForEmptyResultsEnabled = false; + private volatile String filteredAliasMode = null; + + public PrivilegesEvaluator( + final ClusterService clusterService, + Supplier clusterStateSupplier, + RoleMapper roleMapper, + ThreadPool threadPool, + final ThreadContext threadContext, + final IndexNameExpressionResolver resolver, + AuditLog auditLog, + final Settings settings, + final PrivilegesInterceptor privilegesInterceptor, + FlattenedActionGroups actionGroups, + FlattenedActionGroups staticActionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration, + Map pluginIdToRolePrivileges + ) { + + super(); + this.resolver = resolver; + this.auditLog = auditLog; + this.roleMapper = roleMapper; + + this.threadContext = threadContext; + this.threadPool = threadPool; + this.privilegesInterceptor = privilegesInterceptor; + this.clusterStateSupplier = clusterStateSupplier; + this.settings = settings; + + this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean( + ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, + ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES + ); + + Supplier isLocalNodeElectedClusterManager = clusterService != null + ? () -> clusterService.state().nodes().isLocalNodeElectedClusterManager() + : () -> false; + + snapshotRestoreEvaluator = new SnapshotRestoreEvaluator(settings, auditLog, isLocalNodeElectedClusterManager); + systemIndexAccessEvaluator = new SystemIndexAccessEvaluator(settings, auditLog); + protectedIndexAccessEvaluator = new ProtectedIndexAccessEvaluator(settings, auditLog); + termsAggregationEvaluator = new TermsAggregationEvaluator(); + this.indicesRequestResolver = new LegacyIndicesRequestResolver(resolver, isLocalNodeElectedClusterManager); + + this.pluginIdToActionPrivileges.putAll(createActionPrivileges(pluginIdToRolePrivileges, staticActionGroups)); + this.updateConfiguration(actionGroups, rolesConfiguration, generalConfiguration); + + } + + @Override + public void updateConfiguration( + FlattenedActionGroups flattenedActionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration + ) { + this.dnfofEnabled = isDnfofEnabled(generalConfiguration); + this.dnfofForEmptyResultsEnabled = isDnfofEmptyEnabled(generalConfiguration); + this.filteredAliasMode = getFilteredAliasMode(generalConfiguration); + + try { + RoleBasedActionPrivileges actionPrivileges = new RoleBasedActionPrivileges( + rolesConfiguration, + flattenedActionGroups, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + settings, + true + ); + Metadata metadata = clusterStateSupplier.get().metadata(); + actionPrivileges.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); + RoleBasedActionPrivileges oldInstance = this.actionPrivileges.getAndSet(actionPrivileges); + + if (oldInstance != null) { + oldInstance.clusterStateMetadataDependentPrivileges().shutdown(); + } + } catch (Exception e) { + log.error("Error while updating ActionPrivileges", e); + } + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public PrivilegesEvaluationContext createContext(User user, String action) { + return createContext(user, action, null, ActionRequestMetadata.empty(), null); + } + + @Override + public PrivilegesEvaluationContext createContext( + User user, + String action0, + ActionRequest request, + ActionRequestMetadata actionRequestMetadata, + Task task + ) { + TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + + ActionPrivileges actionPrivileges; + ImmutableSet mappedRoles; + + if (user.isPluginUser()) { + mappedRoles = ImmutableSet.of(); + actionPrivileges = this.pluginIdToActionPrivileges.get(user.getName()); + if (actionPrivileges == null) { + actionPrivileges = ActionPrivileges.EMPTY; + } + } else { + mappedRoles = this.roleMapper.map(user, caller); + actionPrivileges = this.actionPrivileges.get(); + } + + return new PrivilegesEvaluationContext( + user, + mappedRoles, + action0, + request, + actionRequestMetadata, + task, + resolver, + indicesRequestResolver, + clusterStateSupplier, + actionPrivileges + ); + } + + @Override + public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { + + String action0 = context.getAction(); + ImmutableSet mappedRoles = context.getMappedRoles(); + User user = context.getUser(); + ActionRequest request = context.getRequest(); + Task task = context.getTask(); + + if (action0.startsWith("internal:indices/admin/upgrade")) { + action0 = "indices:admin/upgrade"; + } + + if (AutoCreateAction.NAME.equals(action0)) { + action0 = CreateIndexAction.NAME; + } + + if (AutoPutMappingAction.NAME.equals(action0)) { + action0 = PutMappingAction.NAME; + } + + PrivilegesEvaluatorResponse presponse; + + final boolean isDebugEnabled = log.isDebugEnabled(); + if (isDebugEnabled) { + log.debug("Evaluate permissions for {}", user); + log.debug("Action: {} ({})", action0, request.getClass().getSimpleName()); + log.debug("Mapped roles: {}", mappedRoles.toString()); + } + + ActionPrivileges actionPrivileges = context.getActionPrivileges(); + if (actionPrivileges == null) { + throw new OpenSearchSecurityException("OpenSearch Security is not initialized: roles configuration is missing"); + } + + if (request instanceof BulkRequest && (Strings.isNullOrEmpty(user.getRequestedTenant()))) { + // Shortcut for bulk actions. The details are checked on the lower level of the BulkShardRequests (Action + // indices:data/write/bulk[s]). + // This shortcut is only possible if the default tenant is selected, as we might need to rewrite the request for non-default + // tenants. + // No further access check for the default tenant is necessary, as access will be also checked on the TransportShardBulkAction + // level. + + presponse = actionPrivileges.hasClusterPrivilege(context, action0); + + if (!presponse.isAllowed()) { + log.info( + "No cluster-level perm match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + action0, + mappedRoles, + presponse.getMissingPrivileges() + ); + } + return presponse; + } + + OptionallyResolvedIndices optionallyResolvedIndices = context.getResolvedRequest(); + + if (isDebugEnabled) { + if (request instanceof IndicesRequest indicesRequest) { + log.debug("IndicesRequest: {} {}", indicesRequest.indices(), indicesRequest.indicesOptions()); + } + log.debug("ResolvedIndices: {}", optionallyResolvedIndices); + } + + // check snapshot/restore requests + // NOTE: Has to go first as restore request could be for protected and/or system indices and the request may + // fail with 403 if system index or protected index evaluators are triggered first + presponse = snapshotRestoreEvaluator.evaluate(request, task, action0); + if (presponse != null) { + return presponse; + } + + // System index access + presponse = systemIndexAccessEvaluator.evaluate(request, task, action0, optionallyResolvedIndices, context, actionPrivileges, user); + if (presponse != null) { + return presponse; + } + + // Protected index access + presponse = protectedIndexAccessEvaluator.evaluate(request, task, action0, optionallyResolvedIndices, mappedRoles); + if (presponse != null) { + return presponse; + } + + if (GetAllPitsAction.NAME.equals(action0) && dnfofEnabled) { + // We preserve old behavior here: The "indices:data/read/point_in_time/readall" is allowed when I have privileges on any + // actions. + // This is okay, as the action name includes "readall" anyway. In the new privilege evaluation, this is a cluster privilege. + return actionPrivileges.hasIndexPrivilegeForAnyIndex(context, Set.of(action0)); + } + + if (request instanceof AnalyzeAction.Request + && optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices + && resolvedIndices.isEmpty()) { + // If we have an AnalyzeRequest which does not refer to any index, the user is going to execute an + // index independent analyze action. + } + + final boolean dnfofEnabled = this.dnfofEnabled; + + final boolean isTraceEnabled = log.isTraceEnabled(); + if (isTraceEnabled) { + log.trace("dnfof enabled? {}", dnfofEnabled); + } + + final boolean serviceAccountUser = user.isServiceAccount(); + if (isClusterPermission(action0)) { + if (serviceAccountUser) { + log.info("{} is a service account which doesn't have access to cluster level permission: {}", user, action0); + return PrivilegesEvaluatorResponse.insufficient(action0); + } + + presponse = actionPrivileges.hasClusterPrivilege(context, action0); + + if (!presponse.isAllowed()) { + log.info( + "No cluster-level perm match for {} {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + optionallyResolvedIndices, + action0, + mappedRoles, + presponse.getMissingPrivileges() + ); + return presponse; + } else { + + if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { + if (isDebugEnabled) { + log.debug("Normally allowed but we need to apply some extra checks for a restore request."); + } + } else { + if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { + + final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action0, + user, + context + ); + + if (isDebugEnabled) { + log.debug("Result from privileges interceptor for cluster perm: {}", replaceResult); + } + + if (!replaceResult.continueEvaluation) { + if (replaceResult.accessDenied) { + auditLog.logMissingPrivileges(action0, request, task); + return PrivilegesEvaluatorResponse.insufficient(action0); + } else { + return PrivilegesEvaluatorResponse.ok(replaceResult.createIndexRequestBuilder); + } + } + } + + if (isDebugEnabled) { + log.debug("Allowed because we have cluster permissions for {}", action0); + } + return presponse; + } + } + } + + if (DocumentAllowList.isAllowed(request, threadContext)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // term aggregations + presponse = termsAggregationEvaluator.evaluate(optionallyResolvedIndices, request, context, actionPrivileges); + if (presponse != null) { + return presponse; + } + + ImmutableSet allIndexPermsRequired = evaluateAdditionalIndexPermissions(request, action0); + + if (isDebugEnabled) { + log.debug( + "Requested {} from {}", + allIndexPermsRequired, + threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS) + ); + } + + if (isDebugEnabled) { + log.debug("Requested resolved index types: {}", optionallyResolvedIndices); + log.debug("Security roles: {}", mappedRoles); + } + + // TODO exclude Security index + + if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { + + final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action0, + user, + context + ); + + if (isDebugEnabled) { + log.debug("Result from privileges interceptor: {}", replaceResult); + } + + if (!replaceResult.continueEvaluation) { + if (replaceResult.accessDenied) { + auditLog.logMissingPrivileges(action0, request, task); + return PrivilegesEvaluatorResponse.insufficient(action0); + } else { + return PrivilegesEvaluatorResponse.ok(replaceResult.createIndexRequestBuilder); + } + } + } + + boolean dnfofPossible = dnfofEnabled && DNFOF_MATCHER.test(action0); + + if (optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices && resolvedIndices.isEmpty()) { + // If the request is empty, the normal privilege checks would just pass because technically the question + // "are all indices authorized" is true if the set of indices is empty. This means that certain operations + // would be available to any users regardless of their privileges. Thus, we check first whether the user + // has *any* privilege for the given action. + // The main example for such actions is the _analyze action which can operate on indices, but also can + // operate on an empty set of indices. Without this check, it would be always allowed. + // Note: This is a change from previous versions of OpenSearch. The old IndexResolverReplacer would + // get the state of the no-index AnalyzeAction.Request wrong and would always produce an "IndexNotFoundException" + // for analyze requests without any index. + PrivilegesEvaluatorResponse anyPrivilegesResult = actionPrivileges.hasIndexPrivilegeForAnyIndex(context, allIndexPermsRequired); + if (!anyPrivilegesResult.isAllowed()) { + return anyPrivilegesResult; + } + } + + presponse = actionPrivileges.hasIndexPrivilege(context, allIndexPermsRequired, optionallyResolvedIndices); + + if (presponse.isPartiallyOk()) { + if (dnfofPossible && optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices) { + if (this.indicesRequestModifier.setLocalIndices(request, resolvedIndices, presponse.getAvailableIndices())) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } else if (!presponse.isAllowed()) { + if (dnfofPossible && dnfofForEmptyResultsEnabled && request instanceof IndicesRequest.Replaceable) { + ((IndicesRequest.Replaceable) request).indices(new String[0]); + + if (request instanceof SearchRequest) { + ((SearchRequest) request).indicesOptions(ALLOW_EMPTY); + } else if (request instanceof ClusterSearchShardsRequest) { + ((ClusterSearchShardsRequest) request).indicesOptions(ALLOW_EMPTY); + } else if (request instanceof GetFieldMappingsRequest) { + ((GetFieldMappingsRequest) request).indicesOptions(ALLOW_EMPTY); + } + + return PrivilegesEvaluatorResponse.ok(); + } + } + + if (presponse.isAllowed()) { + if (checkFilteredAliases(optionallyResolvedIndices, action0, isDebugEnabled)) { + return PrivilegesEvaluatorResponse.insufficient(action0) + .reason("It is not possible to read from indices with more than two filtered aliases"); + } + + if (isDebugEnabled) { + log.debug("Allowed because we have all indices permissions for {}", action0); + } + } else { + log.info( + "No {}-level perm match for {} {}: {} [Action [{}]] [RolesChecked {}]", + "index", + user, + optionallyResolvedIndices, + presponse.getReason(), + action0, + mappedRoles + ); + log.info("Index to privilege matrix:\n{}", presponse.getPrivilegeMatrix()); + if (presponse.hasEvaluationExceptions()) { + log.info("Evaluation errors:\n{}", presponse.getEvaluationExceptionInfo()); + } + } + + return presponse; + } + + @Override + public void updateClusterStateMetadata(ClusterService clusterService) { + RoleBasedActionPrivileges actionPrivileges = this.actionPrivileges.get(); + if (actionPrivileges != null) { + actionPrivileges.clusterStateMetadataDependentPrivileges().updateClusterStateMetadataAsync(clusterService, threadPool); + } + } + + @Override + public void shutdown() { + RoleBasedActionPrivileges roleBasedActionPrivileges = this.actionPrivileges.get(); + if (roleBasedActionPrivileges != null) { + roleBasedActionPrivileges.clusterStateMetadataDependentPrivileges().shutdown(); + } + } + + @Override + public boolean notFailOnForbiddenEnabled() { + return dnfofEnabled; + } + + private ImmutableSet evaluateAdditionalIndexPermissions(final ActionRequest request, final String originalAction) { + ImmutableSet.Builder additionalPermissionsRequired = ImmutableSet.builder(); + + if (!isClusterPermission(originalAction)) { + additionalPermissionsRequired.add(originalAction); + } + + if (request instanceof ClusterSearchShardsRequest) { + additionalPermissionsRequired.add(SearchAction.NAME); + } + + if (request instanceof BulkShardRequest) { + BulkShardRequest bsr = (BulkShardRequest) request; + for (BulkItemRequest bir : bsr.items()) { + switch (bir.request().opType()) { + case CREATE: + additionalPermissionsRequired.add(IndexAction.NAME); + break; + case INDEX: + additionalPermissionsRequired.add(IndexAction.NAME); + break; + case DELETE: + additionalPermissionsRequired.add(DeleteAction.NAME); + break; + case UPDATE: + additionalPermissionsRequired.add(UpdateAction.NAME); + break; + } + } + } + + if (request instanceof IndicesAliasesRequest) { + IndicesAliasesRequest bsr = (IndicesAliasesRequest) request; + for (AliasActions bir : bsr.getAliasActions()) { + switch (bir.actionType()) { + case REMOVE_INDEX: + additionalPermissionsRequired.add(DeleteIndexAction.NAME); + break; + default: + break; + } + } + } + + if (request instanceof CreateIndexRequest) { + CreateIndexRequest cir = (CreateIndexRequest) request; + if (cir.aliases() != null && !cir.aliases().isEmpty()) { + additionalPermissionsRequired.add(IndicesAliasesAction.NAME); + } + } + + if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { + additionalPermissionsRequired.addAll(ConfigConstants.SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES); + } + + ImmutableSet result = additionalPermissionsRequired.build(); + + if (result.size() > 1) { + traceAction("Additional permissions required: {}", result); + } + + if (log.isDebugEnabled() && result.size() > 1) { + log.debug("Additional permissions required: {}", result); + } + + return result; + } + + @Override + public boolean isClusterPermission(String action) { + return isClusterPermissionStatic(action); + } + + static boolean isClusterPermissionStatic(String action0) { + return (action0.startsWith("cluster:") + || action0.startsWith("indices:admin/template/") + || action0.startsWith("indices:admin/index_template/") + || action0.startsWith(SearchScrollAction.NAME) + || (action0.equals(BulkAction.NAME)) + || (action0.equals(MultiGetAction.NAME)) + || (action0.startsWith(MultiSearchAction.NAME)) + || (action0.equals(MultiTermVectorsAction.NAME)) + || (action0.equals(ReindexAction.NAME)) + || (action0.equals(RenderSearchTemplateAction.NAME))); + } + + @SuppressWarnings("unchecked") + private boolean checkFilteredAliases(OptionallyResolvedIndices optionallyRequestedResolved, String action, boolean isDebugEnabled) { + final String faMode = this.filteredAliasMode; + + if (!"disallow".equals(faMode)) { + return false; + } + + if (!ACTION_MATCHER.test(action)) { + return false; + } + + if (!(optionallyRequestedResolved instanceof ResolvedIndices requestedResolved)) { + return false; + } + + Iterable indexMetaDataCollection; + + Set indexMetaDataSet = new HashSet<>(requestedResolved.local().names().size()); + + for (String requestAliasOrIndex : requestedResolved.local().names()) { + IndexMetadata indexMetaData = clusterStateSupplier.get().getMetadata().getIndices().get(requestAliasOrIndex); + if (indexMetaData == null) { + if (isDebugEnabled) { + log.debug("{} does not exist in cluster metadata", requestAliasOrIndex); + } + continue; + } + + indexMetaDataSet.add(indexMetaData); + } + + indexMetaDataCollection = indexMetaDataSet; + + // check filtered aliases + for (IndexMetadata indexMetaData : indexMetaDataCollection) { + + final List filteredAliases = new ArrayList(); + + final Map aliases = indexMetaData.getAliases(); + + if (aliases != null && aliases.size() > 0) { + if (isDebugEnabled) { + log.debug("Aliases for {}: {}", indexMetaData.getIndex().getName(), aliases); + } + + final Iterator it = aliases.keySet().iterator(); + while (it.hasNext()) { + final String alias = it.next(); + final AliasMetadata aliasMetadata = aliases.get(alias); + + if (aliasMetadata != null && aliasMetadata.filteringRequired()) { + filteredAliases.add(aliasMetadata); + if (isDebugEnabled) { + log.debug("{} is a filtered alias {}", alias, aliasMetadata.getFilter()); + } + } else { + if (isDebugEnabled) { + log.debug("{} is not an alias or does not have a filter", alias); + } + } + } + } + + if (filteredAliases.size() > 1 && ACTION_MATCHER.test(action)) { + // TODO add queries as dls queries (works only if dls module is installed) + log.error( + "More than one ({}) filtered alias found for same index ({}). This is currently not supported. Aliases: {}", + filteredAliases.size(), + indexMetaData.getIndex().getName(), + toString(filteredAliases) + ); + return true; + } + } // end-for + + return false; + } + + private List toString(List aliases) { + if (aliases == null || aliases.size() == 0) { + return Collections.emptyList(); + } + + final List ret = new ArrayList<>(aliases.size()); + + for (final AliasMetadata amd : aliases) { + if (amd != null) { + ret.add(amd.alias()); + } + } + + return Collections.unmodifiableList(ret); + } + + private static Map createActionPrivileges( + Map pluginIdToRolePrivileges, + FlattenedActionGroups staticActionGroups + ) { + Map result = new HashMap<>(pluginIdToRolePrivileges.size()); + + for (Map.Entry entry : pluginIdToRolePrivileges.entrySet()) { + result.put( + entry.getKey(), + new SubjectBasedActionPrivileges( + entry.getValue(), + staticActionGroups, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + true + ) + ); + } + + return result; + } + + private static boolean isDnfofEnabled(ConfigV7 generalConfiguration) { + return generalConfiguration.dynamic != null && generalConfiguration.dynamic.do_not_fail_on_forbidden; + } + + private static boolean isDnfofEmptyEnabled(ConfigV7 generalConfiguration) { + return generalConfiguration.dynamic != null && generalConfiguration.dynamic.do_not_fail_on_forbidden_empty; + } + + private static String getFilteredAliasMode(ConfigV7 generalConfiguration) { + return generalConfiguration.dynamic != null ? generalConfiguration.dynamic.filtered_alias_mode : "none"; + } + +} diff --git a/src/main/java/org/opensearch/security/privileges/ProtectedIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/ProtectedIndexAccessEvaluator.java similarity index 77% rename from src/main/java/org/opensearch/security/privileges/ProtectedIndexAccessEvaluator.java rename to src/main/java/org/opensearch/security/privileges/actionlevel/legacy/ProtectedIndexAccessEvaluator.java index 877e6fd787..13c9d54a66 100644 --- a/src/main/java/org/opensearch/security/privileges/ProtectedIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/ProtectedIndexAccessEvaluator.java @@ -9,7 +9,7 @@ * GitHub history for details. */ -package org.opensearch.security.privileges; +package org.opensearch.security.privileges.actionlevel.legacy; import java.util.ArrayList; import java.util.List; @@ -21,9 +21,10 @@ import org.opensearch.action.ActionRequest; import org.opensearch.action.RealtimeRequest; import org.opensearch.action.search.SearchRequest; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; import org.opensearch.common.settings.Settings; import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.tasks.Task; @@ -71,32 +72,22 @@ public PrivilegesEvaluatorResponse evaluate( final ActionRequest request, final Task task, final String action, - final IndexResolverReplacer.Resolved requestedResolved, - final PrivilegesEvaluatorResponse presponse, + final OptionallyResolvedIndices requestedResolved, final Set mappedRoles ) { if (!protectedIndexEnabled) { - return presponse; - } - if (!requestedResolved.isLocalAll() - && indexMatcher.matchAny(requestedResolved.getAllIndices()) - && deniedActionMatcher.test(action) - && !allowedRolesMatcher.matchAny(mappedRoles)) { - auditLog.logMissingPrivileges(action, request, task); - log.warn("{} for '{}' index/indices is not allowed for a regular user", action, indexMatcher); - presponse.allowed = false; - return presponse.markComplete(); + return null; } - if (requestedResolved.isLocalAll() && deniedActionMatcher.test(action) && !allowedRolesMatcher.matchAny(mappedRoles)) { + boolean containsProtectedIndex = requestedResolved.local().containsAny(indexMatcher); + + if (containsProtectedIndex && deniedActionMatcher.test(action) && !allowedRolesMatcher.matchAny(mappedRoles)) { auditLog.logMissingPrivileges(action, request, task); - log.warn("{} for '_all' indices is not allowed for a regular user", action); - presponse.allowed = false; - return presponse.markComplete(); + log.warn("{} for '{}' index/indices is not allowed for a regular user", action, indexMatcher); + return PrivilegesEvaluatorResponse.insufficient(action); } - if ((requestedResolved.isLocalAll() || indexMatcher.matchAny(requestedResolved.getAllIndices())) - && !allowedRolesMatcher.matchAny(mappedRoles)) { + if (containsProtectedIndex && !allowedRolesMatcher.matchAny(mappedRoles)) { final boolean isDebugEnabled = log.isDebugEnabled(); if (request instanceof SearchRequest) { ((SearchRequest) request).requestCache(Boolean.FALSE); @@ -112,6 +103,6 @@ public PrivilegesEvaluatorResponse evaluate( } } } - return presponse; + return null; } } diff --git a/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SnapshotRestoreEvaluator.java similarity index 75% rename from src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java rename to src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SnapshotRestoreEvaluator.java index 23612e1a52..8e03c3e40e 100644 --- a/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SnapshotRestoreEvaluator.java @@ -24,9 +24,10 @@ * GitHub history for details. */ -package org.opensearch.security.privileges; +package org.opensearch.security.privileges.actionlevel.legacy; import java.util.List; +import java.util.function.Supplier; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -35,7 +36,7 @@ import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; import org.opensearch.common.settings.Settings; import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.configuration.ClusterInfoHolder; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.SnapshotRestoreHelper; import org.opensearch.tasks.Task; @@ -47,8 +48,13 @@ public class SnapshotRestoreEvaluator { private final String securityIndex; private final AuditLog auditLog; private final boolean restoreSecurityIndexEnabled; + private final Supplier isLocalNodeElectedClusterManagerSupplier; - public SnapshotRestoreEvaluator(final Settings settings, AuditLog auditLog) { + public SnapshotRestoreEvaluator( + final Settings settings, + AuditLog auditLog, + Supplier isLocalNodeElectedClusterManagerSupplier + ) { this.enableSnapshotRestorePrivilege = settings.getAsBoolean( ConfigConstants.SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE, ConfigConstants.SECURITY_DEFAULT_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE @@ -60,37 +66,29 @@ public SnapshotRestoreEvaluator(final Settings settings, AuditLog auditLog) { ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX ); this.auditLog = auditLog; + this.isLocalNodeElectedClusterManagerSupplier = isLocalNodeElectedClusterManagerSupplier; } - public PrivilegesEvaluatorResponse evaluate( - final ActionRequest request, - final Task task, - final String action, - final ClusterInfoHolder clusterInfoHolder, - final PrivilegesEvaluatorResponse presponse - ) { + public PrivilegesEvaluatorResponse evaluate(final ActionRequest request, final Task task, final String action) { if (!(request instanceof RestoreSnapshotRequest)) { - return presponse; + return null; } // snapshot restore for regular users not enabled if (!enableSnapshotRestorePrivilege) { log.warn("{} is not allowed for a regular user", action); - presponse.allowed = false; - return presponse.markComplete(); + return PrivilegesEvaluatorResponse.insufficient(action); } // if this feature is enabled, users can also snapshot and restore // the Security index and the global state if (restoreSecurityIndexEnabled) { - presponse.allowed = true; - return presponse; + return null; } - if (clusterInfoHolder.isLocalNodeElectedClusterManager() == Boolean.FALSE) { - presponse.allowed = true; - return presponse.markComplete(); + if (!isLocalNodeElectedClusterManagerSupplier.get()) { + return PrivilegesEvaluatorResponse.ok(); } final RestoreSnapshotRequest restoreRequest = (RestoreSnapshotRequest) request; @@ -99,8 +97,7 @@ public PrivilegesEvaluatorResponse evaluate( if (restoreRequest.includeGlobalState()) { auditLog.logSecurityIndexAttempt(request, action, task); log.warn("{} with 'include_global_state' enabled is not allowed", action); - presponse.allowed = false; - return presponse.markComplete(); + return PrivilegesEvaluatorResponse.insufficient(action).reason("'include_global_state' is not allowed"); } final List rs = SnapshotRestoreHelper.resolveOriginalIndices(restoreRequest); @@ -108,9 +105,9 @@ public PrivilegesEvaluatorResponse evaluate( if (rs != null && (rs.contains(securityIndex) || rs.contains("_all") || rs.contains("*"))) { auditLog.logSecurityIndexAttempt(request, action, task); log.warn("{} for '{}' as source index is not allowed", action, securityIndex); - presponse.allowed = false; - return presponse.markComplete(); + return PrivilegesEvaluatorResponse.insufficient(action).reason(securityIndex + " as source index is not allowed"); } - return presponse; + + return null; } } diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java similarity index 53% rename from src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java rename to src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java index 68cd42a7a8..81af94c9fc 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java @@ -24,12 +24,13 @@ * GitHub history for details. */ -package org.opensearch.security.privileges; +package org.opensearch.security.privileges.actionlevel.legacy; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import com.google.common.collect.ImmutableSet; @@ -37,19 +38,28 @@ import org.apache.logging.log4j.Logger; import org.opensearch.action.ActionRequest; +import org.opensearch.action.IndicesRequest; import org.opensearch.action.RealtimeRequest; +import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotAction; +import org.opensearch.action.search.GetAllPitsAction; import org.opensearch.action.search.SearchRequest; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; +import org.opensearch.common.regex.Regex; import org.opensearch.common.settings.Settings; import org.opensearch.indices.SystemIndexRegistry; import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; +import org.opensearch.security.privileges.ActionPrivileges; +import org.opensearch.security.privileges.IndicesRequestModifier; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; -import static org.opensearch.security.privileges.PrivilegesEvaluator.isClusterPerm; +import static org.opensearch.security.privileges.actionlevel.legacy.PrivilegesEvaluator.isClusterPermissionStatic; /** * This class performs authorization on requests targeting system indices @@ -63,30 +73,27 @@ public class SystemIndexAccessEvaluator { private final String securityIndex; private final AuditLog auditLog; - private final IndexResolverReplacer irr; private final boolean filterSecurityIndex; // for system-indices configuration private final WildcardMatcher systemIndexMatcher; - private final WildcardMatcher superAdminAccessOnlyIndexMatcher; private final WildcardMatcher deniedActionsMatcher; private final boolean isSystemIndexEnabled; private final boolean isSystemIndexPermissionEnabled; private final static ImmutableSet SYSTEM_INDEX_PERMISSION_SET = ImmutableSet.of(ConfigConstants.SYSTEM_INDEX_PERMISSION); + private final IndicesRequestModifier indicesRequestModifier = new IndicesRequestModifier(); - public SystemIndexAccessEvaluator(final Settings settings, AuditLog auditLog, IndexResolverReplacer irr) { + public SystemIndexAccessEvaluator(final Settings settings, AuditLog auditLog) { this.securityIndex = settings.get( ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX ); this.auditLog = auditLog; - this.irr = irr; this.filterSecurityIndex = settings.getAsBoolean(ConfigConstants.SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS, false); this.systemIndexMatcher = WildcardMatcher.from( settings.getAsList(ConfigConstants.SECURITY_SYSTEM_INDICES_KEY, ConfigConstants.SECURITY_SYSTEM_INDICES_DEFAULT) ); - this.superAdminAccessOnlyIndexMatcher = WildcardMatcher.from(this.securityIndex); this.isSystemIndexEnabled = settings.getAsBoolean( ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY, ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_DEFAULT @@ -127,17 +134,25 @@ public PrivilegesEvaluatorResponse evaluate( final ActionRequest request, final Task task, final String action, - final Resolved requestedResolved, - final PrivilegesEvaluatorResponse presponse, + final OptionallyResolvedIndices requestedResolved, final PrivilegesEvaluationContext context, final ActionPrivileges actionPrivileges, final User user ) { - evaluateSystemIndicesAccess(action, requestedResolved, request, task, presponse, context, actionPrivileges, user); + boolean containsSystemIndex = requestedResolved.local().containsAny(this::isSystemIndex); + + PrivilegesEvaluatorResponse response = evaluateSystemIndicesAccess( + action, + requestedResolved, + request, + task, + context, + actionPrivileges, + user, + containsSystemIndex + ); - if (requestedResolved.isLocalAll() - || requestedResolved.getAllIndices().contains(securityIndex) - || requestContainsAnySystemIndices(requestedResolved)) { + if (containsSystemIndex) { if (request instanceof SearchRequest) { ((SearchRequest) request).requestCache(Boolean.FALSE); @@ -153,70 +168,20 @@ public PrivilegesEvaluatorResponse evaluate( } } } - return presponse; - } - /** - * Checks if request is for any system index - * @param requestedResolved request which contains indices to be matched against system indices - * @return true if a match is found, false otherwise - */ - private boolean requestContainsAnySystemIndices(final Resolved requestedResolved) { - return !getAllSystemIndices(requestedResolved).isEmpty(); + return response; } - /** - * Gets all indices requested in the original request. - * It will always return security index if it is present in the request, as security index is protected regardless - * of feature being enabled or disabled - * @param requestedResolved request which contains indices to be matched against system indices - * @return the set of protected system indices present in the request - */ - private Set getAllSystemIndices(final Resolved requestedResolved) { - final Set systemIndices = requestedResolved.getAllIndices() - .stream() - .filter(securityIndex::equals) - .collect(Collectors.toSet()); - if (isSystemIndexEnabled) { - systemIndices.addAll(systemIndexMatcher.getMatchAny(requestedResolved.getAllIndices(), Collectors.toList())); - systemIndices.addAll(SystemIndexRegistry.matchesSystemIndexPattern(requestedResolved.getAllIndices())); + private boolean isSystemIndex(String index) { + if (this.securityIndex.equals(index)) { + return true; } - return systemIndices; - } - - /** - * Checks if request contains any system index that is non-permission-able - * NOTE: Security index is currently non-permission-able - * @param requestedResolved request which contains indices to be matched against non-permission-able system indices - * @return true if the request contains any non-permission-able index,false otherwise - */ - private boolean requestContainsAnyProtectedSystemIndices(final Resolved requestedResolved) { - return !getAllProtectedSystemIndices(requestedResolved).isEmpty(); - } - - /** - * Filters the request to get all system indices that are protected and are non-permission-able - * @param requestedResolved request which contains indices to be matched against non-permission-able system indices - * @return the list of protected system indices present in the request - */ - private List getAllProtectedSystemIndices(final Resolved requestedResolved) { - return new ArrayList<>(superAdminAccessOnlyIndexMatcher.getMatchAny(requestedResolved.getAllIndices(), Collectors.toList())); - } - /** - * Checks if the request contains any regular (non-system and non-protected) indices. - * Regular indices are those that are not categorized as system indices or protected system indices. - * This method helps in identifying requests that might be accessing regular indices alongside system indices. - * @param requestedResolved The resolved object of the request, which contains the list of indices from the original request. - * @return true if the request contains any regular indices, false otherwise. - */ - private boolean requestContainsAnyRegularIndices(final Resolved requestedResolved) { - Set allIndices = requestedResolved.getAllIndices(); - - Set allSystemIndices = getAllSystemIndices(requestedResolved); - List allProtectedSystemIndices = getAllProtectedSystemIndices(requestedResolved); - - return allIndices.stream().anyMatch(index -> !allSystemIndices.contains(index) && !allProtectedSystemIndices.contains(index)); + if (this.isSystemIndexEnabled) { + return this.systemIndexMatcher.test(index) || SystemIndexRegistry.matchesSystemIndexPattern(index); + } else { + return false; + } } /** @@ -234,46 +199,42 @@ private boolean isActionAllowed(String action) { * @param requestedResolved this object contains all indices this request is resolved to * @param request the action request to be used for audit logging * @param task task in which this access check will be performed - * @param presponse the pre-response object that will eventually become a response and returned to the requester * @param context conveys information about user and mapped roles, etc. * @param actionPrivileges the up-to-date ActionPrivileges instance * @param user this user's permissions will be looked up */ - private void evaluateSystemIndicesAccess( + private PrivilegesEvaluatorResponse evaluateSystemIndicesAccess( final String action, - final Resolved requestedResolved, + final OptionallyResolvedIndices requestedResolved, final ActionRequest request, final Task task, - final PrivilegesEvaluatorResponse presponse, final PrivilegesEvaluationContext context, final ActionPrivileges actionPrivileges, - final User user + final User user, + final boolean containsSystemIndex ) { - // Perform access check is system index permissions are enabled - boolean containsSystemIndex = requestContainsAnySystemIndices(requestedResolved); - boolean containsRegularIndex = requestContainsAnyRegularIndices(requestedResolved); boolean serviceAccountUser = user.isServiceAccount(); - if (isSystemIndexPermissionEnabled) { + if (isSystemIndexPermissionEnabled + && (!isClusterPermissionStatic(action) || RestoreSnapshotAction.NAME.equals(action)) + && backwartsCompatGateForSystemIndexPrivileges(action, request)) { + boolean containsRegularIndex = requestedResolved.local().containsAny(index -> !isSystemIndex(index)); + if (serviceAccountUser && containsRegularIndex) { auditLog.logSecurityIndexAttempt(request, action, task); if (!containsSystemIndex && log.isInfoEnabled()) { log.info("{} not permitted for a service account {} on non-system indices.", action, context.getMappedRoles()); } else if (containsSystemIndex && log.isDebugEnabled()) { - List regularIndices = requestedResolved.getAllIndices() + List regularIndices = requestedResolved.local() + .names(context.clusterState()) .stream() - .filter( - index -> !getAllSystemIndices(requestedResolved).contains(index) - && !getAllProtectedSystemIndices(requestedResolved).contains(index) - ) + .filter(index -> !isSystemIndex(index)) .collect(Collectors.toList()); log.debug("Service account cannot access regular indices: {}", regularIndices); } - presponse.allowed = false; - presponse.markComplete(); - return; + return PrivilegesEvaluatorResponse.insufficient(action).reason("Service account cannot access regular indices"); } - boolean containsProtectedIndex = requestContainsAnyProtectedSystemIndices(requestedResolved); + boolean containsProtectedIndex = requestedResolved.local().containsAny(this.securityIndex::equals); if (containsProtectedIndex) { auditLog.logSecurityIndexAttempt(request, action, task); if (log.isInfoEnabled()) { @@ -281,12 +242,10 @@ private void evaluateSystemIndicesAccess( "{} not permitted for a regular user {} on protected system indices {}", action, context.getMappedRoles(), - String.join(", ", getAllProtectedSystemIndices(requestedResolved)) + String.join(", ", this.securityIndex) ); } - presponse.allowed = false; - presponse.markComplete(); - return; + return PrivilegesEvaluatorResponse.insufficient(action); } else if (containsSystemIndex && !actionPrivileges.hasExplicitIndexPrivilege(context, SYSTEM_INDEX_PERMISSION_SET, requestedResolved).isAllowed()) { auditLog.logSecurityIndexAttempt(request, action, task); @@ -295,100 +254,169 @@ private void evaluateSystemIndicesAccess( "No {} permission for user roles {} to System Indices {}", action, context.getMappedRoles(), - String.join(", ", getAllSystemIndices(requestedResolved)) + requestedResolved.local() + .names(context.clusterState()) + .stream() + .filter(this::isSystemIndex) + .collect(Collectors.joining(", ")) ); } - presponse.allowed = false; - presponse.markComplete(); - return; + return PrivilegesEvaluatorResponse.insufficient(action); } } // the following section should only be run for index actions - if (user.isPluginUser() && !isClusterPerm(action)) { + if (user.isPluginUser() && !isClusterPermissionStatic(action)) { if (this.isSystemIndexEnabled) { - Set matchingPluginIndices = SystemIndexRegistry.matchesPluginSystemIndexPattern( + PluginSystemIndexSelection pluginSystemIndexSelection = areIndicesPluginSystemIndices( + context, user.getName().replace("plugin:", ""), - requestedResolved.getAllIndices() + requestedResolved ); - if (requestedResolved.getAllIndices().equals(matchingPluginIndices)) { + if (pluginSystemIndexSelection == PluginSystemIndexSelection.CONTAINS_ONLY_PLUGIN_SYSTEM_INDICES) { // plugin is authorized to perform any actions on its own registered system indices - presponse.allowed = true; - presponse.markComplete(); - return; - } else { - Set matchingSystemIndices = SystemIndexRegistry.matchesSystemIndexPattern(requestedResolved.getAllIndices()); - matchingSystemIndices.removeAll(matchingPluginIndices); - // See if request matches other system indices not belong to the plugin - if (!matchingSystemIndices.isEmpty()) { - if (log.isInfoEnabled()) { - log.info( - "Plugin {} can only perform {} on it's own registered System Indices. System indices from request that match plugin's registered system indices: {}", - user.getName(), - action, - matchingPluginIndices - ); - } - presponse.allowed = false; - presponse.getMissingPrivileges(); - presponse.markComplete(); - return; + return PrivilegesEvaluatorResponse.ok(); + } else if (pluginSystemIndexSelection == PluginSystemIndexSelection.CONTAINS_OTHER_SYSTEM_INDICES) { + if (log.isInfoEnabled()) { + log.info( + "Plugin {} can only perform {} on it's own registered System Indices. Resolved indices: {}", + user.getName(), + action, + requestedResolved + ); } + return PrivilegesEvaluatorResponse.insufficient(action); } } else { // no system index protection and request originating from plugin, allow - presponse.allowed = true; - presponse.markComplete(); - return; + return PrivilegesEvaluatorResponse.ok(); } } if (isActionAllowed(action)) { - if (requestedResolved.isLocalAll()) { + if (!(requestedResolved instanceof ResolvedIndices resolvedIndices)) { if (filterSecurityIndex) { - irr.replace(request, false, "*", "-" + securityIndex); - if (log.isDebugEnabled()) { - log.debug( - "Filtered '{}' from {}, resulting list with *,-{} is {}", - securityIndex, - requestedResolved, - securityIndex, - irr.resolveRequest(request) - ); - } + // TODO + // irr.replace(request, false, "*", "-" + securityIndex); } else { auditLog.logSecurityIndexAttempt(request, action, task); log.warn("{} for '_all' indices is not allowed for a regular user", action); - presponse.allowed = false; - presponse.markComplete(); + return PrivilegesEvaluatorResponse.insufficient(action); } } // if system index is enabled and system index permissions are enabled we don't need to perform any further // checks as it has already been performed via hasExplicitIndexPermission else if (containsSystemIndex && !isSystemIndexPermissionEnabled) { if (filterSecurityIndex) { - Set allWithoutSecurity = new HashSet<>(requestedResolved.getAllIndices()); + Set allWithoutSecurity = new HashSet<>(requestedResolved.local().names(context.clusterState())); allWithoutSecurity.remove(securityIndex); if (allWithoutSecurity.isEmpty()) { if (log.isDebugEnabled()) { log.debug("Filtered '{}' but resulting list is empty", securityIndex); } - presponse.allowed = false; - presponse.markComplete(); - return; + return PrivilegesEvaluatorResponse.insufficient(action); } - irr.replace(request, false, allWithoutSecurity.toArray(new String[0])); + this.indicesRequestModifier.setLocalIndices(request, resolvedIndices, allWithoutSecurity); if (log.isDebugEnabled()) { log.debug("Filtered '{}', resulting list is {}", securityIndex, allWithoutSecurity); } } else { auditLog.logSecurityIndexAttempt(request, action, task); - final String foundSystemIndexes = String.join(", ", getAllSystemIndices(requestedResolved)); + final String foundSystemIndexes = requestedResolved.local() + .names(context.clusterState()) + .stream() + .filter(this::isSystemIndex) + .collect(Collectors.joining(", ")); log.warn("{} for '{}' index is not allowed for a regular user", action, foundSystemIndexes); - presponse.allowed = false; - presponse.markComplete(); + return PrivilegesEvaluatorResponse.insufficient(action); } } } + + return null; + } + + private PluginSystemIndexSelection areIndicesPluginSystemIndices( + PrivilegesEvaluationContext context, + String pluginClassName, + OptionallyResolvedIndices optionallyResolvedIndices + ) { + if (optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices) { + Predicate pluginSystemIndexPredicate = SystemIndexRegistry.getPluginSystemIndexPredicate(pluginClassName); + + boolean containsNonPluginSystemIndex = false; + boolean containsOtherSystemIndex = false; + + for (String index : resolvedIndices.local().namesOfIndices(context.clusterState())) { + if (!pluginSystemIndexPredicate.test(index)) { + containsNonPluginSystemIndex = true; + if (SystemIndexRegistry.matchesSystemIndexPattern(index)) { + containsOtherSystemIndex = true; + } + } + } + + if (!containsNonPluginSystemIndex) { + return PluginSystemIndexSelection.CONTAINS_ONLY_PLUGIN_SYSTEM_INDICES; + } else if (containsOtherSystemIndex) { + return PluginSystemIndexSelection.CONTAINS_OTHER_SYSTEM_INDICES; + } else { + return PluginSystemIndexSelection.NO_SYSTEM_INDICES; + } + } else { + // If we have an unknown state, we must assume that other system indices are contained + return PluginSystemIndexSelection.CONTAINS_OTHER_SYSTEM_INDICES; + } + } + + /** + * Previous versions of OpenSearch had the bug that indices requests on "*" (or _all) did not get checked + * in the block of evaluateSystemIndicesAccess() that checks for the explicit system index privileges. + * This is not nice, but also not a big problem, as write operations would be denied anyway at the end of + * the evaluateSystemIndicesAccess(). Read operations would be blocked anyway on the Lucene level. + * With the introduction of the ResolvedIndices object, we do not have a real notion of "is all" any more; + * thus, this method would now block many requests, even if these would be filtered out later by the DNFOF mode + * in PrivilegeEvaluator. + *

    + * To keep backwards compatibility, we have this method which disables the first block of evaluateSystemIndicesAccess() + * for the same cases that were previously skipping its execution. Of course, this is totally hacky, but there + * is no better way to keep the available functionality other than rewriting the whole logic; which is actually done + * in the next gen privilege evaluation code. + * @return true, if the explicit privilege check in evaluateSystemIndicesAccess() shall be executed; false it it shall + * be skipped. + */ + private boolean backwartsCompatGateForSystemIndexPrivileges(String action, ActionRequest actionRequest) { + if (GetAllPitsAction.NAME.equals(action)) { + // The indices:data/read/point_in_time/readall action does not give access to index contents + return false; + } + + if (!(actionRequest instanceof IndicesRequest indicesRequest)) { + // If we cannot resolve indices, we go into the explicit privilege check code; the code will then deny the request + return true; + } + + if (deniedActionsMatcher.test(action)) { + // If this is an action that manipulates documents or indices, we also need to do the explicit privilege check. + return true; + } + + String[] indices = indicesRequest.indices(); + boolean isAll = indices == null + || indices.length == 0 + || (indices.length == 1 && (indices[0] == null || Metadata.ALL.equals(indices[0]) || Regex.isMatchAllPattern(indices[0]))); + if (!isAll) { + // For non-is-all requests, previous versions also went through the checks + return true; + } else { + // For is-all requests, we can skip the checks; any data in the indices will be filtered out on the Lucene level + return false; + } + } + + enum PluginSystemIndexSelection { + CONTAINS_ONLY_PLUGIN_SYSTEM_INDICES, + CONTAINS_OTHER_SYSTEM_INDICES, + NO_SYSTEM_INDICES } } diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/TermsAggregationEvaluator.java b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/TermsAggregationEvaluator.java new file mode 100644 index 0000000000..7b45c72f16 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/TermsAggregationEvaluator.java @@ -0,0 +1,126 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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.privileges.actionlevel.legacy; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Streams; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.fieldcaps.FieldCapabilitiesAction; +import org.opensearch.action.get.GetAction; +import org.opensearch.action.get.MultiGetAction; +import org.opensearch.action.search.MultiSearchAction; +import org.opensearch.action.search.SearchAction; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; +import org.opensearch.index.query.MatchNoneQueryBuilder; +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.index.query.TermsQueryBuilder; +import org.opensearch.search.aggregations.AggregationBuilder; +import org.opensearch.search.aggregations.bucket.terms.TermsAggregationBuilder; +import org.opensearch.security.privileges.ActionPrivileges; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; + +public class TermsAggregationEvaluator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + + private static final ImmutableSet READ_ACTIONS = ImmutableSet.of( + MultiSearchAction.NAME, + MultiGetAction.NAME, + GetAction.NAME, + SearchAction.NAME, + FieldCapabilitiesAction.NAME + ); + + private static final QueryBuilder NONE_QUERY = new MatchNoneQueryBuilder(); + + public TermsAggregationEvaluator() {} + + public PrivilegesEvaluatorResponse evaluate( + OptionallyResolvedIndices optionallyResolvedIndices, + ActionRequest request, + PrivilegesEvaluationContext context, + ActionPrivileges actionPrivileges + ) { + // This is only applicable for SearchRequests and for present ResolvedIndices information (for SearchRequests that is usually the + // case) + if (!(request instanceof SearchRequest sr) || !(optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices)) { + return null; + } + + try { + + if (sr.source() != null + && sr.source().query() == null + && sr.source().aggregations() != null + && sr.source().aggregations().getAggregatorFactories() != null + && sr.source().aggregations().getAggregatorFactories().size() == 1 + && sr.source().size() == 0) { + AggregationBuilder ab = sr.source().aggregations().getAggregatorFactories().iterator().next(); + if (ab instanceof TermsAggregationBuilder && "terms".equals(ab.getType()) && "indices".equals(ab.getName())) { + if ("_index".equals(((TermsAggregationBuilder) ab).field()) + && ab.getPipelineAggregations().isEmpty() + && ab.getSubAggregations().isEmpty()) { + + PrivilegesEvaluatorResponse subResponse = actionPrivileges.hasIndexPrivilege( + context, + READ_ACTIONS, + ResolvedIndices.unknown() + ); + + if (subResponse.isPartiallyOk()) { + sr.source() + .query( + new TermsQueryBuilder( + "_index", + Streams.concat( + subResponse.getAvailableIndices().stream(), + resolvedIndices.remote().asRawExpressions().stream() + ).toArray(String[]::new) + ) + ); + } else if (!subResponse.isAllowed()) { + sr.source().query(NONE_QUERY); + } + + return PrivilegesEvaluatorResponse.ok(); + } + } + } + + } catch (Exception e) { + log.warn("Unable to evaluate terms aggregation", e); + } + + return null; + } +} diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/ActionConfiguration.java b/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/ActionConfiguration.java new file mode 100644 index 0000000000..34bd625163 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/ActionConfiguration.java @@ -0,0 +1,200 @@ +/* + * 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.privileges.actionlevel.nextgen; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.admin.indices.create.AutoCreateAction; +import org.opensearch.action.admin.indices.create.CreateIndexAction; +import org.opensearch.action.admin.indices.mapping.put.AutoPutMappingAction; +import org.opensearch.action.admin.indices.mapping.put.PutMappingAction; +import org.opensearch.action.admin.indices.template.delete.DeleteComposableIndexTemplateAction; +import org.opensearch.action.admin.indices.template.delete.DeleteIndexTemplateAction; +import org.opensearch.action.admin.indices.template.get.GetComposableIndexTemplateAction; +import org.opensearch.action.admin.indices.template.get.GetIndexTemplatesAction; +import org.opensearch.action.admin.indices.template.post.SimulateIndexTemplateAction; +import org.opensearch.action.admin.indices.template.post.SimulateTemplateAction; +import org.opensearch.action.admin.indices.template.put.PutComposableIndexTemplateAction; +import org.opensearch.action.admin.indices.template.put.PutIndexTemplateAction; +import org.opensearch.action.admin.indices.upgrade.post.UpgradeAction; +import org.opensearch.action.admin.indices.upgrade.post.UpgradeSettingsAction; +import org.opensearch.action.search.GetAllPitsAction; +import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Settings; +import org.opensearch.index.reindex.ReindexAction; +import org.opensearch.script.mustache.MultiSearchTemplateAction; +import org.opensearch.script.mustache.RenderSearchTemplateAction; +import org.opensearch.security.privileges.actionlevel.WellKnownActions; + +/** + * This class encapsulates some logic and configuration related to action names used in the PrivilegesEvaluator. + * It exposes a number of config options which affect the way action names are treated. See below. + *

    + * The purpose of these settings is mainly to have an emergency measure in case an action is incorrectly handled + * in PrivilegesEvaluator. That's why they are just documented in this class, but not in the user docs. + */ +class ActionConfiguration { + + /** + * This setting expects a list of action names (like "indices:data/read/search"); all action names + * that are listed here will be treated as "cluster privileges" by the PrivilegesEvaluator. + * That means privileges for these actions must be specified in the cluster_privileges section in roles.yml. + */ + public static Setting> FORCE_AS_CLUSTER_ACTIONS = Setting.listSetting( + "plugins.security.privileges_evaluation.actions.force_as_cluster_actions", + Collections.emptyList(), + Function.identity(), + Setting.Property.NodeScope + ); + + /** + * This setting expects a list of action mapping strings; these need to be formatted like + * "indices:data/write/bulk>indices:data/write/index". Whenever PrivilegesEvaluator receives an action called + * X, it will check in the mapping wheter X is mapped to Y. If so, it will check privileges for Y. + */ + public static Setting> MAP_ACTION_NAMES = Setting.listSetting( + "plugins.security.privileges_evaluation.actions.map_action_names", + Collections.emptyList(), + Function.identity(), + Setting.Property.NodeScope + ); + + /** + * A list of action names which will be always denied by PrivilegesEvaluator, regardless of any + * other setting. The only way to execute such actions will be using a super admin certificate. + */ + public static Setting> UNIVERSALLY_DENIED_ACTIONS = Setting.listSetting( + "plugins.security.privileges_evaluation.actions.universally_denied_actions", + Collections.emptyList(), + Function.identity(), + Setting.Property.NodeScope + ); + + private static final Logger log = LogManager.getLogger(ActionConfiguration.class); + + private final ImmutableMap actionToActionMap; + private final ImmutableSet explicitIndexActions; + private final ImmutableSet clusterActions; + private final ImmutableSet universallyDeniedActions; + + ActionConfiguration(Settings settings) { + this.actionToActionMap = buildActionToActionMap(settings); + this.explicitIndexActions = buildExplicitIndexActionSet(settings); + this.clusterActions = buildClusterActionSet(settings); + this.universallyDeniedActions = ImmutableSet.copyOf(UNIVERSALLY_DENIED_ACTIONS.get(settings)); + } + + /** + * Checks the action mapping and normalizes the given action name. In most cases, this will just return the + * original action name. + */ + String normalize(String action) { + String mapped = this.actionToActionMap.get(action); + if (mapped != null) { + return mapped; + } else { + return action; + } + } + + /** + * Returns true if the given action is supposed to be a cluster action according to the configuration. + */ + boolean isClusterPermission(String action) { + if (this.explicitIndexActions.contains(action)) { + return false; + } else if (this.clusterActions.contains(action)) { + return true; + } else { + // TODO maybe move to "indices:" prefix + return action.startsWith("cluster:") + || action.startsWith("indices:admin/template/") + || action.startsWith("indices:admin/index_template/"); + } + } + + boolean isUniversallyDenied(String action) { + return this.universallyDeniedActions.contains(action); + } + + private static ImmutableMap buildActionToActionMap(Settings settings) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + + // The following mappings were originally defined at + // https://github.com/opensearch-project/security/blob/eb7153d772e9e00d49d9cb5ffafb33b5f02399fc/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java#L392 + builder.put(UpgradeSettingsAction.NAME, UpgradeAction.NAME); + builder.put(AutoCreateAction.NAME, CreateIndexAction.NAME); + builder.put(AutoPutMappingAction.NAME, PutMappingAction.NAME); + + for (String entry : MAP_ACTION_NAMES.get(settings)) { + String[] parts = entry.split(">"); + if (parts.length == 2) { + builder.put(parts[0], parts[1]); + } else { + log.error("Invalid value for {}: {}", MAP_ACTION_NAMES.getKey(), entry); + } + } + + return builder.build(); + } + + private static ImmutableSet buildClusterActionSet(Settings settings) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + + // A couple of "indices:" actions are considered as cluster level privileges for a number of different reasons. + // See below for details + builder.addAll(WellKnownActions.CLUSTER_ACTIONS); + + // The _msearch action triggers under the hood an additional _search action; thus it is sufficient to check index specific + // privileges on the _search level + builder.add(MultiSearchTemplateAction.NAME); + + // The _reindex action triggers under the hood _search and _bulk actions; thus, index privileges can be checked on these levels + builder.add(ReindexAction.NAME); + + // The _render/template action actually does not operate on indices at all + builder.add(RenderSearchTemplateAction.NAME); + + // The _search/point_in_time/_all action provides no possibility to specify/reduce indices. Thus, it should be a cluster action + builder.add(GetAllPitsAction.NAME); + + // The index template and composable template actions do not specify indices, but specify patterns for potentially non-existing + // indices. + // This makes it difficult (or rather impossible) to match these against the privilege definition index patterns. + // Thus, we treat these as cluster privileges + builder.add(PutIndexTemplateAction.NAME); + builder.add(DeleteIndexTemplateAction.NAME); + builder.add(GetIndexTemplatesAction.NAME); + builder.add(PutComposableIndexTemplateAction.NAME); + builder.add(DeleteComposableIndexTemplateAction.NAME); + builder.add(GetComposableIndexTemplateAction.NAME); + builder.add(SimulateIndexTemplateAction.NAME); + builder.add(SimulateTemplateAction.NAME); + + builder.addAll(FORCE_AS_CLUSTER_ACTIONS.get(settings)); + return builder.build(); + } + + ImmutableSet buildExplicitIndexActionSet(Settings settings) { + Set builder = new HashSet<>(WellKnownActions.INDEX_ACTIONS); + builder.removeAll(FORCE_AS_CLUSTER_ACTIONS.get(settings)); + return ImmutableSet.copyOf(builder); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/PrivilegesEvaluator.java new file mode 100644 index 0000000000..f87589316a --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/PrivilegesEvaluator.java @@ -0,0 +1,816 @@ +/* + * 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.privileges.actionlevel.nextgen; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.AliasesRequest; +import org.opensearch.action.IndicesRequest; +import org.opensearch.action.admin.cluster.shards.ClusterSearchShardsRequest; +import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.opensearch.action.admin.indices.alias.get.GetAliasesAction; +import org.opensearch.action.admin.indices.alias.get.GetAliasesRequest; +import org.opensearch.action.admin.indices.segments.PitSegmentsRequest; +import org.opensearch.action.bulk.BulkItemRequest; +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.bulk.BulkShardRequest; +import org.opensearch.action.delete.DeleteAction; +import org.opensearch.action.index.IndexAction; +import org.opensearch.action.search.CreatePitRequest; +import org.opensearch.action.search.SearchAction; +import org.opensearch.action.support.ActionRequestMetadata; +import org.opensearch.action.update.UpdateAction; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.regex.Regex; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.common.Strings; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.security.privileges.ActionPrivileges; +import org.opensearch.security.privileges.DocumentAllowList; +import org.opensearch.security.privileges.IndicesRequestModifier; +import org.opensearch.security.privileges.IndicesRequestResolver; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.privileges.PrivilegesInterceptor; +import org.opensearch.security.privileges.RoleMapper; +import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; +import org.opensearch.security.privileges.actionlevel.RuntimeOptimizedActionPrivileges; +import org.opensearch.security.privileges.actionlevel.SubjectBasedActionPrivileges; +import org.opensearch.security.securityconf.FlattenedActionGroups; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; +import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.SnapshotRestoreHelper; +import org.opensearch.security.user.User; +import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; + +/** + * A next generation implementation of PrivilegesEvaluator with the following properties: + *

      + *
    • By default, it tries to reduce the requested indices in IndicesRequests to the set of allowed indices (formerly known as do_not_fail_on_forbidden). + * This is done for requests with ignore_unavailable=true or requests using wildcards or patterns.
    • + *
    • For complex actions, are more fine-grained permission model is employed by using the sub-actions property of + * the ResolvedIndices class. For example, if an IndicesAliasesRequest contained a single item for + * deleting an index, the old privileges evaluator would require an indices:admin/delete privilege for + * all requested indices. The new implementation only requires privileges for the indices that are + * actually going to be deleted.
    • + *
    • No longer breaks apart search operations on aliases into member indices. Thus, for a search on an alias, + * you always need to have the privileges for all member indices. This preserves certain alias semantics, + * such as filtered aliases. The old implementation would just drop the filter semantics when the + * requested indices were reduced.
    • + *
    • Integrates the former SystemIndexAccessEvaluator and ProtectedIndexAccessEvaluator completely into + * the ActionPrivileges evaluation. This allows us to fully support the reduction of requested indices if such + * indices are requested.
    • + *
    • The direct support of index reduction also makes the former TermsAggregationEvaluator redundant.
    • + *
    • Adding an index to an alias now additionally requires privileges on the name of the alias.
    • + *
    • A number of config options is no longer supported in order to simplify the code and the configuration + * complexity (relevant for both UX and robustness reasons). The discontinued config options are: + *
        + *
      • "config.dynamic.filtered_alias_mode": Filtered alias checks are no longer performed because they served no actual purpose. See https://github.com/opensearch-project/security/issues/5599
      • + *
      • "config.dynamic.do_not_fail_on_forbidden": Reduction of indices is always performed when possible
      • + *
      • "config.dynamic.do_not_fail_on_forbidden_empty": Reduction to empty requests is always performed when possible.
      • + *
      • "config.dynamic.respect_request_indices_options": By using the ActionRequestMetadata, this is no longer necessary.
      • + *
      • "plugins.security.check_snapshot_restore_write_privileges": The write privileges are always checked for restoring snapshots.
      • + *
      • "plugins.security.enable_snapshot_restore_privilege": Normal users can use the restore API. If you want to forbid normal users to use this API, you can use "plugins.security.privileges_evaluation.actions.universally_denied_actions" instead.
      • + *
      • "plugins.security.unsupported.restore.securityindex.enabled": This is always disabled for normal users.
      • + *
      • "plugins.security.filter_securityindex_from_all_requests": The filtering of the security index has been integrated in the normal index filtering and is thus always available.
      • + *
      • "plugins.security.system_indices.enabled": System index handling is always enabled.
      • + *
      • "plugins.security.system_indices.permission.enabled": The ability to use the explicit system index permission is always enabled.
      • + *
      + *
    • + *
    • A few new config options have been introduced to allow some control over the behavior, mostly for emergency or + * mitigation purposes: + *
        + *
      • "plugins.security.privileges_evaluation.actions.force_as_cluster_actions": Allows to treat actions that are usually considered index privileges, explicitly as cluster privileges instead.
      • + *
      • "plugins.security.privileges_evaluation.actions.universally_denied_actions": Denies all requests of normal users for these actions. Only super admins can use these actions.
      • + *
      • "plugins.security.privileges_evaluation.actions.map_action_names": Allows remapping of action names to privilege names.
      • + *
      + *
    • + *
    + */ +public class PrivilegesEvaluator implements org.opensearch.security.privileges.PrivilegesEvaluator { + private static final Logger log = LogManager.getLogger(PrivilegesEvaluator.class); + + private final Supplier clusterStateSupplier; + private final IndexNameExpressionResolver indexNameExpressionResolver; + private final ThreadContext threadContext; + private final PrivilegesInterceptor privilegesInterceptor; + private final Settings settings; + private final AtomicReference actionPrivileges = new AtomicReference<>(); + private final ImmutableMap pluginIdToActionPrivileges; + private final IndicesRequestResolver indicesRequestResolver; + private final IndicesRequestModifier indicesRequestModifier = new IndicesRequestModifier(); + private final RoleMapper roleMapper; + private final ThreadPool threadPool; + private final RuntimeOptimizedActionPrivileges.SpecialIndexProtection specialIndexProtection; + private final ActionConfiguration actionConfiguration; + + public PrivilegesEvaluator( + Supplier clusterStateSupplier, + RoleMapper roleMapper, + ThreadPool threadPool, + ThreadContext threadContext, + IndexNameExpressionResolver indexNameExpressionResolver, + Settings settings, + PrivilegesInterceptor privilegesInterceptor, + FlattenedActionGroups actionGroups, + FlattenedActionGroups staticActionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration, + Map pluginIdToRolePrivileges, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection specialIndexProtection + ) { + this.indexNameExpressionResolver = indexNameExpressionResolver; + this.roleMapper = roleMapper; + this.threadContext = threadContext; + this.threadPool = threadPool; + this.privilegesInterceptor = privilegesInterceptor; + this.clusterStateSupplier = clusterStateSupplier; + this.settings = settings; + this.specialIndexProtection = specialIndexProtection; + + this.actionConfiguration = new ActionConfiguration(settings); + this.indicesRequestResolver = new IndicesRequestResolver(indexNameExpressionResolver); + + this.pluginIdToActionPrivileges = SubjectBasedActionPrivileges.buildFromMap( + pluginIdToRolePrivileges, + staticActionGroups, + specialIndexProtection + ); + this.updateConfiguration(actionGroups, rolesConfiguration, generalConfiguration); + } + + @Override + public void updateConfiguration( + FlattenedActionGroups flattenedActionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration + ) { + + try { + RoleBasedActionPrivileges actionPrivileges = new RoleBasedActionPrivileges( + rolesConfiguration, + flattenedActionGroups, + this.specialIndexProtection, + this.settings, + false + ); + Metadata metadata = clusterStateSupplier.get().metadata(); + actionPrivileges.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); + RoleBasedActionPrivileges oldInstance = this.actionPrivileges.getAndSet(actionPrivileges); + + if (oldInstance != null) { + oldInstance.clusterStateMetadataDependentPrivileges().shutdown(); + } + } catch (Exception e) { + log.error("Error while updating ActionPrivileges", e); + } + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public PrivilegesEvaluationContext createContext(User user, String action) { + return createContext(user, action, null, ActionRequestMetadata.empty(), null); + } + + @Override + public PrivilegesEvaluationContext createContext( + User user, + String action, + ActionRequest request, + ActionRequestMetadata actionRequestMetadata, + Task task + ) { + TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + + ActionPrivileges actionPrivileges; + ImmutableSet mappedRoles; + + if (user.isPluginUser()) { + mappedRoles = ImmutableSet.of(); + actionPrivileges = this.pluginIdToActionPrivileges.getOrDefault(user.getName(), ActionPrivileges.EMPTY); + } else { + mappedRoles = this.roleMapper.map(user, caller); + actionPrivileges = this.actionPrivileges.get(); + } + + return new PrivilegesEvaluationContext( + user, + mappedRoles, + action, + request, + actionRequestMetadata, + task, + indexNameExpressionResolver, + indicesRequestResolver, + clusterStateSupplier, + actionPrivileges + ); + } + + @Override + public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { + String action = this.actionConfiguration.normalize(context.getAction()); + User user = context.getUser(); + ActionRequest request = context.getRequest(); + + if (request instanceof PitSegmentsRequest pitSegmentsRequest && isAllPitsRequest(pitSegmentsRequest)) { + // We treat this as a separate cluster action. This is because there is no way to reduce the requested + // indices in an _all pits request. + action = "cluster:monitor/point_in_time/segments/_all"; + } + + if (this.actionConfiguration.isUniversallyDenied(action)) { + return PrivilegesEvaluatorResponse.insufficient(action).reason("The action is universally denied"); + } + + ActionPrivileges actionPrivileges = context.getActionPrivileges(); + if (actionPrivileges == null) { + throw new OpenSearchSecurityException("OpenSearch Security is not initialized: roles configuration is missing"); + } + + if (request instanceof BulkRequest && Strings.isNullOrEmpty(user.getRequestedTenant())) { + // Shortcut for bulk actions. The details are checked on the lower level of the BulkShardRequests (Action + // indices:data/write/bulk[s]). + // This shortcut is only possible if the default tenant is selected, as we might need to rewrite the request for non-default + // tenants. + // No further access check for the default tenant is necessary, as access will be also checked on the TransportShardBulkAction + // level. + + PrivilegesEvaluatorResponse result = actionPrivileges.hasClusterPrivilege(context, action); + logPrivilegeEvaluationResult(context, result, "cluster"); + return result; + } + + if (isClusterPermission(action)) { + PrivilegesEvaluatorResponse result = checkClusterPermission(context, action, request); + logPrivilegeEvaluationResult(context, result, "cluster"); + return result; + } else { + PrivilegesEvaluatorResponse result = checkIndexPermission(context, action, request); + logPrivilegeEvaluationResult(context, result, "index"); + return result; + } + } + + PrivilegesEvaluatorResponse checkClusterPermission(PrivilegesEvaluationContext context, String action, ActionRequest request) { + if (context.getUser().isServiceAccount()) { + return PrivilegesEvaluatorResponse.insufficient(action) + .reason("User is a service account which does not have access to any cluster action"); + } + + PrivilegesEvaluatorResponse presponse = context.getActionPrivileges().hasClusterPrivilege(context, action); + if (!presponse.isAllowed()) { + return presponse; + } + + PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action, + context.getUser(), + context + ); + + log.trace("Result from privileges interceptor for cluster perm: {}", replaceResult); + + if (!replaceResult.continueEvaluation) { + if (replaceResult.accessDenied) { + return PrivilegesEvaluatorResponse.insufficient(action).reason("Insufficient tenant privileges"); + } else { + return PrivilegesEvaluatorResponse.ok(replaceResult.createIndexRequestBuilder); + } + } + + if (request instanceof RestoreSnapshotRequest restoreSnapshotRequest) { + return handleRestoreSnapshot(context, restoreSnapshotRequest); + } + + return presponse; + } + + PrivilegesEvaluatorResponse checkIndexPermission(PrivilegesEvaluationContext context, String action, ActionRequest request) { + if (DocumentAllowList.isAllowed(request, threadContext)) { + return PrivilegesEvaluatorResponse.ok(); + } + + PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action, + context.getUser(), + context + ); + + log.trace("Result from privileges interceptor: {}", replaceResult); + + if (!replaceResult.continueEvaluation) { + if (replaceResult.accessDenied) { + return PrivilegesEvaluatorResponse.insufficient(action).reason("Insufficient tenant privileges"); + } else { + return PrivilegesEvaluatorResponse.ok(replaceResult.createIndexRequestBuilder); + } + } + + OptionallyResolvedIndices optionallyResolvedIndices = context.getResolvedRequest(); + + if (request instanceof GetAliasesRequest getAliasesRequest + && optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices) { + // The GetAliasesAction is such a special thing that we need a special case for it + return handleGetAliases(context, getAliasesRequest, resolvedIndices); + } + + return checkIndexPermissionBasic(context, requiredIndexPermissions(request, action), optionallyResolvedIndices, request); + } + + /** + * Checks whether the user has the necessary privileges for the given requiredIndexPermissions set and the given + * resolvedIndices. Reduces the requested indices to authorized indices if possible. This method contains the + * generic part of the privilege evaluation check; all special cases like Dashboards index handing and similar are + * in the checkIndexPermission() method. + */ + PrivilegesEvaluatorResponse checkIndexPermissionBasic( + PrivilegesEvaluationContext context, + Set requiredIndexPermissions, + OptionallyResolvedIndices optionallyResolvedIndices, + ActionRequest request + ) { + + ActionPrivileges actionPrivileges = context.getActionPrivileges(); + + if (optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices && resolvedIndices.isEmpty()) { + // If the request is empty, the normal privilege checks would just pass because technically the question + // "are all indices authorized" is true if the set of indices is empty. This means that certain operations + // would be available to any users regardless of their privileges. Thus, we check first whether the user + // has *any* privilege for the given action. + // The main example for such actions is the _analyze action which can operate on indices, but also can + // operate on an empty set of indices. Without this check, it would be always allowed. + PrivilegesEvaluatorResponse anyPrivilegesResult = actionPrivileges.hasIndexPrivilegeForAnyIndex( + context, + requiredIndexPermissions + ); + if (!anyPrivilegesResult.isAllowed()) { + return anyPrivilegesResult; + } + } + + PrivilegesEvaluatorResponse presponse = actionPrivileges.hasIndexPrivilege( + context, + requiredIndexPermissions, + optionallyResolvedIndices + ); + + if (optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices && !resolvedIndices.local().subActions().isEmpty()) { + // Sub-actions represent situations like a CreateIndexRequest which is configured to add the index also to an alias + // In these cases, we check also privileges for sub-actions. Sub-actions are not eligible for index reduction, + // i.e., they can be only successful or fail. + presponse = checkSubActionPermissions(context, resolvedIndices, presponse); + } + + if (presponse.isPartiallyOk()) { + // If the user has privileges only for a sub-set of indices, we try to scope the request only to these indices if the conditions + // allow. + // These are: + // - The action supports it + // - The index expression contains a pattern expression or ignore_unavailable is true + + if (isIndexReductionForIncompletePrivilegesPossible(request) + && optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices) { + if (this.indicesRequestModifier.setLocalIndices(request, resolvedIndices, presponse.getAvailableIndices())) { + return PrivilegesEvaluatorResponse.ok().reason("Only allowed for a sub-set of indices").originalResult(presponse); + } + } + } else if (!presponse.isAllowed()) { + + if (isIndexReductionForIncompletePrivilegesPossible(request) + && optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices + && !resolvedIndices.remote().isEmpty()) { + // If remote indices are requested, we reduce to these and let the request pass + if (this.indicesRequestModifier.setLocalIndicesToEmpty(request, resolvedIndices)) { + return PrivilegesEvaluatorResponse.ok().reason("Only allowed for remote indices").originalResult(presponse); + } + } else if (isIndexReductionForNoPrivilegesPossible(request) + && optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices) { + // If the user has no privileges, there are certain conditions where we return an empty result instead of a 403 error + // These are: + // - The action supports it + // - The index expression contains a pattern expression or ignore_unavailable is true + // - The user has privileges for the given actions on some indices + + PrivilegesEvaluatorResponse allowedForAnyIndex = actionPrivileges.hasIndexPrivilegeForAnyIndex( + context, + requiredIndexPermissions + ); + + if (allowedForAnyIndex.isAllowed() && this.indicesRequestModifier.setLocalIndicesToEmpty(request, resolvedIndices)) { + return PrivilegesEvaluatorResponse.ok() + .reason("Not allowed for any indices; returning empty result") + .originalResult(presponse); + } + } + } + + return presponse; + } + + @Override + public boolean isClusterPermission(String action) { + return this.actionConfiguration.isClusterPermission(action); + } + + @Override + public void updateClusterStateMetadata(ClusterService clusterService) { + RoleBasedActionPrivileges actionPrivileges = this.actionPrivileges.get(); + if (actionPrivileges != null) { + actionPrivileges.clusterStateMetadataDependentPrivileges().updateClusterStateMetadataAsync(clusterService, threadPool); + } + } + + @Override + public void shutdown() { + RoleBasedActionPrivileges roleBasedActionPrivileges = this.actionPrivileges.get(); + if (roleBasedActionPrivileges != null) { + roleBasedActionPrivileges.clusterStateMetadataDependentPrivileges().shutdown(); + } + } + + @Override + public boolean notFailOnForbiddenEnabled() { + return true; + } + + void logPrivilegeEvaluationResult(PrivilegesEvaluationContext context, PrivilegesEvaluatorResponse result, String privilegeType) { + if (result.isAllowed()) { + if (log.isDebugEnabled()) { + String reason = result.getReason(); + if (result.hasEvaluationExceptions()) { + reason = "There were errors during privilege evaluation"; + } + String requestInfo = getRequestInfo(context.getRequest()); + + if (reason == null) { + log.debug(""" + Allowing {} action because all privileges are present. + Action: {} + Request: {} + Resolved indices: {} + User: {} + """, privilegeType, context.getAction(), requestInfo, context.getResolvedRequest(), context.getUser()); + } else if (result.privilegesAreComplete()) { + log.debug( + """ + Allowing {} action, but: {} + Action: {} + Request: {} + Resolved indices: {} + User: {} + Roles: {} + Errors: {} + """, + privilegeType, + reason, + context.getAction(), + requestInfo, + context.getResolvedRequest(), + context.getUser(), + context.getMappedRoles(), + result.getEvaluationExceptionInfo() + ); + } else { + log.debug( + """ + Allowing {} action, but: {} + Action: {} + Request: {} + Resolved indices: {} + User: {} + Roles: {} + Available privileges: + {} + Errors: {} + """, + privilegeType, + reason, + context.getAction(), + requestInfo, + context.getResolvedRequest(), + context.getUser(), + context.getMappedRoles(), + result.originalResult() != null ? result.originalResult().getPrivilegeMatrix() : result.getPrivilegeMatrix(), + result.getEvaluationExceptionInfo() + ); + } + } + } else { + log.info( + """ + Not allowing {} action: {} + Action: {} + Request: {} + Resolved indices: {} + User: {} + Roles: {} + Available privileges: + {} + Errors: {} + """, + privilegeType, + result.getReason(), + context.getAction(), + getRequestInfo(context.getRequest()), + context.getResolvedRequest(), + context.getUser(), + context.getMappedRoles(), + result.originalResult() != null ? result.originalResult().getPrivilegeMatrix() : result.getPrivilegeMatrix(), + result.getEvaluationExceptionInfo() + ); + } + } + + String getRequestInfo(ActionRequest request) { + StringBuilder result = new StringBuilder(request.getClass().getSimpleName()); + if (request instanceof IndicesRequest indicesRequest) { + String[] indices = indicesRequest.indices(); + result.append("; indices: ").append(indices != null ? Arrays.asList(indices) : "null"); + result.append("; indicesOptions: ").append(indicesRequest.indicesOptions()); + } + if (request instanceof AliasesRequest aliasesRequest) { + String[] aliases = aliasesRequest.aliases(); + result.append("; aliases: ").append(aliases != null ? Arrays.asList(aliases) : "null"); + } + return result.toString(); + } + + /** + * The GetAliasesRequest has such a complicated logic that we need to handle it with a special case. It has two dimensions: + * indices and aliases which can be independently specified; indices can be reduced, but reducing aliases is not really + * possible due to a special logic which exists in the RestGetAliasesAction (an unusual location for such logic): + * https://github.com/opensearch-project/OpenSearch/blob/1df543e04d7605b7ee37587ff5c635609ebdafbd/server/src/main/java/org/opensearch/rest/action/admin/indices/RestGetAliasesAction.java#L94 + * Another effect of this logic is that if there are explicitly specified aliases in the request which are not matched + * by any indices, the action fails with a 404 error. + * In order to avoid these 404 errors, we fail with an "insufficient" error whenever there are explicit aliases + * and there are no sufficient privileges for these aliases. + * If there are no explicit aliases, we can do index reduction, though. + */ + PrivilegesEvaluatorResponse handleGetAliases( + PrivilegesEvaluationContext context, + GetAliasesRequest request, + ResolvedIndices resolvedIndices + ) { + ActionPrivileges actionPrivileges = context.getActionPrivileges(); + String aliasesSubActionKey = GetAliasesAction.NAME + "[aliases]"; + Set indices = resolvedIndices.local().names(); + Set aliases = resolvedIndices.local().subActions().containsKey(aliasesSubActionKey) + ? resolvedIndices.local().subActions().get(aliasesSubActionKey).names() + : Collections.emptySet(); + + PrivilegesEvaluatorResponse indicesResult = actionPrivileges.hasIndexPrivilege( + context, + Set.of(context.getAction()), + ResolvedIndices.of(indices) + ); + PrivilegesEvaluatorResponse aliasesResult = actionPrivileges.hasIndexPrivilege( + context, + Set.of(context.getAction()), + ResolvedIndices.of(aliases) + ); + + if (!aliasesResult.isAllowed() && request.aliases().length != 0) { + // The RestGetAliasesAction does not allow reducing aliases (Even though the GetAliasesRequest has a method for + // setting aliases retroactively). Thus, if explicit aliases were specified, we will always fail with an + // "insufficient" error. + return indicesResult.insufficient(List.of(aliasesResult)) + .reason("No privileges for aliases while explicit aliases were specified in the request"); + } + + if (!indicesResult.isAllowed() && indicesResult.getAvailableIndices().isEmpty()) { + // If the user does not have privileges for any index, we deny the request here completely + PrivilegesEvaluatorResponse anyPrivilegesResult = actionPrivileges.hasIndexPrivilegeForAnyIndex( + context, + Set.of(context.getAction()) + ); + if (anyPrivilegesResult != null && !anyPrivilegesResult.isAllowed()) { + return indicesResult; + } + } + + if (!indicesResult.isAllowed()) { + // If we reached this block, the user has privileges for a sub-set of indices or at least for other indices. + // Then, we will return either a reduced or empty result + if (this.indicesRequestModifier.setLocalIndices(request, resolvedIndices, indicesResult.getAvailableIndices())) { + return PrivilegesEvaluatorResponse.ok().originalResult(indicesResult).reason("Only allowed for a subset of indices"); + } + } + + return indicesResult; + } + + /** + * Special handling for RestoreSnapshotRequests. This includes especially the check of the privileges for the restored + * indices. The check will be performed using the standard checkIndexPermission() method; thus, the standard restrictions + * on the security index and system indices apply (incl. system index permission handling). + */ + PrivilegesEvaluatorResponse handleRestoreSnapshot(PrivilegesEvaluationContext context, RestoreSnapshotRequest request) { + if (request.includeGlobalState()) { + return PrivilegesEvaluatorResponse.insufficient(context.getAction()) + .reason("Restoring snapshot with 'include_global_state' enabled is not allowed"); + } + + if (!clusterStateSupplier.get().nodes().isLocalNodeElectedClusterManager()) { + // We need to return ok here, because we can only retrieve the snapshot info on a cluster manager node. + // This is fine, as the next thing the TransportAction implementation will do, is forwarding the request to a + // cluster manager node. + return PrivilegesEvaluatorResponse.ok(); + } + + OptionallyResolvedIndices optionallyResolvedIndices = SnapshotRestoreHelper.resolveTargetIndices(request); + if (!(optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices)) { + return PrivilegesEvaluatorResponse.insufficient(context.getAction()) + .reason("Could not retrieve information for snapshot " + request.repository() + "/" + request.snapshot()); + } + + return checkIndexPermissionBasic( + context, + ConfigConstants.SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES, + resolvedIndices, + request + ); + } + + /** + * Checks the permissions for the sub-actions given in the ResolvedIndices object. Sub-actions describe complex + * action requests, which might do different things with different indices. One example is the IndicesAliasesRequest + * which can also just delete indices; in this case, the index to be deleted is contained in the sub-action + * with the key "indices:admin/delete". + *

    + * This will return the value given as the originalResult parameter if all sub-action privileges are present. If a + * privilege is missing, this returns an insufficient PrivilegesEvaluatorResponse. + *

    + * Reduction of requested indices is not possible for sub-actions, thus this only return "ok" or "insufficient", + * but never "partially sufficient". + */ + PrivilegesEvaluatorResponse checkSubActionPermissions( + PrivilegesEvaluationContext context, + ResolvedIndices resolvedIndices, + PrivilegesEvaluatorResponse originalResult + ) { + ActionPrivileges actionPrivileges = context.getActionPrivileges(); + List subActionResults = new ArrayList<>(resolvedIndices.local().subActions().size()); + boolean allowed = true; + + for (Map.Entry subAction : resolvedIndices.local().subActions().entrySet()) { + PrivilegesEvaluatorResponse subResponse = actionPrivileges.hasIndexPrivilege( + context, + Set.of(subAction.getKey()), + ResolvedIndices.of(subAction.getValue()) + ); + subActionResults.add(subResponse); + if (!subResponse.isAllowed()) { + allowed = false; + } + } + if (allowed) { + return originalResult; + } else { + return originalResult.insufficient(subActionResults); + } + } + + /** + * This returns the set of required privileges for a particular action. This is usually just the set containing + * exactly the given action name. There are some exceptions where more than one action privilege is required. + * See the implementation for these cases. + */ + Set requiredIndexPermissions(ActionRequest request, String originalAction) { + if (request instanceof ClusterSearchShardsRequest) { + return Set.of(originalAction, SearchAction.NAME); + } else if (request instanceof BulkShardRequest bulkShardRequest) { + ImmutableSet.Builder allRequiredPermissions = ImmutableSet.builderWithExpectedSize(2); + allRequiredPermissions.add(originalAction); + for (BulkItemRequest item : bulkShardRequest.items()) { + switch (item.request().opType()) { + case CREATE: + case INDEX: + allRequiredPermissions.add(IndexAction.NAME); + break; + case DELETE: + allRequiredPermissions.add(DeleteAction.NAME); + break; + case UPDATE: + allRequiredPermissions.add(UpdateAction.NAME); + break; + } + } + return allRequiredPermissions.build(); + } else { + return Set.of(originalAction); + } + } + + /** + * Returns true if it is possible to reduce the requested indices in the given request to allow its execution + * given the user's available privileges. + *

    + * This is the case when: + *

      + *
    • The request implements IndicesRequest.Replaceable
    • + *
    • AND, the ignore_unavailable index option has been specified or the request contains patterns (like "index_a*")
    • + *
    + */ + boolean isIndexReductionForIncompletePrivilegesPossible(ActionRequest request) { + if (!(request instanceof IndicesRequest.Replaceable indicesRequest)) { + return false; + } + + if (request instanceof PitSegmentsRequest) { + // PitSegmentsRequest implements IndicesRequest.Replaceable, but ignores all specified indices + return false; + } + + if (indicesRequest.indicesOptions().ignoreUnavailable()) { + return true; + } + + return indicesRequest.indicesOptions().expandWildcardsOpen() && containsPattern(indicesRequest); + } + + /** + * Returns true if it is possible to reduce the requested indices in the given request to NONE to allow its + * execution. The execution should just return an empty response then. + *

    + * This is the case when the conditions for isIndexReductionForIncompletePrivilegesPossible() hold and the index + * option allow_no_indices has been specified. + *

    + * Additionally, there might be exceptions for actions which just do not support an empty set of indices. + */ + boolean isIndexReductionForNoPrivilegesPossible(ActionRequest request) { + if (!isIndexReductionForIncompletePrivilegesPossible(request)) { + return false; + } + + if (request instanceof CreatePitRequest) { + // The creation of PIT search contexts is not possible for no indices + return false; + } + + return ((IndicesRequest) request).indicesOptions().allowNoIndices(); + } + + /** + * Returns if the given IndicesRequest contains a wildcard, index pattern or refers to all indices via "_all" or + * an empty index expression. + */ + boolean containsPattern(IndicesRequest indicesRequest) { + String[] indices = indicesRequest.indices(); + + if (indices == null + || indices.length == 0 + || (indices.length == 1 && (Metadata.ALL.equals(indices[0]) || Regex.isMatchAllPattern(indices[0])))) { + return true; + } + + for (String index : indices) { + if (Regex.isSimpleMatchPattern(index)) { + return true; + } + } + + return false; + } + + private boolean isAllPitsRequest(PitSegmentsRequest request) { + return request.getPitIds().size() == 1 && "_all".equals(request.getPitIds().get(0)); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/dlsfls/AbstractRuleBasedPrivileges.java b/src/main/java/org/opensearch/security/privileges/dlsfls/AbstractRuleBasedPrivileges.java index f7ba4442c8..fcae0e7cbc 100644 --- a/src/main/java/org/opensearch/security/privileges/dlsfls/AbstractRuleBasedPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/dlsfls/AbstractRuleBasedPrivileges.java @@ -25,13 +25,15 @@ import org.apache.logging.log4j.Logger; import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.common.settings.Settings; import org.opensearch.security.privileges.IndexPattern; import org.opensearch.security.privileges.PrivilegesConfigurationValidationException; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluationException; import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; -import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.ConfigConstants; @@ -131,7 +133,7 @@ public boolean isUniversallyUnrestricted(PrivilegesEvaluationContext context) { * @throws PrivilegesEvaluationException If something went wrong during privileges evaluation. In such cases, any * access should be denied to make sure that no unauthorized information is exposed. */ - public boolean isUnrestricted(PrivilegesEvaluationContext context, IndexResolverReplacer.Resolved resolved) + public boolean isUnrestricted(PrivilegesEvaluationContext context, OptionallyResolvedIndices optionallyResolvedIndices) throws PrivilegesEvaluationException { if (context.getMappedRoles().isEmpty()) { return false; @@ -142,11 +144,12 @@ public boolean isUnrestricted(PrivilegesEvaluationContext context, IndexResolver return true; } - if (resolved == null) { + if (this.hasRestrictedRulesWithIndexWildcard(context)) { return false; } - if (this.hasRestrictedRulesWithIndexWildcard(context)) { + if (!(optionallyResolvedIndices instanceof ResolvedIndices resolved)) { + // If we do not have resolved indices information, we can assume a restriction return false; } @@ -156,7 +159,7 @@ public boolean isUnrestricted(PrivilegesEvaluationContext context, IndexResolver // If we found an unrestricted role, we continue with the next index/alias/data stream. If we found a restricted role, we abort // early and return true. - for (String index : resolved.getAllIndicesResolved(context.getClusterStateSupplier(), context.getIndexNameExpressionResolver())) { + for (String index : resolved.local().names(context.clusterState())) { if (this.dfmEmptyOverridesAll) { // We assume that we have a restriction unless there are roles without restriction. // Thus, we only have to check the roles without restriction. @@ -228,6 +231,46 @@ public boolean isUnrestricted(PrivilegesEvaluationContext context, String index) private boolean hasUnrestrictedRulesExplicit(PrivilegesEvaluationContext context, StatefulRules statefulRules, String index) throws PrivilegesEvaluationException { + if (hasUnrestrictedRulesExplicitNoRecursion(context, statefulRules, index)) { + return true; + } + + IndexAbstraction indexAbstraction = context.getIndicesLookup().get(index); + if (indexAbstraction instanceof IndexAbstraction.Index) { + for (String parent : getParents(indexAbstraction)) { + if (hasUnrestrictedRulesExplicitNoRecursion(context, statefulRules, parent)) { + return true; + } + } + } else if (indexAbstraction instanceof IndexAbstraction.DataStream || indexAbstraction instanceof IndexAbstraction.Alias) { + // If we got an alias or a data stream, we might be also unrestricted if all member indices are unrestricted + + List memberIndices = indexAbstraction.getIndices(); + int unrestrictedMemberIndices = 0; + + for (IndexMetadata memberIndex : memberIndices) { + if (hasUnrestrictedRulesExplicitNoRecursion(context, statefulRules, memberIndex.getIndex().getName())) { + unrestrictedMemberIndices++; + } + } + + if (unrestrictedMemberIndices == memberIndices.size()) { + return true; + } + } + + return false; + } + + /** + * Should be only called by hasUnrestrictedRulesExplicit() + */ + private boolean hasUnrestrictedRulesExplicitNoRecursion( + PrivilegesEvaluationContext context, + StatefulRules statefulRules, + String index + ) throws PrivilegesEvaluationException { + if (statefulRules != null && statefulRules.covers(index)) { Set roleWithoutRule = statefulRules.indexToRoleWithoutRule.get(index); @@ -244,17 +287,7 @@ private boolean hasUnrestrictedRulesExplicit(PrivilegesEvaluationContext context return true; } - IndexAbstraction indexAbstraction = context.getIndicesLookup().get(index); - if (indexAbstraction != null) { - for (String parent : getParents(indexAbstraction)) { - if (hasUnrestrictedRulesExplicit(context, statefulRules, parent)) { - return true; - } - } - } - return false; - } /** @@ -281,9 +314,17 @@ private boolean hasRestrictedRulesExplicit(PrivilegesEvaluationContext context, } IndexAbstraction indexAbstraction = context.getIndicesLookup().get(index); - if (indexAbstraction != null) { + if (indexAbstraction instanceof IndexAbstraction.Index) { for (String parent : getParents(indexAbstraction)) { - if (hasRestrictedRulesExplicit(context, statefulRules, parent)) { + if (hasRestrictedRulesExplicitNoRecursion(context, statefulRules, parent)) { + return true; + } + } + } else if (indexAbstraction instanceof IndexAbstraction.DataStream || indexAbstraction instanceof IndexAbstraction.Alias) { + // If we got an alias or a data stream, we might be also restricted if there is a member index that is restricted + + for (IndexMetadata memberIndex : indexAbstraction.getIndices()) { + if (hasRestrictedRulesExplicitNoRecursion(context, statefulRules, memberIndex.getIndex().getName())) { return true; } } @@ -292,6 +333,34 @@ private boolean hasRestrictedRulesExplicit(PrivilegesEvaluationContext context, return false; } + /** + * Should be only called by hasRestrictedRulesExplicit() + */ + private boolean hasRestrictedRulesExplicitNoRecursion( + PrivilegesEvaluationContext context, + StatefulRules statefulRules, + String index + ) throws PrivilegesEvaluationException { + + if (statefulRules != null && statefulRules.covers(index)) { + Map roleWithRule = statefulRules.indexToRoleToRule.get(index); + + if (roleWithRule != null && CollectionUtils.containsAny(roleWithRule.keySet(), context.getMappedRoles())) { + return true; + } + } else { + if (this.staticRules.hasRestrictedPatterns(context, index)) { + return true; + } + } + + if (this.staticRules.hasRestrictedPatternTemplates(context, index)) { + return true; + } + + return false; + } + /** * Returns true if the user specified by the given context parameter has roles which apply for the index wildcard ("*") * and which specify DLS rules. @@ -617,7 +686,7 @@ static class StaticRules { } } else { SingleRule singleRule = this.roleToRule(rolePermissions); - IndexPattern indexPattern = IndexPattern.from(rolePermissions.getIndex_patterns()); + IndexPattern indexPattern = IndexPattern.from(rolePermissions.getIndex_patterns(), false); if (indexPattern.hasStaticPattern()) { if (singleRule == null) { @@ -781,7 +850,7 @@ static class StatefulRules { continue; } - WildcardMatcher indexMatcher = IndexPattern.from(indexPermissions.getIndex_patterns()).getStaticPattern(); + WildcardMatcher indexMatcher = IndexPattern.from(indexPermissions.getIndex_patterns(), false).getStaticPattern(); if (indexMatcher == WildcardMatcher.NONE) { // The pattern is likely blank because there are only dynamic patterns. @@ -840,5 +909,4 @@ static interface RoleToRuleFunction { static abstract class Rule { abstract boolean isUnrestricted(); } - } diff --git a/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java b/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java index 8a27fef255..682b0583b2 100644 --- a/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java +++ b/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java @@ -12,8 +12,8 @@ import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; -import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; import org.opensearch.security.user.User; @@ -22,12 +22,12 @@ * Node global context data for DLS/FLS. The lifecycle of an instance of this class is equal to the lifecycle of a running node. */ public class DlsFlsBaseContext { - private final PrivilegesEvaluator privilegesEvaluator; + private final PrivilegesConfiguration privilegesConfiguration; private final ThreadContext threadContext; private final AdminDNs adminDNs; - public DlsFlsBaseContext(PrivilegesEvaluator privilegesEvaluator, ThreadContext threadContext, AdminDNs adminDNs) { - this.privilegesEvaluator = privilegesEvaluator; + public DlsFlsBaseContext(PrivilegesConfiguration privilegesConfiguration, ThreadContext threadContext, AdminDNs adminDNs) { + this.privilegesConfiguration = privilegesConfiguration; this.threadContext = threadContext; this.adminDNs = adminDNs; } @@ -43,7 +43,7 @@ public PrivilegesEvaluationContext getPrivilegesEvaluationContext() { return null; } - return this.privilegesEvaluator.createContext(user, null); + return this.privilegesConfiguration.privilegesEvaluator().createContext(user, null); } public boolean isDlsDoneOnFilterLevel() { diff --git a/src/main/java/org/opensearch/security/resolver/IndexResolverReplacer.java b/src/main/java/org/opensearch/security/resolver/IndexResolverReplacer.java deleted file mode 100644 index 2239a0239a..0000000000 --- a/src/main/java/org/opensearch/security/resolver/IndexResolverReplacer.java +++ /dev/null @@ -1,863 +0,0 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * 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.resolver; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Supplier; -import java.util.regex.PatternSyntaxException; -import java.util.stream.Collectors; - -import com.google.common.collect.ImmutableSet; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.DocWriteRequest; -import org.opensearch.action.IndicesRequest; -import org.opensearch.action.IndicesRequest.Replaceable; -import org.opensearch.action.OriginalIndices; -import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; -import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; -import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; -import org.opensearch.action.admin.indices.create.CreateIndexRequest; -import org.opensearch.action.admin.indices.datastream.CreateDataStreamAction; -import org.opensearch.action.admin.indices.mapping.put.PutMappingRequest; -import org.opensearch.action.admin.indices.resolve.ResolveIndexAction; -import org.opensearch.action.admin.indices.rollover.RolloverRequest; -import org.opensearch.action.admin.indices.shrink.ResizeRequest; -import org.opensearch.action.admin.indices.template.put.PutComponentTemplateAction; -import org.opensearch.action.bulk.BulkRequest; -import org.opensearch.action.bulk.BulkShardRequest; -import org.opensearch.action.delete.DeleteRequest; -import org.opensearch.action.fieldcaps.FieldCapabilitiesIndexRequest; -import org.opensearch.action.fieldcaps.FieldCapabilitiesRequest; -import org.opensearch.action.get.MultiGetRequest; -import org.opensearch.action.get.MultiGetRequest.Item; -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.main.MainRequest; -import org.opensearch.action.search.ClearScrollRequest; -import org.opensearch.action.search.MultiSearchRequest; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchScrollRequest; -import org.opensearch.action.support.IndicesOptions; -import org.opensearch.action.support.nodes.BaseNodesRequest; -import org.opensearch.action.support.replication.ReplicationRequest; -import org.opensearch.action.support.single.shard.SingleShardRequest; -import org.opensearch.action.termvectors.MultiTermVectorsRequest; -import org.opensearch.action.termvectors.TermVectorsRequest; -import org.opensearch.action.update.UpdateRequest; -import org.opensearch.cluster.ClusterState; -import org.opensearch.cluster.metadata.IndexAbstraction; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.util.IndexUtils; -import org.opensearch.core.index.Index; -import org.opensearch.index.IndexNotFoundException; -import org.opensearch.index.reindex.ReindexRequest; -import org.opensearch.security.OpenSearchSecurityPlugin; -import org.opensearch.security.configuration.ClusterInfoHolder; -import org.opensearch.security.securityconf.DynamicConfigModel; -import org.opensearch.security.support.SnapshotRestoreHelper; -import org.opensearch.security.support.WildcardMatcher; -import org.opensearch.snapshots.SnapshotInfo; -import org.opensearch.transport.RemoteClusterService; -import org.opensearch.transport.TransportRequest; - -import org.greenrobot.eventbus.Subscribe; - -import static org.opensearch.cluster.metadata.IndexAbstraction.Type.ALIAS; - -public class IndexResolverReplacer { - - private static final Set NULL_SET = new HashSet<>(Collections.singleton(null)); - private final Logger log = LogManager.getLogger(this.getClass()); - private final IndexNameExpressionResolver resolver; - private final Supplier clusterStateSupplier; - private final ClusterInfoHolder clusterInfoHolder; - private volatile boolean respectRequestIndicesOptions = false; - - public IndexResolverReplacer( - IndexNameExpressionResolver resolver, - Supplier clusterStateSupplier, - ClusterInfoHolder clusterInfoHolder - ) { - this.resolver = resolver; - this.clusterStateSupplier = clusterStateSupplier; - this.clusterInfoHolder = clusterInfoHolder; - } - - private static boolean isAllWithNoRemote(final String... requestedPatterns) { - - final List patterns = requestedPatterns == null ? null : Arrays.asList(requestedPatterns); - - if (IndexNameExpressionResolver.isAllIndices(patterns)) { - return true; - } - - if (patterns.size() == 1 && patterns.contains("*")) { - return true; - } - - if (new HashSet(patterns).equals(NULL_SET)) { - return true; - } - - return false; - } - - private static boolean isLocalAll(String... requestedPatterns) { - return isLocalAll(requestedPatterns == null ? null : Arrays.asList(requestedPatterns)); - } - - private static boolean isLocalAll(Collection patterns) { - if (IndexNameExpressionResolver.isAllIndices(patterns)) { - return true; - } - - if (patterns.contains("_all")) { - return true; - } - - if (new HashSet(patterns).equals(NULL_SET)) { - return true; - } - - return false; - } - - private class ResolvedIndicesProvider implements IndicesProvider { - private final ImmutableSet.Builder aliases; - private final ImmutableSet.Builder allIndices; - private final ImmutableSet.Builder originalRequested; - private final ImmutableSet.Builder remoteIndices; - // set of previously resolved index requests to avoid resolving - // the same index more than once while processing bulk requests - private final Set alreadyResolved; - private final String name; - - private final class AlreadyResolvedKey { - - private final IndicesOptions indicesOptions; - - private final boolean enableCrossClusterResolution; - - private final String[] original; - - private AlreadyResolvedKey(final IndicesOptions indicesOptions, final boolean enableCrossClusterResolution) { - this(indicesOptions, enableCrossClusterResolution, null); - } - - private AlreadyResolvedKey( - final IndicesOptions indicesOptions, - final boolean enableCrossClusterResolution, - final String[] original - ) { - this.indicesOptions = indicesOptions; - this.enableCrossClusterResolution = enableCrossClusterResolution; - this.original = original; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - AlreadyResolvedKey that = (AlreadyResolvedKey) o; - return enableCrossClusterResolution == that.enableCrossClusterResolution - && Objects.equals(indicesOptions, that.indicesOptions) - && Arrays.equals(original, that.original); - } - - @Override - public int hashCode() { - int result = Objects.hash(indicesOptions, enableCrossClusterResolution); - result = 31 * result + Arrays.hashCode(original); - return result; - } - } - - ResolvedIndicesProvider(Object request) { - aliases = ImmutableSet.builder(); - allIndices = ImmutableSet.builder(); - originalRequested = ImmutableSet.builder(); - remoteIndices = ImmutableSet.builder(); - alreadyResolved = new HashSet<>(); - name = request.getClass().getSimpleName(); - } - - private void resolveIndexPatterns( - final String name, - final IndicesOptions indicesOptions, - final boolean enableCrossClusterResolution, - final String[] original - ) { - final boolean isTraceEnabled = log.isTraceEnabled(); - if (isTraceEnabled) { - log.trace("resolve requestedPatterns: " + Arrays.toString(original)); - } - - if (isAllWithNoRemote(original)) { - if (isTraceEnabled) { - log.trace(Arrays.toString(original) + " is an ALL pattern without any remote indices"); - } - resolveToLocalAll(); - return; - } - - Set remoteIndices; - final List localRequestedPatterns = new ArrayList<>(Arrays.asList(original)); - - final RemoteClusterService remoteClusterService = OpenSearchSecurityPlugin.GuiceHolder.getRemoteClusterService(); - - if (remoteClusterService != null && remoteClusterService.isCrossClusterSearchEnabled() && enableCrossClusterResolution) { - remoteIndices = new HashSet<>(); - final Map remoteClusterIndices = OpenSearchSecurityPlugin.GuiceHolder.getRemoteClusterService() - .groupIndices(indicesOptions, original, idx -> resolver.hasIndexAbstraction(idx, clusterStateSupplier.get())); - final Set remoteClusters = remoteClusterIndices.keySet() - .stream() - .filter(k -> !RemoteClusterService.LOCAL_CLUSTER_GROUP_KEY.equals(k)) - .collect(Collectors.toSet()); - for (String remoteCluster : remoteClusters) { - for (String remoteIndex : remoteClusterIndices.get(remoteCluster).indices()) { - remoteIndices.add(RemoteClusterService.buildRemoteIndexName(remoteCluster, remoteIndex)); - } - } - - final Iterator iterator = localRequestedPatterns.iterator(); - while (iterator.hasNext()) { - final String[] split = iterator.next().split(String.valueOf(RemoteClusterService.REMOTE_CLUSTER_INDEX_SEPARATOR), 2); - final WildcardMatcher matcher = WildcardMatcher.from(split[0]); - if (split.length > 1 && matcher.matchAny(remoteClusters)) { - iterator.remove(); - } - } - - if (isTraceEnabled) { - log.trace( - "CCS is enabled, we found this local patterns " - + localRequestedPatterns - + " and this remote patterns: " - + remoteIndices - ); - } - - } else { - remoteIndices = Collections.emptySet(); - } - - final Collection matchingAliases; - Collection matchingAllIndices; - Collection matchingDataStreams = null; - - if (isLocalAll(original)) { - if (isTraceEnabled) { - log.trace(Arrays.toString(original) + " is an LOCAL ALL pattern"); - } - matchingAliases = Resolved.All_SET; - matchingAllIndices = Resolved.All_SET; - - } else if (!remoteIndices.isEmpty() && localRequestedPatterns.isEmpty()) { - if (isTraceEnabled) { - log.trace(Arrays.toString(original) + " is an LOCAL EMPTY request"); - } - matchingAllIndices = Collections.emptySet(); - matchingAliases = Collections.emptySet(); - } - - else { - final ClusterState state = clusterStateSupplier.get(); - final Set dateResolvedLocalRequestedPatterns = localRequestedPatterns.stream() - .map(resolver::resolveDateMathExpression) - .collect(Collectors.toSet()); - final WildcardMatcher dateResolvedMatcher = WildcardMatcher.from(dateResolvedLocalRequestedPatterns); - // fill matchingAliases - final Map lookup = state.metadata().getIndicesLookup(); - matchingAliases = lookup.entrySet() - .stream() - .filter(e -> e.getValue().getType() == ALIAS) - .map(Map.Entry::getKey) - .filter(dateResolvedMatcher) - .collect(Collectors.toSet()); - - final boolean isDebugEnabled = log.isDebugEnabled(); - try { - matchingAllIndices = Arrays.asList( - resolver.concreteIndexNames(state, indicesOptions, localRequestedPatterns.toArray(new String[0])) - ); - matchingDataStreams = resolver.dataStreamNames(state, indicesOptions, localRequestedPatterns.toArray(new String[0])); - - if (isDebugEnabled) { - log.debug( - "Resolved pattern {} to indices: {} and data-streams: {}", - localRequestedPatterns, - matchingAllIndices, - matchingDataStreams - ); - } - } catch (IndexNotFoundException e1) { - if (isDebugEnabled) { - log.debug("No such indices for pattern {}, use raw value", localRequestedPatterns); - } - - matchingAllIndices = dateResolvedLocalRequestedPatterns; - } - } - - if (matchingDataStreams == null || matchingDataStreams.size() == 0) { - matchingDataStreams = Arrays.asList(NOOP); - } - - if (isTraceEnabled) { - log.trace( - "Resolved patterns {} for {} ({}) to [aliases {}, allIndices {}, dataStreams {}, originalRequested{}, remote indices {}]", - original, - name, - this.name, - matchingAliases, - matchingAllIndices, - matchingDataStreams, - Arrays.toString(original), - remoteIndices - ); - } - - resolveTo(matchingAliases, matchingAllIndices, matchingDataStreams, original, remoteIndices); - } - - private void resolveToLocalAll() { - aliases.add(Resolved.ANY); - allIndices.add(Resolved.ANY); - originalRequested.add(Resolved.ANY); - } - - private void resolveTo( - Iterable matchingAliases, - Iterable matchingAllIndices, - Iterable matchingDataStreams, - String[] original, - Iterable remoteIndices - ) { - aliases.addAll(matchingAliases); - allIndices.addAll(matchingAllIndices); - allIndices.addAll(matchingDataStreams); - originalRequested.add(original); - this.remoteIndices.addAll(remoteIndices); - } - - @Override - public String[] provide(String[] original, Object localRequest, boolean supportsReplace) { - final IndicesOptions indicesOptions = indicesOptionsFrom(localRequest); - final boolean enableCrossClusterResolution = localRequest instanceof FieldCapabilitiesRequest - || localRequest instanceof SearchRequest - || localRequest instanceof ResolveIndexAction.Request; - // skip the whole thing if we have seen this exact resolveIndexPatterns request - final AlreadyResolvedKey alreadyResolvedKey; - if (original != null) { - alreadyResolvedKey = new AlreadyResolvedKey(indicesOptions, enableCrossClusterResolution, original); - } else { - alreadyResolvedKey = new AlreadyResolvedKey(indicesOptions, enableCrossClusterResolution); - } - if (alreadyResolved.add(alreadyResolvedKey)) { - resolveIndexPatterns(localRequest.getClass().getSimpleName(), indicesOptions, enableCrossClusterResolution, original); - } - return IndicesProvider.NOOP; - } - - Resolved resolved(IndicesOptions indicesOptions) { - final Resolved resolved = alreadyResolved.isEmpty() - ? Resolved._LOCAL_ALL - : new Resolved(aliases.build(), allIndices.build(), originalRequested.build(), remoteIndices.build(), indicesOptions); - - if (log.isTraceEnabled()) { - log.trace("Finally resolved for {}: {}", name, resolved); - } - - return resolved; - } - } - - // dnfof - public boolean replace(final TransportRequest request, boolean retainMode, String... replacements) { - return getOrReplaceAllIndices(request, new IndicesProvider() { - - @Override - public String[] provide(String[] original, Object request, boolean supportsReplace) { - if (supportsReplace) { - if (retainMode && !isAllWithNoRemote(original)) { - final Resolved resolved = resolveRequest(request); - final List retained = WildcardMatcher.from(resolved.getAllIndices()) - .getMatchAny(replacements, Collectors.toList()); - retained.addAll(resolved.getRemoteIndices()); - return retained.toArray(new String[0]); - } - return replacements; - } else { - return NOOP; - } - } - }, false); - } - - public boolean replace(final TransportRequest request, boolean retainMode, Collection replacements) { - return replace(request, retainMode, replacements.toArray(new String[replacements.size()])); - } - - public Resolved resolveRequest(final Object request) { - if (log.isDebugEnabled()) { - log.debug("Resolve aliases, indices and types from {}", request.getClass().getSimpleName()); - } - - final ResolvedIndicesProvider resolvedIndicesProvider = new ResolvedIndicesProvider(request); - - getOrReplaceAllIndices(request, resolvedIndicesProvider, false); - - return resolvedIndicesProvider.resolved(indicesOptionsFrom(request)); - } - - public final static class Resolved { - private static final String ANY = "*"; - private static final ImmutableSet All_SET = ImmutableSet.of(ANY); - private static final Set types = All_SET; - public static final Resolved _LOCAL_ALL = new Resolved( - All_SET, - All_SET, - All_SET, - ImmutableSet.of(), - SearchRequest.DEFAULT_INDICES_OPTIONS - ); - - private static final IndicesOptions EXACT_INDEX_OPTIONS = new IndicesOptions( - EnumSet.of(IndicesOptions.Option.FORBID_ALIASES_TO_MULTIPLE_INDICES), - EnumSet.noneOf(IndicesOptions.WildcardStates.class) - ); - - private final Set aliases; - private final Set allIndices; - private final Set originalRequested; - private final Set remoteIndices; - private final boolean isLocalAll; - private final IndicesOptions indicesOptions; - - public Resolved( - final ImmutableSet aliases, - final ImmutableSet allIndices, - final ImmutableSet originalRequested, - final ImmutableSet remoteIndices, - IndicesOptions indicesOptions - ) { - this.aliases = aliases; - this.allIndices = allIndices; - this.originalRequested = originalRequested; - this.remoteIndices = remoteIndices; - this.isLocalAll = IndexResolverReplacer.isLocalAll(originalRequested.toArray(new String[0])) - || (aliases.contains("*") && allIndices.contains("*")); - this.indicesOptions = indicesOptions; - } - - public boolean isLocalAll() { - return isLocalAll; - } - - public Set getAliases() { - return aliases; - } - - public Set getAllIndices() { - return allIndices; - } - - public Set getOriginalRequested() { - return originalRequested; - } - - public Set getAllIndicesResolved(ClusterService clusterService, IndexNameExpressionResolver resolver) { - return getAllIndicesResolved(clusterService::state, resolver); - } - - public Set getAllIndicesResolved(Supplier clusterStateSupplier, IndexNameExpressionResolver resolver) { - if (isLocalAll) { - return new HashSet<>(Arrays.asList(resolver.concreteIndexNames(clusterStateSupplier.get(), indicesOptions, "*"))); - } else { - return allIndices; - } - } - - public boolean isAllIndicesEmpty() { - return allIndices.isEmpty(); - } - - public Set getTypes() { - return types; - } - - public Set getRemoteIndices() { - return remoteIndices; - } - - @Override - public String toString() { - return "Resolved [aliases=" - + aliases - + ", allIndices=" - + allIndices - + ", types=" - + types - + ", originalRequested=" - + originalRequested - + ", remoteIndices=" - + remoteIndices - + "]"; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((aliases == null) ? 0 : aliases.hashCode()); - result = prime * result + ((allIndices == null) ? 0 : allIndices.hashCode()); - result = prime * result + ((originalRequested == null) ? 0 : originalRequested.hashCode()); - result = prime * result + ((remoteIndices == null) ? 0 : remoteIndices.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - Resolved other = (Resolved) obj; - if (aliases == null) { - if (other.aliases != null) return false; - } else if (!aliases.equals(other.aliases)) return false; - if (allIndices == null) { - if (other.allIndices != null) return false; - } else if (!allIndices.equals(other.allIndices)) return false; - if (originalRequested == null) { - if (other.originalRequested != null) return false; - } else if (!originalRequested.equals(other.originalRequested)) return false; - if (remoteIndices == null) { - if (other.remoteIndices != null) return false; - } else if (!remoteIndices.equals(other.remoteIndices)) return false; - return true; - } - - public static Resolved ofIndex(String index) { - ImmutableSet indexSet = ImmutableSet.of(index); - return new Resolved(ImmutableSet.of(), indexSet, indexSet, ImmutableSet.of(), EXACT_INDEX_OPTIONS); - } - } - - private List renamedIndices(final RestoreSnapshotRequest request, final List filteredIndices) { - try { - final List renamedIndices = new ArrayList<>(); - for (final String index : filteredIndices) { - String renamedIndex = index; - if (request.renameReplacement() != null && request.renamePattern() != null) { - renamedIndex = index.replaceAll(request.renamePattern(), request.renameReplacement()); - } - renamedIndices.add(renamedIndex); - } - return renamedIndices; - } catch (PatternSyntaxException e) { - log.error("Unable to parse the regular expression denoted in 'rename_pattern'. Please correct the pattern an try again."); - throw e; - } - } - - // -- - - @FunctionalInterface - public interface IndicesProvider { - public static final String[] NOOP = new String[0]; - - String[] provide(String[] original, Object request, boolean supportsReplace); - } - - private boolean checkIndices(Object request, String[] indices, boolean needsToBeSizeOne, boolean allowEmpty) { - - if (indices == IndicesProvider.NOOP) { - return false; - } - - final boolean isTraceEnabled = log.isTraceEnabled(); - if (!allowEmpty && (indices == null || indices.length == 0)) { - if (isTraceEnabled && request != null) { - log.trace("Null or empty indices for " + request.getClass().getName()); - } - return false; - } - - if (!allowEmpty && needsToBeSizeOne && indices.length != 1) { - if (isTraceEnabled && request != null) { - log.trace("To much indices for " + request.getClass().getName()); - } - return false; - } - - for (int i = 0; i < indices.length; i++) { - final String index = indices[i]; - if (index == null || index.isEmpty()) { - // not allowed - if (isTraceEnabled && request != null) { - log.trace("At least one null or empty index for " + request.getClass().getName()); - } - return false; - } - } - - return true; - } - - /** - * new - * @param request - * @param allowEmptyIndices - * @return - */ - @SuppressWarnings("rawtypes") - private boolean getOrReplaceAllIndices(final Object request, final IndicesProvider provider, boolean allowEmptyIndices) { - final boolean isDebugEnabled = log.isDebugEnabled(); - final boolean isTraceEnabled = log.isTraceEnabled(); - if (isTraceEnabled) { - log.trace("getOrReplaceAllIndices() for " + request.getClass()); - } - - boolean result = true; - - if (request instanceof BulkRequest) { - - for (DocWriteRequest ar : ((BulkRequest) request).requests()) { - result = getOrReplaceAllIndices(ar, provider, false) && result; - } - - } else if (request instanceof MultiGetRequest) { - - for (ListIterator it = ((MultiGetRequest) request).getItems().listIterator(); it.hasNext();) { - Item item = it.next(); - result = getOrReplaceAllIndices(item, provider, false) && result; - /*if(item.index() == null || item.indices() == null || item.indices().length == 0) { - it.remove(); - }*/ - } - - } else if (request instanceof MultiSearchRequest) { - - for (ListIterator it = ((MultiSearchRequest) request).requests().listIterator(); it.hasNext();) { - SearchRequest ar = it.next(); - result = getOrReplaceAllIndices(ar, provider, false) && result; - /*if(ar.indices() == null || ar.indices().length == 0) { - it.remove(); - }*/ - } - - } else if (request instanceof MultiTermVectorsRequest) { - - for (ActionRequest ar : (Iterable) () -> ((MultiTermVectorsRequest) request).iterator()) { - result = getOrReplaceAllIndices(ar, provider, false) && result; - } - - } else if (request instanceof PutMappingRequest) { - PutMappingRequest pmr = (PutMappingRequest) request; - Index concreteIndex = pmr.getConcreteIndex(); - if (concreteIndex != null && (pmr.indices() == null || pmr.indices().length == 0)) { - String[] newIndices = provider.provide(new String[] { concreteIndex.getName() }, request, true); - if (checkIndices(request, newIndices, true, allowEmptyIndices) == false) { - return false; - } - - ((PutMappingRequest) request).indices(newIndices); - ((PutMappingRequest) request).setConcreteIndex(null); - } else { - String[] newIndices = provider.provide(((PutMappingRequest) request).indices(), request, true); - if (checkIndices(request, newIndices, false, allowEmptyIndices) == false) { - return false; - } - ((PutMappingRequest) request).indices(newIndices); - } - } else if (request instanceof RestoreSnapshotRequest) { - - if (clusterInfoHolder.isLocalNodeElectedClusterManager() == Boolean.FALSE) { - return true; - } - - final RestoreSnapshotRequest restoreRequest = (RestoreSnapshotRequest) request; - final SnapshotInfo snapshotInfo = SnapshotRestoreHelper.getSnapshotInfo(restoreRequest); - - if (snapshotInfo == null) { - log.warn( - "snapshot repository '" + restoreRequest.repository() + "', snapshot '" + restoreRequest.snapshot() + "' not found" - ); - provider.provide(new String[] { "*" }, request, false); - } else { - final List requestedResolvedIndices = IndexUtils.filterIndices( - snapshotInfo.indices(), - restoreRequest.indices(), - restoreRequest.indicesOptions() - ); - final List renamedTargetIndices = renamedIndices(restoreRequest, requestedResolvedIndices); - // final Set indices = new HashSet<>(requestedResolvedIndices); - // indices.addAll(renamedTargetIndices); - if (isDebugEnabled) { - log.debug("snapshot: {} contains this indices: {}", snapshotInfo.snapshotId().getName(), renamedTargetIndices); - } - provider.provide(renamedTargetIndices.toArray(new String[0]), request, false); - } - - } else if (request instanceof IndicesAliasesRequest) { - for (AliasActions ar : ((IndicesAliasesRequest) request).getAliasActions()) { - result = getOrReplaceAllIndices(ar, provider, false) && result; - } - } else if (request instanceof DeleteRequest) { - String[] newIndices = provider.provide(((DeleteRequest) request).indices(), request, true); - if (checkIndices(request, newIndices, true, allowEmptyIndices) == false) { - return false; - } - ((DeleteRequest) request).index(newIndices.length != 1 ? null : newIndices[0]); - } else if (request instanceof UpdateRequest) { - String[] newIndices = provider.provide(((UpdateRequest) request).indices(), request, true); - if (checkIndices(request, newIndices, true, allowEmptyIndices) == false) { - return false; - } - ((UpdateRequest) request).index(newIndices.length != 1 ? null : newIndices[0]); - } else if (request instanceof SingleShardRequest) { - final SingleShardRequest singleShardRequest = (SingleShardRequest) request; - final String index = singleShardRequest.index(); - String[] indices = provider.provide(index == null ? null : new String[] { index }, request, true); - if (!checkIndices(request, indices, true, allowEmptyIndices)) { - return false; - } - singleShardRequest.index(indices.length != 1 ? null : indices[0]); - } else if (request instanceof FieldCapabilitiesIndexRequest) { - // FieldCapabilitiesIndexRequest does not support replacing the indexes. - // However, the indexes are always determined by FieldCapabilitiesRequest which will be reduced below - // (implements Replaceable). So IF an index arrives here, we can be sure that we have - // at least privileges for indices:data/read/field_caps - FieldCapabilitiesIndexRequest fieldCapabilitiesRequest = (FieldCapabilitiesIndexRequest) request; - - String index = fieldCapabilitiesRequest.index(); - - String[] newIndices = provider.provide(new String[] { index }, request, true); - if (!checkIndices(request, newIndices, true, allowEmptyIndices)) { - return false; - } - } else if (request instanceof IndexRequest) { - String[] newIndices = provider.provide(((IndexRequest) request).indices(), request, true); - if (checkIndices(request, newIndices, true, allowEmptyIndices) == false) { - return false; - } - ((IndexRequest) request).index(newIndices.length != 1 ? null : newIndices[0]); - } else if (request instanceof Replaceable) { - String[] newIndices = provider.provide(((Replaceable) request).indices(), request, true); - if (checkIndices(request, newIndices, false, allowEmptyIndices) == false) { - return false; - } - ((Replaceable) request).indices(newIndices); - } else if (request instanceof RolloverRequest rolloverRequest) { - provider.provide(rolloverRequest.indices(), request, false); - if (rolloverRequest.getNewIndexName() != null) { // only when target index is explicitly provided - provider.provide(new String[] { rolloverRequest.getNewIndexName() }, request, false); - } - } else if (request instanceof BulkShardRequest) { - provider.provide(((ReplicationRequest) request).indices(), request, false); - // replace not supported? - } else if (request instanceof ReplicationRequest) { - String[] newIndices = provider.provide(((ReplicationRequest) request).indices(), request, true); - if (checkIndices(request, newIndices, true, allowEmptyIndices) == false) { - return false; - } - ((ReplicationRequest) request).index(newIndices.length != 1 ? null : newIndices[0]); - } else if (request instanceof MultiGetRequest.Item) { - String[] newIndices = provider.provide(((MultiGetRequest.Item) request).indices(), request, true); - if (checkIndices(request, newIndices, true, allowEmptyIndices) == false) { - return false; - } - ((MultiGetRequest.Item) request).index(newIndices.length != 1 ? null : newIndices[0]); - } else if (request instanceof CreateIndexRequest) { - String[] newIndices = provider.provide(((CreateIndexRequest) request).indices(), request, true); - if (checkIndices(request, newIndices, true, allowEmptyIndices) == false) { - return false; - } - ((CreateIndexRequest) request).index(newIndices.length != 1 ? null : newIndices[0]); - } else if (request instanceof ResizeRequest) { - // clone or shrink operations - provider.provide(((ResizeRequest) request).indices(), request, true); - provider.provide(((ResizeRequest) request).getTargetIndexRequest().indices(), request, true); - } else if (request instanceof CreateDataStreamAction.Request) { - provider.provide(((CreateDataStreamAction.Request) request).indices(), request, false); - } else if (request instanceof ReindexRequest) { - result = getOrReplaceAllIndices(((ReindexRequest) request).getDestination(), provider, false) && result; - result = getOrReplaceAllIndices(((ReindexRequest) request).getSearchRequest(), provider, false) && result; - } else if (request instanceof BaseNodesRequest) { - // do nothing - } else if (request instanceof MainRequest) { - // do nothing - } else if (request instanceof ClearScrollRequest) { - // do nothing - } else if (request instanceof SearchScrollRequest) { - // do nothing - } else if (request instanceof PutComponentTemplateAction.Request) { - // do nothing - } else { - if (isDebugEnabled) { - log.debug(request.getClass() + " not supported (It is likely not a indices related request)"); - } - result = false; - } - - return result; - } - - private IndicesOptions indicesOptionsFrom(Object localRequest) { - - if (!respectRequestIndicesOptions) { - return IndicesOptions.fromOptions(false, true, true, false, true); - } - - if (IndicesRequest.class.isInstance(localRequest)) { - return ((IndicesRequest) localRequest).indicesOptions(); - } else if (RestoreSnapshotRequest.class.isInstance(localRequest)) { - return ((RestoreSnapshotRequest) localRequest).indicesOptions(); - } else { - return IndicesOptions.fromOptions(false, true, true, false, true); - } - } - - @Subscribe - public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { - respectRequestIndicesOptions = dcm.isRespectRequestIndicesEnabled(); - } -} diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index e547732a9f..7a3b153f3d 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -28,7 +28,7 @@ import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.resources.sharing.Recipient; import org.opensearch.security.resources.sharing.ResourceSharing; import org.opensearch.security.resources.sharing.ShareWith; @@ -53,7 +53,7 @@ public class ResourceAccessHandler { private final ThreadContext threadContext; private final ResourceSharingIndexHandler resourceSharingIndexHandler; private final AdminDNs adminDNs; - private final PrivilegesEvaluator privilegesEvaluator; + private final PrivilegesConfiguration privilegesConfiguration; private final ResourcePluginInfo resourcePluginInfo; @Inject @@ -61,13 +61,14 @@ public ResourceAccessHandler( final ThreadPool threadPool, final ResourceSharingIndexHandler resourceSharingIndexHandler, AdminDNs adminDns, - PrivilegesEvaluator evaluator, + PrivilegesConfiguration privilegesConfiguration, ResourcePluginInfo resourcePluginInfo + ) { this.threadContext = threadPool.getThreadContext(); this.resourceSharingIndexHandler = resourceSharingIndexHandler; this.adminDNs = adminDns; - this.privilegesEvaluator = evaluator; + this.privilegesConfiguration = privilegesConfiguration; this.resourcePluginInfo = resourcePluginInfo; } @@ -160,6 +161,7 @@ public void hasPermission( listener.onResponse(true); return; } + Set userRoles = new HashSet<>(user.getSecurityRoles()); Set userBackendRoles = new HashSet<>(user.getRoles()); diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingDlsUtils.java b/src/main/java/org/opensearch/security/resources/ResourceSharingDlsUtils.java index 86ac188b37..67d3cf9e12 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingDlsUtils.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingDlsUtils.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import com.google.common.collect.ImmutableMap; @@ -22,7 +23,6 @@ import org.opensearch.security.privileges.dlsfls.DlsRestriction; import org.opensearch.security.privileges.dlsfls.DocumentPrivileges; import org.opensearch.security.privileges.dlsfls.IndexToRuleMap; -import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.user.User; public class ResourceSharingDlsUtils { @@ -30,7 +30,7 @@ public class ResourceSharingDlsUtils { public static IndexToRuleMap resourceRestrictions( NamedXContentRegistry xContentRegistry, - IndexResolverReplacer.Resolved resolved, + Collection resolvedIndices, User user ) { @@ -63,7 +63,7 @@ public static IndexToRuleMap resourceRestrictions( } ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); - for (String index : resolved.getAllIndices()) { + for (String index : resolvedIndices) { mapBuilder.put(index, restriction); } return new IndexToRuleMap<>(mapBuilder.build()); diff --git a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java index 7353633071..1e744be83c 100644 --- a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java @@ -33,14 +33,21 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.common.settings.Settings; 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.RestChannel; +import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.privileges.DashboardsMultiTenancyConfiguration; +import org.opensearch.security.privileges.PrivilegesConfiguration; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.DashboardSignInOption; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -78,7 +85,8 @@ public class DashboardsInfoAction extends BaseRestHandler { .build(); private final Logger log = LogManager.getLogger(this.getClass()); - private final PrivilegesEvaluator evaluator; + private final PrivilegesConfiguration privilegesConfiguration; + private final ConfigurationRepository configurationRepository; private final ThreadContext threadContext; private final OpensearchDynamicSetting resourceSharingEnabledSetting; @@ -89,14 +97,18 @@ public class DashboardsInfoAction extends BaseRestHandler { public static final String DEFAULT_PASSWORD_REGEX = "(?=.*[A-Z])(?=.*[^a-zA-Z\\d])(?=.*[0-9])(?=.*[a-z]).{8,}"; public DashboardsInfoAction( - final PrivilegesEvaluator evaluator, + final Settings settings, + final RestController controller, + final PrivilegesConfiguration privilegesConfiguration, + final ConfigurationRepository configurationRepository, final ThreadPool threadPool, OpensearchDynamicSetting resourceSharingEnabledSetting ) { super(); this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; this.threadContext = threadPool.getThreadContext(); - this.evaluator = evaluator; + this.privilegesConfiguration = privilegesConfiguration; + this.configurationRepository = configurationRepository; } @Override @@ -122,16 +134,21 @@ public void accept(RestChannel channel) throws Exception { final User user = (User) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + DashboardsMultiTenancyConfiguration multiTenancyConfiguration = privilegesConfiguration.multiTenancyConfiguration(); + builder.startObject(); builder.field("user_name", user == null ? null : user.getName()); - builder.field("not_fail_on_forbidden_enabled", evaluator.notFailOnForbiddenEnabled()); - builder.field("opensearch_dashboards_mt_enabled", evaluator.multitenancyEnabled()); - builder.field("opensearch_dashboards_index", evaluator.dashboardsIndex()); - builder.field("opensearch_dashboards_server_user", evaluator.dashboardsServerUsername()); - builder.field("multitenancy_enabled", evaluator.multitenancyEnabled()); - builder.field("private_tenant_enabled", evaluator.privateTenantEnabled()); - builder.field("default_tenant", evaluator.dashboardsDefaultTenant()); - builder.field("sign_in_options", evaluator.getSignInOptions()); + builder.field( + "not_fail_on_forbidden_enabled", + privilegesConfiguration.privilegesEvaluator().notFailOnForbiddenEnabled() + ); + builder.field("opensearch_dashboards_mt_enabled", multiTenancyConfiguration.multitenancyEnabled()); + builder.field("opensearch_dashboards_index", multiTenancyConfiguration.dashboardsIndex()); + builder.field("opensearch_dashboards_server_user", multiTenancyConfiguration.dashboardsServerUsername()); + builder.field("multitenancy_enabled", multiTenancyConfiguration.multitenancyEnabled()); + builder.field("private_tenant_enabled", multiTenancyConfiguration.privateTenantEnabled()); + builder.field("default_tenant", multiTenancyConfiguration.dashboardsDefaultTenant()); + builder.field("sign_in_options", getSignInOptions()); builder.field( "password_validation_error_message", client.settings().get(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, DEFAULT_PASSWORD_MESSAGE) @@ -167,4 +184,13 @@ public String getName() { return "Kibana Info Action"; } + private List getSignInOptions() { + ConfigV7 generalConfig = configurationRepository.getConfiguration(CType.CONFIG).getCEntry(CType.CONFIG.name()); + if (generalConfig != null && generalConfig.dynamic != null && generalConfig.dynamic.kibana != null) { + return generalConfig.dynamic.kibana.sign_in_options; + } else { + return new ConfigV7.Kibana().sign_in_options; + } + } + } diff --git a/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java b/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java index 3de69a1a34..7319bfa8ca 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java @@ -40,7 +40,7 @@ import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.security.auth.BackendRegistry; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.transport.client.node.NodeClient; import static org.opensearch.rest.RestRequest.Method.GET; @@ -67,17 +67,17 @@ public class SecurityHealthAction extends BaseRestHandler { ); private final BackendRegistry registry; - private final PrivilegesEvaluator privilegesEvaluator; + private final PrivilegesConfiguration privilegesConfiguration; public SecurityHealthAction( final Settings settings, final RestController controller, final BackendRegistry registry, - final PrivilegesEvaluator privilegesEvaluator + final PrivilegesConfiguration privilegesConfiguration ) { super(); this.registry = registry; - this.privilegesEvaluator = privilegesEvaluator; + this.privilegesConfiguration = privilegesConfiguration; } @Override @@ -108,7 +108,7 @@ public void accept(RestChannel channel) throws Exception { builder.startObject(); - if ("strict".equalsIgnoreCase(mode) && !(registry.isInitialized() && privilegesEvaluator.isInitialized())) { + if ("strict".equalsIgnoreCase(mode) && !(registry.isInitialized() && privilegesConfiguration.isInitialized())) { status = "DOWN"; message = "Not initialized"; restStatus = RestStatus.SERVICE_UNAVAILABLE; diff --git a/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java b/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java index 41b8cc98be..6932462f48 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java @@ -48,8 +48,8 @@ import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; -import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.support.Base64Helper; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -79,18 +79,18 @@ public class SecurityInfoAction extends BaseRestHandler { ); private final Logger log = LogManager.getLogger(this.getClass()); - private final PrivilegesEvaluator evaluator; + private final PrivilegesConfiguration privilegesConfiguration; private final ThreadContext threadContext; public SecurityInfoAction( final Settings settings, final RestController controller, - final PrivilegesEvaluator evaluator, + final PrivilegesConfiguration privilegesConfiguration, final ThreadPool threadPool ) { super(); this.threadContext = threadPool.getThreadContext(); - this.evaluator = evaluator; + this.privilegesConfiguration = privilegesConfiguration; } @Override @@ -122,7 +122,7 @@ public void accept(RestChannel channel) throws Exception { final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); final TransportAddress remoteAddress = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - PrivilegesEvaluationContext context = evaluator.createContext(user, null); + PrivilegesEvaluationContext context = privilegesConfiguration.privilegesEvaluator().createContext(user, null); builder.startObject(); builder.field("user", user == null ? null : user.toString()); @@ -132,7 +132,7 @@ public void accept(RestChannel channel) throws Exception { builder.field("backend_roles", user == null ? null : user.getRoles()); builder.field("custom_attribute_names", user == null ? null : user.getCustomAttributesMap().keySet()); builder.field("roles", context.getMappedRoles()); - builder.field("tenants", evaluator.tenantPrivileges().tenantMap(context)); + builder.field("tenants", privilegesConfiguration.tenantPrivileges().tenantMap(context)); builder.field("principal", (String) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL)); builder.field("peer_certificates", certs != null && certs.length > 0 ? certs.length + "" : "0"); builder.field("sso_logout_url", (String) threadContext.getTransient(ConfigConstants.SSO_LOGOUT_URL)); diff --git a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java index 47c4b61cc2..6628048546 100644 --- a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java @@ -49,7 +49,9 @@ import org.opensearch.rest.RestRequest; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.DashboardsMultiTenancyConfiguration; +import org.opensearch.security.privileges.PrivilegesConfiguration; +import org.opensearch.security.privileges.TenantPrivileges; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.RoleMappings; import org.opensearch.security.securityconf.impl.CType; @@ -82,7 +84,7 @@ public class TenantInfoAction extends BaseRestHandler { ); private final Logger log = LogManager.getLogger(this.getClass()); - private final PrivilegesEvaluator evaluator; + private final PrivilegesConfiguration privilegesConfiguration; private final ThreadContext threadContext; private final ClusterService clusterService; private final AdminDNs adminDns; @@ -91,7 +93,7 @@ public class TenantInfoAction extends BaseRestHandler { public TenantInfoAction( final Settings settings, final RestController controller, - final PrivilegesEvaluator evaluator, + final PrivilegesConfiguration privilegesConfiguration, final ThreadPool threadPool, final ClusterService clusterService, final AdminDNs adminDns, @@ -99,7 +101,7 @@ public TenantInfoAction( ) { super(); this.threadContext = threadPool.getThreadContext(); - this.evaluator = evaluator; + this.privilegesConfiguration = privilegesConfiguration; this.clusterService = clusterService; this.adminDns = adminDns; this.configurationRepository = configurationRepository; @@ -134,10 +136,12 @@ public void accept(RestChannel channel) throws Exception { } else { builder.startObject(); + DashboardsMultiTenancyConfiguration multiTenancyConfiguration = privilegesConfiguration.multiTenancyConfiguration(); + TenantPrivileges tenantPrivileges = privilegesConfiguration.tenantPrivileges(); final SortedMap lookup = clusterService.state().metadata().getIndicesLookup(); for (final String indexOrAlias : lookup.keySet()) { - final String tenant = tenantNameForIndex(indexOrAlias); + final String tenant = tenantNameForIndex(indexOrAlias, multiTenancyConfiguration, tenantPrivileges); if (tenant != null) { builder.field(indexOrAlias, tenant); } @@ -172,8 +176,10 @@ private boolean isAuthorized() { return false; } + DashboardsMultiTenancyConfiguration multiTenancyConfiguration = privilegesConfiguration.multiTenancyConfiguration(); + // check if the user is a kibanauser or super admin - if (user.getName().equals(evaluator.dashboardsServerUsername()) || adminDns.isAdmin(user)) { + if (user.getName().equals(multiTenancyConfiguration.dashboardsServerUsername()) || adminDns.isAdmin(user)) { return true; } @@ -182,7 +188,7 @@ private boolean isAuthorized() { // check if dashboardsOpenSearchRole is present in RolesMapping and if yes, check if user is a part of this role if (rolesMappingConfiguration != null) { - String dashboardsOpenSearchRole = evaluator.dashboardsOpenSearchRole(); + String dashboardsOpenSearchRole = multiTenancyConfiguration.dashboardsOpenSearchRole(); if (Strings.isNullOrEmpty(dashboardsOpenSearchRole)) { return false; } @@ -201,13 +207,17 @@ private final SecurityDynamicConfiguration load(final CType config, boolea return DynamicConfigFactory.addStatics(loaded); } - private String tenantNameForIndex(String index) { + private String tenantNameForIndex( + String index, + DashboardsMultiTenancyConfiguration multiTenancyConfiguration, + TenantPrivileges tenantPrivileges + ) { String[] indexParts; if (index == null || (indexParts = index.split("_")).length != 3) { return null; } - if (!indexParts[0].equals(evaluator.dashboardsIndex())) { + if (!indexParts[0].equals(multiTenancyConfiguration.dashboardsIndex())) { return null; } @@ -215,7 +225,7 @@ private String tenantNameForIndex(String index) { final int expectedHash = Integer.parseInt(indexParts[1]); final String sanitizedName = indexParts[2]; - for (String tenant : evaluator.tenantPrivileges().allTenantNames()) { + for (String tenant : tenantPrivileges.allTenantNames()) { if (tenant.hashCode() == expectedHash && sanitizedName.equals(tenant.toLowerCase().replaceAll("[^a-z0-9]+", ""))) { return tenant; } diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModel.java b/src/main/java/org/opensearch/security/securityconf/ConfigModel.java deleted file mode 100644 index a1546de0f4..0000000000 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModel.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * 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.securityconf; - -import java.util.Set; - -import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.security.user.User; - -public abstract class ConfigModel { - public abstract Set mapSecurityRoles(User user, TransportAddress caller); -} diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java deleted file mode 100644 index e811a267a8..0000000000 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2015-2018 floragunn GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package org.opensearch.security.securityconf; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map.Entry; -import java.util.Set; - -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ListMultimap; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.common.settings.Settings; -import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; -import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7; -import org.opensearch.security.securityconf.impl.v7.RoleV7; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.support.HostResolverMode; -import org.opensearch.security.support.WildcardMatcher; -import org.opensearch.security.user.User; - -public class ConfigModelV7 extends ConfigModel { - - protected final Logger log = LogManager.getLogger(this.getClass()); - private ConfigConstants.RolesMappingResolution rolesMappingResolution; - private RoleMappingHolder roleMappingHolder; - private SecurityDynamicConfiguration roles; - - public ConfigModelV7( - SecurityDynamicConfiguration roles, - SecurityDynamicConfiguration rolemappings, - DynamicConfigModel dcm, - Settings opensearchSettings - ) { - - this.roles = roles; - - try { - rolesMappingResolution = ConfigConstants.RolesMappingResolution.valueOf( - opensearchSettings.get( - ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, - ConfigConstants.RolesMappingResolution.MAPPING_ONLY.toString() - ).toUpperCase() - ); - } catch (Exception e) { - log.error("Cannot apply roles mapping resolution", e); - rolesMappingResolution = ConfigConstants.RolesMappingResolution.MAPPING_ONLY; - } - - roleMappingHolder = new RoleMappingHolder(rolemappings, dcm.getHostsResolverMode()); - } - - private class RoleMappingHolder { - - private ListMultimap users; - private ListMultimap, String> abars; - private ListMultimap bars; - private ListMultimap hosts; - private final String hostResolverMode; - - private List userMatchers; - private List barMatchers; - private List hostMatchers; - - private RoleMappingHolder(final SecurityDynamicConfiguration rolemappings, final String hostResolverMode) { - - this.hostResolverMode = hostResolverMode; - - if (roles != null) { - - users = ArrayListMultimap.create(); - abars = ArrayListMultimap.create(); - bars = ArrayListMultimap.create(); - hosts = ArrayListMultimap.create(); - - for (final Entry roleMap : rolemappings.getCEntries().entrySet()) { - final String roleMapKey = roleMap.getKey(); - final RoleMappingsV7 roleMapValue = roleMap.getValue(); - - for (String u : roleMapValue.getUsers()) { - users.put(u, roleMapKey); - } - - final Set abar = new HashSet<>(roleMapValue.getAnd_backend_roles()); - - if (!abar.isEmpty()) { - abars.put(WildcardMatcher.matchers(abar), roleMapKey); - } - - for (String bar : roleMapValue.getBackend_roles()) { - bars.put(bar, roleMapKey); - } - - for (String host : roleMapValue.getHosts()) { - hosts.put(host, roleMapKey); - } - } - - userMatchers = WildcardMatcher.matchers(users.keySet()); - barMatchers = WildcardMatcher.matchers(bars.keySet()); - hostMatchers = WildcardMatcher.matchers(hosts.keySet()); - } - } - - private Set map(final User user, final TransportAddress caller) { - - if (user == null || users == null || abars == null || bars == null || hosts == null) { - return Collections.emptySet(); - } - - final Set securityRoles = new HashSet<>(user.getSecurityRoles()); - - if (rolesMappingResolution == ConfigConstants.RolesMappingResolution.BOTH - || rolesMappingResolution == ConfigConstants.RolesMappingResolution.BACKENDROLES_ONLY) { - if (log.isDebugEnabled()) { - log.debug("Pass backendroles from {}", user); - } - securityRoles.addAll(user.getRoles()); - } - - if (((rolesMappingResolution == ConfigConstants.RolesMappingResolution.BOTH - || rolesMappingResolution == ConfigConstants.RolesMappingResolution.MAPPING_ONLY))) { - - for (String p : WildcardMatcher.getAllMatchingPatterns(userMatchers, user.getName())) { - securityRoles.addAll(users.get(p)); - } - for (String p : WildcardMatcher.getAllMatchingPatterns(barMatchers, user.getRoles())) { - securityRoles.addAll(bars.get(p)); - } - - for (List patterns : abars.keySet()) { - if (patterns.stream().allMatch(p -> p.matchAny(user.getRoles()))) { - securityRoles.addAll(abars.get(patterns)); - } - } - - if (caller != null) { - // IPV4 or IPv6 (compressed and without scope identifiers) - final String ipAddress = caller.getAddress(); - - for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, ipAddress)) { - securityRoles.addAll(hosts.get(p)); - } - - if (caller.address() != null - && (hostResolverMode.equalsIgnoreCase(HostResolverMode.IP_HOSTNAME.getValue()) - || hostResolverMode.equalsIgnoreCase(HostResolverMode.IP_HOSTNAME_LOOKUP.getValue()))) { - final String hostName = caller.address().getHostString(); - - for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, hostName)) { - securityRoles.addAll(hosts.get(p)); - } - } - - if (caller.address() != null && hostResolverMode.equalsIgnoreCase(HostResolverMode.IP_HOSTNAME_LOOKUP.getValue())) { - - final String resolvedHostName = caller.address().getHostName(); - - for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, resolvedHostName)) { - securityRoles.addAll(hosts.get(p)); - } - } - } - } - - return Collections.unmodifiableSet(securityRoles); - - } - } - - public Set mapSecurityRoles(User user, TransportAddress caller) { - return roleMappingHolder.map(user, caller); - } -} diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java index 249c1a8a15..cb124d8c51 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java @@ -236,7 +236,6 @@ public void onChange(ConfigurationMap typeToConfig) { final DynamicConfigModel dcm; final InternalUsersModel ium; - final ConfigModel cm; final NodesDnModel nm = new NodesDnModelImpl(nodesDn); final AllowlistingSettings allowlist = cr.getConfiguration(CType.ALLOWLIST).getCEntry("config"); final AuditConfig audit = cr.getConfiguration(CType.AUDIT).getCEntry("config"); @@ -278,10 +277,8 @@ public void onChange(ConfigurationMap typeToConfig) { // rebuild v7 Models dcm = new DynamicConfigModelV7(getConfigV7(config), opensearchSettings, configPath, iab, this.cih); ium = new InternalUsersModelV7(internalusers, roles, rolesmapping); - cm = new ConfigModelV7(roles, rolesmapping, dcm, opensearchSettings); // notify subscribers - eventBus.post(cm); eventBus.post(dcm); eventBus.post(ium); eventBus.post(nm); diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java index def5247590..1659b532f0 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java @@ -86,6 +86,9 @@ public static class Dynamic { public String transport_userrname_attribute; public boolean do_not_fail_on_forbidden_empty; public OnBehalfOfSettings on_behalf_of = new OnBehalfOfSettings(); + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonProperty("privileges_evaluation_type") + public String privilegesEvaluationType = null; @Override public String toString() { diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 27e936f451..e164400da5 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -329,12 +329,6 @@ public class ConfigConstants { public static final String SECURITY_DISABLE_ENVVAR_REPLACEMENT = SECURITY_SETTINGS_PREFIX + "disable_envvar_replacement"; public static final String SECURITY_DFM_EMPTY_OVERRIDES_ALL = SECURITY_SETTINGS_PREFIX + "dfm_empty_overrides_all"; - public enum RolesMappingResolution { - MAPPING_ONLY, - BACKENDROLES_ONLY, - BOTH - } - public static final String SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS = SECURITY_SETTINGS_PREFIX + "filter_securityindex_from_all_requests"; public static final String SECURITY_DLS_MODE = SECURITY_SETTINGS_PREFIX + "dls.mode"; diff --git a/src/main/java/org/opensearch/security/support/HostResolverMode.java b/src/main/java/org/opensearch/security/support/HostResolverMode.java index 00ce6e9117..ef23381826 100644 --- a/src/main/java/org/opensearch/security/support/HostResolverMode.java +++ b/src/main/java/org/opensearch/security/support/HostResolverMode.java @@ -13,7 +13,8 @@ public enum HostResolverMode { IP_HOSTNAME("ip-hostname"), - IP_HOSTNAME_LOOKUP("ip-hostname-lookup"); + IP_HOSTNAME_LOOKUP("ip-hostname-lookup"), + DISABLED("disabled"); private final String value; @@ -24,4 +25,14 @@ public enum HostResolverMode { public String getValue() { return value; } + + public static HostResolverMode fromConfig(String hostResolverModeConfig) { + if (hostResolverModeConfig == null || hostResolverModeConfig.equalsIgnoreCase(IP_HOSTNAME.value)) { + return HostResolverMode.IP_HOSTNAME; + } else if (hostResolverModeConfig.equalsIgnoreCase(IP_HOSTNAME_LOOKUP.value)) { + return HostResolverMode.IP_HOSTNAME_LOOKUP; + } else { + return HostResolverMode.DISABLED; + } + } } diff --git a/src/main/java/org/opensearch/security/support/SnapshotRestoreHelper.java b/src/main/java/org/opensearch/security/support/SnapshotRestoreHelper.java index fbd8eb4884..2b384b7135 100644 --- a/src/main/java/org/opensearch/security/support/SnapshotRestoreHelper.java +++ b/src/main/java/org/opensearch/security/support/SnapshotRestoreHelper.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -36,6 +37,8 @@ import org.opensearch.action.support.PlainActionFuture; import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.RestoreInProgress; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.util.IndexUtils; import org.opensearch.core.index.Index; @@ -60,7 +63,23 @@ public static List resolveOriginalIndices(RestoreSnapshotRequest restore } else { return IndexUtils.filterIndices(snapshotInfo.indices(), restoreRequest.indices(), restoreRequest.indicesOptions()); } + } + + public static OptionallyResolvedIndices resolveTargetIndices(RestoreSnapshotRequest request) { + List indices = resolveOriginalIndices(request); + if (indices == null) { + return ResolvedIndices.unknown(); + } + if (request.renameReplacement() != null && request.renamePattern() != null) { + return ResolvedIndices.of( + indices.stream() + .map(index -> index.replaceAll(request.renamePattern(), request.renameReplacement())) + .collect(Collectors.toSet()) + ); + } else { + return ResolvedIndices.of(indices); + } } public static SnapshotInfo getSnapshotInfo(RestoreSnapshotRequest restoreRequest) { diff --git a/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java b/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java new file mode 100644 index 0000000000..88a22ad5e6 --- /dev/null +++ b/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java @@ -0,0 +1,79 @@ +package org.opensearch.security.user; + +import java.util.HashMap; +import java.util.StringJoiner; + +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.common.Strings; +import org.opensearch.security.privileges.PrivilegesConfiguration; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.TenantPrivileges; +import org.opensearch.security.support.Base64Helper; + +import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; +import static org.opensearch.security.support.ConfigConstants.USER_ATTRIBUTE_SERIALIZATION_ENABLED; +import static org.opensearch.security.support.ConfigConstants.USER_ATTRIBUTE_SERIALIZATION_ENABLED_DEFAULT; +import static org.opensearch.security.support.SecurityUtils.escapePipe; + +/** + * Functionality to add parseable information about the current user to the thread context. Usually called + * in the SecurityFilter. + *

    + * Moved from https://github.com/opensearch-project/security/blob/d29095f26dba1a26308c69b608dc926bd40c0f52/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java#L293 + */ +public class ThreadContextUserInfo { + private static final String READ_ACCESS = "READ"; + private static final String WRITE_ACCESS = "WRITE"; + private static final String NO_ACCESS = "NONE"; + private static final String GLOBAL_TENANT = "global_tenant"; + + private final boolean userAttributeSerializationEnabled; + private final ThreadContext threadContext; + private final PrivilegesConfiguration privilegesConfiguration; + + public ThreadContextUserInfo(ThreadContext threadContext, PrivilegesConfiguration privilegesConfiguration, Settings settings) { + this.threadContext = threadContext; + this.userAttributeSerializationEnabled = settings.getAsBoolean( + USER_ATTRIBUTE_SERIALIZATION_ENABLED, + USER_ATTRIBUTE_SERIALIZATION_ENABLED_DEFAULT + ); + this.privilegesConfiguration = privilegesConfiguration; + } + + public void setUserInfoInThreadContext(PrivilegesEvaluationContext context) { + if (threadContext.getTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT) == null) { + StringJoiner joiner = new StringJoiner("|"); + // Escape any pipe characters in the values before joining + joiner.add(escapePipe(context.getUser().getName())); + joiner.add(escapePipe(String.join(",", context.getUser().getRoles()))); + joiner.add(escapePipe(String.join(",", context.getMappedRoles()))); + + String requestedTenant = context.getUser().getRequestedTenant(); + joiner.add(requestedTenant); + + String tenantAccessToCheck = getTenancyAccess(context); + joiner.add(tenantAccessToCheck); + + if (userAttributeSerializationEnabled) { + joiner.add(Base64Helper.serializeObject(new HashMap<>(context.getUser().getCustomAttributesMap()))); + } + + threadContext.putTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT, joiner.toString()); + } + } + + private String getTenancyAccess(PrivilegesEvaluationContext context) { + String requestedTenant = context.getUser().getRequestedTenant(); + TenantPrivileges tenantPrivileges = privilegesConfiguration.tenantPrivileges(); + final String tenant = Strings.isNullOrEmpty(requestedTenant) ? GLOBAL_TENANT : requestedTenant; + if (tenantPrivileges.hasTenantPrivilege(context, tenant, TenantPrivileges.ActionType.WRITE)) { + return WRITE_ACCESS; + } else if (tenantPrivileges.hasTenantPrivilege(context, tenant, TenantPrivileges.ActionType.READ)) { + return READ_ACCESS; + } else { + return NO_ACCESS; + } + } + +} diff --git a/src/test/java/org/opensearch/security/IndexIntegrationTests.java b/src/test/java/org/opensearch/security/IndexIntegrationTests.java index fa996c0cd5..265a34450f 100644 --- a/src/test/java/org/opensearch/security/IndexIntegrationTests.java +++ b/src/test/java/org/opensearch/security/IndexIntegrationTests.java @@ -26,7 +26,6 @@ package org.opensearch.security; -import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; @@ -595,6 +594,9 @@ public void testAliases() throws Exception { ); assertContains(res, "*\"hits\" : {*\"value\" : 0,*\"hits\" : [ ]*"); + res = rh.executePutRequest("/logstash-1/_alias/alog1", "", encodeBasicHeader("aliasmngt", "nagilum")); + System.out.println(res.getBody()); + // add alias to allowed index assertThat( HttpStatus.SC_OK, @@ -664,11 +666,17 @@ public void testIndexResolveInvalidIndexName() throws Exception { setup(); final RestHelper rh = nonSslRestHelper(); - // invalid_index_name_exception should be thrown and responded when invalid index name is mentioned in requests. - HttpResponse res = rh.executeGetRequest( - URLEncoder.encode("_##pdt_data/_search", "UTF-8"), - encodeBasicHeader("ccsresolv", "nagilum") - ); + // invalid_index_name_exception should be thrown and responded when invalid index name is mentioned in requests AND if the + // user has in theory privileges for the (invalid) index name. This is because the index name validation takes + // place in the transport action itself. + // The security plugin should not engage itself in any validation logic that is outside of its scope. + + // We do not have privileges for the index below, thus we get a 403 error + HttpResponse res = rh.executeGetRequest("/_pdt_data/_search", encodeBasicHeader("ccsresolv", "nagilum")); + assertThat(res.getStatusCode(), is(HttpStatus.SC_FORBIDDEN)); + + // We have privileges for the invalid index name below, thus we get through to the validation logic + res = rh.executeGetRequest("/_abcdata/_search", encodeBasicHeader("ccsresolv", "nagilum")); assertThat(res.getStatusCode(), is(HttpStatus.SC_BAD_REQUEST)); Assert.assertTrue(res.getBody().contains("invalid_index_name_exception")); } diff --git a/src/test/java/org/opensearch/security/TransportUserInjectorIntegTest.java b/src/test/java/org/opensearch/security/TransportUserInjectorIntegTest.java index ff37fad282..6dc1b84621 100644 --- a/src/test/java/org/opensearch/security/TransportUserInjectorIntegTest.java +++ b/src/test/java/org/opensearch/security/TransportUserInjectorIntegTest.java @@ -131,7 +131,10 @@ public void testSecurityUserInjection() throws Exception { exception = ex; log.debug(ex.toString()); Assert.assertNotNull(exception); - Assert.assertTrue(exception.getMessage().toString().contains("no permissions for [indices:admin/create]")); + Assert.assertTrue( + exception.getMessage(), + exception.getMessage().toString().contains("no permissions for [indices:admin/create]") + ); } // 3. with valid backend roles for injected user diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java index bbe1bf90f8..e8172d7723 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java @@ -20,7 +20,6 @@ import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestRequest; import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; @@ -37,7 +36,7 @@ public void setUp() { this.privilegesEvaluator = new RestApiPrivilegesEvaluator( Settings.EMPTY, mock(AdminDNs.class), - mock(PrivilegesEvaluator.class), + (user, caller) -> user.getSecurityRoles(), mock(PrincipalExtractor.class), mock(Path.class), mock(ThreadPool.class) diff --git a/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java b/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java index 5a311bec8e..431b7316e7 100644 --- a/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java +++ b/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java @@ -20,6 +20,7 @@ import org.junit.runners.Parameterized; import org.opensearch.OpenSearchSecurityException; +import org.opensearch.action.support.ActionRequestMetadata; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; import org.opensearch.core.action.ActionListener; @@ -30,9 +31,9 @@ import org.opensearch.security.configuration.CompatConfig; import org.opensearch.security.configuration.DlsFlsRequestValve; import org.opensearch.security.http.XFFResolver; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.ResourceAccessEvaluator; -import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.threadpool.ThreadPool; @@ -80,7 +81,8 @@ public static Collection data() { public void testImmutableIndicesWildcardMatcher() { final SecurityFilter filter = new SecurityFilter( settings, - mock(PrivilegesEvaluator.class), + mock(PrivilegesConfiguration.class), + mock(RoleMapper.class), mock(AdminDNs.class), mock(DlsFlsRequestValve.class), mock(AuditLog.class), @@ -88,7 +90,6 @@ public void testImmutableIndicesWildcardMatcher() { mock(ClusterService.class), mock(ClusterInfoHolder.class), mock(CompatConfig.class), - mock(IndexResolverReplacer.class), mock(XFFResolver.class), mock(ResourceAccessEvaluator.class) ); @@ -105,7 +106,8 @@ public void testUnexepectedCausesAreNotSendToCallers() { final SecurityFilter filter = new SecurityFilter( settings, - mock(PrivilegesEvaluator.class), + mock(PrivilegesConfiguration.class), + mock(RoleMapper.class), mock(AdminDNs.class), mock(DlsFlsRequestValve.class), auditLog, @@ -113,13 +115,12 @@ public void testUnexepectedCausesAreNotSendToCallers() { mock(ClusterService.class), mock(ClusterInfoHolder.class), mock(CompatConfig.class), - mock(IndexResolverReplacer.class), mock(XFFResolver.class), mock(ResourceAccessEvaluator.class) ); // Act - filter.apply(null, null, null, null, listener, null); + filter.apply(null, null, null, ActionRequestMetadata.empty(), listener, null); // Verify verify(auditLog).getComplianceConfig(); // Make sure the exception was thrown diff --git a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java index 7558533656..a805ec88ba 100644 --- a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java +++ b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java @@ -13,7 +13,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Set; import com.google.common.io.BaseEncoding; import org.junit.After; @@ -31,7 +30,6 @@ import org.opensearch.identity.tokens.OnBehalfOfClaims; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; import org.opensearch.security.authtoken.jwt.JwtVendor; -import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -71,7 +69,7 @@ public class SecurityTokenManagerTest { @Before public void setup() { - tokenManager = spy(new SecurityTokenManager(cs, threadPool, userService)); + tokenManager = spy(new SecurityTokenManager(cs, threadPool, userService, (user, caller) -> user.getSecurityRoles())); } @After @@ -83,26 +81,12 @@ public void after() { "This is my super safe signing key that no one will ever be able to guess. It's would take billions of years and the world's most powerful quantum computer to crack"; final static String signingKeyB64Encoded = BaseEncoding.base64().encode(signingKey.getBytes(StandardCharsets.UTF_8)); - @Test - public void onConfigModelChanged_oboNotSupported() { - final ConfigModel configModel = mock(ConfigModel.class); - - tokenManager.onConfigModelChanged(configModel); - - assertThat(tokenManager.issueOnBehalfOfTokenAllowed(), equalTo(false)); - verifyNoMoreInteractions(configModel); - } - @Test public void onDynamicConfigModelChanged_JwtVendorEnabled() { - final ConfigModel configModel = mock(ConfigModel.class); final DynamicConfigModel mockConfigModel = createMockJwtVendorInTokenManager(true); - tokenManager.onConfigModelChanged(configModel); - assertThat(tokenManager.issueOnBehalfOfTokenAllowed(), equalTo(true)); verify(mockConfigModel).getDynamicOnBehalfOfSettings(); - verifyNoMoreInteractions(configModel); } @Test @@ -211,9 +195,6 @@ public void issueOnBehalfOfToken_jwtGenerationFailure() throws Exception { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon")); when(threadPool.getThreadContext()).thenReturn(threadContext); - final ConfigModel configModel = mock(ConfigModel.class); - tokenManager.onConfigModelChanged(configModel); - when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); createMockJwtVendorInTokenManager(true); @@ -235,9 +216,6 @@ public void issueOnBehalfOfToken_success() throws Exception { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon")); when(threadPool.getThreadContext()).thenReturn(threadContext); - final ConfigModel configModel = mock(ConfigModel.class); - tokenManager.onConfigModelChanged(configModel); - when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); createMockJwtVendorInTokenManager(true); @@ -257,9 +235,6 @@ public void testCreateJwtWithNegativeExpiry() { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon")); when(threadPool.getThreadContext()).thenReturn(threadContext); - final ConfigModel configModel = mock(ConfigModel.class); - tokenManager.onConfigModelChanged(configModel); - when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); createMockJwtVendorInTokenManager(true); @@ -280,9 +255,6 @@ public void testCreateJwtWithExceededExpiry() throws Exception { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon")); when(threadPool.getThreadContext()).thenReturn(threadContext); - final ConfigModel configModel = mock(ConfigModel.class); - tokenManager.onConfigModelChanged(configModel); - when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); createMockJwtVendorInTokenManager(true); @@ -300,9 +272,6 @@ public void testCreateJwtWithBadEncryptionKey() { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon")); when(threadPool.getThreadContext()).thenReturn(threadContext); - final ConfigModel configModel = mock(ConfigModel.class); - tokenManager.onConfigModelChanged(configModel); - when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); createMockJwtVendorInTokenManager(false); @@ -315,26 +284,4 @@ public void testCreateJwtWithBadEncryptionKey() { }); assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: encryption_key cannot be null")); } - - @Test - public void testCreateJwtWithBadRoles() { - doAnswer(invocation -> true).when(tokenManager).issueOnBehalfOfTokenAllowed(); - final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon")); - when(threadPool.getThreadContext()).thenReturn(threadContext); - final ConfigModel configModel = mock(ConfigModel.class); - tokenManager.onConfigModelChanged(configModel); - when(configModel.mapSecurityRoles(any(), any())).thenReturn(null); - - createMockJwtVendorInTokenManager(true); - - final Throwable exception = assertThrows(RuntimeException.class, () -> { - try { - tokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims("elmo", 90000000L)); - } catch (final Exception e) { - throw new RuntimeException(e); - } - }); - assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: Roles cannot be null")); - } } diff --git a/src/test/java/org/opensearch/security/multitenancy/test/MultitenancyTests.java b/src/test/java/org/opensearch/security/multitenancy/test/MultitenancyTests.java index 1373aa1741..c49f771856 100644 --- a/src/test/java/org/opensearch/security/multitenancy/test/MultitenancyTests.java +++ b/src/test/java/org/opensearch/security/multitenancy/test/MultitenancyTests.java @@ -407,6 +407,7 @@ public void testMtMulti() throws Exception { // get assertThat( + res.getBody(), HttpStatus.SC_OK, is( (res = rh.executeGetRequest( @@ -416,10 +417,10 @@ public void testMtMulti() throws Exception { )).getStatusCode() ) ); - Assert.assertFalse(res.getBody().contains("exception")); - Assert.assertTrue(res.getBody().contains("humanresources")); - Assert.assertTrue(res.getBody().contains("\"found\" : true")); - Assert.assertTrue(res.getBody().contains(dashboardsIndex)); + Assert.assertFalse(res.getBody(), res.getBody().contains("exception")); + Assert.assertTrue(res.getBody(), res.getBody().contains("humanresources")); + Assert.assertTrue(res.getBody(), res.getBody().contains("\"found\" : true")); + Assert.assertTrue(res.getBody(), res.getBody().contains(dashboardsIndex)); // mget body = "{\"docs\" : [{\"_index\" : \".kibana\",\"_id\" : \"index-pattern:9fbbd1a0-c3c5-11e8-a13f-71b8ea5a4f7b\"}]}"; @@ -563,7 +564,7 @@ public void testDashboardsAlias65() throws Exception { )).getStatusCode() ) ); - Assert.assertTrue(res.getBody().contains(".kibana_-900636979_kibanaro")); + Assert.assertTrue(res.getBody(), res.getBody().contains(".kibana_-900636979_kibanaro")); } @Test @@ -638,12 +639,12 @@ public void testMultitenancyAnonymousUser() throws Exception { /* The anonymous user has access to its tenant */ res = rh.executeGetRequest(url, new BasicHeader("securitytenant", anonymousTenant)); - assertThat(res.getStatusCode(), is(HttpStatus.SC_OK)); - assertThat(res.findValueInJson("_source.tenant"), is(anonymousTenant)); + assertThat(res.getBody(), res.getStatusCode(), is(HttpStatus.SC_OK)); + assertThat(res.getBody(), res.findValueInJson("_source.tenant"), is(anonymousTenant)); /* No access to other tenants */ res = rh.executeGetRequest(url, new BasicHeader("securitytenant", "human_resources")); - assertThat(res.getStatusCode(), is(HttpStatus.SC_FORBIDDEN)); + assertThat(res.getBody(), res.getStatusCode(), is(HttpStatus.SC_FORBIDDEN)); } @Test diff --git a/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java index ff70fe8d9f..864262a536 100644 --- a/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java @@ -88,7 +88,6 @@ private void assertEvaluateAsync(boolean hasPermission, boolean expectedAllowed) PrivilegesEvaluatorResponse out = captor.getValue(); assertThat(out.allowed, equalTo(expectedAllowed)); - assertThat(out.isComplete(), equalTo(true)); } @Test diff --git a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index cf52f9ea54..6b3718e81e 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -12,84 +12,33 @@ package org.opensearch.security.privileges; import java.util.Set; -import java.util.TreeMap; - -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.core.config.Configurator; -import org.junit.After; -import org.junit.Before; + import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.OpenSearchSecurityException; -import org.opensearch.cluster.ClusterState; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.metadata.Metadata; -import org.opensearch.cluster.node.DiscoveryNode; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.support.ActionRequestMetadata; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.settings.ClusterSettings; import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.security.auditlog.NullAuditLog; -import org.opensearch.security.configuration.ClusterInfoHolder; -import org.opensearch.security.securityconf.ConfigModel; -import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; +import org.opensearch.security.privileges.actionlevel.RuntimeOptimizedActionPrivileges; +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.ConfigV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.user.User; -import org.opensearch.threadpool.ThreadPool; +import org.opensearch.tasks.Task; -import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.quality.Strictness; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.opensearch.security.support.SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING; -import static org.junit.Assert.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; @RunWith(MockitoJUnitRunner.class) public class RestLayerPrivilegesEvaluatorTest { - @Mock(strictness = Mock.Strictness.LENIENT) - private ClusterService clusterService; - @Mock - private ConfigModel configModel; - @Mock - private DynamicConfigModel dynamicConfigModel; - @Mock - private ClusterInfoHolder clusterInfoHolder; - - private static final User TEST_USER = new User("test_user"); - - private void setLoggingLevel(final Level level) { - final Logger restLayerPrivilegesEvaluatorLogger = LogManager.getLogger(RestLayerPrivilegesEvaluator.class); - Configurator.setLevel(restLayerPrivilegesEvaluatorLogger, level); - } - - @Before - public void setUp() { - when(clusterService.localNode()).thenReturn(mock(DiscoveryNode.class, withSettings().strictness(Strictness.LENIENT))); - when(configModel.mapSecurityRoles(TEST_USER, null)).thenReturn(Set.of("test_role")); - setLoggingLevel(Level.DEBUG); // Enable debug logging scenarios for verification - ClusterState clusterState = mock(ClusterState.class); - when(clusterService.state()).thenReturn(clusterState); - when(clusterService.getClusterSettings()).thenReturn(new ClusterSettings(Settings.EMPTY, Set.of(USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING))); - Metadata metadata = mock(Metadata.class); - when(clusterState.metadata()).thenReturn(metadata); - when(metadata.getIndicesLookup()).thenReturn(new TreeMap<>()); - } - - @After - public void after() { - setLoggingLevel(Level.INFO); - } + private static final User TEST_USER = new User("test_user").withSecurityRoles(Set.of("test_role")); @Test public void testEvaluate_Initialized_Success() throws Exception { @@ -98,8 +47,8 @@ public void testEvaluate_Initialized_Success() throws Exception { " cluster_permissions:\n" + // " - any", CType.ROLES); - PrivilegesEvaluator privilegesEvaluator = createPrivilegesEvaluator(roles); - RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesEvaluator); + PrivilegesConfiguration privilegesConfiguration = createPrivilegesConfiguration(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesConfiguration); PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); @@ -107,30 +56,14 @@ public void testEvaluate_Initialized_Success() throws Exception { assertThat(response.getMissingPrivileges(), equalTo(Set.of(action))); } - @Test - public void testEvaluate_NotInitialized_NullModel_ExceptionThrown() { - PrivilegesEvaluator privilegesEvaluator = createPrivilegesEvaluator(null); - RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesEvaluator); - when(clusterInfoHolder.hasClusterManager()).thenReturn(true); - OpenSearchSecurityException exception = assertThrows( - OpenSearchSecurityException.class, - () -> restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", null) - ); - assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized.")); - - when(clusterInfoHolder.hasClusterManager()).thenReturn(false); - exception = assertThrows(OpenSearchSecurityException.class, () -> restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", null)); - assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized. Cluster manager not present")); - } - @Test public void testEvaluate_Successful_NewPermission() throws Exception { String action = "hw:greet"; SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // " cluster_permissions:\n" + // " - hw:greet", CType.ROLES); - PrivilegesEvaluator privilegesEvaluator = createPrivilegesEvaluator(roles); - RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesEvaluator); + PrivilegesConfiguration privilegesConfiguration = createPrivilegesConfiguration(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesConfiguration); PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); assertThat(response.allowed, equalTo(true)); } @@ -141,8 +74,8 @@ public void testEvaluate_Successful_LegacyPermission() throws Exception { SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // " cluster_permissions:\n" + // " - cluster:admin/opensearch/hw/greet", CType.ROLES); - PrivilegesEvaluator privilegesEvaluator = createPrivilegesEvaluator(roles); - RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesEvaluator); + PrivilegesConfiguration privilegesConfiguration = createPrivilegesConfiguration(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesConfiguration); PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); assertThat(response.allowed, equalTo(true)); } @@ -153,36 +86,89 @@ public void testEvaluate_Unsuccessful() throws Exception { SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // " cluster_permissions:\n" + // " - other_action", CType.ROLES); - PrivilegesEvaluator privilegesEvaluator = createPrivilegesEvaluator(roles); - RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesEvaluator); + PrivilegesConfiguration privilegesConfiguration = createPrivilegesConfiguration(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesConfiguration); PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); assertThat(response.allowed, equalTo(false)); } + PrivilegesConfiguration createPrivilegesConfiguration(SecurityDynamicConfiguration roles) { + return new PrivilegesConfiguration(createPrivilegesEvaluator(roles)); + } + PrivilegesEvaluator createPrivilegesEvaluator(SecurityDynamicConfiguration roles) { - PrivilegesEvaluator privilegesEvaluator = new PrivilegesEvaluator( - clusterService, - () -> clusterService.state(), - mock(ThreadPool.class), - new ThreadContext(Settings.EMPTY), - null, - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - new NullAuditLog(), + ActionPrivileges actionPrivileges = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, Settings.EMPTY, - null, - clusterInfoHolder, - null + false ); - privilegesEvaluator.onConfigModelChanged(configModel); // Defaults to the mocked config model - privilegesEvaluator.onDynamicConfigModelChanged(dynamicConfigModel); - - if (roles != null) { - privilegesEvaluator.updateConfiguration( - SecurityDynamicConfiguration.empty(CType.ACTIONGROUPS), - roles, - SecurityDynamicConfiguration.empty(CType.TENANTS) - ); - } - return privilegesEvaluator; + + return new PrivilegesEvaluator() { + + @Override + public PrivilegesEvaluationContext createContext( + User user, + String action, + ActionRequest actionRequest, + ActionRequestMetadata actionRequestMetadata, + Task task + ) { + return new PrivilegesEvaluationContext( + user, + user.getSecurityRoles(), + action, + actionRequest, + ActionRequestMetadata.empty(), + task, + null, + null, + null, + actionPrivileges + ); + } + + @Override + public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { + return null; + } + + @Override + public boolean isClusterPermission(String action) { + return false; + } + + @Override + public void updateConfiguration( + FlattenedActionGroups actionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration + ) { + + } + + @Override + public void updateClusterStateMetadata(ClusterService clusterService) { + + } + + @Override + public void shutdown() { + + } + + @Override + public boolean notFailOnForbiddenEnabled() { + return false; + } + + @Override + public boolean isInitialized() { + return true; + } + }; + } + } diff --git a/src/test/java/org/opensearch/security/privileges/UserAttributesUnitTest.java b/src/test/java/org/opensearch/security/privileges/UserAttributesUnitTest.java index 05da8d5419..e4538a6025 100644 --- a/src/test/java/org/opensearch/security/privileges/UserAttributesUnitTest.java +++ b/src/test/java/org/opensearch/security/privileges/UserAttributesUnitTest.java @@ -44,6 +44,7 @@ public void testReplaceProperties() { null, null, null, + null, null ); diff --git a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java b/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorUnitTest.java similarity index 54% rename from src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java rename to src/test/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorUnitTest.java index 66aa954de4..cd5feff67f 100644 --- a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java +++ b/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorUnitTest.java @@ -6,43 +6,22 @@ * compatible open source license. */ -package org.opensearch.security.privileges; +package org.opensearch.security.privileges.actionlevel.legacy; import java.util.List; -import java.util.Set; -import java.util.function.Supplier; import com.google.common.collect.ImmutableList; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.OpenSearchSecurityException; -import org.opensearch.cluster.ClusterState; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.settings.ClusterSettings; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.configuration.ClusterInfoHolder; -import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.threadpool.ThreadPool; - -import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.opensearch.security.privileges.PrivilegesEvaluator.DNFOF_MATCHER; -import static org.opensearch.security.privileges.PrivilegesEvaluator.isClusterPerm; -import static org.opensearch.security.support.SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING; +import static org.opensearch.security.privileges.actionlevel.legacy.PrivilegesEvaluator.DNFOF_MATCHER; +import static org.opensearch.security.privileges.actionlevel.legacy.PrivilegesEvaluator.isClusterPermissionStatic; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class PrivilegesEvaluatorUnitTest { @@ -122,66 +101,6 @@ public class PrivilegesEvaluatorUnitTest { "indices:monitor/upgrade" ); - @Mock - private ClusterService clusterService; - - @Mock - private ThreadPool threadPool; - - @Mock - private ConfigurationRepository configurationRepository; - - @Mock - private IndexNameExpressionResolver resolver; - - @Mock - private AuditLog auditLog; - - @Mock - private PrivilegesInterceptor privilegesInterceptor; - - @Mock - private ClusterInfoHolder clusterInfoHolder; - - @Mock - private IndexResolverReplacer irr; - - @Mock - private NamedXContentRegistry namedXContentRegistry; - - @Mock - private ClusterState clusterState; - - private Settings settings; - private Supplier clusterStateSupplier; - private ThreadContext threadContext; - private PrivilegesEvaluator privilegesEvaluator; - - @Before - public void setUp() { - settings = Settings.builder().build(); - clusterStateSupplier = () -> clusterState; - threadContext = new ThreadContext(Settings.EMPTY); - - when(clusterService.getClusterSettings()).thenReturn( - new ClusterSettings(Settings.EMPTY, Set.of(USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING)) - ); - - privilegesEvaluator = new PrivilegesEvaluator( - clusterService, - clusterStateSupplier, - threadPool, - threadContext, - configurationRepository, - resolver, - auditLog, - settings, - privilegesInterceptor, - clusterInfoHolder, - irr - ); - } - @Test public void testClusterPerm() { String multiSearchTemplate = "indices:data/read/msearch/template"; @@ -191,13 +110,13 @@ public void testClusterPerm() { String monitorUpgrade = "indices:monitor/upgrade"; // Cluster Permissions - assertTrue(isClusterPerm(multiSearchTemplate)); - assertTrue(isClusterPerm(writeIndex)); - assertTrue(isClusterPerm(monitorHealth)); + assertTrue(isClusterPermissionStatic(multiSearchTemplate)); + assertTrue(isClusterPermissionStatic(writeIndex)); + assertTrue(isClusterPermissionStatic(monitorHealth)); // Index Permissions - assertFalse(isClusterPerm(adminClose)); - assertFalse(isClusterPerm(monitorUpgrade)); + assertFalse(isClusterPermissionStatic(adminClose)); + assertFalse(isClusterPermissionStatic(monitorUpgrade)); } @Test @@ -214,20 +133,4 @@ public void testDnfofPermissions_positive() { } } - @Test - public void testEvaluate_NotInitialized_ExceptionThrown() { - when(clusterInfoHolder.hasClusterManager()).thenReturn(true); - OpenSearchSecurityException exception = assertThrows( - OpenSearchSecurityException.class, - () -> privilegesEvaluator.evaluate(null) - ); - assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized.")); - - when(clusterInfoHolder.hasClusterManager()).thenReturn(false); - exception = assertThrows( - OpenSearchSecurityException.class, - () -> privilegesEvaluator.evaluate(null) - ); - assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized. Cluster manager not present")); - } } diff --git a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluatorTest.java similarity index 62% rename from src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java rename to src/test/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluatorTest.java index 17a78501c9..706708a194 100644 --- a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluatorTest.java @@ -9,7 +9,7 @@ * GitHub history for details. */ -package org.opensearch.security.privileges; +package org.opensearch.security.privileges.actionlevel.legacy; import java.util.Arrays; import java.util.List; @@ -21,25 +21,26 @@ import com.google.common.collect.ImmutableSet; import com.fasterxml.jackson.core.JsonProcessingException; import org.apache.logging.log4j.Logger; -import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.action.ActionRequest; import org.opensearch.action.get.MultiGetRequest; import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.support.IndicesOptions; +import org.opensearch.action.support.ActionRequestMetadata; import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; -import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; +import org.opensearch.security.privileges.actionlevel.RuntimeOptimizedActionPrivileges; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -54,12 +55,11 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.opensearch.security.support.ConfigConstants.SYSTEM_INDEX_PERMISSION; -import static org.mockito.Mockito.mock; +import static org.junit.Assert.assertFalse; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) @@ -68,14 +68,8 @@ public class SystemIndexAccessEvaluatorTest { @Mock private AuditLog auditLog; @Mock - private IndexResolverReplacer irr; - @Mock - private ActionRequest request; - @Mock private Task task; @Mock - private PrivilegesEvaluatorResponse presponse; - @Mock private Logger log; @Mock ClusterService cs; @@ -138,7 +132,13 @@ public void setup( CType.ROLES ); - this.actionPrivileges = new RoleBasedActionPrivileges(rolesConfig, FlattenedActionGroups.EMPTY, Settings.EMPTY); + this.actionPrivileges = new RoleBasedActionPrivileges( + rolesConfig, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY, + false + ); } catch (JsonProcessingException e) { throw new RuntimeException(e); } @@ -154,8 +154,7 @@ public void setup( .put(ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY, isSystemIndexEnabled) .put(ConfigConstants.SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY, isSystemIndexPermissionsEnabled) .build(), - auditLog, - irr + auditLog ); evaluator.log = log; @@ -170,125 +169,112 @@ PrivilegesEvaluationContext ctx(String action) { user, ImmutableSet.of("role_a"), action, - request, - null, + new SearchRequest(), + ActionRequestMetadata.empty(), null, indexNameExpressionResolver, + null, () -> clusterState, actionPrivileges ); } - @After - public void after() { - verifyNoMoreInteractions(auditLog, irr, request, task, presponse, log); - } - @Test public void testUnprotectedActionOnRegularIndex_systemIndexDisabled() { setup(false, false, TEST_INDEX, false); - final Resolved resolved = createResolved(TEST_INDEX); + final ResolvedIndices resolved = createResolved(TEST_INDEX); // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( - request, + new SearchRequest(TEST_INDEX), null, UNPROTECTED_ACTION, resolved, - presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user ); - verifyNoInteractions(presponse); - assertThat(response, is(presponse)); + assertThat(response, is(nullValue())); } @Test public void testUnprotectedActionOnRegularIndex_systemIndexPermissionDisabled() { setup(true, false, TEST_INDEX, false); - final Resolved resolved = createResolved(TEST_INDEX); + final ResolvedIndices resolved = createResolved(TEST_INDEX); // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( - request, + new SearchRequest(TEST_INDEX), null, UNPROTECTED_ACTION, resolved, - presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user ); - verifyNoInteractions(presponse); - assertThat(response, is(presponse)); + assertThat(response, is(nullValue())); } @Test public void testUnprotectedActionOnRegularIndex_systemIndexPermissionEnabled() { setup(true, true, TEST_INDEX, false); - final Resolved resolved = createResolved(TEST_INDEX); + final ResolvedIndices resolved = createResolved(TEST_INDEX); // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( - request, + new SearchRequest(TEST_INDEX), null, UNPROTECTED_ACTION, resolved, - presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user ); - verifyNoInteractions(presponse); - assertThat(response, is(presponse)); + assertThat(response, is(nullValue())); } @Test public void testUnprotectedActionOnSystemIndex_systemIndexDisabled() { setup(false, false, TEST_SYSTEM_INDEX, false); - final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + final ResolvedIndices resolved = createResolved(TEST_SYSTEM_INDEX); // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( - request, + new SearchRequest(TEST_SYSTEM_INDEX), null, UNPROTECTED_ACTION, resolved, - presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user ); - verifyNoInteractions(presponse); - assertThat(response, is(presponse)); + assertThat(response, is(nullValue())); } @Test public void testUnprotectedActionOnSystemIndex_systemIndexPermissionDisabled() { setup(true, false, TEST_SYSTEM_INDEX, false); - final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + final ResolvedIndices resolved = createResolved(TEST_SYSTEM_INDEX); // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( - request, + new SearchRequest(TEST_SYSTEM_INDEX), null, UNPROTECTED_ACTION, resolved, - presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user ); - verifyNoInteractions(presponse); - assertThat(response, is(presponse)); + assertThat(response, is(nullValue())); } @Test public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_WithoutSystemIndexPermission() { setup(true, true, TEST_SYSTEM_INDEX, false); - final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + final ResolvedIndices resolved = createResolved(TEST_SYSTEM_INDEX); + SearchRequest request = new SearchRequest(TEST_SYSTEM_INDEX); // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( @@ -296,13 +282,11 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_With null, UNPROTECTED_ACTION, resolved, - presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user ); - verify(presponse).markComplete(); - assertThat(response, is(presponse)); + assertThat(response.isAllowed(), is(false)); verify(auditLog).logSecurityIndexAttempt(request, UNPROTECTED_ACTION, null); verify(log).isInfoEnabled(); @@ -317,57 +301,57 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_With @Test public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_WithSystemIndexPermission() { setup(true, true, TEST_SYSTEM_INDEX, true); - final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + final ResolvedIndices resolved = createResolved(TEST_SYSTEM_INDEX); // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( - request, + new SearchRequest(TEST_SYSTEM_INDEX), null, UNPROTECTED_ACTION, resolved, - presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user ); - assertThat(response, is(presponse)); - // unprotected action is not allowed on a system index - assertThat(presponse.allowed, is(false)); + // user has system index permission; let them pass + assertThat(response, is(nullValue())); } @Test public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexDisabled() { setup(false, false, TEST_SYSTEM_INDEX, false); - final SearchRequest searchRequest = mock(SearchRequest.class); - final MultiGetRequest realtimeRequest = mock(MultiGetRequest.class); - final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + final ResolvedIndices resolved = createResolved(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); - evaluator.evaluate(searchRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); - evaluator.evaluate(realtimeRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); - - verifyNoInteractions(presponse); + PrivilegesEvaluatorResponse response = evaluator.evaluate( + new SearchRequest(TEST_SYSTEM_INDEX), + null, + UNPROTECTED_ACTION, + resolved, + ctx(UNPROTECTED_ACTION), + actionPrivileges, + user + ); + assertThat(response, is(nullValue())); } @Test public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionDisabled() { setup(true, false, TEST_SYSTEM_INDEX, false); - final SearchRequest searchRequest = mock(SearchRequest.class); - final MultiGetRequest realtimeRequest = mock(MultiGetRequest.class); - final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + final SearchRequest searchRequest = new SearchRequest(TEST_SYSTEM_INDEX); + final MultiGetRequest realtimeRequest = new MultiGetRequest().add(TEST_SYSTEM_INDEX, "id"); + final ResolvedIndices resolved = createResolved(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); - evaluator.evaluate(searchRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); - evaluator.evaluate(realtimeRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); + evaluator.evaluate(searchRequest, null, UNPROTECTED_ACTION, resolved, ctx(UNPROTECTED_ACTION), actionPrivileges, user); + evaluator.evaluate(realtimeRequest, null, UNPROTECTED_ACTION, resolved, ctx(UNPROTECTED_ACTION), actionPrivileges, user); - verify(searchRequest).requestCache(Boolean.FALSE); - verify(realtimeRequest).realtime(Boolean.FALSE); + assertFalse(searchRequest.requestCache()); + ; + assertFalse(realtimeRequest.realtime()); - verify(log, times(2)).isDebugEnabled(); verify(log).debug("Disable search request cache for this request"); verify(log).debug("Disable realtime for this request"); } @@ -376,28 +360,25 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionDisable public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled_withoutSystemIndexPermission() { setup(true, true, TEST_SYSTEM_INDEX, false); - final SearchRequest searchRequest = mock(SearchRequest.class); - final MultiGetRequest realtimeRequest = mock(MultiGetRequest.class); - final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + final SearchRequest searchRequest = new SearchRequest(TEST_SYSTEM_INDEX); + final MultiGetRequest realtimeRequest = new MultiGetRequest().add(TEST_SYSTEM_INDEX, "id"); + final ResolvedIndices resolved = createResolved(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); - evaluator.evaluate(searchRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); - evaluator.evaluate(realtimeRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); + evaluator.evaluate(searchRequest, null, UNPROTECTED_ACTION, resolved, ctx(UNPROTECTED_ACTION), actionPrivileges, user); + evaluator.evaluate(realtimeRequest, null, UNPROTECTED_ACTION, resolved, ctx(UNPROTECTED_ACTION), actionPrivileges, user); - verify(searchRequest).requestCache(Boolean.FALSE); - verify(realtimeRequest).realtime(Boolean.FALSE); + assertFalse(searchRequest.requestCache()); + ; + assertFalse(realtimeRequest.realtime()); - verify(log, times(2)).isDebugEnabled(); verify(log).debug("Disable search request cache for this request"); verify(log).debug("Disable realtime for this request"); - verify(auditLog).logSecurityIndexAttempt(request, UNPROTECTED_ACTION, null); verify(auditLog).logSecurityIndexAttempt(searchRequest, UNPROTECTED_ACTION, null); verify(auditLog).logSecurityIndexAttempt(realtimeRequest, UNPROTECTED_ACTION, null); - verify(presponse, times(3)).markComplete(); verify(log, times(2)).isDebugEnabled(); - verify(log, times(3)).isInfoEnabled(); - verify(log, times(3)).info( + verify(log, times(2)).isInfoEnabled(); + verify(log, times(2)).info( "No {} permission for user roles {} to System Indices {}", UNPROTECTED_ACTION, user.getSecurityRoles(), @@ -411,17 +392,17 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled_withSystemIndexPermission() { setup(true, true, TEST_SYSTEM_INDEX, true); - final SearchRequest searchRequest = mock(SearchRequest.class); - final MultiGetRequest realtimeRequest = mock(MultiGetRequest.class); - final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + final SearchRequest searchRequest = new SearchRequest(TEST_SYSTEM_INDEX); + final MultiGetRequest realtimeRequest = new MultiGetRequest().add(TEST_SYSTEM_INDEX, "id"); + final ResolvedIndices resolved = createResolved(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); - evaluator.evaluate(searchRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); - evaluator.evaluate(realtimeRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); + evaluator.evaluate(searchRequest, null, UNPROTECTED_ACTION, resolved, ctx(UNPROTECTED_ACTION), actionPrivileges, user); + evaluator.evaluate(realtimeRequest, null, UNPROTECTED_ACTION, resolved, ctx(UNPROTECTED_ACTION), actionPrivileges, user); - verify(searchRequest).requestCache(Boolean.FALSE); - verify(realtimeRequest).realtime(Boolean.FALSE); + assertFalse(searchRequest.requestCache()); + ; + assertFalse(realtimeRequest.realtime()); verify(log, times(2)).isDebugEnabled(); verify(log).debug("Disable search request cache for this request"); @@ -431,114 +412,195 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled @Test public void testProtectedActionLocalAll_systemIndexDisabled() { setup(false, false, TEST_SYSTEM_INDEX, false); - final Resolved resolved = Resolved._LOCAL_ALL; + final OptionallyResolvedIndices resolved = ResolvedIndices.unknown(); + final SearchRequest request = new SearchRequest(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); + PrivilegesEvaluatorResponse presponse = evaluator.evaluate( + request, + task, + PROTECTED_ACTION, + resolved, + ctx(PROTECTED_ACTION), + actionPrivileges, + user + ); verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); - assertThat(presponse.allowed, is(false)); - verify(presponse).markComplete(); + assertThat(presponse.isAllowed(), is(false)); verify(log).warn("{} for '_all' indices is not allowed for a regular user", "indices:data/write"); } @Test public void testProtectedActionLocalAll_systemIndexPermissionDisabled() { setup(true, false, TEST_SYSTEM_INDEX, false); - final Resolved resolved = Resolved._LOCAL_ALL; + final OptionallyResolvedIndices resolved = ResolvedIndices.unknown(); + final SearchRequest request = new SearchRequest(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); + PrivilegesEvaluatorResponse presponse = evaluator.evaluate( + request, + task, + PROTECTED_ACTION, + resolved, + ctx(PROTECTED_ACTION), + actionPrivileges, + user + ); verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); - assertThat(presponse.allowed, is(false)); - verify(presponse).markComplete(); + assertThat(presponse.isAllowed(), is(false)); verify(log).warn("{} for '_all' indices is not allowed for a regular user", PROTECTED_ACTION); } @Test public void testProtectedActionLocalAll_systemIndexPermissionEnabled() { setup(true, true, TEST_SYSTEM_INDEX, false); - final Resolved resolved = Resolved._LOCAL_ALL; + final OptionallyResolvedIndices resolved = ResolvedIndices.unknown(); + final SearchRequest request = new SearchRequest(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); + PrivilegesEvaluatorResponse presponse = evaluator.evaluate( + request, + task, + PROTECTED_ACTION, + resolved, + ctx(PROTECTED_ACTION), + actionPrivileges, + user + ); verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); - assertThat(presponse.allowed, is(false)); - verify(presponse).markComplete(); - verify(log).warn("{} for '_all' indices is not allowed for a regular user", PROTECTED_ACTION); + assertThat(presponse.isAllowed(), is(false)); + verify(log).info( + "{} not permitted for a regular user {} on protected system indices {}", + PROTECTED_ACTION, + Set.of("role_a"), + ".opendistro_security" + ); } @Test public void testProtectedActionOnRegularIndex_systemIndexDisabled() { setup(false, false, TEST_INDEX, false); - final Resolved resolved = createResolved(TEST_INDEX); + final ResolvedIndices resolved = createResolved(TEST_INDEX); + final SearchRequest request = new SearchRequest(TEST_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); + PrivilegesEvaluatorResponse presponse = evaluator.evaluate( + request, + task, + PROTECTED_ACTION, + resolved, + ctx(PROTECTED_ACTION), + actionPrivileges, + user + ); - assertThat(presponse.allowed, is(false)); + assertThat(presponse, is(nullValue())); } @Test public void testProtectedActionOnRegularIndex_systemIndexPermissionDisabled() { setup(true, false, TEST_INDEX, false); - final Resolved resolved = createResolved(TEST_INDEX); + final ResolvedIndices resolved = createResolved(TEST_INDEX); + final SearchRequest request = new SearchRequest(TEST_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); + PrivilegesEvaluatorResponse presponse = evaluator.evaluate( + request, + task, + PROTECTED_ACTION, + resolved, + ctx(PROTECTED_ACTION), + actionPrivileges, + user + ); - assertThat(presponse.allowed, is(false)); + assertThat(presponse, is(nullValue())); } @Test public void testProtectedActionOnRegularIndex_systemIndexPermissionEnabled() { setup(true, true, TEST_INDEX, false); - final Resolved resolved = createResolved(TEST_INDEX); + final ResolvedIndices resolved = createResolved(TEST_INDEX); + final SearchRequest request = new SearchRequest(TEST_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); + PrivilegesEvaluatorResponse presponse = evaluator.evaluate( + request, + task, + PROTECTED_ACTION, + resolved, + ctx(PROTECTED_ACTION), + actionPrivileges, + user + ); - assertThat(presponse.allowed, is(false)); + assertThat(presponse, is(nullValue())); } @Test public void testProtectedActionOnSystemIndex_systemIndexDisabled() { setup(false, false, TEST_SYSTEM_INDEX, false); - final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + final ResolvedIndices resolved = createResolved(TEST_SYSTEM_INDEX); + final SearchRequest request = new SearchRequest(TEST_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); + PrivilegesEvaluatorResponse presponse = evaluator.evaluate( + request, + task, + PROTECTED_ACTION, + resolved, + ctx(PROTECTED_ACTION), + actionPrivileges, + user + ); - assertThat(presponse.allowed, is(false)); + assertThat(presponse, is(nullValue())); } @Test public void testProtectedActionOnSystemIndex_systemIndexPermissionDisabled() { setup(true, false, TEST_SYSTEM_INDEX, false); - final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + final ResolvedIndices resolved = createResolved(TEST_SYSTEM_INDEX); + final SearchRequest request = new SearchRequest(TEST_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); + PrivilegesEvaluatorResponse presponse = evaluator.evaluate( + request, + task, + PROTECTED_ACTION, + resolved, + ctx(PROTECTED_ACTION), + actionPrivileges, + user + ); verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); - assertThat(presponse.allowed, is(false)); - verify(presponse).markComplete(); + assertThat(presponse.isAllowed(), is(false)); verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, TEST_SYSTEM_INDEX); } @Test public void testProtectedActionOnSystemIndex_systemIndexPermissionEnabled_withoutSystemIndexPermission() { setup(true, true, TEST_SYSTEM_INDEX, false); - final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + final ResolvedIndices resolved = createResolved(TEST_SYSTEM_INDEX); + final SearchRequest request = new SearchRequest(TEST_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); + PrivilegesEvaluatorResponse presponse = evaluator.evaluate( + request, + task, + PROTECTED_ACTION, + resolved, + ctx(PROTECTED_ACTION), + actionPrivileges, + user + ); verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); - assertThat(presponse.allowed, is(false)); - verify(presponse).markComplete(); + assertThat(presponse.isAllowed(), is(false)); verify(log).isInfoEnabled(); verify(log).info( "No {} permission for user roles {} to System Indices {}", @@ -552,25 +614,42 @@ public void testProtectedActionOnSystemIndex_systemIndexPermissionEnabled_withou public void testProtectedActionOnSystemIndex_systemIndexPermissionEnabled_withSystemIndexPermission() { setup(true, true, TEST_SYSTEM_INDEX, true); - final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + final ResolvedIndices resolved = createResolved(TEST_SYSTEM_INDEX); + final SearchRequest request = new SearchRequest(TEST_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); + PrivilegesEvaluatorResponse presponse = evaluator.evaluate( + request, + task, + PROTECTED_ACTION, + resolved, + ctx(PROTECTED_ACTION), + actionPrivileges, + user + ); - assertThat(presponse.allowed, is(false)); + assertThat(presponse, is(nullValue())); } @Test public void testProtectedActionOnProtectedSystemIndex_systemIndexDisabled() { setup(false, false, SECURITY_INDEX, false); - final Resolved resolved = createResolved(SECURITY_INDEX); + final ResolvedIndices resolved = createResolved(SECURITY_INDEX); + final SearchRequest request = new SearchRequest(TEST_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); + PrivilegesEvaluatorResponse presponse = evaluator.evaluate( + request, + task, + PROTECTED_ACTION, + resolved, + ctx(PROTECTED_ACTION), + actionPrivileges, + user + ); verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); - assertThat(presponse.allowed, is(false)); - verify(presponse).markComplete(); + assertThat(presponse.isAllowed(), is(false)); verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, SECURITY_INDEX); } @@ -578,14 +657,22 @@ public void testProtectedActionOnProtectedSystemIndex_systemIndexDisabled() { @Test public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionDisabled() { setup(true, false, SECURITY_INDEX, false); - final Resolved resolved = createResolved(SECURITY_INDEX); + final ResolvedIndices resolved = createResolved(SECURITY_INDEX); + final SearchRequest request = new SearchRequest(TEST_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); + PrivilegesEvaluatorResponse presponse = evaluator.evaluate( + request, + task, + PROTECTED_ACTION, + resolved, + ctx(PROTECTED_ACTION), + actionPrivileges, + user + ); verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); - assertThat(presponse.allowed, is(false)); - verify(presponse).markComplete(); + assertThat(presponse.isAllowed(), is(false)); verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, SECURITY_INDEX); } @@ -613,14 +700,14 @@ public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionEnabl private void testSecurityIndexAccess(String action) { setup(true, true, SECURITY_INDEX, true); - final Resolved resolved = createResolved(SECURITY_INDEX); + final OptionallyResolvedIndices resolved = ResolvedIndices.of(SECURITY_INDEX); + final SearchRequest request = new SearchRequest(SECURITY_INDEX); // Action - evaluator.evaluate(request, task, action, resolved, presponse, ctx(action), actionPrivileges, user); + PrivilegesEvaluatorResponse presponse = evaluator.evaluate(request, task, action, resolved, ctx(action), actionPrivileges, user); verify(auditLog).logSecurityIndexAttempt(request, action, task); - assertThat(presponse.allowed, is(false)); - verify(presponse).markComplete(); + assertThat(presponse.isAllowed(), is(false)); verify(log).isInfoEnabled(); verify(log).info( @@ -631,13 +718,7 @@ private void testSecurityIndexAccess(String action) { ); } - private Resolved createResolved(final String... indexes) { - return new Resolved( - ImmutableSet.of(), - ImmutableSet.copyOf(indexes), - ImmutableSet.copyOf(indexes), - ImmutableSet.of(), - IndicesOptions.STRICT_EXPAND_OPEN - ); + private ResolvedIndices createResolved(final String... indexes) { + return ResolvedIndices.of(indexes); } } diff --git a/src/test/java/org/opensearch/security/protected_indices/ProtectedIndicesTests.java b/src/test/java/org/opensearch/security/protected_indices/ProtectedIndicesTests.java index fc5c72ec44..807447383d 100644 --- a/src/test/java/org/opensearch/security/protected_indices/ProtectedIndicesTests.java +++ b/src/test/java/org/opensearch/security/protected_indices/ProtectedIndicesTests.java @@ -67,10 +67,6 @@ public class ProtectedIndicesTests extends SingleClusterTest { // This user is mapped to all_access, but is not mapped to any protectedIndexRoles private static final String indexAccessNoRoleUser = "indexAccessNoRoleUser"; private static final Header indexAccessNoRoleUserHeader = encodeBasicHeader(indexAccessNoRoleUser, indexAccessNoRoleUser); - private static final String generalErrorMessage = String.format( - "no permissions for [] and User [name=%s, backend_roles=[], requestedTenant=null]", - indexAccessNoRoleUser - ); // This user is mapped to all_access and protected_index_role1 private static final String protectedIndexUser = "protectedIndexUser"; private static final Header protectedIndexUserHeader = encodeBasicHeader(protectedIndexUser, protectedIndexUser); @@ -363,7 +359,6 @@ public void testNonAccessCreateDocument() throws Exception { String doc = "{\"foo\": \"bar\"}"; RestHelper.HttpResponse response = rh.executePostRequest(index + "/_doc", doc, indexAccessNoRoleUserHeader); assertTrue(response.getStatusCode() == RestStatus.FORBIDDEN.getStatus()); - assertTrue(response.getBody().contains(generalErrorMessage)); } } @@ -387,7 +382,6 @@ public void testNonAccessCreateDocumentPatternSetting() throws Exception { String index = pattern.replace("*", "1"); RestHelper.HttpResponse response = rh.executePostRequest(index + "/_doc", doc, indexAccessNoRoleUserHeader); assertTrue(response.getStatusCode() == RestStatus.FORBIDDEN.getStatus()); - assertTrue(response.getBody().contains(generalErrorMessage)); } } @@ -420,7 +414,6 @@ public void testNonAccessDeleteDocument() throws Exception { // Try to delete documents RestHelper.HttpResponse response = rh.executeDeleteRequest(index + "/_doc/document1", indexAccessNoRoleUserHeader); assertTrue(response.getStatusCode() == RestStatus.FORBIDDEN.getStatus()); - assertTrue(response.getBody().contains(generalErrorMessage)); } } @@ -452,7 +445,6 @@ public void testNonAccessDeleteIndex() throws Exception { // Try to delete documents RestHelper.HttpResponse response = rh.executeDeleteRequest(index, indexAccessNoRoleUserHeader); assertTrue(response.getStatusCode() == RestStatus.FORBIDDEN.getStatus()); - assertTrue(response.getBody().contains(generalErrorMessage)); } } @@ -486,7 +478,6 @@ public void testNonAccessUpdateMappings() throws Exception { RestHelper.HttpResponse response = rh.executePutRequest(index + "/_mapping", newMappings, indexAccessNoRoleUserHeader); assertTrue(response.getStatusCode() == RestStatus.FORBIDDEN.getStatus()); - assertTrue(response.getBody().contains(generalErrorMessage)); } } @@ -519,7 +510,6 @@ public void testNonAccessCloseIndex() throws Exception { RestHelper.HttpResponse response = rh.executePostRequest(index + "/_close", "", indexAccessNoRoleUserHeader); assertTrue(response.getStatusCode() == RestStatus.FORBIDDEN.getStatus()); - assertTrue(response.getBody().contains(generalErrorMessage)); } } @@ -542,7 +532,6 @@ public void testNonAccessAliasOperations() throws Exception { indexAccessNoRoleUserHeader ); assertTrue(response.getStatusCode() == RestStatus.FORBIDDEN.getStatus()); - assertTrue(response.getBody().contains(generalErrorMessage)); } // Test remove alias @@ -555,7 +544,6 @@ public void testNonAccessAliasOperations() throws Exception { indexAccessNoRoleUserHeader ); assertTrue(response.getStatusCode() == RestStatus.FORBIDDEN.getStatus()); - assertTrue(response.getBody().contains(generalErrorMessage)); } // Test remove index @@ -568,7 +556,6 @@ public void testNonAccessAliasOperations() throws Exception { indexAccessNoRoleUserHeader ); assertTrue(response.getStatusCode() == RestStatus.FORBIDDEN.getStatus()); - assertTrue(response.getBody().contains(generalErrorMessage)); } } @@ -593,7 +580,6 @@ public void testNonAccessUpdateIndexSettings() throws Exception { RestHelper.HttpResponse response = rh.executePutRequest(index + "/_settings", indexSettings, indexAccessNoRoleUserHeader); assertTrue(response.getStatusCode() == RestStatus.FORBIDDEN.getStatus()); - assertTrue(response.getBody().contains(generalErrorMessage)); } } diff --git a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java index 1a972f842d..47f3307aa6 100644 --- a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java +++ b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java @@ -23,6 +23,7 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.resources.sharing.Recipient; import org.opensearch.security.resources.sharing.ResourceSharing; @@ -70,7 +71,13 @@ public class ResourceAccessHandlerTest { public void setup() { threadContext = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext); - handler = new ResourceAccessHandler(threadPool, sharingIndexHandler, adminDNs, privilegesEvaluator, resourcePluginInfo); + handler = new ResourceAccessHandler( + threadPool, + sharingIndexHandler, + adminDNs, + new PrivilegesConfiguration(privilegesEvaluator), + resourcePluginInfo + ); // For tests that verify permission with action-group when(resourcePluginInfo.flattenedForType(any())).thenReturn(mock(FlattenedActionGroups.class)); diff --git a/src/test/java/org/opensearch/security/support/HostResolverModeTest.java b/src/test/java/org/opensearch/security/support/HostResolverModeTest.java index bee18e5242..b74d46a29a 100644 --- a/src/test/java/org/opensearch/security/support/HostResolverModeTest.java +++ b/src/test/java/org/opensearch/security/support/HostResolverModeTest.java @@ -27,9 +27,4 @@ public void testIpHostnameValue() { public void testIpHostnameLookupValue() { assertThat(HostResolverMode.IP_HOSTNAME_LOOKUP.getValue(), is("ip-hostname-lookup")); } - - @Test - public void testEnumCount() { - assertThat(HostResolverMode.values().length, is(2)); - } } diff --git a/src/test/java/org/opensearch/security/system_indices/AbstractSystemIndicesTests.java b/src/test/java/org/opensearch/security/system_indices/AbstractSystemIndicesTests.java index 1fd8b88eed..31cc3ea8de 100644 --- a/src/test/java/org/opensearch/security/system_indices/AbstractSystemIndicesTests.java +++ b/src/test/java/org/opensearch/security/system_indices/AbstractSystemIndicesTests.java @@ -194,7 +194,7 @@ void shouldBeAllowedOnlyForAuthorizedIndices(String index, RestHelper.HttpRespon boolean isRequestingAccessToNonAuthorizedSystemIndex = (!user.equals(allAccessUser) && index.equals(SYSTEM_INDEX_WITH_NO_ASSOCIATED_ROLE_PERMISSIONS)); if (isSecurityIndexRequest || isRequestingAccessToNonAuthorizedSystemIndex) { - validateForbiddenResponse(response, isSecurityIndexRequest ? "" : action, user); + assertThat(response.getStatusCode(), is(RestStatus.FORBIDDEN.getStatus())); } else { assertThat(response.getStatusCode(), is(RestStatus.OK.getStatus())); } diff --git a/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java b/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java index 5a1ba1a131..cfb08349ad 100644 --- a/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java +++ b/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java @@ -123,7 +123,7 @@ public void testDeleteAsSuperAdmin() { @Test public void testDeleteAsAdmin() { - testDeleteWithUser(allAccessUser, allAccessUserHeader, "", ""); + testDeleteWithUser(allAccessUser, allAccessUserHeader, "indices:admin/delete", "indices:data/write/delete"); } @Test @@ -175,10 +175,10 @@ public void testCloseOpenAsAdmin() { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_close", "", allAccessUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, response, "", allAccessUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:admin/close", allAccessUser); // User can open the index but cannot close it - response = restHelper.executePostRequest(index + "/_open", "", allAccessUserHeader); + response = restHelper.executePostRequest(index + "/_open", "indices:admin/open", allAccessUserHeader); assertThat(response.getStatusCode(), is(RestStatus.OK.getStatus())); } } @@ -201,7 +201,7 @@ private void testCloseOpenWithUser(String user, Header header) { shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:admin/close", user); // User can open the index but cannot close it - response = restHelper.executePostRequest(index + "/_open", "", header); + response = restHelper.executePostRequest(index + "/_open", "indices:admin/open", header); if (index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN) || index.equals(SYSTEM_INDEX_WITH_NO_ASSOCIATED_ROLE_PERMISSIONS)) { validateForbiddenResponse(response, "indices:admin/open", user); } else { @@ -348,14 +348,14 @@ public void testSnapshotSystemIndicesAsAdmin() { assertThat(res.getStatusCode(), is(HttpStatus.SC_UNAUTHORIZED)); res = restHelper.executePostRequest(snapshotRequest + "/_restore?wait_for_completion=true", "", allAccessUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, res, "", allAccessUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, res, "cluster:admin/snapshot/restore", allAccessUser); res = restHelper.executePostRequest( snapshotRequest + "/_restore?wait_for_completion=true", "{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_with_global_state_$1\" }", allAccessUserHeader ); - shouldBeAllowedOnlyForAuthorizedIndices(index, res, "", allAccessUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, res, "cluster:admin/snapshot/restore", allAccessUser); } } @@ -384,7 +384,7 @@ private void testSnapshotWithUser(String user, Header header) { RestHelper.HttpResponse res = restHelper.executeGetRequest(snapshotRequest); assertThat(res.getStatusCode(), is(HttpStatus.SC_UNAUTHORIZED)); - String action = index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN) ? "" : "indices:data/write/index, indices:admin/create"; + String action = "indices:data/write/index, indices:admin/create"; res = restHelper.executePostRequest(snapshotRequest + "/_restore?wait_for_completion=true", "", header); shouldBeAllowedOnlyForAuthorizedIndices(index, res, action, user); @@ -394,7 +394,7 @@ private void testSnapshotWithUser(String user, Header header) { "{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_with_global_state_$1\" }", header ); - validateForbiddenResponse(res, action, user); + assertThat(res.getStatusCode(), is(HttpStatus.SC_FORBIDDEN)); } } } diff --git a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java index dc69450e5a..52f9dcf0e2 100644 --- a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java +++ b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java @@ -140,10 +140,10 @@ private void testDeleteWithUser(String user, Header header) { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executeDeleteRequest(index + "/_doc/document1", header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:data/write/delete", user); response = restHelper.executeDeleteRequest(index, header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/delete", user); } } @@ -169,7 +169,7 @@ public void testCloseOpenAsAdmin() { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_close", "", allAccessUserHeader); - validateForbiddenResponse(response, "", allAccessUser); + validateForbiddenResponse(response, "indices:admin/close", allAccessUser); // admin cannot close any system index but can open them response = restHelper.executePostRequest(index + "/_open", "", allAccessUserHeader); @@ -192,7 +192,7 @@ private void testCloseOpenWithUser(String user, Header header) { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_close", "", header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/close", user); // normal user cannot open or close security index response = restHelper.executePostRequest(index + "/_open", "", header); @@ -284,10 +284,10 @@ private void testUpdateWithUser(String user, Header header) { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePutRequest(index + "/_mapping", newMappings, header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/mapping/put", user); response = restHelper.executePutRequest(index + "/_settings", updateIndexSettings, header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/settings/update", user); } } @@ -346,14 +346,14 @@ public void testSnapshotSystemIndicesAsAdmin() { "", allAccessUserHeader ); - validateForbiddenResponse(res, "", allAccessUser); + validateForbiddenResponse(res, "cluster:admin/snapshot/restore", allAccessUser); res = restHelper.executePostRequest( "_snapshot/" + index + "/" + index + "_1/_restore?wait_for_completion=true", "{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_with_global_state_$1\" }", allAccessUserHeader ); - shouldBeAllowedOnlyForAuthorizedIndices(index, res, "", allAccessUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, res, "cluster:admin/snapshot/restore", allAccessUser); } } @@ -382,7 +382,11 @@ private void testSnapshotSystemIndexWithUser(String user, Header header) { assertThat(res.getStatusCode(), is(HttpStatus.SC_UNAUTHORIZED)); res = restHelper.executePostRequest("_snapshot/" + index + "/" + index + "_1/_restore?wait_for_completion=true", "", header); - validateForbiddenResponse(res, "", user); + if ("normal_user".equals(user) || "normal_user_without_system_index".equals(user)) { + validateForbiddenResponse(res, "cluster:admin/snapshot/restore", user); + } else { + validateForbiddenResponse(res, "indices:admin/close", user); + } res = restHelper.executePostRequest( "_snapshot/" + index + "/" + index + "_1/_restore?wait_for_completion=true", @@ -390,7 +394,7 @@ private void testSnapshotSystemIndexWithUser(String user, Header header) { header ); if (index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN)) { - validateForbiddenResponse(res, "", user); + validateForbiddenResponse(res, "cluster:admin/snapshot/restore", user); } else { validateForbiddenResponse(res, "indices:data/write/index, indices:admin/create", user); } diff --git a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java index 346052c13c..78b9bfda8e 100644 --- a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java +++ b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java @@ -59,7 +59,7 @@ public void testSearchAsAdmin() { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_search", matchAllQuery, allAccessUserHeader); // no system indices are searchable by admin - validateForbiddenResponse(response, "", allAccessUser); + validateForbiddenResponse(response, "indices:data/read/search", allAccessUser); } // search all indices @@ -78,7 +78,7 @@ public void testSearchAsNormalUser() throws Exception { // security index is only accessible by super-admin RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_search", "", normalUserHeader); if (index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN) || index.equals(SYSTEM_INDEX_WITH_NO_ASSOCIATED_ROLE_PERMISSIONS)) { - validateForbiddenResponse(response, "", normalUser); + validateForbiddenResponse(response, "indices:data/read/search", normalUser); } else { // got 1 hits because system index permissions are enabled validateSearchResponse(response, 1); @@ -98,7 +98,7 @@ public void testSearchAsNormalUserWithoutSystemIndexAccess() { // search system indices for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_search", "", normalUserWithoutSystemIndexHeader); - validateForbiddenResponse(response, "", normalUserWithoutSystemIndex); + validateForbiddenResponse(response, "indices:data/read/search", normalUserWithoutSystemIndex); } // search all indices @@ -151,10 +151,10 @@ public void testDeleteAsAdmin() { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executeDeleteRequest(index + "/_doc/document1", allAccessUserHeader); - validateForbiddenResponse(response, "", allAccessUser); + validateForbiddenResponse(response, "indices:data/write/delete", allAccessUser); response = restHelper.executeDeleteRequest(index, allAccessUserHeader); - validateForbiddenResponse(response, "", allAccessUser); + validateForbiddenResponse(response, "indices:admin/delete", allAccessUser); } } @@ -166,10 +166,10 @@ public void testDeleteAsNormalUser() { // permission for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executeDeleteRequest(index + "/_doc/document1", normalUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, response, "", normalUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:data/write/delete", normalUser); response = restHelper.executeDeleteRequest(index, normalUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, response, "", normalUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:admin/delete", normalUser); } } @@ -183,10 +183,10 @@ public void testDeleteAsNormalUserWithoutSystemIndexAccess() { index + "/_doc/document1", normalUserWithoutSystemIndexHeader ); - validateForbiddenResponse(response, "", normalUserWithoutSystemIndex); + validateForbiddenResponse(response, "indices:data/write/delete", normalUserWithoutSystemIndex); response = restHelper.executeDeleteRequest(index, normalUserWithoutSystemIndexHeader); - validateForbiddenResponse(response, "", normalUserWithoutSystemIndex); + validateForbiddenResponse(response, "indices:admin/delete", normalUserWithoutSystemIndex); } } @@ -217,11 +217,11 @@ public void testCloseOpenAsNormalUser() { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_close", "", normalUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, response, "", normalUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:admin/close", normalUser); // normal user cannot open or close security index response = restHelper.executePostRequest(index + "/_open", "", normalUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, response, "", normalUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:admin/open", normalUser); } } @@ -235,11 +235,11 @@ private void testCloseOpenWithUser(String user, Header header) { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePostRequest(index + "/_close", "", header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/close", user); // admin or normal user (without system index permission) cannot open or close any system index response = restHelper.executePostRequest(index + "/_open", "", header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/open", user); } } @@ -314,10 +314,10 @@ public void testUpdateAsNormalUser() { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePutRequest(index + "/_settings", updateIndexSettings, normalUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, response, "", normalUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:admin/settings/update", normalUser); response = restHelper.executePutRequest(index + "/_mapping", newMappings, normalUserHeader); - shouldBeAllowedOnlyForAuthorizedIndices(index, response, "", normalUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, response, "indices:admin/mapping/put", normalUser); } } @@ -331,10 +331,10 @@ private void testUpdateWithUser(String user, Header header) { for (String index : SYSTEM_INDICES) { RestHelper.HttpResponse response = restHelper.executePutRequest(index + "/_settings", updateIndexSettings, header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/settings/update", user); response = restHelper.executePutRequest(index + "/_mapping", newMappings, header); - validateForbiddenResponse(response, "", user); + validateForbiddenResponse(response, "indices:admin/mapping/put", user); } } @@ -393,14 +393,14 @@ public void testSnapshotSystemIndicesAsAdmin() { "", allAccessUserHeader ); - validateForbiddenResponse(res, "", allAccessUser); + validateForbiddenResponse(res, "cluster:admin/snapshot/restore", allAccessUser); res = restHelper.executePostRequest( "_snapshot/" + index + "/" + index + "_1/_restore?wait_for_completion=true", "{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_with_global_state_$1\" }", allAccessUserHeader ); - shouldBeAllowedOnlyForAuthorizedIndices(index, res, "", allAccessUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, res, "cluster:admin/snapshot/restore", allAccessUser); } } @@ -424,7 +424,7 @@ public void testSnapshotSystemIndicesAsNormalUser() { "", normalUserHeader ); - shouldBeAllowedOnlyForAuthorizedIndices(index, res, "", normalUser); + shouldBeAllowedOnlyForAuthorizedIndices(index, res, "cluster:admin/snapshot/restore", normalUser); res = restHelper.executePostRequest( "_snapshot/" + index + "/" + index + "_1/_restore?wait_for_completion=true", @@ -432,8 +432,7 @@ public void testSnapshotSystemIndicesAsNormalUser() { normalUserHeader ); - String action = index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN) ? "" : "indices:data/write/index, indices:admin/create"; - validateForbiddenResponse(res, action, normalUser); + assertThat(res.getStatusCode(), is(RestStatus.FORBIDDEN.getStatus())); } } @@ -457,15 +456,14 @@ public void testSnapshotSystemIndicesAsNormalUserWithoutSystemIndexAccess() { "", normalUserWithoutSystemIndexHeader ); - validateForbiddenResponse(res, "", normalUserWithoutSystemIndex); + validateForbiddenResponse(res, "cluster:admin/snapshot/restore", normalUserWithoutSystemIndex); res = restHelper.executePostRequest( "_snapshot/" + index + "/" + index + "_1/_restore?wait_for_completion=true", "{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_with_global_state_$1\" }", normalUserWithoutSystemIndexHeader ); - String action = index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN) ? "" : "indices:data/write/index, indices:admin/create"; - validateForbiddenResponse(res, action, normalUserWithoutSystemIndex); + assertThat(res.getStatusCode(), is(RestStatus.FORBIDDEN.getStatus())); } } } diff --git a/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java b/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java index 1a75bb7399..69353c5b97 100644 --- a/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java +++ b/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java @@ -33,8 +33,8 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.runner.RunWith; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.tools.Hasher; diff --git a/src/test/resources/log4j2-test.properties b/src/test/resources/log4j2-test.properties index 5f9d82a25d..89e24f2e95 100644 --- a/src/test/resources/log4j2-test.properties +++ b/src/test/resources/log4j2-test.properties @@ -13,7 +13,7 @@ appender.file.layout.type=PatternLayout appender.file.layout.pattern=[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n -rootLogger.level = warn +rootLogger.level = info rootLogger.appenderRef.console.ref = console rootLogger.appenderRef.file.ref = LOGFILE @@ -29,8 +29,8 @@ logger.sslConfig.level = info #logger.resolver.name = org.opensearch.security.resolver #logger.resolver.level = trace -#logger.pe.name = org.opensearch.security.configuration.PrivilegesEvaluator -#logger.pe.level = trace +logger.pe.name = org.opensearch.security.privileges +logger.pe.level = debug logger.cas.name = org.opensearch.cluster.service.ClusterApplierService logger.cas.level = error @@ -39,3 +39,5 @@ logger.cas.level = error #logger.ncs.level = off #logger.ssl.name = org.opensearch.security.ssl.transport.SecuritySSLNettyTransport #logger.ssl.level = warn +logger.pi.name=org.opensearch.security.configuration.PrivilegesInterceptorImpl +logger.pi.level=TRACE \ No newline at end of file