From 46f6ea1b885510a14ce9a4605fde2092a03bf9fa Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 10 Jun 2025 15:42:16 +0200 Subject: [PATCH 01/29] Introduced explicit index resolution API Signed-off-by: Nils Bandener --- .../security/privileges/IndexPatternTest.java | 27 +- .../EmptyActionPrivilegesTest.java | 6 +- .../RoleBasedActionPrivilegesTest.java | 82 +- .../SubjectBasedActionPrivilegesTest.java | 44 +- .../dlsfls/DlsFlsLegacyHeadersTest.java | 36 +- .../dlsfls/DocumentPrivilegesTest.java | 41 +- .../privileges/dlsfls/FieldMaskingTest.java | 16 +- .../dlsfls/FieldPrivilegesTest.java | 16 +- ...MockPrivilegeEvaluationContextBuilder.java | 23 +- .../security/OpenSearchSecurityPlugin.java | 8 +- .../DlsFilterLevelActionHandler.java | 8 +- .../configuration/DlsFlsValveImpl.java | 12 +- .../PrivilegesInterceptorImpl.java | 19 +- .../SystemIndexSearcherWrapper.java | 4 +- .../security/filter/SecurityFilter.java | 35 +- .../security/privileges/ActionPrivileges.java | 10 +- .../privileges/IndicesRequestModifier.java | 77 ++ .../privileges/IndicesRequestResolver.java | 66 ++ .../privileges/PitPrivilegesEvaluator.java | 96 -- .../PrivilegesEvaluationContext.java | 32 +- .../privileges/PrivilegesEvaluator.java | 86 +- .../privileges/PrivilegesInterceptor.java | 4 +- .../ProtectedIndexAccessEvaluator.java | 16 +- .../SystemIndexAccessEvaluator.java | 197 ++-- .../privileges/TermsAggregationEvaluator.java | 13 +- .../RoleBasedActionPrivileges.java | 9 +- .../RuntimeOptimizedActionPrivileges.java | 48 +- .../SubjectBasedActionPrivileges.java | 4 +- .../dlsfls/AbstractRuleBasedPrivileges.java | 7 +- .../resolver/IndexResolverReplacer.java | 863 ------------------ .../security/filter/SecurityFilterTests.java | 6 +- .../PrivilegesEvaluatorUnitTest.java | 11 +- .../RestLayerPrivilegesEvaluatorTest.java | 3 +- .../SystemIndexAccessEvaluatorTest.java | 73 +- 34 files changed, 508 insertions(+), 1490 deletions(-) create mode 100644 src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java create mode 100644 src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java delete mode 100644 src/main/java/org/opensearch/security/privileges/PitPrivilegesEvaluator.java delete mode 100644 src/main/java/org/opensearch/security/resolver/IndexResolverReplacer.java 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/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..672f8dcf00 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; @@ -352,13 +354,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, @@ -481,14 +483,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); } } @@ -648,28 +644,24 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness stat 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); + static ResolvedIndices resolved(String... indices) { + return ResolvedIndices.of(indices); + // TODO check + // 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); + // } - 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 - ); } } @@ -871,7 +863,7 @@ public void hasIndexPrivilege_errors() throws Exception { 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()); @@ -897,7 +889,7 @@ public void hasExplicitIndexPrivilege_positive() throws Exception { 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()); } @@ -917,7 +909,7 @@ public void hasExplicitIndexPrivilege_positive_wildcard() throws Exception { 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()); } @@ -934,7 +926,7 @@ public void hasExplicitIndexPrivilege_noWildcard() throws Exception { 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()); } @@ -954,7 +946,7 @@ public void hasExplicitIndexPrivilege_negative_wrongAction() throws Exception { 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()); } @@ -974,7 +966,7 @@ public void hasExplicitIndexPrivilege_errors() throws Exception { 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()); @@ -1004,14 +996,14 @@ public void aliasesOnDataStreamBackingIndices() throws Exception { 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()); } 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..839ff0e5ea 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; @@ -224,13 +226,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, @@ -330,14 +332,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); } } @@ -473,7 +469,9 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec) throws Exception 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) { + static ResolvedIndices resolved(String... indices) { + return ResolvedIndices.of(indices); + /* TODO CHECK ImmutableSet.Builder allIndices = ImmutableSet.builder(); for (String index : indices) { @@ -495,6 +493,8 @@ static IndexResolverReplacer.Resolved resolved(String... indices) { ImmutableSet.of(), IndicesOptions.LENIENT_EXPAND_OPEN ); + + */ } } @@ -630,7 +630,7 @@ public void hasExplicitIndexPrivilege_positive() throws Exception { PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), Set.of("system:admin/system_index"), - IndexResolverReplacer.Resolved.ofIndex("test_index") + ResolvedIndices.of("test_index") ); assertThat(result, isAllowed()); } @@ -648,7 +648,7 @@ public void hasExplicitIndexPrivilege_positive_pattern() throws Exception { PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), Set.of("system:admin/system_index"), - IndexResolverReplacer.Resolved.ofIndex("test_index") + ResolvedIndices.of("test_index") ); assertThat(result, isAllowed()); } @@ -665,7 +665,7 @@ public void hasExplicitIndexPrivilege_noWildcard() throws Exception { PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), Set.of("system:admin/system_index"), - IndexResolverReplacer.Resolved.ofIndex("test_index") + ResolvedIndices.of("test_index") ); assertThat(result, isForbidden()); } @@ -682,7 +682,7 @@ public void hasExplicitIndexPrivilege_negative_wrongAction() throws Exception { PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), Set.of("system:admin/system_foo"), - IndexResolverReplacer.Resolved.ofIndex("test_index") + ResolvedIndices.of("test_index") ); assertThat(result, isForbidden()); } @@ -699,7 +699,7 @@ public void hasExplicitIndexPrivilege_errors() throws Exception { 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()); 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..7cd8eb25ec 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; @@ -531,6 +531,7 @@ public IndicesAndAliases_getRestriction( null, null, null, + null, () -> CLUSTER_STATE, ActionPrivileges.EMPTY ); @@ -567,17 +568,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 +681,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 +737,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 +767,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 +824,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 +1132,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/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/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 24bfd79932..2bc1b0cc63 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -172,7 +172,6 @@ import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; 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; @@ -284,7 +283,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; @@ -1121,7 +1119,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); @@ -1177,8 +1174,7 @@ public Collection createComponents( auditLog, settings, privilegesInterceptor, - cih, - irr + cih ); dlsFlsBaseContext = new DlsFlsBaseContext(evaluator, threadPool.getThreadContext(), adminDns); @@ -1234,7 +1230,6 @@ public Collection createComponents( cs, cih, compatConfig, - irr, xffResolver, resourceAccessEvaluator ); @@ -1262,7 +1257,6 @@ 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); diff --git a/src/main/java/org/opensearch/security/configuration/DlsFilterLevelActionHandler.java b/src/main/java/org/opensearch/security/configuration/DlsFilterLevelActionHandler.java index 1e179b1243..72a8e5f49f 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFilterLevelActionHandler.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFilterLevelActionHandler.java @@ -39,6 +39,7 @@ import org.opensearch.action.search.SearchScrollAction; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +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 +64,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 +131,7 @@ public static boolean handle( private final ActionRequest request; private final ActionListener listener; private final IndexToRuleMap dlsRestrictionMap; - private final Resolved resolved; + private final ResolvedIndices resolved; private final boolean requiresIndexScoping; private final Client nodeClient; private final ClusterService clusterService; @@ -162,7 +162,7 @@ public static boolean handle( this.threadContext = threadContext; this.resolver = resolver; - this.requiresIndexScoping = resolved.isLocalAll() || resolved.getAllIndicesResolved(clusterService, resolver).size() != 1; + this.requiresIndexScoping = resolved.local().isAll() || resolved.local().names().size() != 1; } private boolean handle() { @@ -474,7 +474,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..89ab3d327a 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -39,6 +39,7 @@ import org.opensearch.action.search.SearchRequest; import org.opensearch.action.update.UpdateRequest; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +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; @@ -76,7 +77,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; @@ -162,7 +162,7 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< ActionRequest request = context.getRequest(); if (HeaderHelper.isInternalOrPluginRequest(threadContext)) { if (resourceSharingEnabledSetting.getDynamicSettingValue() && request instanceof SearchRequest) { - IndexResolverReplacer.Resolved resolved = context.getResolvedRequest(); + ResolvedIndices resolved = context.getResolvedRequest(); Set protectedIndices = resourcePluginInfo.getResourceIndicesForProtectedTypes(); WildcardMatcher resourceIndicesMatcher = WildcardMatcher.from(protectedIndices); if (resourceIndicesMatcher.matchAll(resolved.getAllIndices())) { @@ -187,7 +187,7 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< return true; } DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get(); - IndexResolverReplacer.Resolved resolved = context.getResolvedRequest(); + ResolvedIndices resolved = context.getResolvedRequest(); try { boolean hasDlsRestrictions = !config.getDocumentPrivileges().isUnrestricted(context, resolved); @@ -214,14 +214,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; diff --git a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java index 6a64eada3e..118d1c9ff2 100644 --- a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java +++ b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java @@ -44,12 +44,12 @@ import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.cluster.service.ClusterService; 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; @@ -98,7 +98,7 @@ public ReplaceResult replaceDashboardsIndex( final String action, final User user, final DynamicConfigModel config, - final Resolved requestedResolved, + final ResolvedIndices requestedResolved, final PrivilegesEvaluationContext context, final TenantPrivileges tenantPrivileges ) { @@ -161,8 +161,8 @@ public ReplaceResult replaceDashboardsIndex( } // 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) && !requestedResolved.local().isAll()) { + final Set indices = requestedResolved.local().names(); final String tenantIndexName = toUserIndexName(dashboardsIndexName, requestedTenant); if (indices.size() == 1 && indices.iterator().next().startsWith(tenantIndexName) @@ -393,15 +393,10 @@ 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()) { + private static boolean resolveToDashboardsIndexOrAlias(final ResolvedIndices requestedResolved, final String dashboardsIndexName) { + if (requestedResolved.local().isAll()) { 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)); + return requestedResolved.local().names().size() == 1 && requestedResolved.local().names().contains(dashboardsIndexName); } } diff --git a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java index 447f134877..1ff295cd94 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; @@ -43,7 +44,6 @@ 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.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; @@ -171,7 +171,7 @@ protected final boolean isBlockedSystemIndexRequest() { String permission = ConfigConstants.SYSTEM_INDEX_PERMISSION; PrivilegesEvaluationContext context = evaluator.createContext(user, permission); PrivilegesEvaluatorResponse result = context.getActionPrivileges() - .hasExplicitIndexPrivilege(context, Set.of(permission), IndexResolverReplacer.Resolved.ofIndex(index.getName())); + .hasExplicitIndexPrivilege(context, Set.of(permission), ResolvedIndices.of(index.getName())); return !result.isAllowed(); } diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index a23db341fd..9bc69d4207 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -27,6 +27,7 @@ package org.opensearch.security.filter; import java.util.Collections; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.function.Consumer; @@ -63,6 +64,7 @@ import org.opensearch.action.support.ActionFilterChain; import org.opensearch.action.support.ActionRequestMetadata; import org.opensearch.action.update.UpdateRequest; +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; @@ -115,7 +117,7 @@ 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; @@ -131,7 +133,6 @@ public SecurityFilter( ClusterService cs, final ClusterInfoHolder clusterInfoHolder, final CompatConfig compatConfig, - final IndexResolverReplacer indexResolverReplacer, final XFFResolver xffResolver, ResourceAccessEvaluator resourceAccessEvaluator ) { @@ -143,7 +144,7 @@ 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()) ); @@ -174,7 +175,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 +187,7 @@ private void ap Task task, final String action, Request request, + ActionRequestMetadata actionRequestMetadata, ActionListener listener, ActionFilterChain chain ) { @@ -310,13 +312,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) { @@ -397,7 +399,7 @@ private void ap 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, injectedRoles); User finalUser = user; Consumer handleUnauthorized = response -> { auditLog.logMissingPrivileges(action, request, task); @@ -567,7 +569,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 +579,27 @@ 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) { + Optional optionalResolvedIndices = actionRequestMetadata.resolvedIndices(); + if (!optionalResolvedIndices.isPresent()) { + return true; + } + ResolvedIndices resolvedIndices = optionalResolvedIndices.get(); + if (resolvedIndices.local().isAll()) { return true; } - final Set allIndices = resolved.getAllIndices(); - return immutableIndicesMatcher.matchAny(allIndices); + return immutableIndicesMatcher.matchAny(resolvedIndices.local().names()); } } diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 9ee104246f..c47d9a8450 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.ResolvedIndices; /** * Defines the general interface for evaluating privileges on actions. References to ActionPrivileges instances @@ -77,7 +77,7 @@ public interface ActionPrivileges { PrivilegesEvaluatorResponse hasIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices + ResolvedIndices resolvedIndices ); /** @@ -90,7 +90,7 @@ PrivilegesEvaluatorResponse hasIndexPrivilege( PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices + ResolvedIndices resolvedIndices ); ActionPrivileges EMPTY = new ActionPrivileges() { @@ -113,7 +113,7 @@ public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluat public PrivilegesEvaluatorResponse hasIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices + ResolvedIndices resolvedIndices ) { return PrivilegesEvaluatorResponse.insufficient("all of " + actions).reason("User has no privileges"); } @@ -122,7 +122,7 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices + ResolvedIndices resolvedIndices ) { return PrivilegesEvaluatorResponse.insufficient("all of " + actions).reason("User has no privileges"); } 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..9852641607 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java @@ -0,0 +1,77 @@ +/* + * 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.cluster.metadata.ResolvedIndices; + +public class IndicesRequestModifier { + + public boolean reduceLocalIndices(ActionRequest targetRequest, ResolvedIndices resolvedIndices, Collection newIndices) { + if (newIndices.isEmpty()) { + return setLocalIndicesToEmpty(targetRequest, resolvedIndices); + } + + 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 IndicesRequest.Replaceable replaceable) { + if (resolvedIndices.remote().isEmpty()) { + if (replaceable.indicesOptions().expandWildcardsOpen() + || replaceable.indicesOptions().expandWildcardsClosed() + || replaceable.indicesOptions().expandWildcardsHidden()) { + // If the request expands wildcards, we use an index expression which resolves to no indices + // This expression cannot resolve to anything because indices with a leading underscore are not allowed + replaceable.indices("_empty*,-*"); + return true; + } else if (replaceable.indicesOptions().allowNoIndices()) { + // If the request does not expand wildcards, we have to look for two different conditions due to + // a slightly odd behavior of IndexNameExpressionResolver: + // https://github.com/opensearch-project/OpenSearch/blob/afb08a071269b234936b778f62800bded0e5ea7a/server/src/main/java/org/opensearch/cluster/metadata/IndexNameExpressionResolver.java#L249 + // For allowNoIndices(), we just select a non-existing index. Again, index names with leading + // underscores never exist. + replaceable.indices("_empty"); + return true; + } else if (replaceable.indicesOptions().ignoreUnavailable()) { + // Second case for the special behavior of IndexNameExpressionResolver: + replaceable.indices("_empty", "-_empty*"); + 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..67e52ee189 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java @@ -0,0 +1,66 @@ +/* + * 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.Optional; +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.ResolvedIndices; + +public class IndicesRequestResolver { + private final IndexNameExpressionResolver indexNameExpressionResolver; + + public IndicesRequestResolver(IndexNameExpressionResolver indexNameExpressionResolver) { + this.indexNameExpressionResolver = indexNameExpressionResolver; + } + + public ResolvedIndices resolve( + ActionRequest request, + ActionRequestMetadata actionRequestMetadata, + Supplier clusterStateSupplier + ) { + Optional providedIndices = actionRequestMetadata.resolvedIndices(); + if (providedIndices.isPresent()) { + return providedIndices.get(); + } else { + // The action does not implement the resolution mechanism; we have to do it by ourselves + return resolveFallback(request, clusterStateSupplier.get()); + } + } + + public ResolvedIndices resolve( + ActionRequest request, + ActionRequestMetadata actionRequestMetadata, + PrivilegesEvaluationContext context + ) { + Optional providedIndices = actionRequestMetadata.resolvedIndices(); + if (providedIndices.isPresent()) { + return providedIndices.get(); + } else { + // The action does not implement the resolution mechanism; we have to do it by ourselves + return resolveFallback(request, context.clusterState()); + } + } + + private ResolvedIndices resolveFallback(ActionRequest request, ClusterState clusterState) { + if (request instanceof IndicesRequest indicesRequest) { + return ResolvedIndices.of(this.indexNameExpressionResolver.concreteIndexNames(clusterState, indicesRequest)); + } else { + return ResolvedIndices.all(); + } + } + +} 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/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index b4cc2fe805..0838199bef 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.ResolvedIndices; 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 ResolvedIndices 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 ResolvedIndices getResolvedRequest() { + if (PrivilegesEvaluator.isClusterPerm(action)) { + return ResolvedIndices.all(); + } + ResolvedIndices result = this.resolvedIndices; if (result == null) { - result = indexResolverReplacer.resolveRequest(request); - this.resolvedRequest = result; + result = this.indicesRequestResolver.resolve(this.request, this.actionRequestMetadata, this.clusterStateSupplier); } return result; @@ -149,8 +155,8 @@ void setMappedRoles(ImmutableSet mappedRoles) { this.mappedRoles = mappedRoles; } - public Supplier getClusterStateSupplier() { - return clusterStateSupplier; + public ClusterState clusterState() { + return clusterStateSupplier.get(); } public Map getIndicesLookup() { @@ -182,8 +188,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..ea439b0821 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -71,6 +71,7 @@ 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; @@ -79,6 +80,7 @@ import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.ClusterSettings; import org.opensearch.common.settings.Settings; @@ -92,8 +94,6 @@ 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; @@ -160,17 +160,17 @@ public class PrivilegesEvaluator { 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<>(); + private final IndicesRequestResolver indicesRequestResolver; + private final IndicesRequestModifier indicesRequestModifier = new IndicesRequestModifier(); /** * The pure static action groups should be ONLY used by action privileges for plugins; only those cannot and should @@ -190,8 +190,7 @@ public PrivilegesEvaluator( AuditLog auditLog, final Settings settings, final PrivilegesInterceptor privilegesInterceptor, - final ClusterInfoHolder clusterInfoHolder, - final IndexResolverReplacer irr + final ClusterInfoHolder clusterInfoHolder ) { super(); @@ -213,13 +212,13 @@ public PrivilegesEvaluator( ); this.clusterInfoHolder = clusterInfoHolder; - this.irr = irr; snapshotRestoreEvaluator = new SnapshotRestoreEvaluator(settings, auditLog); - systemIndexAccessEvaluator = new SystemIndexAccessEvaluator(settings, auditLog, irr); + systemIndexAccessEvaluator = new SystemIndexAccessEvaluator(settings, auditLog); protectedIndexAccessEvaluator = new ProtectedIndexAccessEvaluator(settings, auditLog); termsAggregationEvaluator = new TermsAggregationEvaluator(); - pitPrivilegesEvaluator = new PitPrivilegesEvaluator(); this.configurationRepository = configurationRepository; + this.indicesRequestResolver = new IndicesRequestResolver(resolver); + this.staticActionGroups = new FlattenedActionGroups( DynamicConfigFactory.addStatics(SecurityDynamicConfiguration.empty(CType.ACTIONGROUPS)) ); @@ -332,7 +331,7 @@ private void setUserInfoInThreadContext(PrivilegesEvaluationContext context) { } public PrivilegesEvaluationContext createContext(User user, String action) { - return createContext(user, action, null, null, null); + return createContext(user, action, null, ActionRequestMetadata.empty(), null, null); } private String getTenancyAccess(PrivilegesEvaluationContext context) { @@ -351,6 +350,7 @@ public PrivilegesEvaluationContext createContext( User user, String action0, ActionRequest request, + ActionRequestMetadata actionRequestMetadata, Task task, Set injectedRoles ) { @@ -383,9 +383,10 @@ public PrivilegesEvaluationContext createContext( mappedRoles, action0, request, + actionRequestMetadata, task, - irr, resolver, + indicesRequestResolver, clusterStateSupplier, actionPrivileges ); @@ -472,10 +473,10 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) return presponse; } - final Resolved requestedResolved = context.getResolvedRequest(); + ResolvedIndices resolvedIndices = context.getResolvedRequest(); if (isDebugEnabled) { - log.debug("RequestedResolved : {}", requestedResolved); + log.debug("RequestedResolved : {}", resolvedIndices); } // check snapshot/restore requests @@ -486,18 +487,13 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) } // System index access - if (systemIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, context, actionPrivileges, user) + if (systemIndexAccessEvaluator.evaluate(request, task, action0, resolvedIndices, 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()) { + if (protectedIndexAccessEvaluator.evaluate(request, task, action0, resolvedIndices, presponse, mappedRoles).isComplete()) { return presponse; } @@ -521,7 +517,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) log.info( "No cluster-level perm match for {} {} [Action [{}]] [RolesChecked {}]. No permissions for {}", user, - requestedResolved, + resolvedIndices, action0, mappedRoles, presponse.getMissingPrivileges() @@ -541,7 +537,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) action0, user, dcm, - requestedResolved, + resolvedIndices, context, this.tenantPrivileges.get() ); @@ -576,7 +572,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) } // term aggregations - if (termsAggregationEvaluator.evaluate(requestedResolved, request, context, actionPrivileges, presponse).isComplete()) { + if (termsAggregationEvaluator.evaluate(resolvedIndices, request, context, actionPrivileges, presponse).isComplete()) { return presponse; } @@ -591,7 +587,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) } if (isDebugEnabled) { - log.debug("Requested resolved index types: {}", requestedResolved); + log.debug("Requested resolved index types: {}", resolvedIndices); log.debug("Security roles: {}", mappedRoles); } @@ -604,7 +600,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) action0, user, dcm, - requestedResolved, + resolvedIndices, context, this.tenantPrivileges.get() ); @@ -627,11 +623,11 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) boolean dnfofPossible = dnfofEnabled && DNFOF_MATCHER.test(action0); - presponse = actionPrivileges.hasIndexPrivilege(context, allIndexPermsRequired, requestedResolved); + presponse = actionPrivileges.hasIndexPrivilege(context, allIndexPermsRequired, resolvedIndices); if (presponse.isPartiallyOk()) { if (dnfofPossible) { - if (irr.replace(request, true, presponse.getAvailableIndices())) { + if (this.indicesRequestModifier.reduceLocalIndices(request, resolvedIndices, presponse.getAvailableIndices())) { return PrivilegesEvaluatorResponse.ok(); } } @@ -652,7 +648,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) } if (presponse.isAllowed()) { - if (checkFilteredAliases(requestedResolved, action0, isDebugEnabled)) { + if (checkFilteredAliases(resolvedIndices, action0, isDebugEnabled)) { presponse.allowed = false; return presponse; } @@ -665,7 +661,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) "No {}-level perm match for {} {}: {} [Action [{}]] [RolesChecked {}]", "index", user, - requestedResolved, + resolvedIndices, presponse.getReason(), action0, mappedRoles @@ -801,7 +797,7 @@ public static boolean isClusterPerm(String action0) { } @SuppressWarnings("unchecked") - private boolean checkFilteredAliases(Resolved requestedResolved, String action, boolean isDebugEnabled) { + private boolean checkFilteredAliases(ResolvedIndices requestedResolved, String action, boolean isDebugEnabled) { final String faMode = dcm.getFilteredAliasMode();// getConfigSettings().dynamic.filtered_alias_mode; if (!"disallow".equals(faMode)) { @@ -814,30 +810,22 @@ private boolean checkFilteredAliases(Resolved requestedResolved, String action, Iterable indexMetaDataCollection; - 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()); + Set indexMetaDataSet = new HashSet<>(requestedResolved.local().names().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; + 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); } - - indexMetaDataSet.add(indexMetaData); + continue; } - indexMetaDataCollection = indexMetaDataSet; + indexMetaDataSet.add(indexMetaData); } + + indexMetaDataCollection = indexMetaDataSet; + // check filtered aliases for (IndexMetadata indexMetaData : indexMetaDataCollection) { diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java index 0ae809bc9d..1ef32fe7c3 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java @@ -29,9 +29,9 @@ import org.opensearch.action.ActionRequest; import org.opensearch.action.admin.indices.create.CreateIndexRequestBuilder; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.ResolvedIndices; 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; @@ -81,7 +81,7 @@ public ReplaceResult replaceDashboardsIndex( final String action, final User user, final DynamicConfigModel config, - final Resolved requestedResolved, + final ResolvedIndices requestedResolved, final PrivilegesEvaluationContext context, final TenantPrivileges tenantPrivileges ) { diff --git a/src/main/java/org/opensearch/security/privileges/ProtectedIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/ProtectedIndexAccessEvaluator.java index 877e6fd787..506898c4bf 100644 --- a/src/main/java/org/opensearch/security/privileges/ProtectedIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/ProtectedIndexAccessEvaluator.java @@ -21,9 +21,9 @@ import org.opensearch.action.ActionRequest; import org.opensearch.action.RealtimeRequest; import org.opensearch.action.search.SearchRequest; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.common.settings.Settings; import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.tasks.Task; @@ -71,15 +71,14 @@ public PrivilegesEvaluatorResponse evaluate( final ActionRequest request, final Task task, final String action, - final IndexResolverReplacer.Resolved requestedResolved, + final ResolvedIndices requestedResolved, final PrivilegesEvaluatorResponse presponse, final Set mappedRoles ) { if (!protectedIndexEnabled) { return presponse; } - if (!requestedResolved.isLocalAll() - && indexMatcher.matchAny(requestedResolved.getAllIndices()) + if (indexMatcher.matchAny(requestedResolved.local().names()) && deniedActionMatcher.test(action) && !allowedRolesMatcher.matchAny(mappedRoles)) { auditLog.logMissingPrivileges(action, request, task); @@ -88,14 +87,7 @@ public PrivilegesEvaluatorResponse evaluate( return presponse.markComplete(); } - if (requestedResolved.isLocalAll() && 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(); - } - if ((requestedResolved.isLocalAll() || indexMatcher.matchAny(requestedResolved.getAllIndices())) - && !allowedRolesMatcher.matchAny(mappedRoles)) { + if (indexMatcher.matchAny(requestedResolved.local().names()) && !allowedRolesMatcher.matchAny(mappedRoles)) { final boolean isDebugEnabled = log.isDebugEnabled(); if (request instanceof SearchRequest) { diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 68cd42a7a8..9a50bd453b 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -39,11 +39,10 @@ import org.opensearch.action.ActionRequest; import org.opensearch.action.RealtimeRequest; import org.opensearch.action.search.SearchRequest; +import org.opensearch.cluster.metadata.ResolvedIndices; 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.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; @@ -63,30 +62,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 +123,27 @@ public PrivilegesEvaluatorResponse evaluate( final ActionRequest request, final Task task, final String action, - final Resolved requestedResolved, + final ResolvedIndices requestedResolved, final PrivilegesEvaluatorResponse presponse, 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); + + evaluateSystemIndicesAccess( + action, + requestedResolved, + request, + task, + presponse, + context, + actionPrivileges, + user, + containsSystemIndex + ); - if (requestedResolved.isLocalAll() - || requestedResolved.getAllIndices().contains(securityIndex) - || requestContainsAnySystemIndices(requestedResolved)) { + if (containsSystemIndex) { if (request instanceof SearchRequest) { ((SearchRequest) request).requestCache(Boolean.FALSE); @@ -156,67 +162,17 @@ 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(); - } - - /** - * 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) { + // TODO Simplify SystemIndexRegistry.matchesSystemIndexPattern() call + return this.systemIndexMatcher.test(index) || !SystemIndexRegistry.matchesSystemIndexPattern(Set.of(index)).isEmpty(); + } else { + return false; + } } /** @@ -241,31 +197,27 @@ private boolean isActionAllowed(String action) { */ private void evaluateSystemIndicesAccess( final String action, - final Resolved requestedResolved, + final ResolvedIndices 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 (serviceAccountUser && containsRegularIndex) { + if (serviceAccountUser && requestedResolved.local().containsAny(index -> !isSystemIndex(index))) { 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() .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); } @@ -273,7 +225,7 @@ private void evaluateSystemIndicesAccess( presponse.markComplete(); return; } - boolean containsProtectedIndex = requestContainsAnyProtectedSystemIndices(requestedResolved); + boolean containsProtectedIndex = requestedResolved.local().containsAny(this.securityIndex::equals); if (containsProtectedIndex) { auditLog.logSecurityIndexAttempt(request, action, task); if (log.isInfoEnabled()) { @@ -281,7 +233,7 @@ 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; @@ -295,7 +247,7 @@ private void evaluateSystemIndicesAccess( "No {} permission for user roles {} to System Indices {}", action, context.getMappedRoles(), - String.join(", ", getAllSystemIndices(requestedResolved)) + requestedResolved.local().names().stream().filter(this::isSystemIndex).collect(Collectors.joining(", ")) ); } presponse.allowed = false; @@ -307,34 +259,34 @@ private void evaluateSystemIndicesAccess( // the following section should only be run for index actions if (user.isPluginUser() && !isClusterPerm(action)) { if (this.isSystemIndexEnabled) { - Set matchingPluginIndices = SystemIndexRegistry.matchesPluginSystemIndexPattern( - user.getName().replace("plugin:", ""), - requestedResolved.getAllIndices() - ); - if (requestedResolved.getAllIndices().equals(matchingPluginIndices)) { - // plugin is authorized to perform any actions on its own registered system indices - presponse.allowed = true; + Set matchingPluginIndices = SystemIndexRegistry.matchesPluginSystemIndexPattern( + user.getName().replace("plugin:", ""), + requestedResolved.local().names() + ); + if (requestedResolved.local().names().equals(matchingPluginIndices)) { + // 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; - } 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; - } } + } } else { // no system index protection and request originating from plugin, allow presponse.allowed = true; @@ -344,18 +296,11 @@ private void evaluateSystemIndicesAccess( } if (isActionAllowed(action)) { - if (requestedResolved.isLocalAll()) { + // TODO requestedResolved.isLocalAll() + if (false) { 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); @@ -367,7 +312,7 @@ private void evaluateSystemIndicesAccess( // 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()); allWithoutSecurity.remove(securityIndex); if (allWithoutSecurity.isEmpty()) { if (log.isDebugEnabled()) { @@ -377,13 +322,17 @@ else if (containsSystemIndex && !isSystemIndexPermissionEnabled) { presponse.markComplete(); return; } - irr.replace(request, false, allWithoutSecurity.toArray(new String[0])); + this.indicesRequestModifier.reduceLocalIndices(request, requestedResolved, 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() + .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(); diff --git a/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java b/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java index a2cd1c16a7..fe2726d91b 100644 --- a/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java @@ -27,7 +27,7 @@ package org.opensearch.security.privileges; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; +import com.google.common.collect.Streams; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -38,12 +38,12 @@ import org.opensearch.action.search.MultiSearchAction; import org.opensearch.action.search.SearchAction; import org.opensearch.action.search.SearchRequest; +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.resolver.IndexResolverReplacer.Resolved; public class TermsAggregationEvaluator { @@ -62,7 +62,7 @@ public class TermsAggregationEvaluator { public TermsAggregationEvaluator() {} public PrivilegesEvaluatorResponse evaluate( - final Resolved resolved, + final ResolvedIndices resolved, final ActionRequest request, PrivilegesEvaluationContext context, ActionPrivileges actionPrivileges, @@ -87,7 +87,7 @@ public PrivilegesEvaluatorResponse evaluate( PrivilegesEvaluatorResponse subResponse = actionPrivileges.hasIndexPrivilege( context, READ_ACTIONS, - Resolved._LOCAL_ALL + ResolvedIndices.all() ); if (subResponse.isPartiallyOk()) { @@ -95,7 +95,10 @@ public PrivilegesEvaluatorResponse evaluate( .query( new TermsQueryBuilder( "_index", - Sets.union(subResponse.getAvailableIndices(), resolved.getRemoteIndices()) + Streams.concat( + subResponse.getAvailableIndices().stream(), + resolved.remote().asRawExpressions().stream() + ).toArray(String[]::new) ) ); } else if (!subResponse.isAllowed()) { 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..7a5914fcfc 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java @@ -39,7 +39,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; @@ -495,7 +494,6 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde protected PrivilegesEvaluatorResponse providesPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices, CheckTable checkTable ) { List exceptions = new ArrayList<>(); @@ -529,7 +527,7 @@ protected PrivilegesEvaluatorResponse providesPrivilege( } } - return responseForIncompletePrivileges(context, resolvedIndices, checkTable, exceptions); + return responseForIncompletePrivileges(context, checkTable, exceptions); } /** @@ -798,7 +796,6 @@ 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. @@ -806,18 +803,18 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St @Override protected PrivilegesEvaluatorResponse 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)) { 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..b89257b5ba 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java @@ -22,12 +22,12 @@ import org.apache.logging.log4j.Logger; import org.opensearch.cluster.metadata.IndexAbstraction; +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.WildcardMatcher; import com.selectivem.collections.CheckTable; @@ -84,14 +84,14 @@ public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluat public PrivilegesEvaluatorResponse hasIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices + ResolvedIndices 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,16 +100,13 @@ 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(fullyResolvedIndices(context, resolvedIndices), actions); StatefulIndexPrivileges statefulIndex = this.currentStatefulIndexPrivileges(); PrivilegesEvaluatorResponse resultFromStatefulIndex = null; if (statefulIndex != null) { - resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, resolvedIndices, context, checkTable); + resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, context, checkTable); if (resultFromStatefulIndex != null) { // If we get a result from statefulIndex, we are done. @@ -121,7 +118,7 @@ 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); + return this.index.providesPrivilege(context, actions, checkTable); } /** @@ -135,13 +132,13 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices + ResolvedIndices 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(fullyResolvedIndices(context, resolvedIndices), actions); return this.index.providesExplicitPrivilege(context, actions, checkTable); } @@ -260,7 +257,6 @@ PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext con * This is the slowest way to check for a privilege. */ protected abstract boolean checkPrivilegeViaActionMatcher(PrivilegesEvaluationContext context, String action); - } /** @@ -287,7 +283,6 @@ protected abstract static class StaticIndexPrivileges { protected abstract PrivilegesEvaluatorResponse providesPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices, CheckTable checkTable ); @@ -411,7 +406,6 @@ protected void checkPrivilegesForNonWellKnownActions( */ protected PrivilegesEvaluatorResponse responseForIncompletePrivileges( PrivilegesEvaluationContext context, - IndexResolverReplacer.Resolved resolvedIndices, CheckTable checkTable, List exceptions ) { @@ -421,13 +415,16 @@ protected PrivilegesEvaluatorResponse responseForIncompletePrivileges( 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); } } @@ -450,17 +447,22 @@ 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( Set actions, - IndexResolverReplacer.Resolved resolvedIndices, PrivilegesEvaluationContext context, CheckTable checkTable ); } + private Set fullyResolvedIndices(PrivilegesEvaluationContext context, ResolvedIndices resolvedIndices) { + if (resolvedIndices.local().isAll()) { + return context.getIndicesLookup().keySet(); + } else { + return resolvedIndices.local().names(); + } + } } 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..155af1e9e2 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java @@ -29,7 +29,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.v7.RoleV7; import org.opensearch.security.support.WildcardMatcher; @@ -305,7 +304,6 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde protected PrivilegesEvaluatorResponse providesPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices, CheckTable checkTable ) { List exceptions = new ArrayList<>(); @@ -326,7 +324,7 @@ protected PrivilegesEvaluatorResponse providesPrivilege( } } - return responseForIncompletePrivileges(context, resolvedIndices, checkTable, exceptions); + return responseForIncompletePrivileges(context, checkTable, exceptions); } /** 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..2e420018d8 100644 --- a/src/main/java/org/opensearch/security/privileges/dlsfls/AbstractRuleBasedPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/dlsfls/AbstractRuleBasedPrivileges.java @@ -25,6 +25,7 @@ import org.apache.logging.log4j.Logger; import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.common.settings.Settings; import org.opensearch.security.privileges.IndexPattern; import org.opensearch.security.privileges.PrivilegesConfigurationValidationException; @@ -131,8 +132,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) - throws PrivilegesEvaluationException { + public boolean isUnrestricted(PrivilegesEvaluationContext context, ResolvedIndices resolved) throws PrivilegesEvaluationException { if (context.getMappedRoles().isEmpty()) { return false; } @@ -156,7 +156,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. @@ -840,5 +840,4 @@ static interface RoleToRuleFunction { static abstract class Rule { abstract boolean isUnrestricted(); } - } 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/test/java/org/opensearch/security/filter/SecurityFilterTests.java b/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java index 5a311bec8e..ad5623c06a 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; @@ -32,7 +33,6 @@ import org.opensearch.security.http.XFFResolver; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.ResourceAccessEvaluator; -import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.threadpool.ThreadPool; @@ -88,7 +88,6 @@ public void testImmutableIndicesWildcardMatcher() { mock(ClusterService.class), mock(ClusterInfoHolder.class), mock(CompatConfig.class), - mock(IndexResolverReplacer.class), mock(XFFResolver.class), mock(ResourceAccessEvaluator.class) ); @@ -113,13 +112,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/privileges/PrivilegesEvaluatorUnitTest.java b/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java index 66aa954de4..6df69ec4e8 100644 --- a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java +++ b/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java @@ -24,11 +24,9 @@ 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; @@ -143,12 +141,6 @@ public class PrivilegesEvaluatorUnitTest { @Mock private ClusterInfoHolder clusterInfoHolder; - @Mock - private IndexResolverReplacer irr; - - @Mock - private NamedXContentRegistry namedXContentRegistry; - @Mock private ClusterState clusterState; @@ -177,8 +169,7 @@ public void setUp() { auditLog, settings, privilegesInterceptor, - clusterInfoHolder, - irr + clusterInfoHolder ); } diff --git a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index cf52f9ea54..8abf5586bb 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -170,8 +170,7 @@ PrivilegesEvaluator createPrivilegesEvaluator(SecurityDynamicConfiguration clusterState, actionPrivileges ); @@ -181,13 +178,13 @@ PrivilegesEvaluationContext ctx(String action) { @After public void after() { - verifyNoMoreInteractions(auditLog, irr, request, task, presponse, log); + verifyNoMoreInteractions(auditLog, 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( @@ -208,7 +205,7 @@ public void testUnprotectedActionOnRegularIndex_systemIndexDisabled() { @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( @@ -228,7 +225,7 @@ public void testUnprotectedActionOnRegularIndex_systemIndexPermissionDisabled() @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( @@ -248,7 +245,7 @@ public void testUnprotectedActionOnRegularIndex_systemIndexPermissionEnabled() { @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( @@ -268,7 +265,7 @@ public void testUnprotectedActionOnSystemIndex_systemIndexDisabled() { @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( @@ -288,7 +285,7 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionDisabled() { @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); // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( @@ -317,7 +314,7 @@ 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( @@ -341,7 +338,7 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexDisabled() { 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); @@ -357,7 +354,7 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionDisable 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); @@ -378,7 +375,7 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled 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); @@ -413,7 +410,7 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled 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); @@ -431,7 +428,7 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled @Test public void testProtectedActionLocalAll_systemIndexDisabled() { setup(false, false, TEST_SYSTEM_INDEX, false); - final Resolved resolved = Resolved._LOCAL_ALL; + final ResolvedIndices resolved = ResolvedIndices.all(); // Action evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); @@ -445,7 +442,7 @@ public void testProtectedActionLocalAll_systemIndexDisabled() { @Test public void testProtectedActionLocalAll_systemIndexPermissionDisabled() { setup(true, false, TEST_SYSTEM_INDEX, false); - final Resolved resolved = Resolved._LOCAL_ALL; + final ResolvedIndices resolved = ResolvedIndices.all(); // Action evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); @@ -459,7 +456,7 @@ public void testProtectedActionLocalAll_systemIndexPermissionDisabled() { @Test public void testProtectedActionLocalAll_systemIndexPermissionEnabled() { setup(true, true, TEST_SYSTEM_INDEX, false); - final Resolved resolved = Resolved._LOCAL_ALL; + final ResolvedIndices resolved = ResolvedIndices.all(); // Action evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); @@ -473,7 +470,7 @@ public void testProtectedActionLocalAll_systemIndexPermissionEnabled() { @Test public void testProtectedActionOnRegularIndex_systemIndexDisabled() { setup(false, false, TEST_INDEX, false); - final Resolved resolved = createResolved(TEST_INDEX); + final ResolvedIndices resolved = createResolved(TEST_INDEX); // Action evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); @@ -484,7 +481,7 @@ public void testProtectedActionOnRegularIndex_systemIndexDisabled() { @Test public void testProtectedActionOnRegularIndex_systemIndexPermissionDisabled() { setup(true, false, TEST_INDEX, false); - final Resolved resolved = createResolved(TEST_INDEX); + final ResolvedIndices resolved = createResolved(TEST_INDEX); // Action evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); @@ -495,7 +492,7 @@ public void testProtectedActionOnRegularIndex_systemIndexPermissionDisabled() { @Test public void testProtectedActionOnRegularIndex_systemIndexPermissionEnabled() { setup(true, true, TEST_INDEX, false); - final Resolved resolved = createResolved(TEST_INDEX); + final ResolvedIndices resolved = createResolved(TEST_INDEX); // Action evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); @@ -506,7 +503,7 @@ public void testProtectedActionOnRegularIndex_systemIndexPermissionEnabled() { @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); // Action evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); @@ -517,7 +514,7 @@ public void testProtectedActionOnSystemIndex_systemIndexDisabled() { @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); // Action evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); @@ -531,7 +528,7 @@ public void testProtectedActionOnSystemIndex_systemIndexPermissionDisabled() { @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); // Action evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); @@ -552,7 +549,7 @@ 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); // Action evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); @@ -563,7 +560,7 @@ public void testProtectedActionOnSystemIndex_systemIndexPermissionEnabled_withSy @Test public void testProtectedActionOnProtectedSystemIndex_systemIndexDisabled() { setup(false, false, SECURITY_INDEX, false); - final Resolved resolved = createResolved(SECURITY_INDEX); + final ResolvedIndices resolved = createResolved(SECURITY_INDEX); // Action evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); @@ -578,7 +575,7 @@ 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); // Action evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); @@ -613,7 +610,7 @@ public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionEnabl private void testSecurityIndexAccess(String action) { setup(true, true, SECURITY_INDEX, true); - final Resolved resolved = createResolved(SECURITY_INDEX); + final ResolvedIndices resolved = createResolved(SECURITY_INDEX); // Action evaluator.evaluate(request, task, action, resolved, presponse, ctx(action), actionPrivileges, user); @@ -631,13 +628,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); } } From ed23516a845d5a798d5bea8bc5cf18741e1f4b5a Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Mon, 23 Jun 2025 01:36:38 +0200 Subject: [PATCH 02/29] fixes Signed-off-by: Nils Bandener --- .../privileges/IndexRequestModifierTest.java | 118 ++++++++++++++++++ .../privileges/IndicesRequestModifier.java | 25 ++-- .../SystemIndexAccessEvaluator.java | 2 +- 3 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/privileges/IndexRequestModifierTest.java 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..db7a4013be --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndexRequestModifierTest.java @@ -0,0 +1,118 @@ +/* + * 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.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 java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + IndexRequestModifierTest.SetLocalIndicesToEmpty.class }) +public class IndexRequestModifierTest { + + static final IndexNameExpressionResolver indexNameExpressionResolver = new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)); + static final Metadata metadata = MockIndexMetadataBuilder.indices("index").build(); + final static ClusterState clusterState = ClusterState.builder(ClusterState.EMPTY_STATE).metadata(metadata).build(); + + @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 = new IndicesRequestModifier().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/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java b/src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java index 9852641607..11f136dca6 100644 --- a/src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java +++ b/src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java @@ -18,6 +18,13 @@ import org.opensearch.action.IndicesRequest; 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 reduceLocalIndices(ActionRequest targetRequest, ResolvedIndices resolvedIndices, Collection newIndices) { @@ -37,23 +44,13 @@ public boolean setLocalIndicesToEmpty(ActionRequest targetRequest, ResolvedIndic if (targetRequest instanceof IndicesRequest.Replaceable replaceable) { if (resolvedIndices.remote().isEmpty()) { if (replaceable.indicesOptions().expandWildcardsOpen() - || replaceable.indicesOptions().expandWildcardsClosed() - || replaceable.indicesOptions().expandWildcardsHidden()) { + || replaceable.indicesOptions().expandWildcardsClosed()) { // If the request expands wildcards, we use an index expression which resolves to no indices - // This expression cannot resolve to anything because indices with a leading underscore are not allowed - replaceable.indices("_empty*,-*"); + replaceable.indices(".none*,-*"); return true; } else if (replaceable.indicesOptions().allowNoIndices()) { - // If the request does not expand wildcards, we have to look for two different conditions due to - // a slightly odd behavior of IndexNameExpressionResolver: - // https://github.com/opensearch-project/OpenSearch/blob/afb08a071269b234936b778f62800bded0e5ea7a/server/src/main/java/org/opensearch/cluster/metadata/IndexNameExpressionResolver.java#L249 - // For allowNoIndices(), we just select a non-existing index. Again, index names with leading - // underscores never exist. - replaceable.indices("_empty"); - return true; - } else if (replaceable.indicesOptions().ignoreUnavailable()) { - // Second case for the special behavior of IndexNameExpressionResolver: - replaceable.indices("_empty", "-_empty*"); + // 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 diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 9a50bd453b..466aee7f6b 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -269,7 +269,7 @@ private void evaluateSystemIndicesAccess( presponse.markComplete(); return; } else { - Set matchingSystemIndices = SystemIndexRegistry.matchesSystemIndexPattern(requestedResolved.getAllIndices()); + Set matchingSystemIndices = SystemIndexRegistry.matchesSystemIndexPattern(requestedResolved.local().names()); matchingSystemIndices.removeAll(matchingPluginIndices); // See if request matches other system indices not belong to the plugin if (!matchingSystemIndices.isEmpty()) { From 48d512b418c61b4d9a843e61743da98bdbc1eb15 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Mon, 23 Jun 2025 12:22:27 +0200 Subject: [PATCH 03/29] Tests of IndicesRequestModifier and IndicesRequestResolver Signed-off-by: Nils Bandener --- .../privileges/IndexRequestModifierTest.java | 50 ++++++++++- .../IndicesRequestResolverTest.java | 86 +++++++++++++++++++ .../privileges/IndicesRequestModifier.java | 2 +- .../privileges/IndicesRequestResolver.java | 8 +- .../privileges/PrivilegesEvaluator.java | 2 +- .../SystemIndexAccessEvaluator.java | 4 +- 6 files changed, 139 insertions(+), 13 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/privileges/IndicesRequestResolverTest.java diff --git a/src/integrationTest/java/org/opensearch/security/privileges/IndexRequestModifierTest.java b/src/integrationTest/java/org/opensearch/security/privileges/IndexRequestModifierTest.java index db7a4013be..9e8d2441b4 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/IndexRequestModifierTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndexRequestModifierTest.java @@ -31,20 +31,66 @@ import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Map; +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").build(); + 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 { @@ -62,7 +108,7 @@ public void setLocalIndicesToEmpty() { resolvedIndices = resolvedIndices.withRemoteIndices(Map.of("remote", new OriginalIndices(new String [] {"index"}, request.indicesOptions()))); } - boolean success = new IndicesRequestModifier().setLocalIndicesToEmpty((ActionRequest) request, resolvedIndices); + boolean success = subject.setLocalIndicesToEmpty((ActionRequest) request, resolvedIndices); if (!(request instanceof IndicesRequest.Replaceable)) { assertFalse(success); 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..30823e6ab0 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndicesRequestResolverTest.java @@ -0,0 +1,86 @@ +/* + * 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.junit.Test; +import org.opensearch.action.admin.cluster.state.ClusterStateRequest; +import org.opensearch.action.admin.cluster.stats.ClusterStatsRequest; +import org.opensearch.action.admin.indices.settings.put.UpdateSettingsRequest; +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.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 java.util.Optional; +import java.util.Set; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class IndicesRequestResolverTest { + + static final Metadata metadata = MockIndexMetadataBuilder.indices("index1", "index2", "index3").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(Optional.of(resolvedIndices)); + + ResolvedIndices 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(Optional.empty()); + + ResolvedIndices returnedResolvedIndices = subject.resolve(request, actionRequestMetadata, () -> clusterState); + assertEquals(Set.of("index1"), returnedResolvedIndices.local().names()); + } + + @Test + public void resolve_fallbackUnsupported() { + ClusterStatsRequest request = new ClusterStatsRequest(); + ActionRequestMetadata actionRequestMetadata = mock(); + when(actionRequestMetadata.resolvedIndices()).thenReturn(Optional.empty()); + + ResolvedIndices returnedResolvedIndices = subject.resolve(request, actionRequestMetadata, () -> clusterState); + assertTrue("Expected isAll(), got: " + returnedResolvedIndices, returnedResolvedIndices.local().isAll()); + } + + @Test + public void resolve_withPrivilegesEvaluationContext() { + SearchRequest request = new SearchRequest("index*"); + ActionRequestMetadata actionRequestMetadata = mock(); + when(actionRequestMetadata.resolvedIndices()).thenReturn(Optional.empty()); + PrivilegesEvaluationContext context = MockPrivilegeEvaluationContextBuilder.ctx().clusterState(clusterState).get(); + + ResolvedIndices returnedResolvedIndices = subject.resolve(request, actionRequestMetadata, context); + assertEquals(Set.of("index1", "index2", "index3"), returnedResolvedIndices.local().names()); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java b/src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java index 11f136dca6..a7926fc6ad 100644 --- a/src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java +++ b/src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java @@ -27,7 +27,7 @@ */ public class IndicesRequestModifier { - public boolean reduceLocalIndices(ActionRequest targetRequest, ResolvedIndices resolvedIndices, Collection newIndices) { + public boolean setLocalIndices(ActionRequest targetRequest, ResolvedIndices resolvedIndices, Collection newIndices) { if (newIndices.isEmpty()) { return setLocalIndicesToEmpty(targetRequest, resolvedIndices); } diff --git a/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java b/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java index 67e52ee189..58e18b3bee 100644 --- a/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java +++ b/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java @@ -46,13 +46,7 @@ public ResolvedIndices resolve( ActionRequestMetadata actionRequestMetadata, PrivilegesEvaluationContext context ) { - Optional providedIndices = actionRequestMetadata.resolvedIndices(); - if (providedIndices.isPresent()) { - return providedIndices.get(); - } else { - // The action does not implement the resolution mechanism; we have to do it by ourselves - return resolveFallback(request, context.clusterState()); - } + return resolve(request, actionRequestMetadata, context::clusterState); } private ResolvedIndices resolveFallback(ActionRequest request, ClusterState clusterState) { diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index ea439b0821..f45a909c25 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -627,7 +627,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) if (presponse.isPartiallyOk()) { if (dnfofPossible) { - if (this.indicesRequestModifier.reduceLocalIndices(request, resolvedIndices, presponse.getAvailableIndices())) { + if (this.indicesRequestModifier.setLocalIndices(request, resolvedIndices, presponse.getAvailableIndices())) { return PrivilegesEvaluatorResponse.ok(); } } diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 466aee7f6b..5a5cd193fe 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -129,7 +129,7 @@ public PrivilegesEvaluatorResponse evaluate( final ActionPrivileges actionPrivileges, final User user ) { - boolean containsSystemIndex = requestedResolved.local().containsAny(this::isSystemIndex); + boolean containsSystemIndex = false; // TODO requestedResolved.local().containsAny(this::isSystemIndex); evaluateSystemIndicesAccess( action, @@ -322,7 +322,7 @@ else if (containsSystemIndex && !isSystemIndexPermissionEnabled) { presponse.markComplete(); return; } - this.indicesRequestModifier.reduceLocalIndices(request, requestedResolved, allWithoutSecurity); + this.indicesRequestModifier.setLocalIndices(request, requestedResolved, allWithoutSecurity); if (log.isDebugEnabled()) { log.debug("Filtered '{}', resulting list is {}", securityIndex, allWithoutSecurity); } From 0176f05d9da40e66d832ab0a9c66f2969007a527 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Wed, 9 Jul 2025 14:59:52 +0200 Subject: [PATCH 04/29] New privilege evaluation implementation Signed-off-by: Nils Bandener --- .../org/opensearch/security/PerfTest.java | 146 +++ .../privileges/IndexRequestModifierTest.java | 89 +- .../IndicesRequestResolverTest.java | 28 +- .../RestEndpointPermissionTests.java | 8 +- .../RoleBasedActionPrivilegesTest.java | 139 ++- .../SubjectBasedActionPrivilegesTest.java | 96 +- .../dlsfls/DocumentPrivilegesTest.java | 1 - .../security/OpenSearchSecurityPlugin.java | 63 +- .../security/auth/RolesInjector.java | 53 + .../configuration/ClusterInfoHolder.java | 8 + .../DlsFilterLevelActionHandler.java | 7 +- .../configuration/DlsFlsValveImpl.java | 40 +- .../PrivilegesInterceptorImpl.java | 48 +- .../SecurityFlsDlsIndexSearcherWrapper.java | 8 +- .../SystemIndexSearcherWrapper.java | 25 +- .../dlic/rest/api/AccountApiAction.java | 6 +- .../dlic/rest/api/PermissionsInfoAction.java | 12 +- .../api/RestApiAdminPrivilegesEvaluator.java | 18 +- .../rest/api/RestApiPrivilegesEvaluator.java | 10 +- .../rest/api/SecurityApiDependencies.java | 12 +- .../dlic/rest/api/SecurityRestApiActions.java | 99 +- .../security/filter/SecurityFilter.java | 59 +- .../identity/SecurityTokenManager.java | 21 +- .../security/privileges/ActionPrivileges.java | 10 +- .../privileges/ConfigurableRoleMapper.java | 249 +++++ .../DashboardsMultiTenancyConfiguration.java | 72 ++ .../security/privileges/IndexPattern.java | 73 +- .../privileges/IndicesRequestModifier.java | 3 +- .../privileges/IndicesRequestResolver.java | 18 +- .../privileges/PrivilegesConfiguration.java | 272 ++++++ .../PrivilegesEvaluationContext.java | 30 +- .../privileges/PrivilegesEvaluator.java | 910 ++---------------- .../PrivilegesEvaluatorResponse.java | 7 + .../privileges/PrivilegesInterceptor.java | 15 +- .../RestLayerPrivilegesEvaluator.java | 8 +- .../security/privileges/RoleMapper.java | 25 + .../security/privileges/SpecialIndices.java | 34 + .../privileges/TermsAggregationEvaluator.java | 121 --- .../RoleBasedActionPrivileges.java | 121 ++- .../RuntimeOptimizedActionPrivileges.java | 141 ++- .../SubjectBasedActionPrivileges.java | 52 +- .../legacy/PrivilegesEvaluator.java | 800 +++++++++++++++ .../ProtectedIndexAccessEvaluator.java | 25 +- .../legacy}/SnapshotRestoreEvaluator.java | 43 +- .../legacy}/SystemIndexAccessEvaluator.java | 182 ++-- .../legacy/TermsAggregationEvaluator.java | 126 +++ .../nextgen/PrivilegesEvaluator.java | 613 ++++++++++++ .../dlsfls/AbstractRuleBasedPrivileges.java | 10 +- .../privileges/dlsfls/DlsFlsBaseContext.java | 10 +- .../resources/ResourceAccessHandler.java | 11 +- .../security/rest/DashboardsInfoAction.java | 48 +- .../security/rest/SecurityInfoAction.java | 12 +- .../security/rest/TenantInfoAction.java | 30 +- .../security/securityconf/ConfigModel.java | 36 - .../security/securityconf/ConfigModelV7.java | 193 ---- .../securityconf/DynamicConfigFactory.java | 3 - .../securityconf/impl/v7/ConfigV7.java | 3 + .../security/support/ConfigConstants.java | 6 - .../security/support/HostResolverMode.java | 13 +- .../security/user/ThreadContextUserInfo.java | 79 ++ .../api/RestApiPrivilegesEvaluatorTest.java | 3 +- .../security/filter/SecurityFilterTests.java | 9 +- .../identity/SecurityTokenManagerTest.java | 36 +- .../RestLayerPrivilegesEvaluatorTest.java | 194 ++-- .../privileges/UserAttributesUnitTest.java | 1 + .../legacy}/PrivilegesEvaluatorUnitTest.java | 86 +- .../SystemIndexAccessEvaluatorTest.java | 99 +- .../resources/ResourceAccessHandlerTest.java | 1 + 68 files changed, 3733 insertions(+), 2096 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/PerfTest.java create mode 100644 src/main/java/org/opensearch/security/privileges/ConfigurableRoleMapper.java create mode 100644 src/main/java/org/opensearch/security/privileges/DashboardsMultiTenancyConfiguration.java create mode 100644 src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java create mode 100644 src/main/java/org/opensearch/security/privileges/RoleMapper.java create mode 100644 src/main/java/org/opensearch/security/privileges/SpecialIndices.java delete mode 100644 src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java create mode 100644 src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java rename src/main/java/org/opensearch/security/privileges/{ => actionlevel/legacy}/ProtectedIndexAccessEvaluator.java (84%) rename src/main/java/org/opensearch/security/privileges/{ => actionlevel/legacy}/SnapshotRestoreEvaluator.java (75%) rename src/main/java/org/opensearch/security/privileges/{ => actionlevel/legacy}/SystemIndexAccessEvaluator.java (66%) create mode 100644 src/main/java/org/opensearch/security/privileges/actionlevel/legacy/TermsAggregationEvaluator.java create mode 100644 src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/PrivilegesEvaluator.java delete mode 100644 src/main/java/org/opensearch/security/securityconf/ConfigModel.java delete mode 100644 src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java create mode 100644 src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java rename src/test/java/org/opensearch/security/privileges/{ => actionlevel/legacy}/PrivilegesEvaluatorUnitTest.java (68%) rename src/test/java/org/opensearch/security/privileges/{ => actionlevel/legacy}/SystemIndexAccessEvaluatorTest.java (88%) diff --git a/src/integrationTest/java/org/opensearch/security/PerfTest.java b/src/integrationTest/java/org/opensearch/security/PerfTest.java new file mode 100644 index 0000000000..3233af3522 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/PerfTest.java @@ -0,0 +1,146 @@ +package org.opensearch.security; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.action.admin.indices.refresh.RefreshRequest; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.common.Strings; +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.transport.client.Client; + +// io.netty, org.apache.lucene, java.io, java.nio, org.apache.logging. +// org.jcp +//java.security.Provider$Service +//apple.security.AppleProvider$ProviderService +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class PerfTest { + + public static void createTestData(LocalCluster cluster) throws Exception { + try (Client client = cluster.getInternalNodeClient()) { + { + CreateIndexRequest request = new CreateIndexRequest("test").settings( + Map.of("index.number_of_shards", 3, "index.number_of_replicas", 1) + ); + CreateIndexResponse response = client.admin().indices().create(request).actionGet(); + System.out.println(Strings.toString(XContentType.JSON, response)); + } + + IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest(); + + for (int i = 0; i < 1000; i++) { + String index = ".kibana_t_" + i + "_001"; + CreateIndexRequest request = new CreateIndexRequest(index).settings( + Map.of("index.number_of_shards", 1, "index.number_of_replicas", 0) + ); + CreateIndexResponse response = client.admin().indices().create(request).actionGet(); + System.out.println(Strings.toString(XContentType.JSON, response)); + indicesAliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.add().alias(".kibana_t_" + i).indices(index)); + } + + client.admin().indices().aliases(indicesAliasesRequest).actionGet(); + client.admin().indices().refresh(new RefreshRequest()).actionGet(); + client.admin().indices().refresh(new RefreshRequest()).actionGet(); + + } + } + + @Test + public void test() throws Exception { + + try ( + LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.DEFAULT) + .authc(TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL) + .users(TestSecurityConfig.User.USER_ADMIN) + .nodeSettings(Map.of("cluster_manager.throttling.thresholds.auto-create.value", 3000, "cluster.max_shards_per_node", 10000)) + .build() + ) { + + cluster.before(); + + createTestData(cluster); + + System.out.println("*** READY ***"); + + Thread.sleep(60 * 1000); + + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + for (int i = 0; i < 10000; i++) { + StringBuilder bulkBody = new StringBuilder(); + for (int k = 0; k < 10; k++) { + bulkBody.append(""" + { "index": { "_index": "test" } } + { "title": "foo", "year": 2020} + """); + } + try { + TestRestClient.HttpResponse response = client.postJson("_bulk", bulkBody.toString()); + // if (response.getStatusCode() >= 300) { + System.out.println(response.getBody()); + // } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + } + + } + + static String parseNodeStatsResponse(TestRestClient.HttpResponse response) { + if (response.getBody().contains("receive_timeout_transport_exception")) { + return "TIMEOUT\n"; + } else { + JsonNode responseJsonNode = response.bodyAsJsonNode(); + JsonNode nodes = responseJsonNode.get("nodes"); + Iterator fieldNames = nodes.fieldNames(); + StringBuilder result = new StringBuilder(); + while (fieldNames.hasNext()) { + String nodeId = fieldNames.next(); + JsonNode node = nodes.get(nodeId); + JsonNode threadPool = node.get("thread_pool"); + JsonNode managementThreadPool = threadPool.get("management"); + result.append( + nodeId + + ": management thread pool: active: " + + managementThreadPool.get("active") + + "/5" + + "; queue: " + + managementThreadPool.get("queue") + + "\n" + ); + } + + return result.toString(); + } + } + + static TestSecurityConfig.Role[] createTestRoles() { + List result = new ArrayList<>(); + + for (int i = 0; i < 2500; i++) { + result.add(new TestSecurityConfig.Role("role" + i).indexPermissions("crud").on("*example*", ".*example*")); + } + + return result.toArray(new TestSecurityConfig.Role[0]); + } + + static class State { + int pendingCreateUserRequests = 0; + } +} diff --git a/src/integrationTest/java/org/opensearch/security/privileges/IndexRequestModifierTest.java b/src/integrationTest/java/org/opensearch/security/privileges/IndexRequestModifierTest.java index 9e8d2441b4..76be054389 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/IndexRequestModifierTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndexRequestModifierTest.java @@ -11,10 +11,16 @@ 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; @@ -29,23 +35,18 @@ import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.util.MockIndexMetadataBuilder; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; - 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 }) +@Suite.SuiteClasses({ IndexRequestModifierTest.SetLocalIndices.class, IndexRequestModifierTest.SetLocalIndicesToEmpty.class }) public class IndexRequestModifierTest { - static final IndexNameExpressionResolver indexNameExpressionResolver = new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)); + 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(); @@ -58,17 +59,20 @@ public void basic() { boolean success = subject.setLocalIndices(request, resolvedIndices, Collections.singletonList("index1")); assertTrue(success); - assertArrayEquals(new String [] {"index1"}, request.indices()); + 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))); + 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()); + assertArrayEquals(new String[] { "index1", "remote:index_remote" }, request.indices()); } @Test @@ -78,8 +82,8 @@ public void empty() { boolean success = subject.setLocalIndices(request, resolvedIndices, Collections.emptyList()); assertTrue(success); - String [] finalResolvedIndices = indexNameExpressionResolver.concreteIndexNames(clusterState, request); - assertArrayEquals(new String [0], finalResolvedIndices); + String[] finalResolvedIndices = indexNameExpressionResolver.concreteIndexNames(clusterState, request); + assertArrayEquals(new String[0], finalResolvedIndices); } @Test @@ -95,7 +99,6 @@ public void unsupportedType() { @RunWith(Parameterized.class) public static class SetLocalIndicesToEmpty { - String description; IndicesRequest request; @@ -105,53 +108,49 @@ 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()))); + 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()) { + } else if (!request.indicesOptions().allowNoIndices()) { assertFalse(success); } else { assertTrue(success); - String [] finalResolvedIndices = indexNameExpressionResolver.concreteIndexNames(clusterState, request); + String[] finalResolvedIndices = indexNameExpressionResolver.concreteIndexNames(clusterState, request); - assertEquals("Resolved to empty indices: " + Arrays.asList(finalResolvedIndices), 0, finalResolvedIndices.length); + 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") - } + 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") } ); } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/IndicesRequestResolverTest.java b/src/integrationTest/java/org/opensearch/security/privileges/IndicesRequestResolverTest.java index 30823e6ab0..22e70d2eea 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/IndicesRequestResolverTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndicesRequestResolverTest.java @@ -10,38 +10,22 @@ */ package org.opensearch.security.privileges; -import org.junit.Test; -import org.opensearch.action.admin.cluster.state.ClusterStateRequest; -import org.opensearch.action.admin.cluster.stats.ClusterStatsRequest; -import org.opensearch.action.admin.indices.settings.put.UpdateSettingsRequest; -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.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 java.util.Optional; -import java.util.Set; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; public class IndicesRequestResolverTest { static final Metadata metadata = MockIndexMetadataBuilder.indices("index1", "index2", "index3").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))); - + static final IndicesRequestResolver subject = new IndicesRequestResolver( + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)) + ); + /* @Test public void resolve_normal() { SearchRequest request = new SearchRequest("index1"); @@ -82,5 +66,5 @@ public void resolve_withPrivilegesEvaluationContext() { ResolvedIndices returnedResolvedIndices = subject.resolve(request, actionRequestMetadata, context); assertEquals(Set.of("index1", "index2", "index3"), returnedResolvedIndices.local().names()); - } + }*/ } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java index 09c122cc2b..a85971e632 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,12 @@ 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 + ); } @Test 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 672f8dcf00..42a75d8700 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java @@ -82,7 +82,12 @@ 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 + ); assertThat(subject.hasClusterPrivilege(ctx().roles("test_role").get(), "cluster:monitor/nodes/stats"), isAllowed()); assertThat( @@ -101,7 +106,12 @@ 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 + ); assertThat( subject.hasClusterPrivilege(ctx().roles("test_role").get(), "cluster:monitor/nodes/stats/somethingnotwellknown"), @@ -123,7 +133,12 @@ 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 + ); assertThat(subject.hasClusterPrivilege(ctx().roles("test_role").get(), "cluster:whatever"), isAllowed()); assertThat( @@ -146,7 +161,12 @@ 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 + ); assertThat(subject.hasExplicitClusterPrivilege(ctx().roles("explicit_role").get(), "cluster:monitor/nodes/stats"), isAllowed()); assertThat( @@ -177,7 +197,12 @@ 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 + ); assertThat( subject.hasExplicitClusterPrivilege(ctx().roles("explicit_role").get(), "cluster:monitor/nodes/notwellknown"), @@ -203,7 +228,12 @@ 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 + ); assertThat( subject.hasAnyClusterPrivilege(ctx().roles("test_role").get(), ImmutableSet.of("cluster:monitor/nodes/stats")), @@ -233,7 +263,12 @@ 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 + ); assertThat( subject.hasAnyClusterPrivilege(ctx().roles("test_role").get(), ImmutableSet.of("cluster:monitor/nodes/notwellknown")), @@ -270,7 +305,12 @@ 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 + ); assertThat(subject.hasAnyClusterPrivilege(ctx().roles("test_role").get(), ImmutableSet.of("cluster:whatever")), isAllowed()); @@ -464,7 +504,12 @@ 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 + ); if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { this.subject.updateStatefulIndexPrivileges(INDEX_METADATA.getIndicesLookup(), 1); @@ -633,7 +678,12 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness stat .build(); } - this.subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, settings); + this.subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + settings + ); if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { this.subject.updateStatefulIndexPrivileges(INDEX_METADATA.getIndicesLookup(), 1); @@ -800,7 +850,7 @@ public void relevantOnly_identity() throws Exception { assertTrue( "relevantOnly() returned identical object", - RoleBasedActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata) == metadata + RoleBasedActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata, i -> false) == metadata ); } @@ -814,7 +864,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")); @@ -827,7 +880,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")); @@ -858,7 +914,12 @@ 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 + ); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( ctx().roles("role_with_errors").get(), @@ -884,7 +945,12 @@ 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 + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().roles("test_role").get(), @@ -904,7 +970,12 @@ 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 + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().roles("test_role").get(), @@ -921,7 +992,12 @@ 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 + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().roles("test_role").get(), @@ -941,7 +1017,12 @@ 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 + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().roles("test_role").get(), @@ -961,7 +1042,12 @@ 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 + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().roles("role_with_errors").get(), @@ -990,7 +1076,12 @@ 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 + ); subject.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), 2); PrivilegesEvaluatorResponse resultForIndexCoveredByAlias = subject.hasIndexPrivilege( @@ -1021,6 +1112,7 @@ public void statefulDisabled() throws Exception { RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( roles, FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, Settings.builder().put(RoleBasedActionPrivileges.PRECOMPUTED_PRIVILEGES_ENABLED.getKey(), false).build() ); subject.updateStatefulIndexPrivileges(metadata, 1); @@ -1040,7 +1132,12 @@ 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 + ); 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 839ff0e5ea..0b0a82ddc8 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java @@ -72,7 +72,11 @@ 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 + ); assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), isAllowed()); } @@ -83,7 +87,11 @@ 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 + ); assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats/somethingnotwellknown"), isAllowed()); } @@ -94,7 +102,11 @@ 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 + ); assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:monitor/nodes/foo"), isForbidden()); } @@ -105,7 +117,11 @@ public void wildcard() throws Exception { - '*' """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + ); assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:whatever"), isAllowed()); } @@ -116,7 +132,11 @@ 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 + ); assertThat(subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), isAllowed()); } @@ -127,7 +147,11 @@ 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 + ); assertThat(subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/notwellknown"), isAllowed()); } @@ -138,7 +162,11 @@ public void explicit_notExplicit() throws Exception { - '*' """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + ); assertThat( subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) @@ -152,7 +180,11 @@ 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 + ); assertThat(subject.hasAnyClusterPrivilege(ctx().get(), ImmutableSet.of("cluster:monitor/nodes/stats")), isAllowed()); } @@ -163,7 +195,11 @@ public void hasAny_wildcard() throws Exception { - '*' """); - SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( + config, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + ); assertThat(subject.hasAnyClusterPrivilege(ctx().get(), ImmutableSet.of("cluster:monitor/nodes/stats")), isAllowed()); } } @@ -317,7 +353,11 @@ 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 + ); } final static Metadata INDEX_METADATA = // @@ -462,7 +502,11 @@ 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 + ); } final static Metadata INDEX_METADATA = // @@ -625,7 +669,11 @@ 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 + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), @@ -643,7 +691,11 @@ 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 + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), @@ -660,7 +712,11 @@ 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 + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), @@ -677,7 +733,11 @@ 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 + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), @@ -694,7 +754,11 @@ 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 + ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( ctx().get(), 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 7cd8eb25ec..68695db59a 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java @@ -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; diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 2bc1b0cc63..89f64683b4 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; @@ -165,11 +166,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.resources.ResourceAccessControlClient; @@ -267,7 +271,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; @@ -619,7 +624,12 @@ 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( @@ -631,16 +641,19 @@ public List getRestHandlers( ); handlers.add( new DashboardsInfoAction( - Objects.requireNonNull(evaluator), + settings, + restController, + Objects.requireNonNull(privilegesConfiguration), + Objects.requireNonNull(cr), Objects.requireNonNull(threadPool), - resourceSharingEnabledSetting + resourceSharingEnabledSetting ) ); handlers.add( new TenantInfoAction( settings, restController, - Objects.requireNonNull(evaluator), + Objects.requireNonNull(privilegesConfiguration), Objects.requireNonNull(threadPool), Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), @@ -678,7 +691,8 @@ public List getRestHandlers( cr, cs, principalExtractor, - evaluator, + roleMapper, + privilegesConfiguration, threadPool, Objects.requireNonNull(auditLog), sslSettingsManager, @@ -749,7 +763,8 @@ public void onIndexModule(IndexModule indexModule) { cs, auditLog, ciol, - evaluator, + privilegesConfiguration, + roleMapper, dlsFlsValve::getCurrentConfig, dlsFlsBaseContext ) @@ -1134,15 +1149,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); @@ -1159,25 +1170,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); + tokenManager = new SecurityTokenManager(cs, threadPool, userService, roleMapper); final CompatConfig compatConfig = new CompatConfig(environment, transportPassiveAuthSetting); rsIndexHandler = new ResourceSharingIndexHandler(localClient, threadPool, resourcePluginInfo); - evaluator = new PrivilegesEvaluator( + RoleMapper roleMapper = new RolesInjector.InjectedRoleMapper( + new ConfigurableRoleMapper(cr, settings), + threadPool.getThreadContext() + ); + this.roleMapper = roleMapper; + PrivilegesConfiguration privilegesConfiguration = new PrivilegesConfiguration( + cr, clusterService, clusterService::state, + localClient, + roleMapper, threadPool, - threadPool.getThreadContext(), - cr, resolver, auditLog, settings, - privilegesInterceptor, - cih + 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(); @@ -1222,7 +1239,8 @@ public Collection createComponents( sf = new SecurityFilter( settings, - evaluator, + privilegesConfiguration, + roleMapper, adminDns, dlsFlsValve, auditLog, @@ -1242,7 +1260,7 @@ public Collection createComponents( principalExtractor = ReflectionHelper.instantiatePrincipalExtractor(principalExtractorClass); } - restLayerEvaluator = new RestLayerPrivilegesEvaluator(evaluator); + restLayerEvaluator = new RestLayerPrivilegesEvaluator(privilegesConfiguration); securityRestHandler = new SecurityRestFilter( backendRegistry, @@ -1258,7 +1276,6 @@ public Collection createComponents( dcf.registerDCFListener(backendRegistry); dcf.registerDCFListener(compatConfig); dcf.registerDCFListener(xffResolver); - dcf.registerDCFListener(evaluator); dcf.registerDCFListener(securityRestHandler); dcf.registerDCFListener(tokenManager); if (!(auditLog instanceof NullAuditLog)) { @@ -1298,7 +1315,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); @@ -2396,7 +2413,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 72a8e5f49f..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,7 @@ 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; @@ -131,7 +132,7 @@ public static boolean handle( private final ActionRequest request; private final ActionListener listener; private final IndexToRuleMap dlsRestrictionMap; - private final ResolvedIndices 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.local().isAll() || resolved.local().names().size() != 1; + this.requiresIndexScoping = resolved instanceof ResolvedIndices resolvedIndices + ? resolvedIndices.local().names().size() != 1 + : true; } private boolean handle() { diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 89ab3d327a..9c9586e6d7 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -33,13 +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.ResolvedIndices; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; @@ -49,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; @@ -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"; @@ -153,7 +155,7 @@ 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()) && !MultiGetAction.NAME.equals(context.getAction())) { return true; } if (userSubject != null && adminDNs.isAdmin(userSubject.getUser())) { @@ -187,7 +189,7 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< return true; } DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get(); - ResolvedIndices resolved = context.getResolvedRequest(); + OptionallyResolvedIndices resolved = context.getResolvedRequest(); try { boolean hasDlsRestrictions = !config.getDocumentPrivileges().isUnrestricted(context, resolved); @@ -537,6 +539,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 118d1c9ff2..ee118cc704 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.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 ResolvedIndices requestedResolved, - final PrivilegesEvaluationContext context, - final TenantPrivileges tenantPrivileges + final OptionallyResolvedIndices optionallyResolvedIndices, + 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; } + 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.local().isAll()) { - final Set indices = requestedResolved.local().names(); + 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); } } @@ -394,9 +409,6 @@ private String toUserIndexName(final String originalDashboardsIndex, final Strin } private static boolean resolveToDashboardsIndexOrAlias(final ResolvedIndices requestedResolved, final String dashboardsIndexName) { - if (requestedResolved.local().isAll()) { - return false; - } 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 1ff295cd94..e7f12be80c 100644 --- a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java @@ -41,17 +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.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..5203195bdc 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,8 +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.resources.ResourcePluginInfo; +import org.opensearch.security.privileges.PrivilegesConfiguration; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.resources.ResourceSharingIndexHandler; import org.opensearch.security.resources.api.migrate.MigrateResourceSharingInfoApiAction; import org.opensearch.security.ssl.SslSettingsManager; @@ -49,7 +49,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,64 +63,56 @@ 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) ), auditLog, settings ); - List handler = new ArrayList<>( - List.of( - new InternalUsersApiAction(clusterService, threadPool, userService, securityApiDependencies, passwordHasher), - new RolesMappingApiAction(clusterService, threadPool, securityApiDependencies), - new RolesApiAction(clusterService, threadPool, securityApiDependencies), - new ActionGroupsApiAction(clusterService, threadPool, securityApiDependencies), - new FlushCacheApiAction(clusterService, threadPool, securityApiDependencies), - new SecurityConfigApiAction(clusterService, threadPool, securityApiDependencies), - // FIXME Change inheritance for PermissionsInfoAction - new PermissionsInfoAction( - settings, - configPath, - controller, - client, - adminDns, - configurationRepository, - clusterService, - principalExtractor, - evaluator, - threadPool, - auditLog - ), - new AuthTokenProcessorAction(clusterService, threadPool, securityApiDependencies), - new TenantsApiAction(clusterService, threadPool, securityApiDependencies), - new AccountApiAction(clusterService, threadPool, securityApiDependencies, passwordHasher), - new NodesDnApiAction(clusterService, threadPool, securityApiDependencies), - new AllowlistApiAction(Endpoint.ALLOWLIST, clusterService, threadPool, securityApiDependencies), - new AuditApiAction(clusterService, threadPool, securityApiDependencies), - new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies), - new RateLimitersApiAction(clusterService, threadPool, securityApiDependencies), - new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies), - new SecuritySSLCertsApiAction( - clusterService, - threadPool, - sslSettingsManager, - certificatesReloadEnabled, - securityApiDependencies - ), - new CertificatesApiAction(clusterService, threadPool, securityApiDependencies), - new MigrateResourceSharingInfoApiAction( - clusterService, - threadPool, - securityApiDependencies, - resourceSharingIndexHandler, - resourcePluginInfo - ) - ) + return List.of( + new InternalUsersApiAction(clusterService, threadPool, userService, securityApiDependencies, passwordHasher), + new RolesMappingApiAction(clusterService, threadPool, securityApiDependencies), + new RolesApiAction(clusterService, threadPool, securityApiDependencies), + new ActionGroupsApiAction(clusterService, threadPool, securityApiDependencies), + new FlushCacheApiAction(clusterService, threadPool, securityApiDependencies), + new SecurityConfigApiAction(clusterService, threadPool, securityApiDependencies), + // FIXME Change inheritance for PermissionsInfoAction + new PermissionsInfoAction( + settings, + configPath, + controller, + client, + adminDns, + configurationRepository, + clusterService, + principalExtractor, + roleMapper, + threadPool, + auditLog + ), + new AuthTokenProcessorAction(clusterService, threadPool, securityApiDependencies), + new TenantsApiAction(clusterService, threadPool, securityApiDependencies), + new AccountApiAction(clusterService, threadPool, securityApiDependencies, passwordHasher), + new NodesDnApiAction(clusterService, threadPool, securityApiDependencies), + new AllowlistApiAction(Endpoint.ALLOWLIST, clusterService, threadPool, securityApiDependencies), + new AuditApiAction(clusterService, threadPool, securityApiDependencies), + new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies), + new RateLimitersApiAction(clusterService, threadPool, securityApiDependencies), + new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies), + new SecuritySSLCertsApiAction( + clusterService, + threadPool, + sslSettingsManager, + certificatesReloadEnabled, + securityApiDependencies + ), + new CertificatesApiAction(clusterService, threadPool, securityApiDependencies), + new MigrateResourceSharingInfoApiAction(clusterService, threadPool, securityApiDependencies, resourceSharingIndexHandler) ); if (SecurityConfigVersionHandler.isVersionIndexEnabled(settings)) { diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index 9bc69d4207..237f9d73b7 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -27,7 +27,6 @@ package org.opensearch.security.filter; import java.util.Collections; -import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.function.Consumer; @@ -64,6 +63,7 @@ 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; @@ -88,16 +88,19 @@ 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.resources.ResourceAccessHandler; 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; @@ -109,7 +112,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; @@ -122,10 +126,12 @@ public class SecurityFilter implements ActionFilter { 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, @@ -136,7 +142,8 @@ public SecurityFilter( final XFFResolver xffResolver, ResourceAccessEvaluator resourceAccessEvaluator ) { - this.evalp = evalp; + this.privilegesConfiguration = privilegesConfiguration; + this.roleMapper = roleMapper; this.adminDns = adminDns; this.dlsFlsValve = dlsFlsValve; this.auditLog = auditLog; @@ -150,7 +157,8 @@ public SecurityFilter( ); this.rolesInjector = new RolesInjector(auditLog); this.userInjector = new UserInjector(settings, threadPool, auditLog, xffResolver); - this.resourceAccessEvaluator = resourceAccessEvaluator; + this.resourceAccessEvaluator = new ResourceAccessEvaluator(resourceIndices, settings, resourceAccessHandler); + this.threadContextUserInfo = new ThreadContextUserInfo(threadPool.getThreadContext(), privilegesConfiguration, settings); log.info("{} indices are made immutable.", immutableIndicesMatcher); } @@ -201,7 +209,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(); @@ -329,7 +337,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); @@ -381,25 +388,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, actionRequestMetadata, 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); @@ -407,13 +403,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)); @@ -592,14 +582,11 @@ private boolean checkImmutableIndices(Object request, ActionRequestMetadata actionRequestMetadata) { - Optional optionalResolvedIndices = actionRequestMetadata.resolvedIndices(); - if (!optionalResolvedIndices.isPresent()) { - return true; - } - ResolvedIndices resolvedIndices = optionalResolvedIndices.get(); - if (resolvedIndices.local().isAll()) { + OptionallyResolvedIndices optionalResolvedIndices = actionRequestMetadata.resolvedIndices(); + if (optionalResolvedIndices instanceof ResolvedIndices resolvedIndices) { + return immutableIndicesMatcher.matchAny(resolvedIndices.local().namesOfIndices(cs.state())); + } else { return true; } - return immutableIndicesMatcher.matchAny(resolvedIndices.local().names()); } } 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 c47d9a8450..75c1693c2c 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.cluster.metadata.ResolvedIndices; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; /** * Defines the general interface for evaluating privileges on actions. References to ActionPrivileges instances @@ -77,7 +77,7 @@ public interface ActionPrivileges { PrivilegesEvaluatorResponse hasIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - ResolvedIndices resolvedIndices + OptionallyResolvedIndices resolvedIndices ); /** @@ -90,7 +90,7 @@ PrivilegesEvaluatorResponse hasIndexPrivilege( PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - ResolvedIndices resolvedIndices + OptionallyResolvedIndices resolvedIndices ); ActionPrivileges EMPTY = new ActionPrivileges() { @@ -113,7 +113,7 @@ public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluat public PrivilegesEvaluatorResponse hasIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - ResolvedIndices resolvedIndices + OptionallyResolvedIndices resolvedIndices ) { return PrivilegesEvaluatorResponse.insufficient("all of " + actions).reason("User has no privileges"); } @@ -122,7 +122,7 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - ResolvedIndices 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/IndexPattern.java b/src/main/java/org/opensearch/security/privileges/IndexPattern.java index 014af99e54..d8c6dd0899 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; @@ -61,9 +62,53 @@ private IndexPattern(WildcardMatcher staticPattern, ImmutableList patter this.hashCode = staticPattern.hashCode() + patternTemplates.hashCode() + dateMathExpressions.hashCode(); } - public boolean matches(String index, PrivilegesEvaluationContext context, Map indexMetadata) + 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 { + // 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; + } + } + + 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 +117,7 @@ public boolean matches(String index, PrivilegesEvaluationContext context, Map actionRequestMetadata, Supplier clusterStateSupplier ) { - Optional providedIndices = actionRequestMetadata.resolvedIndices(); - if (providedIndices.isPresent()) { - return providedIndices.get(); + 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 ResolvedIndices resolve( + public OptionallyResolvedIndices resolve( ActionRequest request, ActionRequestMetadata actionRequestMetadata, PrivilegesEvaluationContext context @@ -49,11 +49,11 @@ public ResolvedIndices resolve( return resolve(request, actionRequestMetadata, context::clusterState); } - private ResolvedIndices resolveFallback(ActionRequest request, ClusterState clusterState) { + private OptionallyResolvedIndices resolveFallback(ActionRequest request, ClusterState clusterState) { if (request instanceof IndicesRequest indicesRequest) { - return ResolvedIndices.of(this.indexNameExpressionResolver.concreteIndexNames(clusterState, indicesRequest)); + return ResolvedIndices.of(this.indexNameExpressionResolver.concreteResolvedIndices(clusterState, indicesRequest)); } else { - return ResolvedIndices.all(); + return ResolvedIndices.unknown(); } } 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..45d64e4933 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java @@ -0,0 +1,272 @@ +/* + * 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 = currentPrivilegesEvaluator == null ? null + : currentPrivilegesEvaluator instanceof org.opensearch.security.privileges.actionlevel.legacy.PrivilegesEvaluator + ? PrivilegesEvaluationType.LEGACY + : PrivilegesEvaluationType.NEXT_GEN; + + 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( + clusterService, + clusterStateSupplier, + roleMapper, + threadPool, + threadPool.getThreadContext(), + resolver, + auditLog, + 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); + } + + /** + * 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 (config.dynamic.privilegesEvaluationType.equalsIgnoreCase(NEXT_GEN.name())) { + return NEXT_GEN; + } else { + return LEGACY; + } + } + } + + 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 0838199bef..a475a26f4c 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -21,7 +21,7 @@ import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.metadata.ResolvedIndices; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; @@ -39,7 +39,7 @@ public class PrivilegesEvaluationContext { private final User user; private final String action; private final ActionRequest request; - private ResolvedIndices resolvedIndices; + private OptionallyResolvedIndices resolvedIndices; private Map indicesLookup; private final Task task; private ImmutableSet mappedRoles; @@ -122,14 +122,14 @@ public ActionRequest getRequest() { return request; } - public ResolvedIndices getResolvedRequest() { - if (PrivilegesEvaluator.isClusterPerm(action)) { - return ResolvedIndices.all(); - } - - ResolvedIndices result = this.resolvedIndices; + public OptionallyResolvedIndices getResolvedRequest() { + OptionallyResolvedIndices result = this.resolvedIndices; if (result == null) { - result = this.indicesRequestResolver.resolve(this.request, this.actionRequestMetadata, this.clusterStateSupplier); + this.resolvedIndices = result = this.indicesRequestResolver.resolve( + this.request, + this.actionRequestMetadata, + this.clusterStateSupplier + ); } return result; @@ -143,18 +143,6 @@ 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 ClusterState clusterState() { return clusterStateSupplier.get(); } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index f45a909c25..7024293462 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -26,899 +26,121 @@ 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.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.ResolvedIndices; 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.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 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"; - - 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); +public interface PrivilegesEvaluator { - 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 SnapshotRestoreEvaluator snapshotRestoreEvaluator; - private final SystemIndexAccessEvaluator systemIndexAccessEvaluator; - private final ProtectedIndexAccessEvaluator protectedIndexAccessEvaluator; - private final TermsAggregationEvaluator termsAggregationEvaluator; - 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<>(); - private final IndicesRequestResolver indicesRequestResolver; - private final IndicesRequestModifier indicesRequestModifier = new IndicesRequestModifier(); - - /** - * 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 - ) { - - 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; - snapshotRestoreEvaluator = new SnapshotRestoreEvaluator(settings, auditLog); - systemIndexAccessEvaluator = new SystemIndexAccessEvaluator(settings, auditLog); - protectedIndexAccessEvaluator = new ProtectedIndexAccessEvaluator(settings, auditLog); - termsAggregationEvaluator = new TermsAggregationEvaluator(); - this.configurationRepository = configurationRepository; - this.indicesRequestResolver = new IndicesRequestResolver(resolver); - - 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); - - 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()); - } + default PrivilegesEvaluationContext createContext(User user, String action) { + return createContext(user, action, null, ActionRequestMetadata.empty(), null); } - void updateConfiguration( - SecurityDynamicConfiguration actionGroupsConfiguration, - 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, ActionRequestMetadata.empty(), 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( + PrivilegesEvaluationContext createContext( User user, - String action0, - ActionRequest request, + String action, + ActionRequest actionRequest, ActionRequestMetadata actionRequestMetadata, - 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, - actionRequestMetadata, - task, - resolver, - indicesRequestResolver, - 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; - } - - ResolvedIndices resolvedIndices = context.getResolvedRequest(); - - if (isDebugEnabled) { - log.debug("RequestedResolved : {}", resolvedIndices); - } - - // 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, resolvedIndices, presponse, context, actionPrivileges, user) - .isComplete()) { - return presponse; - } - - // Protected index access - if (protectedIndexAccessEvaluator.evaluate(request, task, action0, resolvedIndices, presponse, mappedRoles).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, - resolvedIndices, - 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, - resolvedIndices, - 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(resolvedIndices, 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: {}", resolvedIndices); - log.debug("Security roles: {}", mappedRoles); - } - - // TODO exclude Security index - - if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { - - final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( - request, - action0, - user, - dcm, - resolvedIndices, - 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, resolvedIndices); - - if (presponse.isPartiallyOk()) { - if (dnfofPossible) { - if (this.indicesRequestModifier.setLocalIndices(request, resolvedIndices, 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(resolvedIndices, 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, - resolvedIndices, - 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(); - } - - public String dashboardsServerUsername() { - return dcm.getDashboardsServerUsername(); - } - - public String dashboardsOpenSearchRole() { - return dcm.getDashboardsOpenSearchRole(); - } - - public List getSignInOptions() { - return dcm.getSignInOptions(); - } - - private ImmutableSet evaluateAdditionalIndexPermissions(final ActionRequest request, final String originalAction) { - ImmutableSet.Builder additionalPermissionsRequired = ImmutableSet.builder(); + Task task + ); - if (!isClusterPerm(originalAction)) { - additionalPermissionsRequired.add(originalAction); - } + PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context); - if (request instanceof ClusterSearchShardsRequest) { - additionalPermissionsRequired.add(SearchAction.NAME); - } + boolean isClusterPermission(String action); - 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; - } - } - } + void updateConfiguration( + FlattenedActionGroups actionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration + ); - 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; - } - } - } + void updateClusterStateMetadata(ClusterService clusterService); - if (request instanceof CreateIndexRequest) { - CreateIndexRequest cir = (CreateIndexRequest) request; - if (cir.aliases() != null && !cir.aliases().isEmpty()) { - additionalPermissionsRequired.add(IndicesAliasesAction.NAME); - } - } + /** + * Shuts down any background processes or other resources that need an explicit shut down + */ + void shutdown(); - if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { - additionalPermissionsRequired.addAll(ConfigConstants.SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES); - } + boolean notFailOnForbiddenEnabled(); - ImmutableSet result = additionalPermissionsRequired.build(); + /** + * A PrivilegesEvaluator implementation that just throws "not initialized" exceptions. + * Used initially by PrivilegesConfiguration. + */ + class NotInitialized implements PrivilegesEvaluator { + private final Supplier unavailablityReasonSupplier; - if (result.size() > 1) { - traceAction("Additional permissions required: {}", result); + NotInitialized(Supplier unavailablityReasonSupplier) { + this.unavailablityReasonSupplier = unavailablityReasonSupplier; } - if (log.isDebugEnabled() && result.size() > 1) { - log.debug("Additional permissions required: {}", result); + @Override + public PrivilegesEvaluationContext createContext( + User user, + String action, + ActionRequest actionRequest, + ActionRequestMetadata actionRequestMetadata, + Task task + ) { + throw exception(); } - 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(ResolvedIndices requestedResolved, String action, boolean isDebugEnabled) { - final String faMode = dcm.getFilteredAliasMode();// getConfigSettings().dynamic.filtered_alias_mode; - - if (!"disallow".equals(faMode)) { - return false; + @Override + public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { + throw exception(); } - if (!ACTION_MATCHER.test(action)) { + @Override + public boolean isClusterPermission(String action) { 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; - } + @Override + public void updateConfiguration( + FlattenedActionGroups actionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration + ) { - indexMetaDataSet.add(indexMetaData); } - indexMetaDataCollection = indexMetaDataSet; - - // check filtered aliases - for (IndexMetadata indexMetaData : indexMetaDataCollection) { - - final List filteredAliases = new ArrayList(); - - final Map aliases = indexMetaData.getAliases(); + @Override + public void updateClusterStateMetadata(ClusterService clusterService) { - 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); + @Override + public void shutdown() { - if (docAllowListHeader == null) { - return false; } - if (!(request instanceof GetRequest)) { + @Override + public boolean notFailOnForbiddenEnabled() { return false; } - try { - DocumentAllowList documentAllowList = DocumentAllowList.parse(docAllowListHeader); - GetRequest getRequest = (GetRequest) request; + private OpenSearchSecurityException exception() { + StringBuilder error = new StringBuilder("OpenSearch Security is not initialized"); + String reason = this.unavailablityReasonSupplier.get(); - if (documentAllowList.isAllowed(getRequest.index(), getRequest.id())) { - if (log.isDebugEnabled()) { - log.debug("Request " + request + " is allowed by " + documentAllowList); - } - - 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; - } - } - - private List toString(List aliases) { - if (aliases == null || aliases.size() == 0) { - return Collections.emptyList(); + return new OpenSearchSecurityException(error.toString(), RestStatus.SERVICE_UNAVAILABLE); } + }; - 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..adba2a014b 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java @@ -176,6 +176,13 @@ public static PrivilegesEvaluatorResponse ok() { 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 1ef32fe7c3..e56414757b 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java @@ -29,10 +29,9 @@ import org.opensearch.action.ActionRequest; import org.opensearch.action.admin.indices.create.CreateIndexRequestBuilder; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.metadata.ResolvedIndices; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; @@ -40,9 +39,9 @@ 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; @@ -80,10 +79,8 @@ public ReplaceResult replaceDashboardsIndex( final ActionRequest request, final String action, final User user, - final DynamicConfigModel config, - final ResolvedIndices requestedResolved, - final PrivilegesEvaluationContext context, - final TenantPrivileges tenantPrivileges + final OptionallyResolvedIndices requestedResolved, + final PrivilegesEvaluationContext context ) { throw new RuntimeException("not implemented"); } 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..dd791f71ae --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/SpecialIndices.java @@ -0,0 +1,34 @@ +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 fe2726d91b..0000000000 --- a/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java +++ /dev/null @@ -1,121 +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.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.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; - -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 ResolvedIndices 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, - ResolvedIndices.all() - ); - - if (subResponse.isPartiallyOk()) { - sr.source() - .query( - new TermsQueryBuilder( - "_index", - Streams.concat( - subResponse.getAvailableIndices().stream(), - resolved.remote().asRawExpressions().stream() - ).toArray(String[]::new) - ) - ); - } 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 7a5914fcfc..e1c49c2728 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,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; import java.util.stream.Collectors; import com.google.common.collect.ImmutableMap; @@ -96,8 +97,22 @@ 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. + */ + public RoleBasedActionPrivileges( + SecurityDynamicConfiguration roles, + FlattenedActionGroups actionGroups, + SpecialIndexProtection specialIndexProtection, + Settings settings + ) { + super(new ClusterPrivileges(roles, actionGroups), new IndexPrivileges(roles, actionGroups, specialIndexProtection)); this.roles = roles; this.actionGroups = actionGroups; this.statefulIndexMaxHeapSize = PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.get(settings); @@ -120,7 +135,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(); @@ -347,7 +362,12 @@ 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 + ) { + super(specialIndexProtection); Map> rolesToActionToIndexPattern = new HashMap<>(); Map> rolesToActionPatternToIndexPattern = new HashMap<>(); @@ -491,7 +511,7 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde * checkTable instance as checked. */ @Override - protected PrivilegesEvaluatorResponse providesPrivilege( + protected IntermediateResult providesPrivilege( PrivilegesEvaluationContext context, Set actions, CheckTable checkTable @@ -503,7 +523,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); } } } @@ -521,36 +541,44 @@ 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 responseForIncompletePrivileges(context, checkTable, exceptions); + return new IntermediateResult(checkTable).evaluationExceptions(exceptions); } /** - * 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; + } } /** @@ -597,6 +625,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; + } } /** @@ -801,7 +863,7 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St * @return PrivilegesEvaluatorResponse.ok() or null. */ @Override - protected PrivilegesEvaluatorResponse providesPrivilege( + protected IntermediateResult providesPrivilege( Set actions, PrivilegesEvaluationContext context, CheckTable checkTable @@ -828,7 +890,7 @@ protected PrivilegesEvaluatorResponse providesPrivilege( if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(effectiveRoles)) { if (checkTable.check(index, action)) { - return PrivilegesEvaluatorResponse.ok(); + return new IntermediateResult(checkTable); } } } @@ -866,31 +928,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.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 b89257b5ba..5c88405e5f 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,13 @@ package org.opensearch.security.privileges.actionlevel; +import java.util.ArrayList; 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 +25,13 @@ import org.apache.logging.log4j.Logger; import org.opensearch.cluster.metadata.IndexAbstraction; -import org.opensearch.cluster.metadata.ResolvedIndices; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; 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.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import com.selectivem.collections.CheckTable; @@ -84,13 +88,8 @@ public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluat public PrivilegesEvaluatorResponse hasIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - ResolvedIndices resolvedIndices + OptionallyResolvedIndices resolvedIndices ) { - PrivilegesEvaluatorResponse response = this.index.checkWildcardIndexPrivilegesOnWellKnownActions(context, actions); - if (response != null) { - return response; - } - 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. @@ -100,17 +99,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(fullyResolvedIndices(context, resolvedIndices), 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, 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. @@ -118,7 +121,8 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( // We can carry on using this as an intermediate result and further complete checkTable below. } - return this.index.providesPrivilege(context, actions, checkTable); + IntermediateResult resultFromStaticIndex = this.index.providesPrivilege(context, actions, checkTable); + return this.index.finalizeResult(context, resultFromStaticIndex); } /** @@ -132,13 +136,13 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - ResolvedIndices resolvedIndices + OptionallyResolvedIndices resolvedIndices ) { if (!CollectionUtils.containsAny(actions, WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS)) { return PrivilegesEvaluatorResponse.insufficient(CheckTable.create(ImmutableSet.of("_"), actions)); } - CheckTable checkTable = CheckTable.create(fullyResolvedIndices(context, resolvedIndices), actions); + CheckTable checkTable = CheckTable.create(resolvedIndices.local().names(context.clusterState()), actions); return this.index.providesExplicitPrivilege(context, actions, checkTable); } @@ -263,6 +267,13 @@ PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext con * 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, @@ -280,7 +291,7 @@ protected abstract static class StaticIndexPrivileges { * 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 @@ -299,6 +310,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. @@ -306,9 +324,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 ); /** @@ -395,20 +414,25 @@ 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.
    • - *
    - */ - protected PrivilegesEvaluatorResponse responseForIncompletePrivileges( - PrivilegesEvaluationContext context, - 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) { + // TODO aliases + checkTable.uncheckIf( + index -> this.indicesNeedingSystemIndexPrivileges.test(index) + && !providesExplicitPrivilege(context, index, ConfigConstants.SYSTEM_INDEX_PERMISSION, exceptions), + checkTable.getColumns() + ); + } + + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(); + } + Set availableIndices = checkTable.getCompleteRows(); if (!availableIndices.isEmpty()) { @@ -425,6 +449,7 @@ protected PrivilegesEvaluatorResponse responseForIncompletePrivileges( } return PrivilegesEvaluatorResponse.insufficient(checkTable).reason(reason).evaluationExceptions(exceptions); + } } @@ -451,18 +476,60 @@ protected abstract static class StatefulIndexPrivileges { * @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, PrivilegesEvaluationContext context, CheckTable checkTable ); } - private Set fullyResolvedIndices(PrivilegesEvaluationContext context, ResolvedIndices resolvedIndices) { - if (resolvedIndices.local().isAll()) { - return context.getIndicesLookup().keySet(); - } else { - return resolvedIndices.local().names(); + 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 155af1e9e2..8bf4e8d3a6 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java @@ -58,8 +58,11 @@ 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) { + super( + new ClusterPrivileges(actionGroups.resolve(role.getCluster_permissions())), + new IndexPrivileges(role, actionGroups, specialIndexProtection) + ); } /** @@ -214,7 +217,8 @@ 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) { + super(specialIndexProtection); Map actionToIndexPattern = new HashMap<>(); Map actionPatternToIndexPattern = new HashMap<>(); @@ -301,7 +305,7 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde * checkTable instance as checked. */ @Override - protected PrivilegesEvaluatorResponse providesPrivilege( + protected IntermediateResult providesPrivilege( PrivilegesEvaluationContext context, Set actions, CheckTable checkTable @@ -310,7 +314,7 @@ protected PrivilegesEvaluatorResponse providesPrivilege( 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, @@ -320,11 +324,11 @@ 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 responseForIncompletePrivileges(context, checkTable, exceptions); + return new IntermediateResult(checkTable).evaluationExceptions(exceptions); } /** @@ -333,9 +337,10 @@ 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)) { @@ -343,7 +348,7 @@ protected PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownAct } } - return PrivilegesEvaluatorResponse.ok(); + return new IntermediateResult(checkTable); } /** @@ -384,6 +389,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/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java new file mode 100644 index 0000000000..ae3c60a01e --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java @@ -0,0 +1,800 @@ +/* + * 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.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.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 + ); + + snapshotRestoreEvaluator = new SnapshotRestoreEvaluator( + settings, + auditLog, + clusterService != null ? () -> clusterService.state().nodes().isLocalNodeElectedClusterManager() : () -> false + ); + systemIndexAccessEvaluator = new SystemIndexAccessEvaluator(settings, auditLog); + protectedIndexAccessEvaluator = new ProtectedIndexAccessEvaluator(settings, auditLog); + termsAggregationEvaluator = new TermsAggregationEvaluator(); + this.indicesRequestResolver = new IndicesRequestResolver(resolver); + + 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 + ); + 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 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) { + 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; + } + + 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, + optionallyResolvedIndices, + 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 (checkDocAllowListHeader(user, action0, request)) { + 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, + optionallyResolvedIndices, + 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); + + 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 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, + 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 boolean checkDocAllowListHeader(User user, String action, ActionRequest request) { + 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 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 + ) + ); + } + + 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 84% 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 506898c4bf..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.ResolvedIndices; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; import org.opensearch.common.settings.Settings; import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.tasks.Task; @@ -71,24 +72,22 @@ public PrivilegesEvaluatorResponse evaluate( final ActionRequest request, final Task task, final String action, - final ResolvedIndices requestedResolved, - final PrivilegesEvaluatorResponse presponse, + final OptionallyResolvedIndices requestedResolved, final Set mappedRoles ) { if (!protectedIndexEnabled) { - return presponse; + return null; } - if (indexMatcher.matchAny(requestedResolved.local().names()) - && 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 '{}' index/indices is not allowed for a regular user", action, indexMatcher); - presponse.allowed = false; - return presponse.markComplete(); + return PrivilegesEvaluatorResponse.insufficient(action); } - if (indexMatcher.matchAny(requestedResolved.local().names()) && !allowedRolesMatcher.matchAny(mappedRoles)) { - + if (containsProtectedIndex && !allowedRolesMatcher.matchAny(mappedRoles)) { final boolean isDebugEnabled = log.isDebugEnabled(); if (request instanceof SearchRequest) { ((SearchRequest) request).requestCache(Boolean.FALSE); @@ -104,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 66% 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 5a5cd193fe..e851891e20 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; @@ -39,16 +40,21 @@ 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.cluster.metadata.ResolvedIndices; import org.opensearch.common.settings.Settings; import org.opensearch.indices.SystemIndexRegistry; import org.opensearch.security.auditlog.AuditLog; +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 @@ -123,43 +129,44 @@ public PrivilegesEvaluatorResponse evaluate( final ActionRequest request, final Task task, final String action, - final ResolvedIndices requestedResolved, - final PrivilegesEvaluatorResponse presponse, + final OptionallyResolvedIndices requestedResolved, final PrivilegesEvaluationContext context, final ActionPrivileges actionPrivileges, final User user ) { - boolean containsSystemIndex = false; // TODO requestedResolved.local().containsAny(this::isSystemIndex); + boolean containsSystemIndex = requestedResolved.local().containsAny(this::isSystemIndex); - evaluateSystemIndicesAccess( + PrivilegesEvaluatorResponse response = evaluateSystemIndicesAccess( action, requestedResolved, request, task, - presponse, context, actionPrivileges, user, containsSystemIndex ); - if (containsSystemIndex) { + if (response == null || response.isAllowed()) { + if (containsSystemIndex) { - if (request instanceof SearchRequest) { - ((SearchRequest) request).requestCache(Boolean.FALSE); - if (log.isDebugEnabled()) { - log.debug("Disable search request cache for this request"); + if (request instanceof SearchRequest) { + ((SearchRequest) request).requestCache(Boolean.FALSE); + if (log.isDebugEnabled()) { + log.debug("Disable search request cache for this request"); + } } - } - if (request instanceof RealtimeRequest) { - ((RealtimeRequest) request).realtime(Boolean.FALSE); - if (log.isDebugEnabled()) { - log.debug("Disable realtime for this request"); + if (request instanceof RealtimeRequest) { + ((RealtimeRequest) request).realtime(Boolean.FALSE); + if (log.isDebugEnabled()) { + log.debug("Disable realtime for this request"); + } } } } - return presponse; + + return response; } private boolean isSystemIndex(String index) { @@ -168,8 +175,7 @@ private boolean isSystemIndex(String index) { } if (this.isSystemIndexEnabled) { - // TODO Simplify SystemIndexRegistry.matchesSystemIndexPattern() call - return this.systemIndexMatcher.test(index) || !SystemIndexRegistry.matchesSystemIndexPattern(Set.of(index)).isEmpty(); + return this.systemIndexMatcher.test(index) || SystemIndexRegistry.matchesSystemIndexPattern(index); } else { return false; } @@ -190,17 +196,15 @@ 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 ResolvedIndices requestedResolved, + final OptionallyResolvedIndices requestedResolved, final ActionRequest request, final Task task, - final PrivilegesEvaluatorResponse presponse, final PrivilegesEvaluationContext context, final ActionPrivileges actionPrivileges, final User user, @@ -209,21 +213,21 @@ private void evaluateSystemIndicesAccess( boolean serviceAccountUser = user.isServiceAccount(); if (isSystemIndexPermissionEnabled) { - if (serviceAccountUser && requestedResolved.local().containsAny(index -> !isSystemIndex(index))) { + 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.local() - .names() + .names(context.clusterState()) .stream() .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 = requestedResolved.local().containsAny(this.securityIndex::equals); if (containsProtectedIndex) { @@ -236,9 +240,7 @@ private void evaluateSystemIndicesAccess( 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); @@ -247,97 +249,125 @@ private void evaluateSystemIndicesAccess( "No {} permission for user roles {} to System Indices {}", action, context.getMappedRoles(), - requestedResolved.local().names().stream().filter(this::isSystemIndex).collect(Collectors.joining(", ")) + 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( - user.getName().replace("plugin:", ""), - requestedResolved.local().names() + PluginSystemIndexSelection pluginSystemIndexSelection = areIndicesPluginSystemIndices( + context, + user.getName().replace("plugin:", ""), + requestedResolved ); - if (requestedResolved.local().names().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.local().names()); - 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); + return; } } else { // no system index protection and request originating from plugin, allow - presponse.allowed = true; - presponse.markComplete(); - return; + return PrivilegesEvaluatorResponse.ok(); } } if (isActionAllowed(action)) { - // TODO requestedResolved.isLocalAll() - if (false) { + if (!(requestedResolved instanceof ResolvedIndices resolvedIndices)) { if (filterSecurityIndex) { // 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.local().names()); + 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); } - this.indicesRequestModifier.setLocalIndices(request, requestedResolved, allWithoutSecurity); + 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 = requestedResolved.local() - .names() + .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; + } + } + + 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/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/PrivilegesEvaluator.java new file mode 100644 index 0000000000..625b3c85a6 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/PrivilegesEvaluator.java @@ -0,0 +1,613 @@ +/* + * 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 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.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.ActionRequestMetadata; +import org.opensearch.action.termvectors.MultiTermVectorsAction; +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.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.privileges.actionlevel.legacy.ProtectedIndexAccessEvaluator; +import org.opensearch.security.privileges.actionlevel.legacy.SnapshotRestoreEvaluator; +import org.opensearch.security.privileges.actionlevel.legacy.SystemIndexAccessEvaluator; +import org.opensearch.security.privileges.actionlevel.legacy.TermsAggregationEvaluator; +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.user.User; +import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; + +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 resolver; + private final AuditLog auditLog; + private final ThreadContext threadContext; + private final PrivilegesInterceptor privilegesInterceptor; + private final boolean checkSnapshotRestoreWritePrivileges; + private final SnapshotRestoreEvaluator snapshotRestoreEvaluator; + 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 final RuntimeOptimizedActionPrivileges.SpecialIndexProtection specialIndexProtection; + + + public PrivilegesEvaluator( + ClusterService clusterService, + Supplier clusterStateSupplier, + RoleMapper roleMapper, + ThreadPool threadPool, + ThreadContext threadContext, + IndexNameExpressionResolver resolver, + AuditLog auditLog, + Settings settings, + PrivilegesInterceptor privilegesInterceptor, + FlattenedActionGroups actionGroups, + FlattenedActionGroups staticActionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration, + Map pluginIdToRolePrivileges, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection specialIndexProtection + ) { + + 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.specialIndexProtection = specialIndexProtection; + + this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean( + ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, + ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES + ); + + snapshotRestoreEvaluator = new SnapshotRestoreEvaluator( + settings, + auditLog, + clusterService != null ? () -> clusterService.state().nodes().isLocalNodeElectedClusterManager() : () -> false + ); + + termsAggregationEvaluator = new TermsAggregationEvaluator(); + this.indicesRequestResolver = new IndicesRequestResolver(resolver); + + this.pluginIdToActionPrivileges.putAll(createActionPrivileges(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 + ); + 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 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; + } + + 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. + + PrivilegesEvaluatorResponse 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; + } + + { + PrivilegesEvaluatorResponse presponse = snapshotRestoreEvaluator.evaluate(request, task, action0); + if (presponse != null) { + return presponse; + } + } + + OptionallyResolvedIndices optionallyResolvedIndices = context.getResolvedRequest(); + + if (isClusterPermission(action0)) { + if (user.isServiceAccount()) { + log.info("{} is a service account which doesn't have access to cluster level permission: {}", user, action0); + return PrivilegesEvaluatorResponse.insufficient(action0); + } + + PrivilegesEvaluatorResponse 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 { + + PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action0, + user, + optionallyResolvedIndices, + 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 (checkDocAllowListHeader(user, action0, request)) { + return PrivilegesEvaluatorResponse.ok(); + } + + { + PrivilegesEvaluatorResponse presponse = termsAggregationEvaluator.evaluate(optionallyResolvedIndices, request, context, actionPrivileges); + if (presponse != null) { + return presponse; + } + } + + ImmutableSet allIndexPermsRequired = evaluateAdditionalIndexPermissions(request, action0); + + final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action0, + user, + optionallyResolvedIndices, + 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); + } + } + + + PrivilegesEvaluatorResponse presponse = actionPrivileges.hasIndexPrivilege(context, allIndexPermsRequired, optionallyResolvedIndices); + + if (presponse.isPartiallyOk()) { + if (isIndexReductionForIncompletePrivilegesPossible(request) && optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices) { + if (this.indicesRequestModifier.setLocalIndices(request, resolvedIndices, presponse.getAvailableIndices())) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } else if (!presponse.isAllowed()) { + if (isIndexReductionForNoPrivilegesPossible(request) && optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices) { + indicesRequestModifier.setLocalIndicesToEmpty(request, resolvedIndices); + return PrivilegesEvaluatorResponse.ok(); + } + } + + if (presponse.isAllowed()) { + + 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 boolean isClusterPermission(String action) { + return (action.startsWith("cluster:") + || action.startsWith("indices:admin/template/") + || action.startsWith("indices:admin/index_template/") + || action.startsWith(SearchScrollAction.NAME) + || (action.equals(BulkAction.NAME)) + || (action.equals(MultiGetAction.NAME)) + || (action.startsWith(MultiSearchAction.NAME)) + || (action.equals(MultiTermVectorsAction.NAME)) + || (action.equals(ReindexAction.NAME)) + || (action.equals(RenderSearchTemplateAction.NAME))); } + + @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; + } + + private boolean checkDocAllowListHeader(User user, String action, ActionRequest request) { + 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 Map createActionPrivileges( + 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 + ) + ); + } + + return result; + } + + 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 (IndicesAliasesRequest.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; + } + + boolean isIndexReductionForIncompletePrivilegesPossible(ActionRequest request) { + if (!(request instanceof IndicesRequest.Replaceable indicesRequest)) { + return false; + } + + if (indicesRequest.indicesOptions().ignoreUnavailable()) { + return true; + } + + return indicesRequest.indicesOptions().expandWildcardsOpen() && containsPattern(indicesRequest); + } + + boolean isIndexReductionForNoPrivilegesPossible(ActionRequest request) { + if (!isIndexReductionForIncompletePrivilegesPossible(request)) { + return false; + } + + return ((IndicesRequest) request).indicesOptions().allowNoIndices(); + } + + 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; + } +} 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 2e420018d8..6b0b21cc54 100644 --- a/src/main/java/org/opensearch/security/privileges/dlsfls/AbstractRuleBasedPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/dlsfls/AbstractRuleBasedPrivileges.java @@ -25,6 +25,7 @@ import org.apache.logging.log4j.Logger; import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.cluster.metadata.OptionallyResolvedIndices; import org.opensearch.cluster.metadata.ResolvedIndices; import org.opensearch.common.settings.Settings; import org.opensearch.security.privileges.IndexPattern; @@ -32,7 +33,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.support.ConfigConstants; @@ -132,7 +132,8 @@ 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, ResolvedIndices resolved) throws PrivilegesEvaluationException { + public boolean isUnrestricted(PrivilegesEvaluationContext context, OptionallyResolvedIndices optionallyResolvedIndices) + throws PrivilegesEvaluationException { if (context.getMappedRoles().isEmpty()) { return false; } @@ -142,11 +143,12 @@ public boolean isUnrestricted(PrivilegesEvaluationContext context, ResolvedIndic 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; } 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/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index e547732a9f..ba74c69e14 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -28,7 +28,8 @@ 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.privileges.PrivilegesEvaluationContext; import org.opensearch.security.resources.sharing.Recipient; import org.opensearch.security.resources.sharing.ResourceSharing; import org.opensearch.security.resources.sharing.ShareWith; @@ -53,7 +54,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 +62,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 +162,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/rest/DashboardsInfoAction.java b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java index 7353633071..14f8ca0c45 100644 --- a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java @@ -40,7 +40,12 @@ import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; 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 +83,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 +95,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 +132,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 +182,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/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/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/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 ad5623c06a..41943fccef 100644 --- a/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java +++ b/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java @@ -31,7 +31,8 @@ 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.RoleMapper; import org.opensearch.security.privileges.ResourceAccessEvaluator; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; @@ -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), @@ -104,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, diff --git a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java index 7558533656..1507d77c73 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); @@ -322,9 +291,6 @@ public void testCreateJwtWithBadRoles() { 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); diff --git a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index 8abf5586bb..b911f92dcc 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -12,84 +12,34 @@ 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<>()); - } + public class RestLayerPrivilegesEvaluatorTest { - @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 +48,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 +57,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 +75,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,35 +87,83 @@ 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(), - Settings.EMPTY, - null, - clusterInfoHolder + ActionPrivileges actionPrivileges = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + Settings.EMPTY ); - 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; + } + }; + } + } 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 68% 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 6df69ec4e8..4a67b70f51 100644 --- a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java +++ b/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorUnitTest.java @@ -6,14 +6,13 @@ * 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; @@ -34,13 +33,13 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.security.privileges.actionlevel.legacy.PrivilegesEvaluator.DNFOF_MATCHER; +import static org.opensearch.security.privileges.actionlevel.legacy.PrivilegesEvaluator.isClusterPermissionStatic; 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.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 { @@ -120,59 +119,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 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 - ); - } - @Test public void testClusterPerm() { String multiSearchTemplate = "indices:data/read/msearch/template"; @@ -182,13 +128,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 @@ -205,20 +151,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 88% 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 459a8a46bd..49a3ed9f99 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; @@ -38,7 +38,10 @@ 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.privileges.actionlevel.RuntimeOptimizedActionPrivileges; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -54,10 +57,10 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.opensearch.security.support.ConfigConstants.SYSTEM_INDEX_PERMISSION; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; 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; @@ -135,7 +138,12 @@ 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 + ); } catch (JsonProcessingException e) { throw new RuntimeException(e); } @@ -192,13 +200,11 @@ public void testUnprotectedActionOnRegularIndex_systemIndexDisabled() { null, UNPROTECTED_ACTION, resolved, - presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user ); - verifyNoInteractions(presponse); - assertThat(response, is(presponse)); + assertThat(response, isNull()); } @@ -213,13 +219,11 @@ public void testUnprotectedActionOnRegularIndex_systemIndexPermissionDisabled() null, UNPROTECTED_ACTION, resolved, - presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user ); - verifyNoInteractions(presponse); - assertThat(response, is(presponse)); + assertThat(response, isNull()); } @Test @@ -233,13 +237,11 @@ public void testUnprotectedActionOnRegularIndex_systemIndexPermissionEnabled() { null, UNPROTECTED_ACTION, resolved, - presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user ); - verifyNoInteractions(presponse); - assertThat(response, is(presponse)); + assertThat(response, isNull()); } @Test @@ -253,13 +255,11 @@ public void testUnprotectedActionOnSystemIndex_systemIndexDisabled() { null, UNPROTECTED_ACTION, resolved, - presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user ); - verifyNoInteractions(presponse); - assertThat(response, is(presponse)); + assertThat(response, isNull()); } @Test @@ -273,13 +273,11 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionDisabled() { null, UNPROTECTED_ACTION, resolved, - presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user ); - verifyNoInteractions(presponse); - assertThat(response, is(presponse)); + assertThat(response, isNull()); } @Test @@ -293,13 +291,11 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_With null, UNPROTECTED_ACTION, resolved, - presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user ); - verify(presponse).markComplete(); - assertThat(response, is(presponse)); + assertThat(presponse.isAllowed(), is(false)); verify(auditLog).logSecurityIndexAttempt(request, UNPROTECTED_ACTION, null); verify(log).isInfoEnabled(); @@ -322,14 +318,12 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_With 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)); + assertThat(presponse.isAllowed(), is(false)); } @Test @@ -341,11 +335,16 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexDisabled() { 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( + request, + null, + UNPROTECTED_ACTION, + resolved, + ctx(UNPROTECTED_ACTION), + actionPrivileges, + user + ); + assertThat(response, isNull()); } @Test @@ -357,9 +356,9 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionDisable 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(request, null, UNPROTECTED_ACTION, resolved, 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); @@ -378,9 +377,9 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled 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(request, null, UNPROTECTED_ACTION, resolved, 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); @@ -413,9 +412,9 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled 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(request, null, UNPROTECTED_ACTION, resolved, 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); @@ -424,6 +423,7 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled verify(log).debug("Disable search request cache for this request"); verify(log).debug("Disable realtime for this request"); } + /* @Test public void testProtectedActionLocalAll_systemIndexDisabled() { @@ -605,28 +605,7 @@ public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionEnabl @Test public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionEnabled_withSystemIndexPermission() { testSecurityIndexAccess(PROTECTED_ACTION); - } - - private void testSecurityIndexAccess(String action) { - setup(true, true, SECURITY_INDEX, true); - - final ResolvedIndices resolved = createResolved(SECURITY_INDEX); - - // Action - evaluator.evaluate(request, task, action, resolved, presponse, ctx(action), actionPrivileges, user); - - verify(auditLog).logSecurityIndexAttempt(request, action, task); - assertThat(presponse.allowed, is(false)); - verify(presponse).markComplete(); - - verify(log).isInfoEnabled(); - verify(log).info( - "{} not permitted for a regular user {} on protected system indices {}", - action, - user.getSecurityRoles(), - SECURITY_INDEX - ); - } + }*/ private ResolvedIndices createResolved(final String... indexes) { return ResolvedIndices.of(indexes); diff --git a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java index 1a972f842d..bc8b5b5217 100644 --- a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java +++ b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java @@ -70,6 +70,7 @@ public class ResourceAccessHandlerTest { public void setup() { threadContext = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext); + handler = new ResourceAccessHandler(threadPool, sharingIndexHandler, adminDNs, new PrivilegesConfiguration(privilegesEvaluator)); handler = new ResourceAccessHandler(threadPool, sharingIndexHandler, adminDNs, privilegesEvaluator, resourcePluginInfo); // For tests that verify permission with action-group From 88c06260d7a3f13308323e4fda820f0a056dd40f Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Fri, 19 Sep 2025 15:48:05 +0200 Subject: [PATCH 05/29] fewer resolved indices --- .../IndexAuthorizationReadOnlyIntTests.java | 875 ++++++++++-------- .../security/OpenSearchSecurityPlugin.java | 2 +- .../PrivilegesInterceptorImpl.java | 2 +- .../security/filter/SecurityRestFilter.java | 3 +- .../privileges/IndicesRequestModifier.java | 11 +- .../privileges/PrivilegesConfiguration.java | 52 +- .../privileges/PrivilegesEvaluator.java | 7 + .../PrivilegesEvaluatorResponse.java | 55 +- .../privileges/PrivilegesInterceptor.java | 2 - .../RuntimeOptimizedActionPrivileges.java | 43 +- .../SubjectBasedActionPrivileges.java | 19 + .../legacy/PrivilegesEvaluator.java | 6 +- .../legacy/SystemIndexAccessEvaluator.java | 31 +- .../nextgen/ActionConfiguration.java | 198 ++++ .../nextgen/PrivilegesEvaluator.java | 863 ++++++++++------- .../security/rest/SecurityHealthAction.java | 10 +- .../ResourceAccessEvaluatorTest.java | 1 - .../RestLayerPrivilegesEvaluatorTest.java | 5 + 18 files changed, 1415 insertions(+), 770 deletions(-) create mode 100644 src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/ActionConfiguration.java 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..123cb9d48d 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 @@ -42,6 +42,7 @@ 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.TestIndex.openSearchSecurityConfigIndex; 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.data.TestIndex.openSearchSecurityConfigIndex; @@ -118,6 +119,16 @@ public class IndexAuthorizationReadOnlyIntTests { openSearchSecurityConfigIndex() ); + static final List ALL_INDICES_EXCEPT_HIDDEN = List.of( + index_a1, + index_a2, + index_a3, + index_b1, + index_b2, + index_b3, + index_c1 + ); + static final List ALL_INDICES_AND_ALIASES = List.of( index_a1, index_a2, @@ -194,6 +205,7 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_a*") )// .reference(READ, limitedTo(index_a1, index_a2, index_a3, index_ax))// + .indexMatcher(READ_NEXT_GEN, limitedTo(index_a1, index_a2, index_a3, index_ax))// .reference(GET_ALIAS, limitedToNone()); /** @@ -208,7 +220,9 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_b*") )// .reference(READ, limitedTo(index_b1, index_b2, index_b3))// - .reference(GET_ALIAS, limitedToNone()); + .indexMatcher("read_nextgen", limitedTo(index_b1, index_b2, index_b3))// + + .reference(GET_ALIAS, limitedToNone()); /** * A simple user that can read only from index_b1 @@ -222,7 +236,9 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_b1") )// .reference(READ, limitedTo(index_b1))// - .reference(GET_ALIAS, limitedToNone()); + .indexMatcher("read_nextgen", limitedTo(index_b1))// + + .reference(GET_ALIAS, limitedToNone()); /** * A simple user that can read from index_c* @@ -236,7 +252,9 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_c*") )// .reference(READ, limitedTo(index_c1, alias_c1))// - .reference(GET_ALIAS, limitedToNone()); + .indexMatcher("read_nextgen", limitedTo(index_c1))// + + .reference(GET_ALIAS, limitedToNone()); /** * A user that has read privileges for alias_ab1*; these privileges are inherited to the member indices. @@ -251,7 +269,9 @@ public class IndexAuthorizationReadOnlyIntTests { .on("alias_ab1*") )// .reference(READ, 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)); + .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)); /** * A user that has read privileges for alias_c1; these privileges are inherited to the member indices. @@ -266,7 +286,9 @@ public class IndexAuthorizationReadOnlyIntTests { .on("alias_c1") )// .reference(READ, limitedTo(index_c1, alias_c1))// - .reference(GET_ALIAS, limitedTo(index_c1, alias_c1)); + .indexMatcher(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 +301,8 @@ 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()); /** @@ -302,8 +326,14 @@ public class IndexAuthorizationReadOnlyIntTests { "system:admin/system_index" ) .on(".system_index_plugin") + .indexPermissions("read", "indices_monitor", "indices:admin/analyze") + .on("index_c*")// + .indexPermissions("read", "indices_monitor", "indices:admin/analyze", "system:admin/system_index") + .on(".system_index_plugin", ".alias_with_system_index") )// .reference(READ, limitedTo(index_c1, alias_c1, system_index_plugin, alias_with_system_index))// + .reference(READ_NEXT_GEN, limitedTo(index_c1, system_index_plugin, alias_with_system_index))// + .reference(GET_ALIAS, limitedTo(index_c1, alias_c1, system_index_plugin, alias_with_system_index)); /** @@ -321,6 +351,7 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_does_not_exist_*") )// .reference(READ, limitedToNone())// + .reference(READ,READ_NEXT_GEN limitedToNone())// .reference(GET_ALIAS, limitedToNone()); /** @@ -333,6 +364,8 @@ public class IndexAuthorizationReadOnlyIntTests { .clusterPermissions("cluster_composite_ops_ro", "cluster_monitor") )// .reference(READ, limitedToNone())// + .reference(READ_NEXT_GEN, limitedToNone())// + .reference(GET_ALIAS, limitedToNone()); /** @@ -344,11 +377,13 @@ public class IndexAuthorizationReadOnlyIntTests { .roles( new TestSecurityConfig.Role("r1")// .clusterPermissions("*") + .clusterPermissions("cluster_composite_ops_ro", "cluster_monitor") .indexPermissions("*") .on("*")// )// .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 +394,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 +447,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 +457,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 +467,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 +480,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") + 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()) - ); - } 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()) - ); - } + .whenEmpty(isForbidden()) + ); } } @@ -482,7 +495,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 +505,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 +521,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 +531,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.indexMatcher("read")).whenEmpty(isOk()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } + } } } @@ -546,7 +561,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 +571,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 +587,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 +597,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 +631,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()); + } } } @@ -684,25 +699,37 @@ public void search_staticIndices_systemIndex_alias() throws Exception { if (user == SUPER_UNLIMITED_USER) { assertThat(httpResponse, isOk()); - assertThat(httpResponse, containsExactly(system_index_plugin).at("hits.hits[*]._index")); + assertThat( + httpResponse, + containsExactly(system_index_plugin).at("hits.hits[*]._index")); } else if (clusterConfig == ClusterConfig.LEGACY_PRIVILEGES_EVALUATION) { if (user == UNLIMITED_USER || user == LIMITED_USER_C_WITH_SYSTEM_INDICES) { // 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")); + assertThat( + httpResponse, + containsExactly().at("hits.hits[*]._index")); } else { 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.indexMatcher("read")).whenEmpty(isForbidden())); + } } else { if (user.reference(READ).covers(alias_with_system_index)) { assertThat(httpResponse, isOk()); - assertThat(httpResponse, containsExactly(system_index_plugin).at("hits.hits[*]._index")); + assertThat( + httpResponse, + containsExactly(system_index_plugin).at("hits.hits[*]._index")); } else { assertThat(httpResponse, isForbidden()); } @@ -715,12 +742,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 +761,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 +781,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 +799,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 +827,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 +873,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 +889,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.indexMatcher("read")) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } } @@ -863,6 +928,13 @@ public void search_alias() throws Exception { .reducedBy(user.reference(READ)) .whenEmpty(isForbidden()) ); + } else { + // The new privilege evaluation never replaces aliases + if (user.indexMatcher("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 +944,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 +961,27 @@ 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()); + } } } @@ -917,16 +1006,14 @@ public void search_alias_pattern_includeHidden() throws Exception { ); } else if (clusterConfig == ClusterConfig.LEGACY_PRIVILEGES_EVALUATION_SYSTEM_INDEX_PERMISSION) { // For all users without the system index permission, SystemIndexAccessEvaluator shuts the door - // For the user with the system index permission, that happens as well, as SystemIndexAccessEvaluator expects the - // permission for all requested indices, even if they are not system indices - assertThat(httpResponse, isForbidden()); + // For the user with the system index permission, that happens as well, as SystemIndexAccessEvaluator expects the permission for all requested indices, even if they are not system indices + assertThat(httpResponse, isForbidden()); } else { assertThat( - httpResponse, - containsExactly(index_a1, index_a2, index_a3, index_b1, index_c1, system_index_plugin).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(isOk()) - ); + httpResponse, + containsExactly(index_a1, index_a2, index_a3, index_b1, index_c1, system_index_plugin).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(isOk())); } } else { assertThat(httpResponse, isForbidden()); @@ -934,6 +1021,7 @@ public void search_alias_pattern_includeHidden() throws Exception { } } + @Test public void search_aliasAndIndex_ignoreUnavailable() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { @@ -946,10 +1034,22 @@ 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.indexMatcher("read").covers(alias_ab1)) { + assertThat(httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index")); + } else if (user.indexMatcher("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")); + } + } + } + } @Test public void search_nonExisting_static() throws Exception { @@ -968,7 +1068,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,12 +1092,28 @@ 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()) - ); + 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()) + ); + } } } @@ -1037,23 +1157,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 - ); - } + IndexApiResponseMatchers.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 +1182,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))); } } } @@ -1130,8 +1234,8 @@ public void search_pit_wrongIndex() throws Exception { } else { assertThat( - httpResponse, - isForbidden("/error/root_cause/0/reason", "no permissions for [indices:data/read/point_in_time/create]") + httpResponse, + isForbidden("/error/root_cause/0/reason", "no permissions for [indices:data/read/point_in_time/create]") ); } } @@ -1295,30 +1399,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)) + .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 +1418,16 @@ 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 +1435,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.indexMatcher("read")) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } } @@ -1379,13 +1487,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.indexMatcher("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.indexMatcher("get_alias")).whenEmpty(isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } } } } @@ -1395,7 +1514,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 +1522,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 +1532,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 +1549,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()") + .reducedByuser.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 +1605,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) { + // 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]")); + } } } } @@ -1549,7 +1670,7 @@ public void getAlias_indexPattern_includeHidden() throws Exception { system_index_plugin ).at("$.keys()") ); - } else { + } else if (!user.indexMatcher("get_alias").isEmpty()) { assertThat( httpResponse, containsExactly(alias_ab1, alias_c1, alias_with_system_index).at("$.*.aliases.keys()") @@ -1562,10 +1683,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 +1699,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 +1727,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 +1748,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 +1769,24 @@ 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 +1795,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 +1805,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 +1845,13 @@ public void field_caps_alias() throws Exception { .reducedBy(user.reference(READ)) .whenEmpty(isForbidden()) ); + } else { + if (user.indexMatcher("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 +1860,16 @@ 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 +1891,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,31 +1903,32 @@ 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") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); - assertThat( - httpResponse, - containsExactly(index_a1, index_a2, index_a3, index_b1).at("indices") - .reducedBy(user.reference(READ)) - .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) - ); - + } + } } - } - @Test - public void pit_list_all() throws Exception { - String indexA1pitId = createPit(index_a1); + @Test + public void pit_list_all() throws Exception { + String indexA1pitId = createPit(index_a1); - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("_search/point_in_time/_all"); + 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()); + // 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()); } else { assertThat(httpResponse, isForbidden()); } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 89f64683b4..bfbd463736 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -636,7 +636,7 @@ public List getRestHandlers( settings, restController, Objects.requireNonNull(backendRegistry), - Objects.requireNonNull(evaluator) + Objects.requireNonNull(privilegesConfiguration) ) ); handlers.add( diff --git a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java index ee118cc704..e6dc0f1bae 100644 --- a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java +++ b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java @@ -106,7 +106,6 @@ public ReplaceResult replaceDashboardsIndex( final ActionRequest request, final String action, final User user, - final OptionallyResolvedIndices optionallyResolvedIndices, final PrivilegesEvaluationContext context ) { DashboardsMultiTenancyConfiguration config = this.multiTenancyConfigurationSupplier.get(); @@ -117,6 +116,7 @@ public ReplaceResult replaceDashboardsIndex( return CONTINUE_EVALUATION_REPLACE_RESULT; } + OptionallyResolvedIndices optionallyResolvedIndices = context.getResolvedRequest(); TenantPrivileges tenantPrivileges = this.tenantPrivilegesSupplier.get(); if (!(optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices)) { 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/privileges/IndicesRequestModifier.java b/src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java index 78aef5d7d6..1b6ac780fd 100644 --- a/src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java +++ b/src/main/java/org/opensearch/security/privileges/IndicesRequestModifier.java @@ -16,6 +16,7 @@ import org.opensearch.action.ActionRequest; import org.opensearch.action.IndicesRequest; +import org.opensearch.action.admin.indices.segments.PitSegmentsRequest; import org.opensearch.cluster.metadata.ResolvedIndices; /** @@ -32,7 +33,10 @@ public boolean setLocalIndices(ActionRequest targetRequest, ResolvedIndices reso return setLocalIndicesToEmpty(targetRequest, resolvedIndices); } - if (targetRequest instanceof IndicesRequest.Replaceable) { + 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 { @@ -41,7 +45,10 @@ public boolean setLocalIndices(ActionRequest targetRequest, ResolvedIndices reso } public boolean setLocalIndicesToEmpty(ActionRequest targetRequest, ResolvedIndices resolvedIndices) { - if (targetRequest instanceof IndicesRequest.Replaceable replaceable) { + 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 diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java b/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java index 45d64e4933..d8d63d6fb8 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java @@ -117,10 +117,7 @@ public PrivilegesConfiguration( PrivilegesEvaluationType privilegesEvaluationType = PrivilegesEvaluationType.getFrom( configurationRepository.getConfiguration(CType.CONFIG) ); - PrivilegesEvaluationType currentEvaluationType = currentPrivilegesEvaluator == null ? null - : currentPrivilegesEvaluator instanceof org.opensearch.security.privileges.actionlevel.legacy.PrivilegesEvaluator - ? PrivilegesEvaluationType.LEGACY - : PrivilegesEvaluationType.NEXT_GEN; + PrivilegesEvaluationType currentEvaluationType = PrivilegesEvaluationType.typeOf(currentPrivilegesEvaluator); if (privilegesEvaluationType != currentEvaluationType) { if (privilegesEvaluationType == PrivilegesEvaluationType.LEGACY) { @@ -147,23 +144,24 @@ public PrivilegesConfiguration( } } else { PrivilegesEvaluator oldInstance = privilegesEvaluator.getAndSet( - new org.opensearch.security.privileges.actionlevel.nextgen.PrivilegesEvaluator( - clusterService, - clusterStateSupplier, - roleMapper, - threadPool, - threadPool.getThreadContext(), - resolver, - auditLog, - settings, - privilegesInterceptor, - flattenedActionGroups, - staticActionGroups, - rolesConfiguration, - generalConfiguration, - pluginIdToRolePrivileges, - new RuntimeOptimizedActionPrivileges.SpecialIndexProtection(this.specialIndices::isUniversallyDeniedIndex, this.specialIndices::isSystemIndex) + 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(); @@ -239,6 +237,10 @@ public void updatePluginToActionPrivileges(String pluginIdentifier, RoleV7 plugi pluginIdToRolePrivileges.put(pluginIdentifier, pluginPermissions); } + public boolean isInitialized() { + return this.privilegesEvaluator().isInitialized(); + } + /** * TODO: Think about better names */ @@ -263,6 +265,16 @@ static PrivilegesEvaluationType getFrom(SecurityDynamicConfiguration c 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() { diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 7024293462..df5807a082 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -73,6 +73,8 @@ void updateConfiguration( boolean notFailOnForbiddenEnabled(); + boolean isInitialized(); + /** * A PrivilegesEvaluator implementation that just throws "not initialized" exceptions. * Used initially by PrivilegesConfiguration. @@ -129,6 +131,11 @@ public boolean notFailOnForbiddenEnabled() { return false; } + @Override + public boolean isInitialized() { + return false; + } + private OpenSearchSecurityException exception() { StringBuilder error = new StringBuilder("OpenSearch Security is not initialized"); String reason = this.unavailablityReasonSupplier.get(); diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java index adba2a014b..9c2728bc7c 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java @@ -127,7 +127,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,8 +158,40 @@ public CreateIndexRequestBuilder getCreateIndexRequestBuilder() { return createIndexRequestBuilder; } - public PrivilegesEvaluatorResponse markComplete() { - this.state = PrivilegesEvaluatorResponseState.COMPLETE; + public PrivilegesEvaluatorResponse originalResult() { + return this.originalResult; + } + + 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 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 PrivilegesEvaluatorResponse originalResult(PrivilegesEvaluatorResponse originalResult) { + if (originalResult != null && !originalResult.evaluationExceptions.isEmpty()) { + this.originalResult = originalResult; + this.evaluationExceptions.addAll(originalResult.evaluationExceptions); + } return this; } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java index e56414757b..cc38f52988 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java @@ -29,7 +29,6 @@ import org.opensearch.action.ActionRequest; import org.opensearch.action.admin.indices.create.CreateIndexRequestBuilder; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.metadata.OptionallyResolvedIndices; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.user.User; @@ -79,7 +78,6 @@ public ReplaceResult replaceDashboardsIndex( final ActionRequest request, final String action, final User user, - final OptionallyResolvedIndices requestedResolved, final PrivilegesEvaluationContext context ) { throw new RuntimeException("not implemented"); 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 5c88405e5f..480042bcf7 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java @@ -25,6 +25,7 @@ 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.security.privileges.ActionPrivileges; import org.opensearch.security.privileges.IndexPattern; @@ -414,6 +415,11 @@ protected void checkPrivilegesForNonWellKnownActions( } } + /** + * 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 finalizeResult(PrivilegesEvaluationContext context, IntermediateResult intermediateResult) { CheckTable checkTable = intermediateResult.indexToActionCheckTable; List exceptions = new ArrayList<>(intermediateResult.exceptions); @@ -421,12 +427,7 @@ protected PrivilegesEvaluatorResponse finalizeResult(PrivilegesEvaluationContext checkTable.uncheckIf(this.universallyDeniedIndices, checkTable.getColumns()); } if (this.indicesNeedingSystemIndexPrivileges != null) { - // TODO aliases - checkTable.uncheckIf( - index -> this.indicesNeedingSystemIndexPrivileges.test(index) - && !providesExplicitPrivilege(context, index, ConfigConstants.SYSTEM_INDEX_PERMISSION, exceptions), - checkTable.getColumns() - ); + checkTable.uncheckIf(index -> this.isUnauthorizedSystemIndex(context, index, exceptions), checkTable.getColumns()); } if (checkTable.isComplete()) { @@ -451,6 +452,36 @@ protected PrivilegesEvaluatorResponse finalizeResult(PrivilegesEvaluationContext 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; + } } /** 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 8bf4e8d3a6..31c655e3d2 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java @@ -25,6 +25,7 @@ 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; @@ -47,6 +48,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); /** 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 index ae3c60a01e..ca9e540a8d 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java @@ -234,7 +234,11 @@ public void updateConfiguration( } catch (Exception e) { log.error("Error while updating ActionPrivileges", e); } + } + @Override + public boolean isInitialized() { + return true; } @Override @@ -402,7 +406,6 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) request, action0, user, - optionallyResolvedIndices, context ); @@ -461,7 +464,6 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) request, action0, user, - optionallyResolvedIndices, context ); diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java index e851891e20..acd38639d9 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java @@ -263,26 +263,25 @@ private PrivilegesEvaluatorResponse evaluateSystemIndicesAccess( // the following section should only be run for index actions if (user.isPluginUser() && !isClusterPermissionStatic(action)) { if (this.isSystemIndexEnabled) { - PluginSystemIndexSelection pluginSystemIndexSelection = areIndicesPluginSystemIndices( + PluginSystemIndexSelection pluginSystemIndexSelection = areIndicesPluginSystemIndices( context, user.getName().replace("plugin:", ""), requestedResolved - ); - if (pluginSystemIndexSelection == PluginSystemIndexSelection.CONTAINS_ONLY_PLUGIN_SYSTEM_INDICES) { - // plugin is authorized to perform any actions on its own registered system indices - 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 - ); + ); + if (pluginSystemIndexSelection == PluginSystemIndexSelection.CONTAINS_ONLY_PLUGIN_SYSTEM_INDICES) { + // plugin is authorized to perform any actions on its own registered system indices + 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); } - return PrivilegesEvaluatorResponse.insufficient(action); - return; - } } else { // no system index protection and request originating from plugin, allow return PrivilegesEvaluatorResponse.ok(); 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..0a81e036f2 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/ActionConfiguration.java @@ -0,0 +1,198 @@ +/* + * 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 switch to index: + return action.startsWith("cluster:"); + } + } + + 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 index 625b3c85a6..ffd7fa1956 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/PrivilegesEvaluator.java @@ -10,37 +10,37 @@ */ 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.IndicesAliasesAction; -import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; -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.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.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.CreatePitRequest; 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.termvectors.MultiTermVectorsAction; import org.opensearch.action.update.UpdateAction; import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; @@ -53,9 +53,6 @@ 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; @@ -67,70 +64,94 @@ 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.privileges.actionlevel.legacy.ProtectedIndexAccessEvaluator; -import org.opensearch.security.privileges.actionlevel.legacy.SnapshotRestoreEvaluator; -import org.opensearch.security.privileges.actionlevel.legacy.SystemIndexAccessEvaluator; -import org.opensearch.security.privileges.actionlevel.legacy.TermsAggregationEvaluator; 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; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; - -import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; - -public class PrivilegesEvaluator implements org.opensearch.security.privileges.PrivilegesEvaluator { +/** + * 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 resolver; - private final AuditLog auditLog; + private final IndexNameExpressionResolver indexNameExpressionResolver; private final ThreadContext threadContext; private final PrivilegesInterceptor privilegesInterceptor; - private final boolean checkSnapshotRestoreWritePrivileges; - private final SnapshotRestoreEvaluator snapshotRestoreEvaluator; - private final TermsAggregationEvaluator termsAggregationEvaluator; private final Settings settings; private final AtomicReference actionPrivileges = new AtomicReference<>(); - private final Map pluginIdToActionPrivileges = new HashMap<>(); + 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( - ClusterService clusterService, - Supplier clusterStateSupplier, - RoleMapper roleMapper, - ThreadPool threadPool, - ThreadContext threadContext, - IndexNameExpressionResolver resolver, - AuditLog auditLog, - Settings settings, - PrivilegesInterceptor privilegesInterceptor, - FlattenedActionGroups actionGroups, - FlattenedActionGroups staticActionGroups, - SecurityDynamicConfiguration rolesConfiguration, - ConfigV7 generalConfiguration, - Map pluginIdToRolePrivileges, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection specialIndexProtection + 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 ) { - - super(); - this.resolver = resolver; - this.auditLog = auditLog; + this.indexNameExpressionResolver = indexNameExpressionResolver; this.roleMapper = roleMapper; - this.threadContext = threadContext; this.threadPool = threadPool; this.privilegesInterceptor = privilegesInterceptor; @@ -138,39 +159,31 @@ public PrivilegesEvaluator( this.settings = settings; this.specialIndexProtection = specialIndexProtection; - this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean( - ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, - ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES - ); + this.actionConfiguration = new ActionConfiguration(settings); + this.indicesRequestResolver = new IndicesRequestResolver(indexNameExpressionResolver); - snapshotRestoreEvaluator = new SnapshotRestoreEvaluator( - settings, - auditLog, - clusterService != null ? () -> clusterService.state().nodes().isLocalNodeElectedClusterManager() : () -> false + this.pluginIdToActionPrivileges = SubjectBasedActionPrivileges.buildFromMap( + pluginIdToRolePrivileges, + staticActionGroups, + specialIndexProtection ); - - termsAggregationEvaluator = new TermsAggregationEvaluator(); - this.indicesRequestResolver = new IndicesRequestResolver(resolver); - - this.pluginIdToActionPrivileges.putAll(createActionPrivileges(pluginIdToRolePrivileges, staticActionGroups, specialIndexProtection)); this.updateConfiguration(actionGroups, rolesConfiguration, generalConfiguration); - } @Override public void updateConfiguration( - FlattenedActionGroups flattenedActionGroups, - SecurityDynamicConfiguration rolesConfiguration, - ConfigV7 generalConfiguration + FlattenedActionGroups flattenedActionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration ) { - try { RoleBasedActionPrivileges actionPrivileges = new RoleBasedActionPrivileges( - rolesConfiguration, - flattenedActionGroups, - this.specialIndexProtection, - this.settings + rolesConfiguration, + flattenedActionGroups, + this.specialIndexProtection, + this.settings, + false ); Metadata metadata = clusterStateSupplier.get().metadata(); actionPrivileges.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); @@ -182,7 +195,11 @@ public void updateConfiguration( } catch (Exception e) { log.error("Error while updating ActionPrivileges", e); } + } + @Override + public boolean isInitialized() { + return true; } @Override @@ -192,11 +209,11 @@ public PrivilegesEvaluationContext createContext(User user, String action) { @Override public PrivilegesEvaluationContext createContext( - User user, - String action0, - ActionRequest request, - ActionRequestMetadata actionRequestMetadata, - Task task + User user, + String action, + ActionRequest request, + ActionRequestMetadata actionRequestMetadata, + Task task ) { TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); @@ -205,55 +222,40 @@ public PrivilegesEvaluationContext createContext( if (user.isPluginUser()) { mappedRoles = ImmutableSet.of(); - actionPrivileges = this.pluginIdToActionPrivileges.get(user.getName()); - if (actionPrivileges == null) { - actionPrivileges = ActionPrivileges.EMPTY; - } + actionPrivileges = this.pluginIdToActionPrivileges.getOrDefault(user.getName(), 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 + user, + mappedRoles, + action, + request, + actionRequestMetadata, + task, + indexNameExpressionResolver, + indicesRequestResolver, + clusterStateSupplier, + actionPrivileges ); } @Override public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { - - String action0 = context.getAction(); - ImmutableSet mappedRoles = context.getMappedRoles(); + String action = this.actionConfiguration.normalize(context.getAction()); User user = context.getUser(); ActionRequest request = context.getRequest(); - Task task = context.getTask(); - if (action0.startsWith("internal:indices/admin/upgrade")) { - action0 = "indices:admin/upgrade"; + 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 (AutoCreateAction.NAME.equals(action0)) { - action0 = CreateIndexAction.NAME; - } - - if (AutoPutMappingAction.NAME.equals(action0)) { - action0 = PutMappingAction.NAME; - } - - 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()); + if (this.actionConfiguration.isUniversallyDenied(action)) { + return PrivilegesEvaluatorResponse.insufficient(action).reason("The action is universally denied"); } ActionPrivileges actionPrivileges = context.getActionPrivileges(); @@ -261,7 +263,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) throw new OpenSearchSecurityException("OpenSearch Security is not initialized: roles configuration is missing"); } - if (request instanceof BulkRequest && (Strings.isNullOrEmpty(user.getRequestedTenant()))) { + 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 @@ -269,151 +271,166 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) // No further access check for the default tenant is necessary, as access will be also checked on the TransportShardBulkAction // level. - PrivilegesEvaluatorResponse presponse = actionPrivileges.hasClusterPrivilege(context, action0); + PrivilegesEvaluatorResponse result = actionPrivileges.hasClusterPrivilege(context, action); + logPrivilegeEvaluationResult(context, result, "cluster"); + return result; + } - if (!presponse.isAllowed()) { - log.info( - "No cluster-level perm match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", - user, - action0, - mappedRoles, - presponse.getMissingPrivileges() - ); - } - return presponse; + 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 presponse = snapshotRestoreEvaluator.evaluate(request, task, action0); - if (presponse != null) { - return presponse; - } + 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"); } - OptionallyResolvedIndices optionallyResolvedIndices = context.getResolvedRequest(); + PrivilegesEvaluatorResponse presponse = context.getActionPrivileges().hasClusterPrivilege(context, action); + if (!presponse.isAllowed()) { + return presponse; + } - if (isClusterPermission(action0)) { - if (user.isServiceAccount()) { - log.info("{} is a service account which doesn't have access to cluster level permission: {}", user, action0); - return PrivilegesEvaluatorResponse.insufficient(action0); - } + PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action, + context.getUser(), + context + ); - PrivilegesEvaluatorResponse presponse = actionPrivileges.hasClusterPrivilege(context, action0); + log.trace("Result from privileges interceptor for cluster perm: {}", replaceResult); - if (!presponse.isAllowed()) { - log.info( - "No cluster-level perm match for {} {} [Action [{}]] [RolesChecked {}]. No permissions for {}", - user, - optionallyResolvedIndices, - action0, - mappedRoles, - presponse.getMissingPrivileges() - ); - return presponse; + if (!replaceResult.continueEvaluation) { + if (replaceResult.accessDenied) { + return PrivilegesEvaluatorResponse.insufficient(action).reason("Insufficient tenant privileges"); } else { + return PrivilegesEvaluatorResponse.ok(replaceResult.createIndexRequestBuilder); + } + } - if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { - if (isDebugEnabled) { - log.debug("Normally allowed but we need to apply some extra checks for a restore request."); - } - } else { - - PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( - request, - action0, - user, - optionallyResolvedIndices, - 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 (request instanceof RestoreSnapshotRequest restoreSnapshotRequest) { + return handleRestoreSnapshot(context, restoreSnapshotRequest); } - if (checkDocAllowListHeader(user, action0, request)) { + return presponse; + } + + PrivilegesEvaluatorResponse checkIndexPermission(PrivilegesEvaluationContext context, String action, ActionRequest request) { + if (DocumentAllowList.isAllowed(request, threadContext)) { return PrivilegesEvaluatorResponse.ok(); } - { - PrivilegesEvaluatorResponse presponse = termsAggregationEvaluator.evaluate(optionallyResolvedIndices, request, context, actionPrivileges); - if (presponse != null) { - return presponse; + 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); } } - ImmutableSet allIndexPermsRequired = evaluateAdditionalIndexPermissions(request, action0); + OptionallyResolvedIndices optionallyResolvedIndices = context.getResolvedRequest(); - final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( - request, - action0, - user, - optionallyResolvedIndices, - context - ); + 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); + } - if (isDebugEnabled) { - log.debug("Result from privileges interceptor: {}", replaceResult); - } + return checkIndexPermissionBasic(context, requiredIndexPermissions(request, action), optionallyResolvedIndices, request); + } - if (!replaceResult.continueEvaluation) { - if (replaceResult.accessDenied) { - auditLog.logMissingPrivileges(action0, request, task); - return PrivilegesEvaluatorResponse.insufficient(action0); - } else { - return PrivilegesEvaluatorResponse.ok(replaceResult.createIndexRequestBuilder); - } + /** + * 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 + ); - PrivilegesEvaluatorResponse presponse = actionPrivileges.hasIndexPrivilege(context, allIndexPermsRequired, 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 (isIndexReductionForIncompletePrivilegesPossible(request) && optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices) { + // 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(); + return PrivilegesEvaluatorResponse.ok().reason("Only allowed for a sub-set of indices").originalResult(presponse); } } } else if (!presponse.isAllowed()) { - if (isIndexReductionForNoPrivilegesPossible(request) && optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices) { - indicesRequestModifier.setLocalIndicesToEmpty(request, resolvedIndices); - return PrivilegesEvaluatorResponse.ok(); - } - } + // 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 - if (presponse.isAllowed()) { + if (isIndexReductionForNoPrivilegesPossible(request) && optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices) { + // We only allow returning empty results if the current user has at least the necessary privileges for any index + PrivilegesEvaluatorResponse allowedForAnyIndex = actionPrivileges.hasIndexPrivilegeForAnyIndex( + context, + requiredIndexPermissions + ); - 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()); + if (allowedForAnyIndex.isAllowed() && this.indicesRequestModifier.setLocalIndicesToEmpty(request, resolvedIndices)) { + return PrivilegesEvaluatorResponse.ok() + .reason("Not allowed for any indices; returning empty result") + .originalResult(presponse); + } } } @@ -422,16 +439,8 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) @Override public boolean isClusterPermission(String action) { - return (action.startsWith("cluster:") - || action.startsWith("indices:admin/template/") - || action.startsWith("indices:admin/index_template/") - || action.startsWith(SearchScrollAction.NAME) - || (action.equals(BulkAction.NAME)) - || (action.equals(MultiGetAction.NAME)) - || (action.startsWith(MultiSearchAction.NAME)) - || (action.equals(MultiTermVectorsAction.NAME)) - || (action.equals(ReindexAction.NAME)) - || (action.equals(RenderSearchTemplateAction.NAME))); } + return this.actionConfiguration.isClusterPermission(action); + } @Override public void updateClusterStateMetadata(ClusterService clusterService) { @@ -454,132 +463,294 @@ public boolean notFailOnForbiddenEnabled() { return true; } - private boolean checkDocAllowListHeader(User user, String action, ActionRequest request) { - String docAllowListHeader = threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER); - - if (docAllowListHeader == null) { - return false; + 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() + ); } + } - if (!(request instanceof GetRequest)) { - return false; + 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(); + } - try { - DocumentAllowList documentAllowList = DocumentAllowList.parse(docAllowListHeader); - GetRequest getRequest = (GetRequest) request; + /** + * 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 (documentAllowList.isAllowed(getRequest.index(), getRequest.id())) { - if (log.isDebugEnabled()) { - log.debug("Request " + request + " is allowed by " + documentAllowList); - } + 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"); + } - return true; - } else { - return false; + 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; } + } - } catch (Exception e) { - log.error("Error while handling document allow list: " + docAllowListHeader, e); - return false; + 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(); + } - private static Map createActionPrivileges( - 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 - ) - ); + 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 result; + return checkIndexPermissionBasic( + context, + ConfigConstants.SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES, + resolvedIndices, + request + ); } - private ImmutableSet evaluateAdditionalIndexPermissions(final ActionRequest request, final String originalAction) { - ImmutableSet.Builder additionalPermissionsRequired = ImmutableSet.builder(); - - if (!isClusterPermission(originalAction)) { - additionalPermissionsRequired.add(originalAction); + /** + * 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 (request instanceof ClusterSearchShardsRequest) { - additionalPermissionsRequired.add(SearchAction.NAME); + if (allowed) { + return originalResult; + } else { + return originalResult.insufficient(subActionResults); } + } - if (request instanceof BulkShardRequest) { - BulkShardRequest bsr = (BulkShardRequest) request; - for (BulkItemRequest bir : bsr.items()) { - switch (bir.request().opType()) { + /** + * 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: - additionalPermissionsRequired.add(IndexAction.NAME); - break; case INDEX: - additionalPermissionsRequired.add(IndexAction.NAME); + allRequiredPermissions.add(IndexAction.NAME); break; case DELETE: - additionalPermissionsRequired.add(DeleteAction.NAME); + allRequiredPermissions.add(DeleteAction.NAME); break; case UPDATE: - additionalPermissionsRequired.add(UpdateAction.NAME); + allRequiredPermissions.add(UpdateAction.NAME); break; } } + return allRequiredPermissions.build(); + } else { + return Set.of(originalAction); } - - if (request instanceof IndicesAliasesRequest) { - IndicesAliasesRequest bsr = (IndicesAliasesRequest) request; - for (IndicesAliasesRequest.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; } + /** + * 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; } @@ -587,18 +758,38 @@ boolean isIndexReductionForIncompletePrivilegesPossible(ActionRequest request) { 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(); + String[] indices = indicesRequest.indices(); - if (indices == null || indices.length == 0 || (indices.length == 1 && (Metadata.ALL.equals(indices[0]) || Regex.isMatchAllPattern(indices[0])))) { + if (indices == null + || indices.length == 0 + || (indices.length == 1 && (Metadata.ALL.equals(indices[0]) || Regex.isMatchAllPattern(indices[0])))) { return true; } @@ -610,4 +801,8 @@ boolean containsPattern(IndicesRequest indicesRequest) { 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/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/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 b911f92dcc..bc0178d5e8 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -162,6 +162,11 @@ public void shutdown() { public boolean notFailOnForbiddenEnabled() { return false; } + + @Override + public boolean isInitialized() { + return true; + } }; } From 6274e8f3447f4475857b7ddb4563c70b6f21e42d Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 2 Sep 2025 15:49:42 +0200 Subject: [PATCH 06/29] Added new integration tests Signed-off-by: Nils Bandener --- .../org/opensearch/security/PerfTest.java | 146 ------ .../security/SearchOperationTest.java | 7 +- .../ServiceAccountAuthenticationTest.java | 1 + .../RestEndpointPermissionTests.java | 3 +- .../RoleBasedActionPrivilegesTest.java | 57 +- .../SubjectBasedActionPrivilegesTest.java | 48 +- .../dlsfls/FlsFmIntegrationTests.java | 2 + .../privileges/int_tests/ClusterConfig.java | 24 +- .../CrossClusterAuthorizationIntTests.java | 488 ++++++++++++++++++ ...taStreamAuthorizationReadOnlyIntTests.java | 244 +++++---- ...aStreamAuthorizationReadWriteIntTests.java | 13 +- .../IndexAuthorizationReadOnlyIntTests.java | 328 ++++++++++-- .../IndexAuthorizationReadWriteIntTests.java | 140 ++++- .../MiscPrivilegesIntTests.java} | 56 +- .../SnapshotAuthorizationIntTests.java | 14 + .../opensearch/security/rest/WhoAmITests.java | 1 - .../systemindex/SystemIndexDisabledTests.java | 1 + .../test/framework/TestSecurityConfig.java | 85 ++- .../test/framework/cluster/LocalCluster.java | 10 + .../matcher/RestDocumentMatchers.java | 1 + .../resources/log4j2-test.properties | 2 + .../configuration/DlsFlsValveImpl.java | 1 - .../security/privileges/ActionPrivileges.java | 14 + .../privileges/DocumentAllowList.java | 33 ++ .../security/privileges/IndexPattern.java | 60 ++- .../privileges/IndicesRequestResolver.java | 2 +- .../privileges/PrivilegesConfiguration.java | 2 +- .../PrivilegesEvaluatorResponse.java | 45 +- .../privileges/PrivilegesInterceptor.java | 12 + .../privileges/ResourceAccessEvaluator.java | 16 +- .../security/privileges/SpecialIndices.java | 17 +- .../RoleBasedActionPrivileges.java | 85 ++- .../RuntimeOptimizedActionPrivileges.java | 106 +++- .../SubjectBasedActionPrivileges.java | 73 ++- .../legacy/LegacyIndicesRequestResolver.java | 109 ++++ .../legacy/PrivilegesEvaluator.java | 82 ++- .../legacy/SystemIndexAccessEvaluator.java | 48 +- .../nextgen/ActionConfiguration.java | 6 +- .../nextgen/PrivilegesEvaluator.java | 44 +- .../dlsfls/AbstractRuleBasedPrivileges.java | 4 +- .../support/SnapshotRestoreHelper.java | 19 + .../security/IndexIntegrationTests.java | 20 +- .../TransportUserInjectorIntegTest.java | 5 +- .../RestLayerPrivilegesEvaluatorTest.java | 3 +- .../SystemIndexAccessEvaluatorTest.java | 4 +- 45 files changed, 1907 insertions(+), 574 deletions(-) delete mode 100644 src/integrationTest/java/org/opensearch/security/PerfTest.java create mode 100644 src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java rename src/integrationTest/java/org/opensearch/security/privileges/{PrivilegesEvaluatorTest.java => int_tests/MiscPrivilegesIntTests.java} (66%) create mode 100644 src/main/java/org/opensearch/security/privileges/actionlevel/legacy/LegacyIndicesRequestResolver.java diff --git a/src/integrationTest/java/org/opensearch/security/PerfTest.java b/src/integrationTest/java/org/opensearch/security/PerfTest.java deleted file mode 100644 index 3233af3522..0000000000 --- a/src/integrationTest/java/org/opensearch/security/PerfTest.java +++ /dev/null @@ -1,146 +0,0 @@ -package org.opensearch.security; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import com.fasterxml.jackson.databind.JsonNode; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; -import org.opensearch.action.admin.indices.create.CreateIndexRequest; -import org.opensearch.action.admin.indices.create.CreateIndexResponse; -import org.opensearch.action.admin.indices.refresh.RefreshRequest; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.common.Strings; -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.transport.client.Client; - -// io.netty, org.apache.lucene, java.io, java.nio, org.apache.logging. -// org.jcp -//java.security.Provider$Service -//apple.security.AppleProvider$ProviderService -@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) -@ThreadLeakScope(ThreadLeakScope.Scope.NONE) -public class PerfTest { - - public static void createTestData(LocalCluster cluster) throws Exception { - try (Client client = cluster.getInternalNodeClient()) { - { - CreateIndexRequest request = new CreateIndexRequest("test").settings( - Map.of("index.number_of_shards", 3, "index.number_of_replicas", 1) - ); - CreateIndexResponse response = client.admin().indices().create(request).actionGet(); - System.out.println(Strings.toString(XContentType.JSON, response)); - } - - IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest(); - - for (int i = 0; i < 1000; i++) { - String index = ".kibana_t_" + i + "_001"; - CreateIndexRequest request = new CreateIndexRequest(index).settings( - Map.of("index.number_of_shards", 1, "index.number_of_replicas", 0) - ); - CreateIndexResponse response = client.admin().indices().create(request).actionGet(); - System.out.println(Strings.toString(XContentType.JSON, response)); - indicesAliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.add().alias(".kibana_t_" + i).indices(index)); - } - - client.admin().indices().aliases(indicesAliasesRequest).actionGet(); - client.admin().indices().refresh(new RefreshRequest()).actionGet(); - client.admin().indices().refresh(new RefreshRequest()).actionGet(); - - } - } - - @Test - public void test() throws Exception { - - try ( - LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.DEFAULT) - .authc(TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL) - .users(TestSecurityConfig.User.USER_ADMIN) - .nodeSettings(Map.of("cluster_manager.throttling.thresholds.auto-create.value", 3000, "cluster.max_shards_per_node", 10000)) - .build() - ) { - - cluster.before(); - - createTestData(cluster); - - System.out.println("*** READY ***"); - - Thread.sleep(60 * 1000); - - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - for (int i = 0; i < 10000; i++) { - StringBuilder bulkBody = new StringBuilder(); - for (int k = 0; k < 10; k++) { - bulkBody.append(""" - { "index": { "_index": "test" } } - { "title": "foo", "year": 2020} - """); - } - try { - TestRestClient.HttpResponse response = client.postJson("_bulk", bulkBody.toString()); - // if (response.getStatusCode() >= 300) { - System.out.println(response.getBody()); - // } - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - } - - } - - static String parseNodeStatsResponse(TestRestClient.HttpResponse response) { - if (response.getBody().contains("receive_timeout_transport_exception")) { - return "TIMEOUT\n"; - } else { - JsonNode responseJsonNode = response.bodyAsJsonNode(); - JsonNode nodes = responseJsonNode.get("nodes"); - Iterator fieldNames = nodes.fieldNames(); - StringBuilder result = new StringBuilder(); - while (fieldNames.hasNext()) { - String nodeId = fieldNames.next(); - JsonNode node = nodes.get(nodeId); - JsonNode threadPool = node.get("thread_pool"); - JsonNode managementThreadPool = threadPool.get("management"); - result.append( - nodeId - + ": management thread pool: active: " - + managementThreadPool.get("active") - + "/5" - + "; queue: " - + managementThreadPool.get("queue") - + "\n" - ); - } - - return result.toString(); - } - } - - static TestSecurityConfig.Role[] createTestRoles() { - List result = new ArrayList<>(); - - for (int i = 0; i < 2500; i++) { - result.add(new TestSecurityConfig.Role("role" + i).indexPermissions("crud").on("*example*", ".*example*")); - } - - return result.toArray(new TestSecurityConfig.Role[0]); - } - - static class State { - int pendingCreateUserRequests = 0; - } -} 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/http/ServiceAccountAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java index 34857ea2a7..d0ad597085 100644 --- a/src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.junit.runner.RunWith; +import org.opensearch.test.framework.data.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; diff --git a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java index a85971e632..7ad030d2f5 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java @@ -122,7 +122,8 @@ public RestEndpointPermissionTests() throws IOException { createRolesConfig(), FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); } 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 42a75d8700..a4c693ebea 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java @@ -86,7 +86,8 @@ public void wellKnown() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); assertThat(subject.hasClusterPrivilege(ctx().roles("test_role").get(), "cluster:monitor/nodes/stats"), isAllowed()); @@ -110,7 +111,8 @@ public void notWellKnown() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); assertThat( @@ -137,7 +139,8 @@ public void wildcard() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); assertThat(subject.hasClusterPrivilege(ctx().roles("test_role").get(), "cluster:whatever"), isAllowed()); @@ -165,7 +168,8 @@ public void explicit_wellKnown() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); assertThat(subject.hasExplicitClusterPrivilege(ctx().roles("explicit_role").get(), "cluster:monitor/nodes/stats"), isAllowed()); @@ -201,7 +205,8 @@ public void explicit_notWellKnown() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); assertThat( @@ -232,7 +237,8 @@ public void hasAny_wellKnown() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); assertThat( @@ -267,7 +273,8 @@ public void hasAny_notWellKnown() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); assertThat( @@ -309,7 +316,8 @@ public void hasAny_wildcard() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); assertThat(subject.hasAnyClusterPrivilege(ctx().roles("test_role").get(), ImmutableSet.of("cluster:whatever")), isAllowed()); @@ -508,7 +516,8 @@ public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec, Statefulnes roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - settings + settings, + false ); if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { @@ -682,7 +691,8 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness stat roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - settings + settings, + false ); if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { @@ -918,7 +928,8 @@ public void hasIndexPrivilege_errors() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( @@ -949,7 +960,8 @@ public void hasExplicitIndexPrivilege_positive() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( @@ -974,7 +986,8 @@ public void hasExplicitIndexPrivilege_positive_wildcard() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( @@ -996,7 +1009,8 @@ public void hasExplicitIndexPrivilege_noWildcard() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( @@ -1021,7 +1035,8 @@ public void hasExplicitIndexPrivilege_negative_wrongAction() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( @@ -1046,7 +1061,8 @@ public void hasExplicitIndexPrivilege_errors() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( @@ -1080,7 +1096,8 @@ public void aliasesOnDataStreamBackingIndices() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + Settings.EMPTY, + false ); subject.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), 2); @@ -1113,7 +1130,8 @@ public void statefulDisabled() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.builder().put(RoleBasedActionPrivileges.PRECOMPUTED_PRIVILEGES_ENABLED.getKey(), false).build() + Settings.builder().put(RoleBasedActionPrivileges.PRECOMPUTED_PRIVILEGES_ENABLED.getKey(), false).build(), + false ); subject.updateStatefulIndexPrivileges(metadata, 1); assertEquals(0, subject.getEstimatedStatefulIndexByteSize()); @@ -1136,7 +1154,8 @@ public void estimatedSize() throws Exception { roles, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - Settings.EMPTY + 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 0b0a82ddc8..f1c01f8bb9 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java @@ -75,7 +75,8 @@ public void wellKnown() throws Exception { SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), isAllowed()); } @@ -90,7 +91,8 @@ public void notWellKnown() throws Exception { SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats/somethingnotwellknown"), isAllowed()); } @@ -105,7 +107,8 @@ public void negative() throws Exception { SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:monitor/nodes/foo"), isForbidden()); } @@ -120,7 +123,8 @@ public void wildcard() throws Exception { SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:whatever"), isAllowed()); } @@ -135,7 +139,8 @@ public void explicit_wellKnown() throws Exception { SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); assertThat(subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), isAllowed()); } @@ -150,7 +155,8 @@ public void explicit_notWellKnown() throws Exception { SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); assertThat(subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/notwellknown"), isAllowed()); } @@ -165,7 +171,8 @@ public void explicit_notExplicit() throws Exception { SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); assertThat( subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), @@ -183,7 +190,8 @@ public void hasAny_wellKnown() throws Exception { SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); assertThat(subject.hasAnyClusterPrivilege(ctx().get(), ImmutableSet.of("cluster:monitor/nodes/stats")), isAllowed()); } @@ -198,7 +206,8 @@ public void hasAny_wildcard() throws Exception { SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); assertThat(subject.hasAnyClusterPrivilege(ctx().get(), ImmutableSet.of("cluster:monitor/nodes/stats")), isAllowed()); } @@ -356,7 +365,8 @@ public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec) throws Exce this.subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); } @@ -505,7 +515,8 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec) throws Exception this.subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); } @@ -672,7 +683,8 @@ public void hasExplicitIndexPrivilege_positive() throws Exception { SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( @@ -694,7 +706,8 @@ public void hasExplicitIndexPrivilege_positive_pattern() throws Exception { SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( @@ -715,7 +728,8 @@ public void hasExplicitIndexPrivilege_noWildcard() throws Exception { SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( @@ -736,7 +750,8 @@ public void hasExplicitIndexPrivilege_negative_wrongAction() throws Exception { SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( @@ -757,7 +772,8 @@ public void hasExplicitIndexPrivilege_errors() throws Exception { SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges( config, FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + false ); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( diff --git a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FlsFmIntegrationTests.java b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FlsFmIntegrationTests.java index abee5eb844..50dd945a5a 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FlsFmIntegrationTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FlsFmIntegrationTests.java @@ -28,6 +28,8 @@ import org.bouncycastle.util.encoders.Hex; import org.opensearch.plugin.mapper.MapperSizePlugin; +import org.opensearch.test.framework.data.TestData; +import org.opensearch.test.framework.data.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; 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/CrossClusterAuthorizationIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java new file mode 100644 index 0000000000..ead08a43f3 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java @@ -0,0 +1,488 @@ +/* + * 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 com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import com.google.common.collect.ImmutableList; +import org.junit.AfterClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.data.TestIndex; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.certificate.TestCertificates; +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.matcher.RestIndexMatchers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +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.RestIndexMatchers.OnUserIndexMatcher.limitedToNone; +import static org.opensearch.test.framework.matcher.RestIndexMatchers.OnUserIndexMatcher.unlimitedIncludingOpenSearchSecurityIndex; +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 CrossClusterAuthorizationIntTests { + + // ------------------------------------------------------------------------------------------------------- + // Test indices used by this test suite + // ------------------------------------------------------------------------------------------------------- + + interface LocalIndices { + TestIndex index_a1 = TestIndex.name("index_a1").documentCount(10).seed(1).build(); + TestIndex index_a2 = TestIndex.name("index_a2").documentCount(11).seed(2).build(); + } + + interface RemoteIndices { + TestIndex index_r1 = TestIndex.name("index_r1").documentCount(212).seed(11).build(); + TestIndex index_r2 = TestIndex.name("index_r2").documentCount(213).seed(12).build(); + TestIndex index_r3 = TestIndex.name("index_r3").documentCount(214).seed(13).build(); + } + + /** + * This key identifies assertion reference data for index search/read permissions of individual users. + */ + static final TestSecurityConfig.User.MetadataKey READ = new TestSecurityConfig.User.MetadataKey<>( + "read", + RestIndexMatchers.IndexMatcher.class + ); + + // ------------------------------------------------------------------------------------------------------- + // Test users with which the tests will be executed; the users need to be added to the list USERS below + // Each user comes with one or two additionally defined TestSecurityConfig.Role objects: + // - If it is two, one is meant for the local cluster, the other is meant for the remote cluster + // - If it is one, both local and remote cluster must get these roles. + // These roles must be passed to the test cluster builders via the roles() method + // ------------------------------------------------------------------------------------------------------- + + static final TestSecurityConfig.Role LIMITED_USER_ROLE_A_R = new TestSecurityConfig.Role("limited_user_A_R_role").clusterPermissions( + "cluster_composite_ops_ro", + "cluster_monitor" + ).indexPermissions("read", "indices_monitor").on("index_a*"); + static final TestSecurityConfig.Role LIMITED_USER_ROLE_A_R_REMOTE = new TestSecurityConfig.Role("limited_user_A_R_role") + .clusterPermissions("cluster_composite_ops_ro", "cluster_monitor") + .indexPermissions("read", "indices_monitor", "indices:admin/shards/search_shards") + .on("index_r*"); + static final TestSecurityConfig.User LIMITED_USER_A_R = new TestSecurityConfig.User("limited_user_A_R")// + .description("index_a*, index_r*")// + .roles(LIMITED_USER_ROLE_A_R)// + .reference( + READ, + limitedTo(LocalIndices.index_a1, LocalIndices.index_a2, RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) + ); + + static final TestSecurityConfig.Role LIMITED_USER_ROLE_R = new TestSecurityConfig.Role("limited_user_R_role").clusterPermissions( + "cluster_composite_ops_ro", + "cluster_monitor" + ); + static final TestSecurityConfig.Role LIMITED_USER_ROLE_R_REMOTE = new TestSecurityConfig.Role("limited_user_R_role").clusterPermissions( + "cluster_composite_ops_ro", + "cluster_monitor" + ).indexPermissions("read", "indices_monitor", "indices:admin/shards/search_shards").on("index_r*"); + static final TestSecurityConfig.User LIMITED_USER_R = new TestSecurityConfig.User("limited_user_R")// + .description("index_r*")// + .roles(LIMITED_USER_ROLE_R)// + .reference(READ, limitedTo(RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3)); + + static final TestSecurityConfig.Role LIMITED_USER_ROLE_R1 = new TestSecurityConfig.Role("limited_user_R1_role").clusterPermissions( + "cluster_composite_ops_ro", + "cluster_monitor" + ); + static final TestSecurityConfig.Role LIMITED_USER_ROLE_R1_REMOTE = new TestSecurityConfig.Role("limited_user_R1_role") + .clusterPermissions("cluster_composite_ops_ro", "cluster_monitor") + .indexPermissions("read", "indices_monitor", "indices:admin/shards/search_shards") + .on("index_${attr.internal.attr_r1}"); + static final TestSecurityConfig.User LIMITED_USER_R1 = new TestSecurityConfig.User("limited_user_R1")// + .description("index_r1, with user attribute")// + .roles(LIMITED_USER_ROLE_R1)// + .attr("attr_r1", "r1") + .reference(READ, limitedTo(RemoteIndices.index_r1)); + + static final TestSecurityConfig.Role LIMITED_ROLE_NONE = new TestSecurityConfig.Role("limited_role_none").clusterPermissions( + "cluster_composite_ops_ro", + "cluster_monitor" + ).clusterPermissions("cluster_composite_ops_ro", "cluster_monitor"); + static final TestSecurityConfig.User LIMITED_USER_NONE = new TestSecurityConfig.User("limited_user_none")// + .description("no index privileges")// + .roles(LIMITED_ROLE_NONE)// + .reference(READ, limitedToNone()); + + static final TestSecurityConfig.Role UNLIMITED_ROLE = new TestSecurityConfig.Role("unlimited_role")// + .clusterPermissions("*") + .indexPermissions("*") + .on("*"); + + static final TestSecurityConfig.User UNLIMITED_USER = new TestSecurityConfig.User("unlimited_user")// + .description("unlimited")// + .roles(UNLIMITED_ROLE)// + .reference( + READ, + limitedTo(LocalIndices.index_a1, LocalIndices.index_a2, RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) + ); + + /** + * The SUPER_UNLIMITED_USER authenticates with an admin cert, which will cause all access control code to be skipped. + * This serves as a base for comparison with the default behavior. + */ + static final TestSecurityConfig.User SUPER_UNLIMITED_USER = new TestSecurityConfig.User("super_unlimited_user")// + .description("super unlimited (admin cert)")// + .adminCertUser()// + .reference(READ, unlimitedIncludingOpenSearchSecurityIndex()); + + static final List USERS = ImmutableList.of( + LIMITED_USER_A_R, + LIMITED_USER_R, + LIMITED_USER_R1, + LIMITED_USER_NONE, + UNLIMITED_USER, + SUPER_UNLIMITED_USER + ); + + static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); + + @ClassRule + public static final LocalCluster remoteCluster = new LocalCluster.Builder().certificates(TEST_CERTIFICATES) + .clusterManager(ClusterManager.SINGLENODE) + .clusterName("remote_1") + .authc(AUTHC_HTTPBASIC_INTERNAL) + .privilegesEvaluationType("next_gen") + .roles(LIMITED_USER_ROLE_A_R_REMOTE, LIMITED_USER_ROLE_R_REMOTE, LIMITED_USER_ROLE_R1_REMOTE, LIMITED_ROLE_NONE, UNLIMITED_ROLE) + .indices(RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) + .build(); + + static LocalCluster.Builder clusterBuilder() { + return new LocalCluster.Builder().clusterManager(ClusterManager.SINGLE_REMOTE_CLIENT) + .remote("remote_1", remoteCluster) + .certificates(TEST_CERTIFICATES) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USERS)// + .roles(LIMITED_USER_ROLE_A_R, LIMITED_USER_ROLE_R, LIMITED_USER_ROLE_R1, LIMITED_ROLE_NONE, UNLIMITED_ROLE) + .indices(LocalIndices.index_a1, LocalIndices.index_a2); + } + + @AfterClass + public static void stopClusters() { + for (ClusterConfig clusterConfig : ClusterConfig.values()) { + clusterConfig.shutdown(); + } + } + + final TestSecurityConfig.User user; + final LocalCluster cluster; + final ClusterConfig clusterConfig; + + @Test + public void search_wildcardWildcard() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("*:*/_search?size=1000"); + assertThat( + httpResponse, + containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) + .at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(isForbidden()) + ); + } + } + + @Test + public void search_remoteWildcard() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("remote_1:*/_search?size=1000"); + assertThat( + httpResponse, + containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) + .at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(isForbidden()) + ); + } + } + + @Test + public void search_remoteWildcard_minimizeRoundtrips() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("remote_1:*/_search?size=1000&ccs_minimize_roundtrips=true"); + assertThat( + httpResponse, + containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) + .at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(isForbidden()) + ); + } + } + + @Test + public void search_remoteStaticIndices() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("remote_1:index_r1/_search?size=1000"); + assertThat( + httpResponse, + containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1) + .at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(isForbidden()) + ); + } + } + + @Test + public void search_remoteStaticIndices_minimizeRoundtrips() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("remote_1:index_r1/_search?size=1000&ccs_minimize_roundtrips=true"); + assertThat( + httpResponse, + containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1) + .at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(isForbidden()) + ); + } + } + + @Test + public void search_remoteIndexPattern() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("remote_1:index_*/_search?size=1000"); + assertThat( + httpResponse, + containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) + .at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(isForbidden()) + ); + } + } + + @Test + public void search_remoteIndexPattern_minimizeRoundtrips() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("remote_1:index_*/_search?size=1000&ccs_minimize_roundtrips=true"); + assertThat( + httpResponse, + containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) + .at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(isForbidden()) + ); + } + } + + @Test + public void search_localStaticIndex_remoteStaticIndices() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("index_a2,remote_1:index_r1/_search?size=1000"); + assertThat( + httpResponse, + containsExactly(LocalIndices.index_a2).andFromRemote("remote_1", RemoteIndices.index_r1) + .at("hits.hits[*]._index") + .butForbiddenIfIncomplete(user.reference(READ)) + ); + } + } + + @Test + public void search_localIndexPattern_remoteIndexPattern() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("index_*,remote_1:index_*/_search?size=1000"); + + if (clusterConfig.legacyPrivilegeEvaluation) { + if (user.reference(READ).covers(LocalIndices.index_a1) || user.reference(READ).covers(LocalIndices.index_a2)) { + // Only if we have privileges for local indices, we also get through + assertThat( + httpResponse, + containsExactly(LocalIndices.index_a1, LocalIndices.index_a2).andFromRemote( + "remote_1", + RemoteIndices.index_r1, + RemoteIndices.index_r2, + RemoteIndices.index_r3 + ).at("hits.hits[*]._index").reducedBy(user.reference(READ)).whenEmpty(isForbidden()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } + } else { + if (user != LIMITED_USER_NONE) { + assertThat( + httpResponse, + containsExactly(LocalIndices.index_a1, LocalIndices.index_a2).andFromRemote( + "remote_1", + RemoteIndices.index_r1, + RemoteIndices.index_r2, + RemoteIndices.index_r3 + ).at("hits.hits[*]._index").reducedBy(user.reference(READ)).whenEmpty(isOk()) + ); + } else { + // No search permissions anywhere will result in a 403 error + assertThat(httpResponse, isForbidden()); + } + } + } + } + + @Test + public void resolve_wildcardWildcard() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("_resolve/index/*:*"); + + assertThat( + httpResponse, + containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) + .at("$.*[*].name") + .reducedBy(user.reference(READ)) + .whenEmpty(isOk()) + ); + } + } + + @Test + public void resolve_remoteWildcard() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("_resolve/index/remote_1:*"); + + assertThat( + httpResponse, + containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) + .at("$.*[*].name") + .reducedBy(user.reference(READ)) + .whenEmpty(isOk()) + ); + } + } + + @Test + public void resolve_localWildcard_remoteWildcard() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("_resolve/index/*,remote_1:*"); + + assertThat( + httpResponse, + containsExactly(LocalIndices.index_a1, LocalIndices.index_a2).andFromRemote( + "remote_1", + RemoteIndices.index_r1, + RemoteIndices.index_r2, + RemoteIndices.index_r3 + ).at("$.*[*].name").reducedBy(user.reference(READ)).whenEmpty(isOk()) + ); + } + } + + @Test + public void resolve_localIndexPattern_remoteIndexPattern() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("_resolve/index/index_a1*,remote_1:index_r1*"); + + if (clusterConfig.legacyPrivilegeEvaluation) { + if (user.reference(READ).covers(LocalIndices.index_a1)) { + // Only if we have privileges for local indices, we also get through + assertThat( + httpResponse, + containsExactly(LocalIndices.index_a1).andFromRemote("remote_1", RemoteIndices.index_r1) + .at("$.*[*].name") + .reducedBy(user.reference(READ)) + .whenEmpty(isOk()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } + } else { + assertThat( + httpResponse, + containsExactly(LocalIndices.index_a1).andFromRemote("remote_1", RemoteIndices.index_r1) + .at("$.*[*].name") + .reducedBy(user.reference(READ)) + .whenEmpty(isOk()) + ); + } + } + } + + @Test + public void field_caps_remoteWildcard() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("remote_1:*/_field_caps?fields=*"); + assertThat( + httpResponse, + containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) + .at("indices") + .reducedBy(user.reference(READ)) + .whenEmpty(isOk()) + ); + } + } + + @Test + public void field_caps_localIndexPattern_remoteIndexPattern() throws Exception { + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("index_*,remote_1:index_*/_field_caps?fields=*"); + if (clusterConfig.legacyPrivilegeEvaluation) { + if (user.reference(READ).covers(LocalIndices.index_a1) || user.reference(READ).covers(LocalIndices.index_a2)) { + // Only if we have privileges for local indices, we also get through + assertThat( + httpResponse, + containsExactly(LocalIndices.index_a1, LocalIndices.index_a2).andFromRemote( + "remote_1", + RemoteIndices.index_r1, + RemoteIndices.index_r2, + RemoteIndices.index_r3 + ).at("indices").reducedBy(user.reference(READ)).whenEmpty(isOk()) + ); + } else { + assertThat(httpResponse, isForbidden()); + } + } else { + assertThat( + httpResponse, + containsExactly(LocalIndices.index_a1, LocalIndices.index_a2).andFromRemote( + "remote_1", + RemoteIndices.index_r1, + RemoteIndices.index_r2, + RemoteIndices.index_r3 + ).at("indices").reducedBy(user.reference(READ)).whenEmpty(isOk()) + ); + } + } + } + + @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 CrossClusterAuthorizationIntTests(ClusterConfig clusterConfig, TestSecurityConfig.User user, String description) + throws Exception { + this.user = user; + this.cluster = clusterConfig.cluster(CrossClusterAuthorizationIntTests::clusterBuilder); + this.clusterConfig = clusterConfig; + } + +} 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..019c553df8 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 @@ -29,12 +29,16 @@ import org.opensearch.test.framework.data.TestIndex; import org.opensearch.test.framework.data.TestIndexOrAliasOrDatastream; import org.opensearch.test.framework.data.TestIndexTemplate; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.matcher.RestIndexMatchers; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.data.TestIndex.openSearchSecurityConfigIndex; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; 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.RestIndexMatchers.OnUserIndexMatcher.limitedToNone; @@ -261,14 +265,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 +274,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).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 +309,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 +332,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)) + ); + } } } @@ -477,7 +479,9 @@ 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 { @@ -531,19 +535,11 @@ 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).at("aggregations.indices.buckets[*].key").reducedBy(user.reference(READ)).whenEmpty(isOk()) + ); + } } @@ -595,11 +591,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 +610,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 +629,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 +648,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 +678,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 +697,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 +716,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 +800,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 +850,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()) + ); } } 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..576b856a6f 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 @@ -36,6 +36,11 @@ import org.opensearch.test.framework.data.TestIndex; import org.opensearch.test.framework.data.TestIndexOrAliasOrDatastream; import org.opensearch.test.framework.data.TestIndexTemplate; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import org.opensearch.test.framework.matcher.RestIndexMatchers; import static org.hamcrest.MatcherAssert.assertThat; @@ -510,13 +515,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 123cb9d48d..2ea9546f59 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 @@ -30,6 +30,7 @@ import org.opensearch.plugins.Plugin; import org.opensearch.plugins.SystemIndexPlugin; import org.opensearch.script.mustache.MustacheModulePlugin; +<<<<<<< HEAD import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -37,15 +38,27 @@ import org.opensearch.test.framework.data.TestData; import org.opensearch.test.framework.data.TestIndex; import org.opensearch.test.framework.data.TestIndexOrAliasOrDatastream; +======= +import org.opensearch.test.framework.data.TestAlias; +import org.opensearch.test.framework.data.TestData; +import org.opensearch.test.framework.data.TestIndex; +import org.opensearch.test.framework.data.TestIndexOrAliasOrDatastream; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +>>>>>>> d463b185 (Added new integration tests) 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.TestIndex.openSearchSecurityConfigIndex; +import static org.opensearch.test.framework.data.TestIndex.openSearchSecurityConfigIndex; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.cluster.TestRestClient.json; +<<<<<<< HEAD import static org.opensearch.test.framework.data.TestIndex.openSearchSecurityConfigIndex; +======= +>>>>>>> d463b185 (Added new integration tests) import static org.opensearch.test.framework.matcher.RestIndexMatchers.IndexMatcher; import static org.opensearch.test.framework.matcher.RestIndexMatchers.OnResponseIndexMatcher.containsExactly; import static org.opensearch.test.framework.matcher.RestIndexMatchers.OnUserIndexMatcher.limitedTo; @@ -119,16 +132,6 @@ public class IndexAuthorizationReadOnlyIntTests { openSearchSecurityConfigIndex() ); - static final List ALL_INDICES_EXCEPT_HIDDEN = List.of( - index_a1, - index_a2, - index_a3, - index_b1, - index_b2, - index_b3, - index_c1 - ); - static final List ALL_INDICES_AND_ALIASES = List.of( index_a1, index_a2, @@ -205,7 +208,11 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_a*") )// .reference(READ, limitedTo(index_a1, index_a2, index_a3, index_ax))// +<<<<<<< HEAD .indexMatcher(READ_NEXT_GEN, limitedTo(index_a1, index_a2, index_a3, index_ax))// +======= + .reference(READ_NEXT_GEN, limitedTo(index_a1, index_a2, index_a3, index_ax))// +>>>>>>> d463b185 (Added new integration tests) .reference(GET_ALIAS, limitedToNone()); /** @@ -220,10 +227,16 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_b*") )// .reference(READ, limitedTo(index_b1, index_b2, index_b3))// +<<<<<<< HEAD .indexMatcher("read_nextgen", limitedTo(index_b1, index_b2, index_b3))// .reference(GET_ALIAS, limitedToNone()); +======= + .reference(READ_NEXT_GEN, limitedTo(index_b1, index_b2, index_b3))// + .reference(GET_ALIAS, limitedToNone()); + +>>>>>>> d463b185 (Added new integration tests) /** * A simple user that can read only from index_b1 */ @@ -236,10 +249,16 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_b1") )// .reference(READ, limitedTo(index_b1))// +<<<<<<< HEAD .indexMatcher("read_nextgen", limitedTo(index_b1))// .reference(GET_ALIAS, limitedToNone()); +======= + .reference(READ_NEXT_GEN, limitedTo(index_b1))// + .reference(GET_ALIAS, limitedToNone()); + +>>>>>>> d463b185 (Added new integration tests) /** * A simple user that can read from index_c* */ @@ -252,10 +271,16 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_c*") )// .reference(READ, limitedTo(index_c1, alias_c1))// +<<<<<<< HEAD .indexMatcher("read_nextgen", limitedTo(index_c1))// .reference(GET_ALIAS, limitedToNone()); +======= + .reference(READ_NEXT_GEN, limitedTo(index_c1))// + .reference(GET_ALIAS, limitedToNone()); + +>>>>>>> d463b185 (Added new integration tests) /** * A user that has read privileges for alias_ab1*; these privileges are inherited to the member indices. * The user has no directly defined privileges on indices. @@ -269,10 +294,16 @@ public class IndexAuthorizationReadOnlyIntTests { .on("alias_ab1*") )// .reference(READ, limitedTo(index_a1, index_a2, index_a3, index_b1, alias_ab1))// +<<<<<<< HEAD .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)); +======= + .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)); + +>>>>>>> d463b185 (Added new integration tests) /** * A user that has read privileges for alias_c1; these privileges are inherited to the member indices. * The user has no directly defined privileges on indices. @@ -286,9 +317,14 @@ public class IndexAuthorizationReadOnlyIntTests { .on("alias_c1") )// .reference(READ, limitedTo(index_c1, alias_c1))// +<<<<<<< HEAD .indexMatcher(READ_NEXT_GEN, limitedTo(index_c1, alias_c1))// .reference(GET_ALIAS, limitedTo(index_c1, alias_c1)); +======= + .reference(READ_NEXT_GEN, limitedTo(index_c1, alias_c1))// + .reference(GET_ALIAS, limitedTo(index_c1, alias_c1)); +>>>>>>> d463b185 (Added new integration tests) /** * Same as LIMITED_USER_A with the addition of read privileges for index_hidden* and .index_hidden* */ @@ -301,10 +337,16 @@ 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))// +<<<<<<< HEAD .reference(READ_NEXT_GEN, limitedTo(index_a1, index_a2, index_a3, index_ax, index_hidden, index_hidden_dot))// .reference(GET_ALIAS, limitedToNone()); +======= + .reference(READ_NEXT_GEN, limitedTo(index_a1, index_a2, index_a3, index_ax, index_hidden, index_hidden_dot))// + .reference(GET_ALIAS, limitedToNone()); + +>>>>>>> d463b185 (Added new integration tests) /** * Same as LIMITED_USER_C with the addition of read privileges for ".system_index_plugin"; they also have the * explicit privilege "system:admin/system_index" that allows them accessing this index. @@ -332,9 +374,14 @@ public class IndexAuthorizationReadOnlyIntTests { .on(".system_index_plugin", ".alias_with_system_index") )// .reference(READ, limitedTo(index_c1, alias_c1, system_index_plugin, alias_with_system_index))// +<<<<<<< HEAD .reference(READ_NEXT_GEN, limitedTo(index_c1, system_index_plugin, alias_with_system_index))// .reference(GET_ALIAS, limitedTo(index_c1, alias_c1, system_index_plugin, alias_with_system_index)); +======= + .reference(READ_NEXT_GEN, limitedTo(index_c1, system_index_plugin, alias_with_system_index))// + .reference(GET_ALIAS, limitedToNone()); +>>>>>>> d463b185 (Added new integration tests) /** * This user has no privileges for indices that are used in this test. But they have privileges for other indices. @@ -351,7 +398,11 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_does_not_exist_*") )// .reference(READ, limitedToNone())// +<<<<<<< HEAD .reference(READ,READ_NEXT_GEN limitedToNone())// +======= + .reference(READ_NEXT_GEN, limitedToNone())// +>>>>>>> d463b185 (Added new integration tests) .reference(GET_ALIAS, limitedToNone()); /** @@ -364,10 +415,16 @@ public class IndexAuthorizationReadOnlyIntTests { .clusterPermissions("cluster_composite_ops_ro", "cluster_monitor") )// .reference(READ, limitedToNone())// +<<<<<<< HEAD .reference(READ_NEXT_GEN, limitedToNone())// .reference(GET_ALIAS, limitedToNone()); +======= + .reference(READ_NEXT_GEN, limitedToNone())// + .reference(GET_ALIAS, limitedToNone()); + +>>>>>>> d463b185 (Added new integration tests) /** * A user with "*" privileges on "*"; as it is a regular user, they are still subject to system index * restrictions and similar things. @@ -377,13 +434,20 @@ public class IndexAuthorizationReadOnlyIntTests { .roles( new TestSecurityConfig.Role("r1")// .clusterPermissions("*") +<<<<<<< HEAD .clusterPermissions("cluster_composite_ops_ro", "cluster_monitor") +======= +>>>>>>> d463b185 (Added new integration tests) .indexPermissions("*") .on("*")// )// .reference(READ, limitedTo(ALL_INDICES_AND_ALIASES_EXCEPT_SYSTEM_INDICES).and(index_ax))// +<<<<<<< HEAD .reference(READ_NEXT_GEN, 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))// +>>>>>>> d463b185 (Added new integration tests) .reference(GET_ALIAS, limitedTo(ALL_INDICES_AND_ALIASES_EXCEPT_SYSTEM_INDICES).and(index_ax)); /** @@ -394,7 +458,11 @@ public class IndexAuthorizationReadOnlyIntTests { .description("super unlimited (admin cert)")// .adminCertUser()// .reference(READ, unlimitedIncludingOpenSearchSecurityIndex())// +<<<<<<< HEAD .reference(READ_NEXT_GEN, unlimitedIncludingOpenSearchSecurityIndex())// +======= + .reference(READ_NEXT_GEN, unlimitedIncludingOpenSearchSecurityIndex())// +>>>>>>> d463b185 (Added new integration tests) .reference(GET_ALIAS, unlimitedIncludingOpenSearchSecurityIndex()); static final List USERS = ImmutableList.of( @@ -484,7 +552,11 @@ public void search_noPattern_allowNoIndicesFalse() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at("hits.hits[*]._index") +<<<<<<< HEAD .reducedBy(user.reference(READ)) +======= + .reducedBy(user.reference(READ)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(isForbidden()) ); } @@ -539,7 +611,11 @@ public void search_all_includeHidden() throws Exception { ? ALL_INDICES : ALL_INDICES_EXCEPT_SYSTEM_INDICES ).at("hits.hits[*]._index") +<<<<<<< HEAD .reducedBy(user.reference(READ)) +======= + .reducedBy(user.reference(READ)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { // next gen privilege evaluation @@ -547,7 +623,7 @@ public void search_all_includeHidden() throws Exception { // 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.indexMatcher("read")).whenEmpty(isOk()) + containsExactly(ALL_INDICES).at("hits.hits[*]._index").reducedBy(user.reference(READ)).whenEmpty(isOk()) ); } else { assertThat(httpResponse, isForbidden()); @@ -605,7 +681,11 @@ public void search_wildcard_includeHidden() throws Exception { ? ALL_INDICES : ALL_INDICES_EXCEPT_SYSTEM_INDICES ).at("hits.hits[*]._index") +<<<<<<< HEAD .reducedBy(user.reference(READ)) +======= + .reducedBy(user.reference(READ)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -635,7 +715,11 @@ public void search_staticIndices_ignoreUnavailable() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_b1).at("hits.hits[*]._index") +<<<<<<< HEAD .reducedBy(user.reference(READ)) +======= + .reducedBy(user.reference(READ)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -699,17 +783,13 @@ public void search_staticIndices_systemIndex_alias() throws Exception { if (user == SUPER_UNLIMITED_USER) { assertThat(httpResponse, isOk()); - assertThat( - httpResponse, - containsExactly(system_index_plugin).at("hits.hits[*]._index")); + assertThat(httpResponse, containsExactly(system_index_plugin).at("hits.hits[*]._index")); } else if (clusterConfig == ClusterConfig.LEGACY_PRIVILEGES_EVALUATION) { if (user == UNLIMITED_USER || user == LIMITED_USER_C_WITH_SYSTEM_INDICES) { // 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")); + assertThat(httpResponse, containsExactly().at("hits.hits[*]._index")); } else { assertThat(httpResponse, isForbidden()); } @@ -721,15 +801,16 @@ public void search_staticIndices_systemIndex_alias() throws Exception { assertThat(httpResponse, containsExactly().at("hits.hits[*]._index")); } else { assertThat( - httpResponse, - containsExactly(system_index_plugin).at("hits.hits[*]._index").reducedBy(user.indexMatcher("read")).whenEmpty(isForbidden())); + 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)) { assertThat(httpResponse, isOk()); - assertThat( - httpResponse, - containsExactly(system_index_plugin).at("hits.hits[*]._index")); + assertThat(httpResponse, containsExactly(system_index_plugin).at("hits.hits[*]._index")); } else { assertThat(httpResponse, isForbidden()); } @@ -746,7 +827,11 @@ public void search_indexPattern() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3).at("hits.hits[*]._index") +<<<<<<< HEAD .reducedBy(user.reference(READ)) +======= + .reducedBy(user.reference(READ)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -765,7 +850,11 @@ public void search_indexPattern_minus() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index") +<<<<<<< HEAD .reducedBy(user.reference(READ)) +======= + .reducedBy(user.reference(READ)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -831,7 +920,11 @@ public void search_indexPatternAndStatic_negation() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index") +<<<<<<< HEAD .reducedBy(user.reference(READ)) +======= + .reducedBy(user.reference(READ)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -906,7 +999,7 @@ public void search_indexPattern_includeHidden() throws Exception { index_hidden_dot, system_index_plugin ).at("hits.hits[*]._index") - .reducedBy(user.indexMatcher("read")) + .reducedBy(user.reference(READ)) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -930,7 +1023,7 @@ public void search_alias() throws Exception { ); } else { // The new privilege evaluation never replaces aliases - if (user.indexMatcher("read").covers(alias_ab1)) { + 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]")); @@ -948,7 +1041,11 @@ public void search_alias_pattern() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index") +<<<<<<< HEAD .reducedBy(user.reference(READ)) +======= + .reducedBy(user.reference(READ)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -964,8 +1061,10 @@ public void search_alias_pattern_negation() throws Exception { 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) + // If the system index privilege is enabled, we might also see the system_index_plugin index (being included via the + // alias) assertThat( +<<<<<<< HEAD httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_c1).at("hits.hits[*]._index") .reducedBy(user.reference(READ)) @@ -977,6 +1076,19 @@ public void search_alias_pattern_negation() throws Exception { containsExactly(index_a1, index_a2, index_a3, index_b1, index_c1).at("hits.hits[*]._index") .reducedBy(user.reference(READ)) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) +======= + 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()) +>>>>>>> d463b185 (Added new integration tests) ); } } else { @@ -1006,14 +1118,23 @@ public void search_alias_pattern_includeHidden() throws Exception { ); } else if (clusterConfig == ClusterConfig.LEGACY_PRIVILEGES_EVALUATION_SYSTEM_INDEX_PERMISSION) { // For all users without the system index permission, SystemIndexAccessEvaluator shuts the door - // For the user with the system index permission, that happens as well, as SystemIndexAccessEvaluator expects the permission for all requested indices, even if they are not system indices - assertThat(httpResponse, isForbidden()); + // For the user with the system index permission, that happens as well, as SystemIndexAccessEvaluator expects the + // permission for all requested indices, even if they are not system indices + assertThat(httpResponse, isForbidden()); } else { assertThat( +<<<<<<< HEAD httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_c1, system_index_plugin).at("hits.hits[*]._index") .reducedBy(user.reference(READ)) .whenEmpty(isOk())); +======= + httpResponse, + containsExactly(index_a1, index_a2, index_a3, index_b1, index_c1, system_index_plugin).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(isOk()) + ); +>>>>>>> d463b185 (Added new integration tests) } } else { assertThat(httpResponse, isForbidden()); @@ -1021,7 +1142,6 @@ public void search_alias_pattern_includeHidden() throws Exception { } } - @Test public void search_aliasAndIndex_ignoreUnavailable() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { @@ -1038,9 +1158,9 @@ public void search_aliasAndIndex_ignoreUnavailable() throws Exception { // 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.indexMatcher("read").covers(alias_ab1)) { + } 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.indexMatcher("read").covers(index_b1)) { + } 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")); @@ -1157,7 +1277,7 @@ 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"); - IndexApiResponseMatchers.OnResponseIndexMatcher indexMatcher = containsExactly( + RestIndexMatchers.OnResponseIndexMatcher indexMatcher = containsExactly( index_a1, index_a2, index_a3, @@ -1220,7 +1340,18 @@ public void search_pit_wrongIndex() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.post("index_a*/_search/point_in_time?keep_alive=1m"); +<<<<<<< HEAD if (user.reference(READ).coversAll(index_a1, index_a2, index_a3)) { +======= + RestIndexMatchers.OnResponseIndexMatcher indexMatcher = containsExactly(index_a1, index_a2, index_a3); + + if (indexMatcher.reducedBy(user.reference(READ)).isEmpty()) { + assertThat( + httpResponse, + isForbidden("/error/root_cause/0/reason", "no permissions for [indices:data/read/point_in_time/create]") + ); + } else { +>>>>>>> d463b185 (Added new integration tests) assertThat(httpResponse, isOk()); String pitId = httpResponse.getTextFromJsonBody("/pit_id"); httpResponse = restClient.postJson("index_b*/_search?size=1000", String.format(""" @@ -1403,7 +1534,11 @@ public void cat_indices_all() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at("$[*].index") +<<<<<<< HEAD .reducedBy(user.reference(READ)) +======= + .reducedBy(user.reference(READ)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); @@ -1422,7 +1557,11 @@ public void cat_indices_pattern() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3).at("$[*].index") +<<<<<<< HEAD .reducedBy(user.reference(READ)) +======= + .reducedBy(user.reference(READ)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -1450,7 +1589,7 @@ public void cat_indices_all_includeHidden() throws Exception { assertThat( httpResponse, containsExactly(ALL_INDICES).at("$[*].index") - .reducedBy(user.indexMatcher("read")) + .reducedBy(user.reference(READ)) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -1491,7 +1630,7 @@ public void cat_aliases_pattern() throws Exception { if (!user.reference(GET_ALIAS).isEmpty()) { assertThat( httpResponse, - containsExactly(alias_ab1).at("$[*].alias").reducedBy(user.indexMatcher("get_alias")).whenEmpty(isOk()) + containsExactly(alias_ab1).at("$[*].alias").reducedBy(user.reference(GET_ALIAS)).whenEmpty(isOk()) ); } else { assertThat(httpResponse, isForbidden()); @@ -1500,7 +1639,7 @@ public void cat_aliases_pattern() throws Exception { if (!user.reference(GET_ALIAS).isEmpty()) { assertThat( httpResponse, - containsExactly(alias_ab1).at("$[*].alias").reducedBy(user.indexMatcher("get_alias")).whenEmpty(isForbidden()) + containsExactly(alias_ab1).at("$[*].alias").reducedBy(user.reference(GET_ALIAS)).whenEmpty(isForbidden()) ); } else { assertThat(httpResponse, isForbidden()); @@ -1536,7 +1675,11 @@ public void index_stats_pattern() throws Exception { assertThat( httpResponse, containsExactly(index_b1, index_b2, index_b3).at("indices.keys()") +<<<<<<< HEAD .reducedBy(user.reference(READ)) +======= + .reducedBy(user.reference(READ)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -1559,7 +1702,11 @@ public void getAlias_all() throws Exception { assertThat( httpResponse, containsExactly(alias_ab1, alias_c1, alias_with_system_index).at("$.*.aliases.keys()") +<<<<<<< HEAD .reducedByuser.reference(GET_ALIAS)) +======= + .reducedBy(user.reference(GET_ALIAS)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); assertThat( @@ -1670,10 +1817,14 @@ public void getAlias_indexPattern_includeHidden() throws Exception { system_index_plugin ).at("$.keys()") ); - } else if (!user.indexMatcher("get_alias").isEmpty()) { + } else if (!user.reference(GET_ALIAS).isEmpty()) { assertThat( httpResponse, +<<<<<<< HEAD containsExactly(alias_ab1, alias_c1, alias_with_system_index).at("$.*.aliases.keys()") +======= + containsExactly(alias_ab1, alias_c1).at("$.*.aliases.keys()") +>>>>>>> d463b185 (Added new integration tests) .reducedBy(user.reference(GET_ALIAS)) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); @@ -1773,7 +1924,11 @@ public void resolve_indexPattern() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3).at("$.*[*].name") +<<<<<<< HEAD .reducedBy(user.reference(READ)) +======= + .reducedBy(user.reference(READ)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -1846,7 +2001,7 @@ public void field_caps_alias() throws Exception { .whenEmpty(isForbidden()) ); } else { - if (user.indexMatcher("read").covers(alias_ab1)) { + if (user.reference(READ).covers(alias_ab1)) { assertThat(httpResponse, isOk()); assertThat(httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("indices")); } else { @@ -1864,7 +2019,11 @@ public void field_caps_aliasPattern() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("indices") +<<<<<<< HEAD .reducedBy(user.reference(READ)) +======= + .reducedBy(user.reference(READ)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -1907,7 +2066,11 @@ public void field_caps_indexPattern_minus() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("indices") +<<<<<<< HEAD .reducedBy(user.reference(READ)) +======= + .reducedBy(user.reference(READ)) +>>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); @@ -1989,6 +2152,97 @@ public void pit_catSegments_all() throws Exception { } } + @Test + public void pit_list_all() throws Exception { + String indexA1pitId = createPit(index_a1); + + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("_search/point_in_time/_all"); + + 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 && user != LIMITED_USER_OTHER_PRIVILEGES) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } + } else { + // 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); + } + } + + @Test + public void pit_delete() throws Exception { + String indexA1pitId = createPit(index_a1); + + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.delete("_search/point_in_time", json("pit_id", List.of(indexA1pitId))); + + if (user.reference(READ).covers(index_a1)) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } + } finally { + deletePit(indexA1pitId); + } + } + + @Test + public void pit_catSegments() throws Exception { + String indexA1pitId = createPit(index_a1); + + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("_cat/pit_segments", json("pit_id", List.of(indexA1pitId))); + + if (user.reference(READ).covers(index_a1)) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } + } finally { + deletePit(indexA1pitId); + } + } + + @Test + public void pit_catSegments_all() throws Exception { + String indexA1pitId = createPit(index_a1); + + try (TestRestClient restClient = cluster.getRestClient(user)) { + TestRestClient.HttpResponse httpResponse = restClient.get("_cat/pit_segments/_all"); + + if (clusterConfig.legacyPrivilegeEvaluation) { + // The user needs to have the privilege for all indices. If it is only granted for a subset of indices, this will be + // forbidden. + if (user == UNLIMITED_USER || user == SUPER_UNLIMITED_USER) { + assertThat(httpResponse, isOk()); + } else { + assertThat(httpResponse, isForbidden()); + } + } else { + // New privilege evaluation: this is now a separate 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); + } + } + @ParametersFactory(shuffle = false, argumentFormatting = "%1$s, %3$s") public static Collection params() { List result = new ArrayList<>(); 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..bf141b9cc4 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 @@ -28,6 +28,9 @@ import org.opensearch.action.admin.indices.refresh.RefreshRequest; import org.opensearch.action.admin.indices.settings.put.UpdateSettingsRequest; import org.opensearch.common.settings.Settings; +import org.opensearch.test.framework.data.TestAlias; +import org.opensearch.test.framework.data.TestIndex; +import org.opensearch.test.framework.data.TestIndexOrAliasOrDatastream; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.cluster.LocalCluster; @@ -52,6 +55,7 @@ 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. @@ -666,6 +670,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 +728,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 +761,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 +771,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 +808,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 +873,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 +902,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 +922,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 +958,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 +986,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 +1016,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 +1040,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 +1148,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(); @@ -1126,12 +1204,20 @@ public void rollover_explicitTargetIndex() throws Exception { } }"""); + System.out.println(httpResponse.getBody()); + if (clusterConfig.legacyPrivilegeEvaluation) { if (user.reference(MANAGE_ALIAS).covers(index_bw1) && user.reference(MANAGE_INDEX).covers(index_bw2)) { assertThat(httpResponse, isOk()); } 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 66% 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..ac60f10273 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; @@ -19,6 +19,7 @@ import org.opensearch.script.mustache.MustacheModulePlugin; import org.opensearch.script.mustache.RenderSearchTemplateAction; +import org.opensearch.test.framework.data.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.cluster.ClusterManager; @@ -30,21 +31,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,11 +54,6 @@ 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}}\\\"]}}}\"}"; @@ -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..84872471a5 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; @@ -25,6 +26,8 @@ import org.junit.runner.RunWith; import org.opensearch.action.admin.indices.refresh.RefreshRequest; +import org.opensearch.test.framework.data.TestIndex; +import org.opensearch.test.framework.data.TestIndexOrAliasOrDatastream; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -47,6 +50,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(); @@ -86,6 +90,7 @@ public class SnapshotAuthorizationIntTests { static TestSecurityConfig.User LIMITED_USER_A = new TestSecurityConfig.User("limited_user_A")// .description("index_a*")// .roles( + // new TestSecurityConfig.Role("r1")// .clusterPermissions("cluster_composite_ops", "cluster_monitor", "manage_snapshots")// .indexPermissions("read", "indices_monitor", "indices:admin/refresh*") @@ -99,6 +104,7 @@ public class SnapshotAuthorizationIntTests { static TestSecurityConfig.User LIMITED_USER_B = new TestSecurityConfig.User("limited_user_B")// .description("index_b*")// .roles( + // new TestSecurityConfig.Role("r1")// .clusterPermissions("cluster_composite_ops", "cluster_monitor", "manage_snapshots")// .indexPermissions("read", "indices_monitor", "indices:admin/refresh*") @@ -112,6 +118,7 @@ public class SnapshotAuthorizationIntTests { static TestSecurityConfig.User LIMITED_USER_B_SYSTEM_INDEX = new TestSecurityConfig.User("limited_user_B_system_index")// .description("index_b*, .system_index_plugin")// .roles( + // new TestSecurityConfig.Role("r1")// .clusterPermissions("cluster_composite_ops", "cluster_monitor", "manage_snapshots")// .indexPermissions("read", "indices_monitor", "indices:admin/refresh*") @@ -130,6 +137,7 @@ public class SnapshotAuthorizationIntTests { static TestSecurityConfig.User LIMITED_USER_AB = new TestSecurityConfig.User("limited_user_AB")// .description("index_a*, index_b*")// .roles( + // new TestSecurityConfig.Role("r1")// .clusterPermissions("cluster_composite_ops", "cluster_monitor", "manage_snapshots")// .indexPermissions("read", "indices_monitor", "indices:admin/refresh*") @@ -218,6 +226,7 @@ public void restore_singleIndex() throws Exception { TestRestClient.HttpResponse httpResponse = restClient.post( "_snapshot/test_repository/single_index_snapshot/_restore?wait_for_completion=true" ); + System.out.println(httpResponse.getBody()); assertThat(httpResponse, containsExactly(index_awx1).at("snapshot.indices").butForbiddenIfIncomplete(user.reference(WRITE))); @@ -237,6 +246,7 @@ public void restore_singleIndex_rename1() throws Exception { "_snapshot/test_repository/single_index_snapshot/_restore?wait_for_completion=true", json("rename_pattern", "index_(.+)x1", "rename_replacement", "index_$1x2") ); + System.out.println(httpResponse.getBody()); assertThat(httpResponse, containsExactly(index_awx2).at("snapshot.indices").butForbiddenIfIncomplete(user.reference(WRITE))); @@ -256,6 +266,7 @@ public void restore_singleIndex_rename2() throws Exception { "_snapshot/test_repository/single_index_snapshot/_restore?wait_for_completion=true", json("rename_pattern", "index_a(.*)", "rename_replacement", "index_b$1") ); + System.out.println(httpResponse.getBody()); assertThat(httpResponse, containsExactly(index_bwx1).at("snapshot.indices").butForbiddenIfIncomplete(user.reference(WRITE))); @@ -275,6 +286,7 @@ public void restore_singleIndex_renameToSystemIndex() throws Exception { "_snapshot/test_repository/single_index_snapshot/_restore?wait_for_completion=true", json("rename_pattern", "index_awx1", "rename_replacement", system_index_plugin_not_existing.name()) ); + System.out.println(httpResponse.getBody()); if (clusterConfig.systemIndexPrivilegeEnabled || user == SUPER_UNLIMITED_USER) { assertThat( @@ -302,6 +314,7 @@ public void restore_singleIndexFromAllIndices() throws Exception { "_snapshot/test_repository/all_index_snapshot/_restore?wait_for_completion=true", json("indices", "index_awx1") ); + System.out.println(httpResponse.getBody()); assertThat(httpResponse, containsExactly(index_awx1).at("snapshot.indices").butForbiddenIfIncomplete(user.reference(WRITE))); @@ -323,6 +336,7 @@ public void restore_all_globalState() throws Exception { "_snapshot/test_repository/all_index_snapshot/_restore?wait_for_completion=true", json("include_global_state", true) ); + System.out.println(httpResponse.getBody()); if (user == SUPER_UNLIMITED_USER) { assertThat(httpResponse, isOk()); diff --git a/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java b/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java index 849d76475a..104bfafa36 100644 --- a/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java +++ b/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java @@ -26,7 +26,6 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; import org.opensearch.core.common.Strings; import org.opensearch.security.auditlog.impl.AuditMessage; diff --git a/src/integrationTest/java/org/opensearch/security/systemindex/SystemIndexDisabledTests.java b/src/integrationTest/java/org/opensearch/security/systemindex/SystemIndexDisabledTests.java index 6fd48fe8f0..52466cb50a 100644 --- a/src/integrationTest/java/org/opensearch/security/systemindex/SystemIndexDisabledTests.java +++ b/src/integrationTest/java/org/opensearch/security/systemindex/SystemIndexDisabledTests.java @@ -13,6 +13,7 @@ import java.util.Map; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 4f94af9796..8d3329c441 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -35,6 +35,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -141,6 +142,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 +169,7 @@ 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); - } - + // The user's roles will be collected by aggregateRoles() when the configuration is written return this; } @@ -199,6 +201,7 @@ public TestSecurityConfig roles(Role... roles) { if (this.roles.containsKey(role.name)) { throw new IllegalStateException("Role with name " + role.name + " is already defined"); } + role.addedIndependentlyOfUser = true; this.roles.put(role.name, role); } @@ -265,6 +268,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 +286,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 +336,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); @@ -457,6 +469,10 @@ public static final class User implements UserCredentialsHolder, ToXContentObjec private String password; List roles = new ArrayList<>(); List backendRoles = new ArrayList<>(); + /** + * This will be initialized by aggregateRoles() + */ + Set roleNames; String requestedTenant; private Map attributes = new HashMap<>(); private Map, Object> matchers = new HashMap<>(); @@ -487,11 +503,7 @@ public User password(String password) { } 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()) - ); + this.roles.addAll(Arrays.asList(roles)); return this; } @@ -538,7 +550,21 @@ public String getPassword() { } public Set getRoleNames() { - return roles.stream().map(Role::getName).collect(Collectors.toSet()); + return roleNames; + } + + public String getDescription() { + return description; + } + + @Override + public boolean isAdminCertUser() { + return adminCertUser; + } + + public User adminCertUser() { + this.adminCertUser = true; + return this; } public String getDescription() { @@ -657,6 +683,12 @@ public static class Role implements ToXContentObject { private String description; + /** + * This will be set to true, if this was added using the roles() method on TestSecurityConfig. + * Then, we will consider this a role which is shared between users and we won't scope its name. + */ + private boolean addedIndependentlyOfUser = false; + public Role(String name) { this(name, null); } @@ -1095,7 +1127,7 @@ public void initIndex(Client client) { if (auditConfiguration != null) { writeSingleEntryConfigToIndex(client, CType.AUDIT, "config", auditConfiguration); } - writeConfigToIndex(client, CType.ROLES, roles); + writeConfigToIndex(client, CType.ROLES, aggregateRoles()); writeConfigToIndex(client, CType.INTERNALUSERS, internalUsers); writeConfigToIndex(client, CType.ROLESMAPPING, rolesMapping); writeConfigToIndex(client, CType.ACTIONGROUPS, actionGroups); @@ -1107,7 +1139,36 @@ public void initIndex(Client client) { writeConfigToIndex(client, entry.getKey(), entry.getValue()); } } + } + + /** + * Merges the globally defined roles with the roles defined by user. Roles defined by user will be scoped + * so that user definitions cannot interfere with others. + */ + private Map aggregateRoles() { + Map result = new HashMap<>(this.roles); + + for (User user : this.internalUsers.values()) { + if (user.roleNames == null) { + user.roleNames = new HashSet<>(); + } + + for (Role role : user.roles) { + if (role.addedIndependentlyOfUser) { + // This is a globally defined role, we just use this + user.roleNames.add(role.name); + } else { + // This is role that is locally defined for the user; let's scope the name + if (!role.name.startsWith("user_" + user.name)) { + role.name = "user_" + user.name + "__" + role.name; + } + user.roleNames.add(role.name); + result.put(role.name, role); + } + } + } + return result; } 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..8c6a17b34c 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -58,6 +58,11 @@ import org.opensearch.test.framework.AuthFailureListeners; import org.opensearch.test.framework.AuthzDomain; import org.opensearch.test.framework.OnBehalfOfConfig; +import org.opensearch.test.framework.data.TestAlias; +import org.opensearch.test.framework.data.TestComponentTemplate; +import org.opensearch.test.framework.data.TestDataStream; +import org.opensearch.test.framework.data.TestIndex; +import org.opensearch.test.framework.data.TestIndexTemplate; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.XffConfig; @@ -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/RestDocumentMatchers.java b/src/integrationTest/java/org/opensearch/test/framework/matcher/RestDocumentMatchers.java index 17e5f73dbb..6bbc10f79c 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/matcher/RestDocumentMatchers.java +++ b/src/integrationTest/java/org/opensearch/test/framework/matcher/RestDocumentMatchers.java @@ -27,6 +27,7 @@ import org.hamcrest.DiagnosingMatcher; import org.opensearch.common.geo.GeoPoint; +import org.opensearch.test.framework.data.TestData; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import org.opensearch.test.framework.data.TestData; diff --git a/src/integrationTest/resources/log4j2-test.properties b/src/integrationTest/resources/log4j2-test.properties index d0bb23fa3f..9609611808 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 diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 9c9586e6d7..138742e112 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -36,7 +36,6 @@ 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; diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index 75c1693c2c..d47d9be4c1 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -80,6 +80,15 @@ PrivilegesEvaluatorResponse hasIndexPrivilege( 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. @@ -118,6 +127,11 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( 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, 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 d8c6dd0899..977cbfe15c 100644 --- a/src/main/java/org/opensearch/security/privileges/IndexPattern.java +++ b/src/main/java/org/opensearch/security/privileges/IndexPattern.java @@ -36,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. @@ -54,12 +54,19 @@ public class IndexPattern { */ private final ImmutableList dateMathExpressions; private final int hashCode; - - private IndexPattern(WildcardMatcher staticPattern, ImmutableList patternTemplates, ImmutableList dateMathExpressions) { + private final boolean memberIndexPrivilegesYieldAliasPrivileges; + + 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( @@ -92,18 +99,21 @@ public boolean matches( } return false; - } else { - // 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. + } 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; + 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; - } + // If we could match all members, we have a match + return true; + } else { + return false; + } } private boolean matchesDirectly(String indexOrAliasOrDatastream, PrivilegesEvaluationContext context) @@ -208,7 +218,12 @@ public IndexPattern dynamicOnly() { if (patternTemplates.isEmpty() && dateMathExpressions.isEmpty()) { return EMPTY; } else { - return new IndexPattern(WildcardMatcher.NONE, this.patternTemplates, this.dateMathExpressions); + return new IndexPattern( + WildcardMatcher.NONE, + this.patternTemplates, + this.dateMathExpressions, + this.memberIndexPrivilegesYieldAliasPrivileges + ); } } @@ -237,6 +252,11 @@ public static class Builder { private List 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++) { @@ -261,18 +281,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/IndicesRequestResolver.java b/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java index 98c506bf2c..da18b4ea06 100644 --- a/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java +++ b/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java @@ -21,7 +21,7 @@ import org.opensearch.cluster.metadata.ResolvedIndices; public class IndicesRequestResolver { - private final IndexNameExpressionResolver indexNameExpressionResolver; + protected final IndexNameExpressionResolver indexNameExpressionResolver; public IndicesRequestResolver(IndexNameExpressionResolver indexNameExpressionResolver) { this.indexNameExpressionResolver = indexNameExpressionResolver; diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java b/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java index d8d63d6fb8..be47961efd 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java @@ -259,7 +259,7 @@ static PrivilegesEvaluationType getFrom(SecurityDynamicConfiguration c if (config == null || config.dynamic == null) { return defaultValue; } - if (config.dynamic.privilegesEvaluationType.equalsIgnoreCase(NEXT_GEN.name())) { + if (NEXT_GEN.name().equalsIgnoreCase(config.dynamic.privilegesEvaluationType)) { return NEXT_GEN; } else { return LEGACY; diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java index 9c2728bc7c..f6cddc7d57 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"); @@ -195,17 +204,24 @@ public PrivilegesEvaluatorResponse originalResult(PrivilegesEvaluatorResponse or return this; } - public PrivilegesEvaluatorResponse markPending() { - this.state = PrivilegesEvaluatorResponseState.PENDING; - return this; - } - - 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); + 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 @@ -225,6 +241,13 @@ 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; diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java index cc38f52988..9486fdff3d 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java @@ -47,6 +47,18 @@ private ReplaceResult(boolean continueEvaluation, boolean accessDenied, CreateIn 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); diff --git a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java index b83e174600..21410a442b 100644 --- a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java @@ -78,21 +78,17 @@ 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/SpecialIndices.java b/src/main/java/org/opensearch/security/privileges/SpecialIndices.java index dd791f71ae..2cc0480978 100644 --- a/src/main/java/org/opensearch/security/privileges/SpecialIndices.java +++ b/src/main/java/org/opensearch/security/privileges/SpecialIndices.java @@ -1,3 +1,13 @@ +/* + * 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; @@ -14,11 +24,11 @@ public class SpecialIndices { public SpecialIndices(Settings settings) { this.securityIndex = settings.get( - ConfigConstants.SECURITY_CONFIG_INDEX_NAME, - ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX + 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) + settings.getAsList(ConfigConstants.SECURITY_SYSTEM_INDICES_KEY, ConfigConstants.SECURITY_SYSTEM_INDICES_DEFAULT) ); } @@ -30,5 +40,4 @@ public boolean isSystemIndex(String index) { return this.manuallyConfiguredSystemIndexMatcher.test(index) || SystemIndexRegistry.matchesSystemIndexPattern(index); } - } 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 e1c49c2728..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,7 @@ 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; @@ -105,14 +106,21 @@ public class RoleBasedActionPrivileges extends RuntimeOptimizedActionPrivileges * @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 + Settings settings, + boolean breakDownAliases ) { - super(new ClusterPrivileges(roles, actionGroups), new IndexPrivileges(roles, actionGroups, specialIndexProtection)); + 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); @@ -365,10 +373,15 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde IndexPrivileges( SecurityDynamicConfiguration roles, FlattenedActionGroups actionGroups, - SpecialIndexProtection specialIndexProtection + SpecialIndexProtection specialIndexProtection, + boolean memberIndexPrivilegesYieldAliasPrivileges ) { super(specialIndexProtection); + Function indexPatternBuilder = k -> new IndexPattern.Builder( + memberIndexPrivilegesYieldAliasPrivileges + ); + Map> rolesToActionToIndexPattern = new HashMap<>(); Map> rolesToActionPatternToIndexPattern = new HashMap<>(); Map> actionToRolesWithWildcardIndexPrivileges = new HashMap<>(); @@ -398,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()); } @@ -418,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("*")) { @@ -430,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) { @@ -438,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()); } } @@ -550,6 +563,56 @@ protected IntermediateResult providesPrivilege( 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 PrivilegesEvaluatorResponse.insufficient(checkTable) + .reason("The user does not have any index privileges for the requested action"); + } + /** * 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 @@ -608,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 @@ -748,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. @@ -938,7 +1001,7 @@ static Map relevantOnly( ImmutableMap.Builder builder = ImmutableMap.builder(); for (IndexAbstraction indexAbstraction : indices.values()) { - if (universallyDeniedIndices.test(indexAbstraction.getName())) { + if (universallyDeniedIndices != null && universallyDeniedIndices.test(indexAbstraction.getName())) { continue; } 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 480042bcf7..80ffc66693 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java @@ -12,6 +12,8 @@ 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; @@ -27,6 +29,7 @@ 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; @@ -55,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 @@ -123,9 +135,30 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( } 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); + } + /** * Checks whether this instance provides explicit privileges for the combination of the provided action, * the provided indices and the provided roles. @@ -153,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. */ @@ -280,12 +363,6 @@ protected StaticIndexPrivileges(SpecialIndexProtection specialIndexProtection) { * 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. @@ -298,6 +375,19 @@ protected abstract IntermediateResult providesPrivilege( 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, + CheckTable checkTable + ); + /** * Checks whether this instance provides explicit privileges for the combination of the provided action, * the provided indices and the provided roles. @@ -431,7 +521,7 @@ protected PrivilegesEvaluatorResponse finalizeResult(PrivilegesEvaluationContext } if (checkTable.isComplete()) { - return PrivilegesEvaluatorResponse.ok(); + return PrivilegesEvaluatorResponse.ok(checkTable); } Set availableIndices = checkTable.getCompleteRows(); 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 31c655e3d2..23d83d3d8b 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; @@ -77,10 +78,16 @@ public static ImmutableMap buildFromMap( * @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, SpecialIndexProtection specialIndexProtection) { + public SubjectBasedActionPrivileges( + RoleV7 role, + FlattenedActionGroups actionGroups, + SpecialIndexProtection specialIndexProtection, + boolean breakDownAliases + ) { super( new ClusterPrivileges(actionGroups.resolve(role.getCluster_permissions())), - new IndexPrivileges(role, actionGroups, specialIndexProtection) + new IndexPrivileges(role, actionGroups, specialIndexProtection, breakDownAliases), + breakDownAliases ); } @@ -236,9 +243,18 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde /** * Creates pre-computed index privileges based on the given parameters. */ - IndexPrivileges(RoleV7 role, FlattenedActionGroups actionGroups, SpecialIndexProtection specialIndexProtection) { + 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<>(); Set actionWithWildcardIndexPrivileges = new HashSet<>(); @@ -256,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()); } @@ -271,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()); } } @@ -350,6 +364,45 @@ protected IntermediateResult providesPrivilege( 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 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 * indices (using *) for the given actions. Returns null otherwise. Then, further checks must be done to check 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 index ca9e540a8d..74e0188bfa 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java @@ -50,6 +50,7 @@ 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; @@ -62,7 +63,6 @@ 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; @@ -192,15 +192,15 @@ public PrivilegesEvaluator( ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES ); - snapshotRestoreEvaluator = new SnapshotRestoreEvaluator( - settings, - auditLog, - clusterService != null ? () -> clusterService.state().nodes().isLocalNodeElectedClusterManager() : () -> false - ); + 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 IndicesRequestResolver(resolver); + this.indicesRequestResolver = new LegacyIndicesRequestResolver(resolver, isLocalNodeElectedClusterManager); this.pluginIdToActionPrivileges.putAll(createActionPrivileges(pluginIdToRolePrivileges, staticActionGroups)); this.updateConfiguration(actionGroups, rolesConfiguration, generalConfiguration); @@ -222,7 +222,8 @@ public void updateConfiguration( rolesConfiguration, flattenedActionGroups, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - settings + settings, + true ); Metadata metadata = clusterStateSupplier.get().metadata(); actionPrivileges.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); @@ -344,6 +345,9 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) OptionallyResolvedIndices optionallyResolvedIndices = context.getResolvedRequest(); if (isDebugEnabled) { + if (request instanceof IndicesRequest indicesRequest) { + log.debug("IndicesRequest: {} {}", indicesRequest.indices(), indicesRequest.indicesOptions()); + } log.debug("ResolvedIndices: {}", optionallyResolvedIndices); } @@ -367,6 +371,13 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) return presponse; } + 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(); @@ -431,7 +442,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) } } - if (checkDocAllowListHeader(user, action0, request)) { + if (DocumentAllowList.isAllowed(request, threadContext)) { return PrivilegesEvaluatorResponse.ok(); } @@ -483,6 +494,22 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) 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()) { @@ -509,7 +536,8 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) if (presponse.isAllowed()) { if (checkFilteredAliases(optionallyResolvedIndices, action0, isDebugEnabled)) { - return presponse; + return PrivilegesEvaluatorResponse.insufficient(action0) + .reason("It is not possible to read from indices with more than two filtered aliases"); } if (isDebugEnabled) { @@ -720,37 +748,6 @@ private boolean checkFilteredAliases(OptionallyResolvedIndices optionallyRequest return false; } - private boolean checkDocAllowListHeader(User user, String action, ActionRequest request) { - 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 List toString(List aliases) { if (aliases == null || aliases.size() == 0) { return Collections.emptyList(); @@ -779,7 +776,8 @@ private static Map createActionPrivileges( new SubjectBasedActionPrivileges( entry.getValue(), staticActionGroups, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + true ) ); } diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java index acd38639d9..af1765c38a 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java @@ -38,10 +38,14 @@ 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.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; @@ -212,7 +216,9 @@ private PrivilegesEvaluatorResponse evaluateSystemIndicesAccess( ) { 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) { @@ -364,6 +370,46 @@ private PluginSystemIndexSelection areIndicesPluginSystemIndices( } } + /** + * 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 (!(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, 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 index 0a81e036f2..34bd625163 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/ActionConfiguration.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/ActionConfiguration.java @@ -123,8 +123,10 @@ boolean isClusterPermission(String action) { } else if (this.clusterActions.contains(action)) { return true; } else { - // TODO maybe switch to index: - return action.startsWith("cluster:"); + // TODO maybe move to "indices:" prefix + return action.startsWith("cluster:") + || action.startsWith("indices:admin/template/") + || action.startsWith("indices:admin/index_template/"); } } 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 index ffd7fa1956..f87589316a 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/PrivilegesEvaluator.java @@ -413,25 +413,33 @@ PrivilegesEvaluatorResponse checkIndexPermissionBasic( } } } else if (!presponse.isAllowed()) { - // 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 - - if (isIndexReductionForNoPrivilegesPossible(request) && optionallyResolvedIndices instanceof ResolvedIndices resolvedIndices) { - // We only allow returning empty results if the current user has at least the necessary privileges for any index - 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); + + 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; 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 6b0b21cc54..0ab44e4b55 100644 --- a/src/main/java/org/opensearch/security/privileges/dlsfls/AbstractRuleBasedPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/dlsfls/AbstractRuleBasedPrivileges.java @@ -619,7 +619,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) { @@ -783,7 +783,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. 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/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/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index bc0178d5e8..61fc5434f2 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -102,7 +102,8 @@ PrivilegesEvaluator createPrivilegesEvaluator(SecurityDynamicConfiguration Date: Wed, 22 Oct 2025 03:18:38 +0200 Subject: [PATCH 07/29] Rebased to recent main Signed-off-by: Nils Bandener --- .../ServiceAccountAuthenticationTest.java | 1 - .../dlsfls/FlsFmIntegrationTests.java | 2 - .../CrossClusterAuthorizationIntTests.java | 2 +- ...taStreamAuthorizationReadOnlyIntTests.java | 57 +-- ...aStreamAuthorizationReadWriteIntTests.java | 5 - .../IndexAuthorizationReadOnlyIntTests.java | 360 +++--------------- .../IndexAuthorizationReadWriteIntTests.java | 6 +- .../int_tests/MiscPrivilegesIntTests.java | 1 - .../SnapshotAuthorizationIntTests.java | 13 - .../opensearch/security/rest/WhoAmITests.java | 1 + .../systemindex/SystemIndexDisabledTests.java | 1 - .../test/framework/TestSecurityConfig.java | 14 - .../test/framework/cluster/LocalCluster.java | 5 - .../matcher/RestDocumentMatchers.java | 1 - .../framework/matcher/RestIndexMatchers.java | 16 +- .../security/OpenSearchSecurityPlugin.java | 13 +- .../configuration/DlsFlsValveImpl.java | 18 +- .../dlic/rest/api/SecurityRestApiActions.java | 87 +++-- .../security/filter/SecurityFilter.java | 3 +- .../PrivilegesEvaluatorResponse.java | 20 - .../resources/ResourceSharingDlsUtils.java | 6 +- .../security/rest/DashboardsInfoAction.java | 2 + .../RestLayerPrivilegesEvaluatorTest.java | 3 +- .../legacy/PrivilegesEvaluatorUnitTest.java | 19 +- 24 files changed, 149 insertions(+), 507 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java index d0ad597085..34857ea2a7 100644 --- a/src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java @@ -20,7 +20,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.test.framework.data.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; diff --git a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FlsFmIntegrationTests.java b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FlsFmIntegrationTests.java index 50dd945a5a..abee5eb844 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FlsFmIntegrationTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FlsFmIntegrationTests.java @@ -28,8 +28,6 @@ import org.bouncycastle.util.encoders.Hex; import org.opensearch.plugin.mapper.MapperSizePlugin; -import org.opensearch.test.framework.data.TestData; -import org.opensearch.test.framework.data.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java index ead08a43f3..e74c7cba48 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java @@ -23,12 +23,12 @@ import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.test.framework.data.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.certificate.TestCertificates; 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.TestIndex; import org.opensearch.test.framework.matcher.RestIndexMatchers; import static org.hamcrest.MatcherAssert.assertThat; 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 019c553df8..19a969cb05 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 @@ -29,16 +29,12 @@ import org.opensearch.test.framework.data.TestIndex; import org.opensearch.test.framework.data.TestIndexOrAliasOrDatastream; import org.opensearch.test.framework.data.TestIndexTemplate; -import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.matcher.RestIndexMatchers; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.data.TestIndex.openSearchSecurityConfigIndex; -import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; 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.RestIndexMatchers.OnUserIndexMatcher.limitedToNone; @@ -277,7 +273,7 @@ public void search_noPattern_allowNoIndicesFalse() throws Exception { if (user != LIMITED_USER_OTHER_PRIVILEGES) { assertThat( httpResponse, - containsExactly(ALL_INDICES).at("hits.hits[*]._index") + containsExactly(ALL_INDICES_EXCEPT_SYSTEM_INDICES).at("hits.hits[*]._index") .reducedBy(user.reference(READ)) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isNotFound() : isForbidden()) ); @@ -391,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( + // 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()) - ); - } 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()) - ); - } - } + ); } } @@ -438,7 +410,6 @@ public void search_indexPattern_minus_backingIndices() throws Exception { .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, @@ -457,19 +428,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 - - 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()) ); - } + } } @@ -484,14 +449,6 @@ public void search_indexPattern_noWildcards() throws Exception { } 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()) - ); } } } @@ -537,7 +494,7 @@ public void search_termsAggregation_index() throws Exception { assertThat( httpResponse, - containsExactly(ALL_INDICES).at("aggregations.indices.buckets[*].key").reducedBy(user.reference(READ)).whenEmpty(isOk()) + containsExactly(ALL_INDICES_EXCEPT_SYSTEM_INDICES).at("aggregations.indices.buckets[*].key").reducedBy(user.reference(READ)).whenEmpty(isOk()) ); } 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 576b856a6f..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 @@ -36,11 +36,6 @@ import org.opensearch.test.framework.data.TestIndex; import org.opensearch.test.framework.data.TestIndexOrAliasOrDatastream; import org.opensearch.test.framework.data.TestIndexTemplate; -import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.TestSecurityConfig.Role; -import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.test.framework.cluster.TestRestClient; -import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import org.opensearch.test.framework.matcher.RestIndexMatchers; import static org.hamcrest.MatcherAssert.assertThat; 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 2ea9546f59..55f8110de6 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; @@ -30,7 +31,6 @@ import org.opensearch.plugins.Plugin; import org.opensearch.plugins.SystemIndexPlugin; import org.opensearch.script.mustache.MustacheModulePlugin; -<<<<<<< HEAD import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -38,27 +38,13 @@ import org.opensearch.test.framework.data.TestData; import org.opensearch.test.framework.data.TestIndex; import org.opensearch.test.framework.data.TestIndexOrAliasOrDatastream; -======= -import org.opensearch.test.framework.data.TestAlias; -import org.opensearch.test.framework.data.TestData; -import org.opensearch.test.framework.data.TestIndex; -import org.opensearch.test.framework.data.TestIndexOrAliasOrDatastream; -import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.test.framework.cluster.TestRestClient; ->>>>>>> d463b185 (Added new integration tests) 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.data.TestIndex.openSearchSecurityConfigIndex; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.cluster.TestRestClient.json; -<<<<<<< HEAD import static org.opensearch.test.framework.data.TestIndex.openSearchSecurityConfigIndex; -======= ->>>>>>> d463b185 (Added new integration tests) import static org.opensearch.test.framework.matcher.RestIndexMatchers.IndexMatcher; import static org.opensearch.test.framework.matcher.RestIndexMatchers.OnResponseIndexMatcher.containsExactly; import static org.opensearch.test.framework.matcher.RestIndexMatchers.OnUserIndexMatcher.limitedTo; @@ -208,11 +194,7 @@ public class IndexAuthorizationReadOnlyIntTests { .on("index_a*") )// .reference(READ, limitedTo(index_a1, index_a2, index_a3, index_ax))// -<<<<<<< HEAD - .indexMatcher(READ_NEXT_GEN, limitedTo(index_a1, index_a2, index_a3, index_ax))// -======= .reference(READ_NEXT_GEN, limitedTo(index_a1, index_a2, index_a3, index_ax))// ->>>>>>> d463b185 (Added new integration tests) .reference(GET_ALIAS, limitedToNone()); /** @@ -227,16 +209,9 @@ >>>>>>> d463b185 (Added new integration tests) .on("index_b*") )// .reference(READ, limitedTo(index_b1, index_b2, index_b3))// -<<<<<<< HEAD - .indexMatcher("read_nextgen", limitedTo(index_b1, index_b2, index_b3))// - - .reference(GET_ALIAS, limitedToNone()); - -======= .reference(READ_NEXT_GEN, limitedTo(index_b1, index_b2, index_b3))// .reference(GET_ALIAS, limitedToNone()); ->>>>>>> d463b185 (Added new integration tests) /** * A simple user that can read only from index_b1 */ @@ -249,16 +224,9 @@ >>>>>>> d463b185 (Added new integration tests) .on("index_b1") )// .reference(READ, limitedTo(index_b1))// -<<<<<<< HEAD - .indexMatcher("read_nextgen", limitedTo(index_b1))// - - .reference(GET_ALIAS, limitedToNone()); - -======= .reference(READ_NEXT_GEN, limitedTo(index_b1))// .reference(GET_ALIAS, limitedToNone()); ->>>>>>> d463b185 (Added new integration tests) /** * A simple user that can read from index_c* */ @@ -271,16 +239,9 @@ >>>>>>> d463b185 (Added new integration tests) .on("index_c*") )// .reference(READ, limitedTo(index_c1, alias_c1))// -<<<<<<< HEAD - .indexMatcher("read_nextgen", limitedTo(index_c1))// - - .reference(GET_ALIAS, limitedToNone()); - -======= .reference(READ_NEXT_GEN, limitedTo(index_c1))// .reference(GET_ALIAS, limitedToNone()); ->>>>>>> d463b185 (Added new integration tests) /** * A user that has read privileges for alias_ab1*; these privileges are inherited to the member indices. * The user has no directly defined privileges on indices. @@ -294,16 +255,9 @@ >>>>>>> d463b185 (Added new integration tests) .on("alias_ab1*") )// .reference(READ, limitedTo(index_a1, index_a2, index_a3, index_b1, alias_ab1))// -<<<<<<< HEAD - .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)); - -======= .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)); ->>>>>>> d463b185 (Added new integration tests) /** * A user that has read privileges for alias_c1; these privileges are inherited to the member indices. * The user has no directly defined privileges on indices. @@ -317,14 +271,8 @@ >>>>>>> d463b185 (Added new integration tests) .on("alias_c1") )// .reference(READ, limitedTo(index_c1, alias_c1))// -<<<<<<< HEAD - .indexMatcher(READ_NEXT_GEN, limitedTo(index_c1, alias_c1))// - - .reference(GET_ALIAS, limitedTo(index_c1, alias_c1)); -======= .reference(READ_NEXT_GEN, limitedTo(index_c1, alias_c1))// .reference(GET_ALIAS, limitedTo(index_c1, alias_c1)); ->>>>>>> d463b185 (Added new integration tests) /** * Same as LIMITED_USER_A with the addition of read privileges for index_hidden* and .index_hidden* */ @@ -337,16 +285,9 @@ >>>>>>> d463b185 (Added new integration tests) .on("index_a*", "index_hidden*", ".index_hidden*") )// .reference(READ, limitedTo(index_a1, index_a2, index_a3, index_ax, index_hidden, index_hidden_dot))// -<<<<<<< HEAD - .reference(READ_NEXT_GEN, limitedTo(index_a1, index_a2, index_a3, index_ax, index_hidden, index_hidden_dot))// - - .reference(GET_ALIAS, limitedToNone()); - -======= .reference(READ_NEXT_GEN, limitedTo(index_a1, index_a2, index_a3, index_ax, index_hidden, index_hidden_dot))// .reference(GET_ALIAS, limitedToNone()); ->>>>>>> d463b185 (Added new integration tests) /** * Same as LIMITED_USER_C with the addition of read privileges for ".system_index_plugin"; they also have the * explicit privilege "system:admin/system_index" that allows them accessing this index. @@ -368,21 +309,10 @@ >>>>>>> d463b185 (Added new integration tests) "system:admin/system_index" ) .on(".system_index_plugin") - .indexPermissions("read", "indices_monitor", "indices:admin/analyze") - .on("index_c*")// - .indexPermissions("read", "indices_monitor", "indices:admin/analyze", "system:admin/system_index") - .on(".system_index_plugin", ".alias_with_system_index") )// .reference(READ, limitedTo(index_c1, alias_c1, system_index_plugin, alias_with_system_index))// -<<<<<<< HEAD - .reference(READ_NEXT_GEN, limitedTo(index_c1, system_index_plugin, alias_with_system_index))// - - .reference(GET_ALIAS, limitedTo(index_c1, alias_c1, system_index_plugin, alias_with_system_index)); -======= .reference(READ_NEXT_GEN, limitedTo(index_c1, system_index_plugin, alias_with_system_index))// - .reference(GET_ALIAS, limitedToNone()); ->>>>>>> d463b185 (Added new integration tests) - + .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. @@ -398,11 +328,7 @@ >>>>>>> d463b185 (Added new integration tests) .on("index_does_not_exist_*") )// .reference(READ, limitedToNone())// -<<<<<<< HEAD - .reference(READ,READ_NEXT_GEN limitedToNone())// -======= .reference(READ_NEXT_GEN, limitedToNone())// ->>>>>>> d463b185 (Added new integration tests) .reference(GET_ALIAS, limitedToNone()); /** @@ -415,16 +341,8 @@ >>>>>>> d463b185 (Added new integration tests) .clusterPermissions("cluster_composite_ops_ro", "cluster_monitor") )// .reference(READ, limitedToNone())// -<<<<<<< HEAD - .reference(READ_NEXT_GEN, limitedToNone())// - - .reference(GET_ALIAS, limitedToNone()); - -======= .reference(READ_NEXT_GEN, limitedToNone())// .reference(GET_ALIAS, limitedToNone()); - ->>>>>>> d463b185 (Added new integration tests) /** * A user with "*" privileges on "*"; as it is a regular user, they are still subject to system index * restrictions and similar things. @@ -434,20 +352,12 @@ >>>>>>> d463b185 (Added new integration tests) .roles( new TestSecurityConfig.Role("r1")// .clusterPermissions("*") -<<<<<<< HEAD - .clusterPermissions("cluster_composite_ops_ro", "cluster_monitor") -======= ->>>>>>> d463b185 (Added new integration tests) .indexPermissions("*") .on("*")// )// .reference(READ, limitedTo(ALL_INDICES_AND_ALIASES_EXCEPT_SYSTEM_INDICES).and(index_ax))// -<<<<<<< HEAD - .reference(READ_NEXT_GEN, 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))// ->>>>>>> d463b185 (Added new integration tests) .reference(GET_ALIAS, limitedTo(ALL_INDICES_AND_ALIASES_EXCEPT_SYSTEM_INDICES).and(index_ax)); /** @@ -458,11 +368,7 @@ >>>>>>> d463b185 (Added new integration tests) .description("super unlimited (admin cert)")// .adminCertUser()// .reference(READ, unlimitedIncludingOpenSearchSecurityIndex())// -<<<<<<< HEAD - .reference(READ_NEXT_GEN, unlimitedIncludingOpenSearchSecurityIndex())// -======= .reference(READ_NEXT_GEN, unlimitedIncludingOpenSearchSecurityIndex())// ->>>>>>> d463b185 (Added new integration tests) .reference(GET_ALIAS, unlimitedIncludingOpenSearchSecurityIndex()); static final List USERS = ImmutableList.of( @@ -552,11 +458,7 @@ public void search_noPattern_allowNoIndicesFalse() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at("hits.hits[*]._index") -<<<<<<< HEAD - .reducedBy(user.reference(READ)) -======= .reducedBy(user.reference(READ)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(isForbidden()) ); } @@ -611,11 +513,7 @@ public void search_all_includeHidden() throws Exception { ? ALL_INDICES : ALL_INDICES_EXCEPT_SYSTEM_INDICES ).at("hits.hits[*]._index") -<<<<<<< HEAD - .reducedBy(user.reference(READ)) -======= .reducedBy(user.reference(READ)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { // next gen privilege evaluation @@ -681,11 +579,7 @@ public void search_wildcard_includeHidden() throws Exception { ? ALL_INDICES : ALL_INDICES_EXCEPT_SYSTEM_INDICES ).at("hits.hits[*]._index") -<<<<<<< HEAD - .reducedBy(user.reference(READ)) -======= .reducedBy(user.reference(READ)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -715,11 +609,7 @@ public void search_staticIndices_ignoreUnavailable() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_b1).at("hits.hits[*]._index") -<<<<<<< HEAD - .reducedBy(user.reference(READ)) -======= .reducedBy(user.reference(READ)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -827,11 +717,7 @@ public void search_indexPattern() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3).at("hits.hits[*]._index") -<<<<<<< HEAD - .reducedBy(user.reference(READ)) -======= .reducedBy(user.reference(READ)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -850,11 +736,7 @@ public void search_indexPattern_minus() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index") -<<<<<<< HEAD - .reducedBy(user.reference(READ)) -======= .reducedBy(user.reference(READ)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -920,11 +802,7 @@ public void search_indexPatternAndStatic_negation() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index") -<<<<<<< HEAD - .reducedBy(user.reference(READ)) -======= .reducedBy(user.reference(READ)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -1041,11 +919,7 @@ public void search_alias_pattern() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("hits.hits[*]._index") -<<<<<<< HEAD - .reducedBy(user.reference(READ)) -======= .reducedBy(user.reference(READ)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -1064,19 +938,6 @@ public void search_alias_pattern_negation() throws Exception { // If the system index privilege is enabled, we might also see the system_index_plugin index (being included via the // alias) assertThat( -<<<<<<< HEAD - 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()) -======= httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_c1).at("hits.hits[*]._index") .reducedBy(user.reference(READ)) @@ -1088,7 +949,6 @@ public void search_alias_pattern_negation() throws Exception { containsExactly(index_a1, index_a2, index_a3, index_b1, index_c1).at("hits.hits[*]._index") .reducedBy(user.reference(READ)) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ->>>>>>> d463b185 (Added new integration tests) ); } } else { @@ -1123,18 +983,11 @@ public void search_alias_pattern_includeHidden() throws Exception { assertThat(httpResponse, isForbidden()); } else { assertThat( -<<<<<<< HEAD - httpResponse, - containsExactly(index_a1, index_a2, index_a3, index_b1, index_c1, system_index_plugin).at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(isOk())); -======= httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_c1, system_index_plugin).at("hits.hits[*]._index") .reducedBy(user.reference(READ)) .whenEmpty(isOk()) ); ->>>>>>> d463b185 (Added new integration tests) } } else { assertThat(httpResponse, isForbidden()); @@ -1340,18 +1193,7 @@ public void search_pit_wrongIndex() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.post("index_a*/_search/point_in_time?keep_alive=1m"); -<<<<<<< HEAD if (user.reference(READ).coversAll(index_a1, index_a2, index_a3)) { -======= - RestIndexMatchers.OnResponseIndexMatcher indexMatcher = containsExactly(index_a1, index_a2, index_a3); - - if (indexMatcher.reducedBy(user.reference(READ)).isEmpty()) { - assertThat( - httpResponse, - isForbidden("/error/root_cause/0/reason", "no permissions for [indices:data/read/point_in_time/create]") - ); - } else { ->>>>>>> d463b185 (Added new integration tests) assertThat(httpResponse, isOk()); String pitId = httpResponse.getTextFromJsonBody("/pit_id"); httpResponse = restClient.postJson("index_b*/_search?size=1000", String.format(""" @@ -1365,8 +1207,8 @@ >>>>>>> d463b185 (Added new integration tests) } else { assertThat( - httpResponse, - isForbidden("/error/root_cause/0/reason", "no permissions for [indices:data/read/point_in_time/create]") + httpResponse, + isForbidden("/error/root_cause/0/reason", "no permissions for [indices:data/read/point_in_time/create]") ); } } @@ -1379,10 +1221,6 @@ >>>>>>> d463b185 (Added new integration tests) @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": { @@ -1397,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, @@ -1534,11 +1371,7 @@ public void cat_indices_all() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3, index_c1).at("$[*].index") -<<<<<<< HEAD - .reducedBy(user.reference(READ)) -======= .reducedBy(user.reference(READ)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); @@ -1557,11 +1390,8 @@ public void cat_indices_pattern() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3).at("$[*].index") -<<<<<<< HEAD - .reducedBy(user.reference(READ)) -======= + .reducedBy(user.reference(READ)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -1675,11 +1505,7 @@ public void index_stats_pattern() throws Exception { assertThat( httpResponse, containsExactly(index_b1, index_b2, index_b3).at("indices.keys()") -<<<<<<< HEAD - .reducedBy(user.reference(READ)) -======= .reducedBy(user.reference(READ)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -1702,11 +1528,7 @@ public void getAlias_all() throws Exception { assertThat( httpResponse, containsExactly(alias_ab1, alias_c1, alias_with_system_index).at("$.*.aliases.keys()") -<<<<<<< HEAD - .reducedByuser.reference(GET_ALIAS)) -======= .reducedBy(user.reference(GET_ALIAS)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); assertThat( @@ -1757,7 +1579,7 @@ public void getAlias_aliasPattern() throws Exception { 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) { + } 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()); @@ -1800,31 +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()") - ); + 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, -<<<<<<< HEAD containsExactly(alias_ab1, alias_c1, alias_with_system_index).at("$.*.aliases.keys()") -======= - containsExactly(alias_ab1, alias_c1).at("$.*.aliases.keys()") ->>>>>>> d463b185 (Added new integration tests) + .reducedBy(user.reference(GET_ALIAS)) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); @@ -1924,11 +1760,8 @@ public void resolve_indexPattern() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1, index_b2, index_b3).at("$.*[*].name") -<<<<<<< HEAD - .reducedBy(user.reference(READ)) -======= + .reducedBy(user.reference(READ)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -2019,11 +1852,8 @@ public void field_caps_aliasPattern() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("indices") -<<<<<<< HEAD - .reducedBy(user.reference(READ)) -======= + .reducedBy(user.reference(READ)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } else { @@ -2066,90 +1896,13 @@ public void field_caps_indexPattern_minus() throws Exception { assertThat( httpResponse, containsExactly(index_a1, index_a2, index_a3, index_b1).at("indices") -<<<<<<< HEAD - .reducedBy(user.reference(READ)) -======= + .reducedBy(user.reference(READ)) ->>>>>>> d463b185 (Added new integration tests) .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) ); } } - } - - @Test - public void pit_list_all() throws Exception { - String indexA1pitId = createPit(index_a1); - - 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()); - } else { - assertThat(httpResponse, isForbidden()); - } - } finally { - deletePit(indexA1pitId); - } - } - - @Test - public void pit_delete() throws Exception { - String indexA1pitId = createPit(index_a1); - - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.delete("_search/point_in_time", json("pit_id", List.of(indexA1pitId))); - - if (user.reference(READ).covers(index_a1)) { - assertThat(httpResponse, isOk()); - } else { - assertThat(httpResponse, isForbidden()); - } - } finally { - deletePit(indexA1pitId); - } - } - - @Test - public void pit_catSegments() throws Exception { - String indexA1pitId = createPit(index_a1); - - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("_cat/pit_segments", json("pit_id", List.of(indexA1pitId))); - - if (user.reference(READ).covers(index_a1)) { - assertThat(httpResponse, isOk()); - } else { - assertThat(httpResponse, isForbidden()); - } - } finally { - deletePit(indexA1pitId); - } - } - - @Test - public void pit_catSegments_all() throws Exception { - String indexA1pitId = createPit(index_a1); - - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("_cat/pit_segments/_all"); - - // The user needs to have the privilege for all indices. If it is only granted for a subset of indices, this will be forbidden. - if (user == UNLIMITED_USER || user == SUPER_UNLIMITED_USER) { - assertThat(httpResponse, isOk()); - } else { - assertThat(httpResponse, isForbidden()); - } - } finally { - deletePit(indexA1pitId); - } } @Test @@ -2221,22 +1974,11 @@ public void pit_catSegments_all() throws Exception { try (TestRestClient restClient = cluster.getRestClient(user)) { TestRestClient.HttpResponse httpResponse = restClient.get("_cat/pit_segments/_all"); - if (clusterConfig.legacyPrivilegeEvaluation) { - // The user needs to have the privilege for all indices. If it is only granted for a subset of indices, this will be - // forbidden. - if (user == UNLIMITED_USER || user == SUPER_UNLIMITED_USER) { - assertThat(httpResponse, isOk()); - } else { - assertThat(httpResponse, isForbidden()); - } + // The user needs to have the privilege for all indices. If it is only granted for a subset of indices, this will be forbidden. + if (user == UNLIMITED_USER || user == SUPER_UNLIMITED_USER) { + assertThat(httpResponse, isOk()); } else { - // New privilege evaluation: this is now a separate 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()); - } + 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 bf141b9cc4..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 @@ -28,9 +28,6 @@ import org.opensearch.action.admin.indices.refresh.RefreshRequest; import org.opensearch.action.admin.indices.settings.put.UpdateSettingsRequest; import org.opensearch.common.settings.Settings; -import org.opensearch.test.framework.data.TestAlias; -import org.opensearch.test.framework.data.TestIndex; -import org.opensearch.test.framework.data.TestIndexOrAliasOrDatastream; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.cluster.LocalCluster; @@ -57,6 +54,7 @@ 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: @@ -1204,8 +1202,6 @@ public void rollover_explicitTargetIndex() throws Exception { } }"""); - System.out.println(httpResponse.getBody()); - if (clusterConfig.legacyPrivilegeEvaluation) { if (user.reference(MANAGE_ALIAS).covers(index_bw1) && user.reference(MANAGE_INDEX).covers(index_bw2)) { assertThat(httpResponse, isOk()); diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java index ac60f10273..b216ccbcd1 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java @@ -19,7 +19,6 @@ import org.opensearch.script.mustache.MustacheModulePlugin; import org.opensearch.script.mustache.RenderSearchTemplateAction; -import org.opensearch.test.framework.data.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.cluster.ClusterManager; 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 84872471a5..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 @@ -26,8 +26,6 @@ import org.junit.runner.RunWith; import org.opensearch.action.admin.indices.refresh.RefreshRequest; -import org.opensearch.test.framework.data.TestIndex; -import org.opensearch.test.framework.data.TestIndexOrAliasOrDatastream; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -90,7 +88,6 @@ public class SnapshotAuthorizationIntTests { static TestSecurityConfig.User LIMITED_USER_A = new TestSecurityConfig.User("limited_user_A")// .description("index_a*")// .roles( - // new TestSecurityConfig.Role("r1")// .clusterPermissions("cluster_composite_ops", "cluster_monitor", "manage_snapshots")// .indexPermissions("read", "indices_monitor", "indices:admin/refresh*") @@ -104,7 +101,6 @@ public class SnapshotAuthorizationIntTests { static TestSecurityConfig.User LIMITED_USER_B = new TestSecurityConfig.User("limited_user_B")// .description("index_b*")// .roles( - // new TestSecurityConfig.Role("r1")// .clusterPermissions("cluster_composite_ops", "cluster_monitor", "manage_snapshots")// .indexPermissions("read", "indices_monitor", "indices:admin/refresh*") @@ -118,7 +114,6 @@ public class SnapshotAuthorizationIntTests { static TestSecurityConfig.User LIMITED_USER_B_SYSTEM_INDEX = new TestSecurityConfig.User("limited_user_B_system_index")// .description("index_b*, .system_index_plugin")// .roles( - // new TestSecurityConfig.Role("r1")// .clusterPermissions("cluster_composite_ops", "cluster_monitor", "manage_snapshots")// .indexPermissions("read", "indices_monitor", "indices:admin/refresh*") @@ -137,7 +132,6 @@ public class SnapshotAuthorizationIntTests { static TestSecurityConfig.User LIMITED_USER_AB = new TestSecurityConfig.User("limited_user_AB")// .description("index_a*, index_b*")// .roles( - // new TestSecurityConfig.Role("r1")// .clusterPermissions("cluster_composite_ops", "cluster_monitor", "manage_snapshots")// .indexPermissions("read", "indices_monitor", "indices:admin/refresh*") @@ -164,7 +158,6 @@ public class SnapshotAuthorizationIntTests { .clusterPermissions("cluster_composite_ops_ro", "cluster_monitor", "manage_snapshots") .indexPermissions("*") .on("*")// - )// .reference( READ, @@ -226,7 +219,6 @@ public void restore_singleIndex() throws Exception { TestRestClient.HttpResponse httpResponse = restClient.post( "_snapshot/test_repository/single_index_snapshot/_restore?wait_for_completion=true" ); - System.out.println(httpResponse.getBody()); assertThat(httpResponse, containsExactly(index_awx1).at("snapshot.indices").butForbiddenIfIncomplete(user.reference(WRITE))); @@ -246,7 +238,6 @@ public void restore_singleIndex_rename1() throws Exception { "_snapshot/test_repository/single_index_snapshot/_restore?wait_for_completion=true", json("rename_pattern", "index_(.+)x1", "rename_replacement", "index_$1x2") ); - System.out.println(httpResponse.getBody()); assertThat(httpResponse, containsExactly(index_awx2).at("snapshot.indices").butForbiddenIfIncomplete(user.reference(WRITE))); @@ -266,7 +257,6 @@ public void restore_singleIndex_rename2() throws Exception { "_snapshot/test_repository/single_index_snapshot/_restore?wait_for_completion=true", json("rename_pattern", "index_a(.*)", "rename_replacement", "index_b$1") ); - System.out.println(httpResponse.getBody()); assertThat(httpResponse, containsExactly(index_bwx1).at("snapshot.indices").butForbiddenIfIncomplete(user.reference(WRITE))); @@ -286,7 +276,6 @@ public void restore_singleIndex_renameToSystemIndex() throws Exception { "_snapshot/test_repository/single_index_snapshot/_restore?wait_for_completion=true", json("rename_pattern", "index_awx1", "rename_replacement", system_index_plugin_not_existing.name()) ); - System.out.println(httpResponse.getBody()); if (clusterConfig.systemIndexPrivilegeEnabled || user == SUPER_UNLIMITED_USER) { assertThat( @@ -314,7 +303,6 @@ public void restore_singleIndexFromAllIndices() throws Exception { "_snapshot/test_repository/all_index_snapshot/_restore?wait_for_completion=true", json("indices", "index_awx1") ); - System.out.println(httpResponse.getBody()); assertThat(httpResponse, containsExactly(index_awx1).at("snapshot.indices").butForbiddenIfIncomplete(user.reference(WRITE))); @@ -336,7 +324,6 @@ public void restore_all_globalState() throws Exception { "_snapshot/test_repository/all_index_snapshot/_restore?wait_for_completion=true", json("include_global_state", true) ); - System.out.println(httpResponse.getBody()); if (user == SUPER_UNLIMITED_USER) { assertThat(httpResponse, isOk()); diff --git a/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java b/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java index 104bfafa36..849d76475a 100644 --- a/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java +++ b/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java @@ -26,6 +26,7 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.opensearch.core.common.Strings; import org.opensearch.security.auditlog.impl.AuditMessage; diff --git a/src/integrationTest/java/org/opensearch/security/systemindex/SystemIndexDisabledTests.java b/src/integrationTest/java/org/opensearch/security/systemindex/SystemIndexDisabledTests.java index 52466cb50a..6fd48fe8f0 100644 --- a/src/integrationTest/java/org/opensearch/security/systemindex/SystemIndexDisabledTests.java +++ b/src/integrationTest/java/org/opensearch/security/systemindex/SystemIndexDisabledTests.java @@ -13,7 +13,6 @@ import java.util.Map; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 8d3329c441..23527744cf 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -567,20 +567,6 @@ public User adminCertUser() { return this; } - public String getDescription() { - return description; - } - - @Override - public boolean isAdminCertUser() { - return adminCertUser; - } - - public User adminCertUser() { - this.adminCertUser = true; - return this; - } - public Object getAttribute(String attributeName) { return attributes.get(attributeName); } 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 8c6a17b34c..20d27016fd 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -58,11 +58,6 @@ import org.opensearch.test.framework.AuthFailureListeners; import org.opensearch.test.framework.AuthzDomain; import org.opensearch.test.framework.OnBehalfOfConfig; -import org.opensearch.test.framework.data.TestAlias; -import org.opensearch.test.framework.data.TestComponentTemplate; -import org.opensearch.test.framework.data.TestDataStream; -import org.opensearch.test.framework.data.TestIndex; -import org.opensearch.test.framework.data.TestIndexTemplate; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.XffConfig; diff --git a/src/integrationTest/java/org/opensearch/test/framework/matcher/RestDocumentMatchers.java b/src/integrationTest/java/org/opensearch/test/framework/matcher/RestDocumentMatchers.java index 6bbc10f79c..17e5f73dbb 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/matcher/RestDocumentMatchers.java +++ b/src/integrationTest/java/org/opensearch/test/framework/matcher/RestDocumentMatchers.java @@ -27,7 +27,6 @@ import org.hamcrest.DiagnosingMatcher; import org.opensearch.common.geo.GeoPoint; -import org.opensearch.test.framework.data.TestData; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import org.opensearch.test.framework.data.TestData; 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/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index bfbd463736..8c57dd66bb 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -150,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; @@ -169,8 +168,6 @@ 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; @@ -646,7 +643,7 @@ public List getRestHandlers( Objects.requireNonNull(privilegesConfiguration), Objects.requireNonNull(cr), Objects.requireNonNull(threadPool), - resourceSharingEnabledSetting + resourceSharingEnabledSetting ) ); handlers.add( @@ -1214,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) diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 138742e112..b2bcbe50c9 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -41,6 +41,7 @@ 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; @@ -153,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 (isApplicable(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) { - ResolvedIndices 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() ); @@ -188,7 +193,6 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< return true; } DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get(); - OptionallyResolvedIndices resolved = context.getResolvedRequest(); try { boolean hasDlsRestrictions = !config.getDocumentPrivileges().isUnrestricted(context, resolved); 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 5203195bdc..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 @@ -28,6 +28,7 @@ import org.opensearch.security.hasher.PasswordHasher; 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; import org.opensearch.security.ssl.SslSettingsManager; @@ -74,45 +75,53 @@ public static Collection getHandler( auditLog, settings ); - return List.of( - new InternalUsersApiAction(clusterService, threadPool, userService, securityApiDependencies, passwordHasher), - new RolesMappingApiAction(clusterService, threadPool, securityApiDependencies), - new RolesApiAction(clusterService, threadPool, securityApiDependencies), - new ActionGroupsApiAction(clusterService, threadPool, securityApiDependencies), - new FlushCacheApiAction(clusterService, threadPool, securityApiDependencies), - new SecurityConfigApiAction(clusterService, threadPool, securityApiDependencies), - // FIXME Change inheritance for PermissionsInfoAction - new PermissionsInfoAction( - settings, - configPath, - controller, - client, - adminDns, - configurationRepository, - clusterService, - principalExtractor, - roleMapper, - threadPool, - auditLog - ), - new AuthTokenProcessorAction(clusterService, threadPool, securityApiDependencies), - new TenantsApiAction(clusterService, threadPool, securityApiDependencies), - new AccountApiAction(clusterService, threadPool, securityApiDependencies, passwordHasher), - new NodesDnApiAction(clusterService, threadPool, securityApiDependencies), - new AllowlistApiAction(Endpoint.ALLOWLIST, clusterService, threadPool, securityApiDependencies), - new AuditApiAction(clusterService, threadPool, securityApiDependencies), - new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies), - new RateLimitersApiAction(clusterService, threadPool, securityApiDependencies), - new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies), - new SecuritySSLCertsApiAction( - clusterService, - threadPool, - sslSettingsManager, - certificatesReloadEnabled, - securityApiDependencies - ), - new CertificatesApiAction(clusterService, threadPool, securityApiDependencies), - new MigrateResourceSharingInfoApiAction(clusterService, threadPool, securityApiDependencies, resourceSharingIndexHandler) + List handler = new ArrayList<>( + List.of( + new InternalUsersApiAction(clusterService, threadPool, userService, securityApiDependencies, passwordHasher), + new RolesMappingApiAction(clusterService, threadPool, securityApiDependencies), + new RolesApiAction(clusterService, threadPool, securityApiDependencies), + new ActionGroupsApiAction(clusterService, threadPool, securityApiDependencies), + new FlushCacheApiAction(clusterService, threadPool, securityApiDependencies), + new SecurityConfigApiAction(clusterService, threadPool, securityApiDependencies), + // FIXME Change inheritance for PermissionsInfoAction + new PermissionsInfoAction( + settings, + configPath, + controller, + client, + adminDns, + configurationRepository, + clusterService, + principalExtractor, + roleMapper, + threadPool, + auditLog + ), + new AuthTokenProcessorAction(clusterService, threadPool, securityApiDependencies), + new TenantsApiAction(clusterService, threadPool, securityApiDependencies), + new AccountApiAction(clusterService, threadPool, securityApiDependencies, passwordHasher), + new NodesDnApiAction(clusterService, threadPool, securityApiDependencies), + new AllowlistApiAction(Endpoint.ALLOWLIST, clusterService, threadPool, securityApiDependencies), + new AuditApiAction(clusterService, threadPool, securityApiDependencies), + new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies), + new RateLimitersApiAction(clusterService, threadPool, securityApiDependencies), + new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies), + new SecuritySSLCertsApiAction( + clusterService, + threadPool, + sslSettingsManager, + certificatesReloadEnabled, + securityApiDependencies + ), + new CertificatesApiAction(clusterService, threadPool, securityApiDependencies), + new MigrateResourceSharingInfoApiAction( + clusterService, + threadPool, + securityApiDependencies, + resourceSharingIndexHandler, + resourcePluginInfo + ) + ) ); if (SecurityConfigVersionHandler.isVersionIndexEnabled(settings)) { diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index 237f9d73b7..eee48f798b 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -94,7 +94,6 @@ import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.privileges.ResourceAccessEvaluator; import org.opensearch.security.privileges.RoleMapper; -import org.opensearch.security.resources.ResourceAccessHandler; import org.opensearch.security.support.Base64Helper; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; @@ -157,7 +156,7 @@ public SecurityFilter( ); this.rolesInjector = new RolesInjector(auditLog); this.userInjector = new UserInjector(settings, threadPool, auditLog, xffResolver); - this.resourceAccessEvaluator = new ResourceAccessEvaluator(resourceIndices, settings, resourceAccessHandler); + this.resourceAccessEvaluator = resourceAccessEvaluator; this.threadContextUserInfo = new ThreadContextUserInfo(threadPool.getThreadContext(), privilegesConfiguration, settings); log.info("{} indices are made immutable.", immutableIndicesMatcher); } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java index f6cddc7d57..d069a7caf0 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java @@ -204,26 +204,6 @@ public PrivilegesEvaluatorResponse originalResult(PrivilegesEvaluatorResponse or return this; } - 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); - return result; - } - - public PrivilegesEvaluatorResponse originalResult(PrivilegesEvaluatorResponse originalResult) { - if (originalResult != null && !originalResult.evaluationExceptions.isEmpty()) { - this.originalResult = originalResult; - this.evaluationExceptions.addAll(originalResult.evaluationExceptions); - } - return this; - } - @Override public String toString() { return "PrivEvalResponse [\nallowed=" 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 14f8ca0c45..1e744be83c 100644 --- a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java @@ -33,12 +33,14 @@ 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.configuration.ConfigurationRepository; import org.opensearch.security.privileges.DashboardsMultiTenancyConfiguration; diff --git a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index 61fc5434f2..6b3718e81e 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -19,7 +19,6 @@ 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.security.privileges.actionlevel.RoleBasedActionPrivileges; import org.opensearch.security.privileges.actionlevel.RuntimeOptimizedActionPrivileges; @@ -37,7 +36,7 @@ import static org.hamcrest.Matchers.equalTo; @RunWith(MockitoJUnitRunner.class) - public class RestLayerPrivilegesEvaluatorTest { +public class RestLayerPrivilegesEvaluatorTest { private static final User TEST_USER = new User("test_user").withSecurityRoles(Set.of("test_role")); diff --git a/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorUnitTest.java b/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorUnitTest.java index 4a67b70f51..022e40f839 100644 --- a/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorUnitTest.java +++ b/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorUnitTest.java @@ -9,35 +9,18 @@ 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.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.security.auditlog.AuditLog; -import org.opensearch.security.configuration.ClusterInfoHolder; -import org.opensearch.security.configuration.ConfigurationRepository; -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.actionlevel.legacy.PrivilegesEvaluator.DNFOF_MATCHER; import static org.opensearch.security.privileges.actionlevel.legacy.PrivilegesEvaluator.isClusterPermissionStatic; -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.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; From ae3bc40cedf638847d64fe4db767ce8a043120ed Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Fri, 24 Oct 2025 13:24:13 +0200 Subject: [PATCH 08/29] Test fixes Signed-off-by: Nils Bandener --- .../CrossClusterAuthorizationIntTests.java | 488 ------------------ ...taStreamAuthorizationReadOnlyIntTests.java | 20 - .../IndexAuthorizationReadOnlyIntTests.java | 4 +- .../int_tests/MiscPrivilegesIntTests.java | 3 +- .../legacy/PrivilegesEvaluator.java | 7 + .../legacy/SystemIndexAccessEvaluator.java | 6 + 6 files changed, 17 insertions(+), 511 deletions(-) delete mode 100644 src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java deleted file mode 100644 index e74c7cba48..0000000000 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/CrossClusterAuthorizationIntTests.java +++ /dev/null @@ -1,488 +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.int_tests; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import com.google.common.collect.ImmutableList; -import org.junit.AfterClass; -import org.junit.ClassRule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.certificate.TestCertificates; -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.TestIndex; -import org.opensearch.test.framework.matcher.RestIndexMatchers; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; -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.RestIndexMatchers.OnUserIndexMatcher.limitedToNone; -import static org.opensearch.test.framework.matcher.RestIndexMatchers.OnUserIndexMatcher.unlimitedIncludingOpenSearchSecurityIndex; -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 CrossClusterAuthorizationIntTests { - - // ------------------------------------------------------------------------------------------------------- - // Test indices used by this test suite - // ------------------------------------------------------------------------------------------------------- - - interface LocalIndices { - TestIndex index_a1 = TestIndex.name("index_a1").documentCount(10).seed(1).build(); - TestIndex index_a2 = TestIndex.name("index_a2").documentCount(11).seed(2).build(); - } - - interface RemoteIndices { - TestIndex index_r1 = TestIndex.name("index_r1").documentCount(212).seed(11).build(); - TestIndex index_r2 = TestIndex.name("index_r2").documentCount(213).seed(12).build(); - TestIndex index_r3 = TestIndex.name("index_r3").documentCount(214).seed(13).build(); - } - - /** - * This key identifies assertion reference data for index search/read permissions of individual users. - */ - static final TestSecurityConfig.User.MetadataKey READ = new TestSecurityConfig.User.MetadataKey<>( - "read", - RestIndexMatchers.IndexMatcher.class - ); - - // ------------------------------------------------------------------------------------------------------- - // Test users with which the tests will be executed; the users need to be added to the list USERS below - // Each user comes with one or two additionally defined TestSecurityConfig.Role objects: - // - If it is two, one is meant for the local cluster, the other is meant for the remote cluster - // - If it is one, both local and remote cluster must get these roles. - // These roles must be passed to the test cluster builders via the roles() method - // ------------------------------------------------------------------------------------------------------- - - static final TestSecurityConfig.Role LIMITED_USER_ROLE_A_R = new TestSecurityConfig.Role("limited_user_A_R_role").clusterPermissions( - "cluster_composite_ops_ro", - "cluster_monitor" - ).indexPermissions("read", "indices_monitor").on("index_a*"); - static final TestSecurityConfig.Role LIMITED_USER_ROLE_A_R_REMOTE = new TestSecurityConfig.Role("limited_user_A_R_role") - .clusterPermissions("cluster_composite_ops_ro", "cluster_monitor") - .indexPermissions("read", "indices_monitor", "indices:admin/shards/search_shards") - .on("index_r*"); - static final TestSecurityConfig.User LIMITED_USER_A_R = new TestSecurityConfig.User("limited_user_A_R")// - .description("index_a*, index_r*")// - .roles(LIMITED_USER_ROLE_A_R)// - .reference( - READ, - limitedTo(LocalIndices.index_a1, LocalIndices.index_a2, RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) - ); - - static final TestSecurityConfig.Role LIMITED_USER_ROLE_R = new TestSecurityConfig.Role("limited_user_R_role").clusterPermissions( - "cluster_composite_ops_ro", - "cluster_monitor" - ); - static final TestSecurityConfig.Role LIMITED_USER_ROLE_R_REMOTE = new TestSecurityConfig.Role("limited_user_R_role").clusterPermissions( - "cluster_composite_ops_ro", - "cluster_monitor" - ).indexPermissions("read", "indices_monitor", "indices:admin/shards/search_shards").on("index_r*"); - static final TestSecurityConfig.User LIMITED_USER_R = new TestSecurityConfig.User("limited_user_R")// - .description("index_r*")// - .roles(LIMITED_USER_ROLE_R)// - .reference(READ, limitedTo(RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3)); - - static final TestSecurityConfig.Role LIMITED_USER_ROLE_R1 = new TestSecurityConfig.Role("limited_user_R1_role").clusterPermissions( - "cluster_composite_ops_ro", - "cluster_monitor" - ); - static final TestSecurityConfig.Role LIMITED_USER_ROLE_R1_REMOTE = new TestSecurityConfig.Role("limited_user_R1_role") - .clusterPermissions("cluster_composite_ops_ro", "cluster_monitor") - .indexPermissions("read", "indices_monitor", "indices:admin/shards/search_shards") - .on("index_${attr.internal.attr_r1}"); - static final TestSecurityConfig.User LIMITED_USER_R1 = new TestSecurityConfig.User("limited_user_R1")// - .description("index_r1, with user attribute")// - .roles(LIMITED_USER_ROLE_R1)// - .attr("attr_r1", "r1") - .reference(READ, limitedTo(RemoteIndices.index_r1)); - - static final TestSecurityConfig.Role LIMITED_ROLE_NONE = new TestSecurityConfig.Role("limited_role_none").clusterPermissions( - "cluster_composite_ops_ro", - "cluster_monitor" - ).clusterPermissions("cluster_composite_ops_ro", "cluster_monitor"); - static final TestSecurityConfig.User LIMITED_USER_NONE = new TestSecurityConfig.User("limited_user_none")// - .description("no index privileges")// - .roles(LIMITED_ROLE_NONE)// - .reference(READ, limitedToNone()); - - static final TestSecurityConfig.Role UNLIMITED_ROLE = new TestSecurityConfig.Role("unlimited_role")// - .clusterPermissions("*") - .indexPermissions("*") - .on("*"); - - static final TestSecurityConfig.User UNLIMITED_USER = new TestSecurityConfig.User("unlimited_user")// - .description("unlimited")// - .roles(UNLIMITED_ROLE)// - .reference( - READ, - limitedTo(LocalIndices.index_a1, LocalIndices.index_a2, RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) - ); - - /** - * The SUPER_UNLIMITED_USER authenticates with an admin cert, which will cause all access control code to be skipped. - * This serves as a base for comparison with the default behavior. - */ - static final TestSecurityConfig.User SUPER_UNLIMITED_USER = new TestSecurityConfig.User("super_unlimited_user")// - .description("super unlimited (admin cert)")// - .adminCertUser()// - .reference(READ, unlimitedIncludingOpenSearchSecurityIndex()); - - static final List USERS = ImmutableList.of( - LIMITED_USER_A_R, - LIMITED_USER_R, - LIMITED_USER_R1, - LIMITED_USER_NONE, - UNLIMITED_USER, - SUPER_UNLIMITED_USER - ); - - static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); - - @ClassRule - public static final LocalCluster remoteCluster = new LocalCluster.Builder().certificates(TEST_CERTIFICATES) - .clusterManager(ClusterManager.SINGLENODE) - .clusterName("remote_1") - .authc(AUTHC_HTTPBASIC_INTERNAL) - .privilegesEvaluationType("next_gen") - .roles(LIMITED_USER_ROLE_A_R_REMOTE, LIMITED_USER_ROLE_R_REMOTE, LIMITED_USER_ROLE_R1_REMOTE, LIMITED_ROLE_NONE, UNLIMITED_ROLE) - .indices(RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) - .build(); - - static LocalCluster.Builder clusterBuilder() { - return new LocalCluster.Builder().clusterManager(ClusterManager.SINGLE_REMOTE_CLIENT) - .remote("remote_1", remoteCluster) - .certificates(TEST_CERTIFICATES) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(USERS)// - .roles(LIMITED_USER_ROLE_A_R, LIMITED_USER_ROLE_R, LIMITED_USER_ROLE_R1, LIMITED_ROLE_NONE, UNLIMITED_ROLE) - .indices(LocalIndices.index_a1, LocalIndices.index_a2); - } - - @AfterClass - public static void stopClusters() { - for (ClusterConfig clusterConfig : ClusterConfig.values()) { - clusterConfig.shutdown(); - } - } - - final TestSecurityConfig.User user; - final LocalCluster cluster; - final ClusterConfig clusterConfig; - - @Test - public void search_wildcardWildcard() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("*:*/_search?size=1000"); - assertThat( - httpResponse, - containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) - .at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(isForbidden()) - ); - } - } - - @Test - public void search_remoteWildcard() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("remote_1:*/_search?size=1000"); - assertThat( - httpResponse, - containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) - .at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(isForbidden()) - ); - } - } - - @Test - public void search_remoteWildcard_minimizeRoundtrips() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("remote_1:*/_search?size=1000&ccs_minimize_roundtrips=true"); - assertThat( - httpResponse, - containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) - .at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(isForbidden()) - ); - } - } - - @Test - public void search_remoteStaticIndices() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("remote_1:index_r1/_search?size=1000"); - assertThat( - httpResponse, - containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1) - .at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(isForbidden()) - ); - } - } - - @Test - public void search_remoteStaticIndices_minimizeRoundtrips() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("remote_1:index_r1/_search?size=1000&ccs_minimize_roundtrips=true"); - assertThat( - httpResponse, - containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1) - .at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(isForbidden()) - ); - } - } - - @Test - public void search_remoteIndexPattern() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("remote_1:index_*/_search?size=1000"); - assertThat( - httpResponse, - containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) - .at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(isForbidden()) - ); - } - } - - @Test - public void search_remoteIndexPattern_minimizeRoundtrips() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("remote_1:index_*/_search?size=1000&ccs_minimize_roundtrips=true"); - assertThat( - httpResponse, - containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) - .at("hits.hits[*]._index") - .reducedBy(user.reference(READ)) - .whenEmpty(isForbidden()) - ); - } - } - - @Test - public void search_localStaticIndex_remoteStaticIndices() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("index_a2,remote_1:index_r1/_search?size=1000"); - assertThat( - httpResponse, - containsExactly(LocalIndices.index_a2).andFromRemote("remote_1", RemoteIndices.index_r1) - .at("hits.hits[*]._index") - .butForbiddenIfIncomplete(user.reference(READ)) - ); - } - } - - @Test - public void search_localIndexPattern_remoteIndexPattern() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("index_*,remote_1:index_*/_search?size=1000"); - - if (clusterConfig.legacyPrivilegeEvaluation) { - if (user.reference(READ).covers(LocalIndices.index_a1) || user.reference(READ).covers(LocalIndices.index_a2)) { - // Only if we have privileges for local indices, we also get through - assertThat( - httpResponse, - containsExactly(LocalIndices.index_a1, LocalIndices.index_a2).andFromRemote( - "remote_1", - RemoteIndices.index_r1, - RemoteIndices.index_r2, - RemoteIndices.index_r3 - ).at("hits.hits[*]._index").reducedBy(user.reference(READ)).whenEmpty(isForbidden()) - ); - } else { - assertThat(httpResponse, isForbidden()); - } - } else { - if (user != LIMITED_USER_NONE) { - assertThat( - httpResponse, - containsExactly(LocalIndices.index_a1, LocalIndices.index_a2).andFromRemote( - "remote_1", - RemoteIndices.index_r1, - RemoteIndices.index_r2, - RemoteIndices.index_r3 - ).at("hits.hits[*]._index").reducedBy(user.reference(READ)).whenEmpty(isOk()) - ); - } else { - // No search permissions anywhere will result in a 403 error - assertThat(httpResponse, isForbidden()); - } - } - } - } - - @Test - public void resolve_wildcardWildcard() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("_resolve/index/*:*"); - - assertThat( - httpResponse, - containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) - .at("$.*[*].name") - .reducedBy(user.reference(READ)) - .whenEmpty(isOk()) - ); - } - } - - @Test - public void resolve_remoteWildcard() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("_resolve/index/remote_1:*"); - - assertThat( - httpResponse, - containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) - .at("$.*[*].name") - .reducedBy(user.reference(READ)) - .whenEmpty(isOk()) - ); - } - } - - @Test - public void resolve_localWildcard_remoteWildcard() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("_resolve/index/*,remote_1:*"); - - assertThat( - httpResponse, - containsExactly(LocalIndices.index_a1, LocalIndices.index_a2).andFromRemote( - "remote_1", - RemoteIndices.index_r1, - RemoteIndices.index_r2, - RemoteIndices.index_r3 - ).at("$.*[*].name").reducedBy(user.reference(READ)).whenEmpty(isOk()) - ); - } - } - - @Test - public void resolve_localIndexPattern_remoteIndexPattern() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("_resolve/index/index_a1*,remote_1:index_r1*"); - - if (clusterConfig.legacyPrivilegeEvaluation) { - if (user.reference(READ).covers(LocalIndices.index_a1)) { - // Only if we have privileges for local indices, we also get through - assertThat( - httpResponse, - containsExactly(LocalIndices.index_a1).andFromRemote("remote_1", RemoteIndices.index_r1) - .at("$.*[*].name") - .reducedBy(user.reference(READ)) - .whenEmpty(isOk()) - ); - } else { - assertThat(httpResponse, isForbidden()); - } - } else { - assertThat( - httpResponse, - containsExactly(LocalIndices.index_a1).andFromRemote("remote_1", RemoteIndices.index_r1) - .at("$.*[*].name") - .reducedBy(user.reference(READ)) - .whenEmpty(isOk()) - ); - } - } - } - - @Test - public void field_caps_remoteWildcard() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("remote_1:*/_field_caps?fields=*"); - assertThat( - httpResponse, - containsExactly().andFromRemote("remote_1", RemoteIndices.index_r1, RemoteIndices.index_r2, RemoteIndices.index_r3) - .at("indices") - .reducedBy(user.reference(READ)) - .whenEmpty(isOk()) - ); - } - } - - @Test - public void field_caps_localIndexPattern_remoteIndexPattern() throws Exception { - try (TestRestClient restClient = cluster.getRestClient(user)) { - TestRestClient.HttpResponse httpResponse = restClient.get("index_*,remote_1:index_*/_field_caps?fields=*"); - if (clusterConfig.legacyPrivilegeEvaluation) { - if (user.reference(READ).covers(LocalIndices.index_a1) || user.reference(READ).covers(LocalIndices.index_a2)) { - // Only if we have privileges for local indices, we also get through - assertThat( - httpResponse, - containsExactly(LocalIndices.index_a1, LocalIndices.index_a2).andFromRemote( - "remote_1", - RemoteIndices.index_r1, - RemoteIndices.index_r2, - RemoteIndices.index_r3 - ).at("indices").reducedBy(user.reference(READ)).whenEmpty(isOk()) - ); - } else { - assertThat(httpResponse, isForbidden()); - } - } else { - assertThat( - httpResponse, - containsExactly(LocalIndices.index_a1, LocalIndices.index_a2).andFromRemote( - "remote_1", - RemoteIndices.index_r1, - RemoteIndices.index_r2, - RemoteIndices.index_r3 - ).at("indices").reducedBy(user.reference(READ)).whenEmpty(isOk()) - ); - } - } - } - - @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 CrossClusterAuthorizationIntTests(ClusterConfig clusterConfig, TestSecurityConfig.User user, String description) - throws Exception { - this.user = user; - this.cluster = clusterConfig.cluster(CrossClusterAuthorizationIntTests::clusterBuilder); - this.clusterConfig = clusterConfig; - } - -} 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 19a969cb05..641ae72488 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 @@ -402,22 +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()) - ); - } } } @@ -822,22 +812,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()) - ); - } } } 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 55f8110de6..2fd4c42019 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 @@ -311,7 +311,7 @@ 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, 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. @@ -698,7 +698,7 @@ public void search_staticIndices_systemIndex_alias() throws Exception { ); } } 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 { diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java index b216ccbcd1..1a4a70c853 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java @@ -54,7 +54,8 @@ public class MiscPrivilegesIntTests { ); 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(); /** 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 index 74e0188bfa..2cbeea89bf 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java @@ -65,6 +65,7 @@ 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; @@ -371,6 +372,12 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) return presponse; } + if (GetAllPitsAction.NAME.equals(action0)) { + // 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()) { diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java index af1765c38a..c7cc7b4c82 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java @@ -41,6 +41,7 @@ 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; @@ -387,6 +388,11 @@ private PluginSystemIndexSelection areIndicesPluginSystemIndices( * 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; From 42b14f0e3f4d210d5c53a91699080a60e9e56976 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Fri, 24 Oct 2025 13:31:00 +0200 Subject: [PATCH 09/29] Spotless Signed-off-by: Nils Bandener --- ...taStreamAuthorizationReadOnlyIntTests.java | 48 +++++++++-------- .../IndexAuthorizationReadOnlyIntTests.java | 54 +++++++++---------- .../int_tests/MiscPrivilegesIntTests.java | 4 +- .../privileges/ResourceAccessEvaluator.java | 11 ++-- .../legacy/PrivilegesEvaluator.java | 3 +- .../resources/ResourceAccessHandler.java | 1 - 6 files changed, 64 insertions(+), 57 deletions(-) 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 641ae72488..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 @@ -390,10 +390,10 @@ public void search_indexPattern_minus() throws Exception { // 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()) + 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()) ); } } @@ -402,12 +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"); - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3, ds_b1).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()) + ); } } @@ -418,12 +418,12 @@ public void search_indexPattern_nonExistingIndex_ignoreUnavailable() throws Exce "ds_a*,ds_b*,xxx_non_existing/_search?size=1000&ignore_unavailable=true" ); - 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, ds_b2, ds_b3).at("hits.hits[*]._index") + .reducedBy(user.reference(READ)) + .whenEmpty(clusterConfig.allowsEmptyResultSets ? isOk() : isForbidden()) + ); } } @@ -484,7 +484,9 @@ public void search_termsAggregation_index() throws Exception { assertThat( httpResponse, - containsExactly(ALL_INDICES_EXCEPT_SYSTEM_INDICES).at("aggregations.indices.buckets[*].key").reducedBy(user.reference(READ)).whenEmpty(isOk()) + containsExactly(ALL_INDICES_EXCEPT_SYSTEM_INDICES).at("aggregations.indices.buckets[*].key") + .reducedBy(user.reference(READ)) + .whenEmpty(isOk()) ); } @@ -812,12 +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=*"); - assertThat( - httpResponse, - containsExactly(ds_a1, ds_a2, ds_a3, ds_b1).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/IndexAuthorizationReadOnlyIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/IndexAuthorizationReadOnlyIntTests.java index 2fd4c42019..5bb1b69a1a 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 @@ -311,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)); + .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. @@ -1626,34 +1626,34 @@ public void getAlias_indexPattern_includeHidden() throws Exception { if (user == UNLIMITED_USER) { 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()") + 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()") + 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()) { diff --git a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java index 1a4a70c853..e3bf7702fe 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/MiscPrivilegesIntTests.java @@ -54,8 +54,8 @@ public class MiscPrivilegesIntTests { ); 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(); /** diff --git a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java index 21410a442b..4d3e2eb4c4 100644 --- a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java @@ -83,12 +83,17 @@ public void evaluateAsync( // 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( + resourceAccessHandler.hasPermission( + req.id(), + req.type(), + action, + ActionListener.wrap( hasAccess -> pResponseListener.onResponse( - hasAccess ? PrivilegesEvaluatorResponse.ok() : PrivilegesEvaluatorResponse.insufficient(action) + hasAccess ? PrivilegesEvaluatorResponse.ok() : PrivilegesEvaluatorResponse.insufficient(action) ), pResponseListener::onFailure - )); + ) + ); } /** 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 index 2cbeea89bf..eda70c078b 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java @@ -373,7 +373,8 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) } if (GetAllPitsAction.NAME.equals(action0)) { - // We preserve old behavior here: The "indices:data/read/point_in_time/readall" is allowed when I have privileges on any actions. + // 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)); } diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index ba74c69e14..7a3b153f3d 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -29,7 +29,6 @@ import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.privileges.PrivilegesConfiguration; -import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.resources.sharing.Recipient; import org.opensearch.security.resources.sharing.ResourceSharing; import org.opensearch.security.resources.sharing.ShareWith; From 5d35b14642af24ecc4f69be24e11e276414773f2 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Fri, 24 Oct 2025 13:54:38 +0200 Subject: [PATCH 10/29] Fixes after rebasing Signed-off-by: Nils Bandener --- .../security/filter/SecurityFilterTests.java | 1 + .../legacy/PrivilegesEvaluatorUnitTest.java | 1 - .../security/resources/ResourceAccessHandlerTest.java | 10 ++++++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java b/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java index 41943fccef..6a8f662e05 100644 --- a/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java +++ b/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java @@ -32,6 +32,7 @@ import org.opensearch.security.configuration.DlsFlsRequestValve; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.privileges.PrivilegesConfiguration; +import org.opensearch.security.privileges.ResourceAccessEvaluator; import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.privileges.ResourceAccessEvaluator; import org.opensearch.security.support.ConfigConstants; diff --git a/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorUnitTest.java b/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorUnitTest.java index 022e40f839..cd5feff67f 100644 --- a/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorUnitTest.java +++ b/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorUnitTest.java @@ -18,7 +18,6 @@ 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.actionlevel.legacy.PrivilegesEvaluator.DNFOF_MATCHER; import static org.opensearch.security.privileges.actionlevel.legacy.PrivilegesEvaluator.isClusterPermissionStatic; import static org.junit.Assert.assertFalse; diff --git a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java index bc8b5b5217..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,8 +71,13 @@ public class ResourceAccessHandlerTest { public void setup() { threadContext = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext); - handler = new ResourceAccessHandler(threadPool, sharingIndexHandler, adminDNs, new PrivilegesConfiguration(privilegesEvaluator)); - 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)); From 86141aa578260354981f0325ba702402aa2b39e5 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Sat, 25 Oct 2025 08:38:17 +0200 Subject: [PATCH 11/29] Typo fix Signed-off-by: Nils Bandener --- .../java/org/opensearch/security/privileges/IndexPattern.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/privileges/IndexPattern.java b/src/main/java/org/opensearch/security/privileges/IndexPattern.java index 977cbfe15c..7197e7aea1 100644 --- a/src/main/java/org/opensearch/security/privileges/IndexPattern.java +++ b/src/main/java/org/opensearch/security/privileges/IndexPattern.java @@ -60,13 +60,13 @@ private IndexPattern( WildcardMatcher staticPattern, ImmutableList patternTemplates, ImmutableList dateMathExpressions, - boolean memberIndexPrivilegesYieldALiasPrivileges + boolean memberIndexPrivilegesYieldAliasPrivileges ) { this.staticPattern = staticPattern; this.patternTemplates = patternTemplates; this.dateMathExpressions = dateMathExpressions; this.hashCode = staticPattern.hashCode() + patternTemplates.hashCode() + dateMathExpressions.hashCode(); - this.memberIndexPrivilegesYieldAliasPrivileges = memberIndexPrivilegesYieldALiasPrivileges; + this.memberIndexPrivilegesYieldAliasPrivileges = memberIndexPrivilegesYieldAliasPrivileges; } public boolean matches( From 1aa1f59347870ddc26f1a695a80f587046d46db8 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Sat, 25 Oct 2025 15:06:42 +0200 Subject: [PATCH 12/29] Removed unnecessary assertions Signed-off-by: Nils Bandener --- .../protected_indices/ProtectedIndicesTests.java | 14 -------------- 1 file changed, 14 deletions(-) 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)); } } From 341144328f6c76e7e7e999582237538f6db5e687 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Sat, 25 Oct 2025 15:16:42 +0200 Subject: [PATCH 13/29] Changed error message to the one that is expected by most tests Signed-off-by: Nils Bandener --- .../org/opensearch/security/privileges/PrivilegesEvaluator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index df5807a082..8b1f5568f9 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -137,7 +137,7 @@ public boolean isInitialized() { } private OpenSearchSecurityException exception() { - StringBuilder error = new StringBuilder("OpenSearch Security is not initialized"); + StringBuilder error = new StringBuilder("OpenSearch Security not initialized"); String reason = this.unavailablityReasonSupplier.get(); if (reason != null) { From 41475848f8b8b934dddb6605c56fa2cb021ebd33 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Sun, 26 Oct 2025 15:03:11 +0100 Subject: [PATCH 14/29] Fixed SystemIndexAccessEvaluatorTest Signed-off-by: Nils Bandener --- .../legacy/SystemIndexAccessEvaluator.java | 22 +- .../SystemIndexAccessEvaluatorTest.java | 291 ++++++++++++------ 2 files changed, 211 insertions(+), 102 deletions(-) diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java index c7cc7b4c82..81af94c9fc 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluator.java @@ -152,21 +152,19 @@ public PrivilegesEvaluatorResponse evaluate( containsSystemIndex ); - if (response == null || response.isAllowed()) { - if (containsSystemIndex) { + if (containsSystemIndex) { - if (request instanceof SearchRequest) { - ((SearchRequest) request).requestCache(Boolean.FALSE); - if (log.isDebugEnabled()) { - log.debug("Disable search request cache for this request"); - } + if (request instanceof SearchRequest) { + ((SearchRequest) request).requestCache(Boolean.FALSE); + if (log.isDebugEnabled()) { + log.debug("Disable search request cache for this request"); } + } - if (request instanceof RealtimeRequest) { - ((RealtimeRequest) request).realtime(Boolean.FALSE); - if (log.isDebugEnabled()) { - log.debug("Disable realtime for this request"); - } + if (request instanceof RealtimeRequest) { + ((RealtimeRequest) request).realtime(Boolean.FALSE); + if (log.isDebugEnabled()) { + log.debug("Disable realtime for this request"); } } } diff --git a/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluatorTest.java index 28db045a73..706708a194 100644 --- a/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluatorTest.java @@ -21,11 +21,9 @@ 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.ActionRequestMetadata; @@ -33,6 +31,7 @@ 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; @@ -56,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.ArgumentMatchers.isNull; -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.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) @@ -70,12 +68,8 @@ public class SystemIndexAccessEvaluatorTest { @Mock private AuditLog auditLog; @Mock - private ActionRequest request; - @Mock private Task task; @Mock - private PrivilegesEvaluatorResponse presponse; - @Mock private Logger log; @Mock ClusterService cs; @@ -175,7 +169,7 @@ PrivilegesEvaluationContext ctx(String action) { user, ImmutableSet.of("role_a"), action, - request, + new SearchRequest(), ActionRequestMetadata.empty(), null, indexNameExpressionResolver, @@ -185,11 +179,6 @@ PrivilegesEvaluationContext ctx(String action) { ); } - @After - public void after() { - verifyNoMoreInteractions(auditLog, request, task, presponse, log); - } - @Test public void testUnprotectedActionOnRegularIndex_systemIndexDisabled() { setup(false, false, TEST_INDEX, false); @@ -197,7 +186,7 @@ public void testUnprotectedActionOnRegularIndex_systemIndexDisabled() { // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( - request, + new SearchRequest(TEST_INDEX), null, UNPROTECTED_ACTION, resolved, @@ -205,7 +194,7 @@ public void testUnprotectedActionOnRegularIndex_systemIndexDisabled() { actionPrivileges, user ); - assertThat(response, isNull()); + assertThat(response, is(nullValue())); } @@ -216,7 +205,7 @@ public void testUnprotectedActionOnRegularIndex_systemIndexPermissionDisabled() // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( - request, + new SearchRequest(TEST_INDEX), null, UNPROTECTED_ACTION, resolved, @@ -224,7 +213,7 @@ public void testUnprotectedActionOnRegularIndex_systemIndexPermissionDisabled() actionPrivileges, user ); - assertThat(response, isNull()); + assertThat(response, is(nullValue())); } @Test @@ -234,7 +223,7 @@ public void testUnprotectedActionOnRegularIndex_systemIndexPermissionEnabled() { // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( - request, + new SearchRequest(TEST_INDEX), null, UNPROTECTED_ACTION, resolved, @@ -242,7 +231,7 @@ public void testUnprotectedActionOnRegularIndex_systemIndexPermissionEnabled() { actionPrivileges, user ); - assertThat(response, isNull()); + assertThat(response, is(nullValue())); } @Test @@ -252,7 +241,7 @@ public void testUnprotectedActionOnSystemIndex_systemIndexDisabled() { // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( - request, + new SearchRequest(TEST_SYSTEM_INDEX), null, UNPROTECTED_ACTION, resolved, @@ -260,7 +249,7 @@ public void testUnprotectedActionOnSystemIndex_systemIndexDisabled() { actionPrivileges, user ); - assertThat(response, isNull()); + assertThat(response, is(nullValue())); } @Test @@ -270,7 +259,7 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionDisabled() { // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( - request, + new SearchRequest(TEST_SYSTEM_INDEX), null, UNPROTECTED_ACTION, resolved, @@ -278,13 +267,14 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionDisabled() { actionPrivileges, user ); - assertThat(response, isNull()); + assertThat(response, is(nullValue())); } @Test public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_WithoutSystemIndexPermission() { setup(true, true, TEST_SYSTEM_INDEX, false); final ResolvedIndices resolved = createResolved(TEST_SYSTEM_INDEX); + SearchRequest request = new SearchRequest(TEST_SYSTEM_INDEX); // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( @@ -296,7 +286,7 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_With actionPrivileges, user ); - assertThat(presponse.isAllowed(), is(false)); + assertThat(response.isAllowed(), is(false)); verify(auditLog).logSecurityIndexAttempt(request, UNPROTECTED_ACTION, null); verify(log).isInfoEnabled(); @@ -315,7 +305,7 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_With // Action final PrivilegesEvaluatorResponse response = evaluator.evaluate( - request, + new SearchRequest(TEST_SYSTEM_INDEX), null, UNPROTECTED_ACTION, resolved, @@ -323,21 +313,19 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_With actionPrivileges, user ); - // unprotected action is not allowed on a system index - assertThat(presponse.isAllowed(), 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 ResolvedIndices resolved = createResolved(TEST_SYSTEM_INDEX); // Action PrivilegesEvaluatorResponse response = evaluator.evaluate( - request, + new SearchRequest(TEST_SYSTEM_INDEX), null, UNPROTECTED_ACTION, resolved, @@ -345,26 +333,25 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexDisabled() { actionPrivileges, user ); - assertThat(response, isNull()); + 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 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, 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"); } @@ -373,27 +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 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, 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(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(), @@ -407,121 +392,193 @@ 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 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, 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"); } - /* @Test public void testProtectedActionLocalAll_systemIndexDisabled() { setup(false, false, TEST_SYSTEM_INDEX, false); - final ResolvedIndices resolved = ResolvedIndices.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 ResolvedIndices resolved = ResolvedIndices.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 ResolvedIndices resolved = ResolvedIndices.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 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 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 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 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 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); } @@ -529,13 +586,21 @@ public void testProtectedActionOnSystemIndex_systemIndexPermissionDisabled() { public void testProtectedActionOnSystemIndex_systemIndexPermissionEnabled_withoutSystemIndexPermission() { setup(true, true, TEST_SYSTEM_INDEX, false); 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 {}", @@ -550,24 +615,41 @@ public void testProtectedActionOnSystemIndex_systemIndexPermissionEnabled_withSy setup(true, true, TEST_SYSTEM_INDEX, true); 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 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); } @@ -576,13 +658,21 @@ public void testProtectedActionOnProtectedSystemIndex_systemIndexDisabled() { public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionDisabled() { setup(true, false, SECURITY_INDEX, false); 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); } @@ -605,7 +695,28 @@ public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionEnabl @Test public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionEnabled_withSystemIndexPermission() { testSecurityIndexAccess(PROTECTED_ACTION); - }*/ + } + + private void testSecurityIndexAccess(String action) { + setup(true, true, SECURITY_INDEX, true); + + final OptionallyResolvedIndices resolved = ResolvedIndices.of(SECURITY_INDEX); + final SearchRequest request = new SearchRequest(SECURITY_INDEX); + + // Action + PrivilegesEvaluatorResponse presponse = evaluator.evaluate(request, task, action, resolved, ctx(action), actionPrivileges, user); + + verify(auditLog).logSecurityIndexAttempt(request, action, task); + assertThat(presponse.isAllowed(), is(false)); + + verify(log).isInfoEnabled(); + verify(log).info( + "{} not permitted for a regular user {} on protected system indices {}", + action, + user.getSecurityRoles(), + SECURITY_INDEX + ); + } private ResolvedIndices createResolved(final String... indexes) { return ResolvedIndices.of(indexes); From aa63a0e0fd2db3293c2b8a96b35472ba19ecd4f6 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Sun, 26 Oct 2025 18:49:56 +0100 Subject: [PATCH 15/29] Fixes Signed-off-by: Nils Bandener --- .../legacy/PrivilegesEvaluator.java | 2 +- .../identity/SecurityTokenManagerTest.java | 19 -------- .../support/HostResolverModeTest.java | 5 -- .../AbstractSystemIndicesTests.java | 2 +- .../SystemIndexDisabledTests.java | 14 +++--- .../SystemIndexPermissionDisabledTests.java | 24 ++++++---- .../SystemIndexPermissionEnabledTests.java | 48 +++++++++---------- 7 files changed, 46 insertions(+), 68 deletions(-) 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 index eda70c078b..336e4698ea 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluator.java @@ -372,7 +372,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) return presponse; } - if (GetAllPitsAction.NAME.equals(action0)) { + 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. diff --git a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java index 1507d77c73..a805ec88ba 100644 --- a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java +++ b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java @@ -284,23 +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); - - 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/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..914e798962 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); + validateForbiddenResponse(response, action, user); } 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..20ade6ae22 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); 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..e5f2f0a545 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); + validateForbiddenResponse(res, "cluster:admin/snapshot/restore", normalUser); } } @@ -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())); } } } From ee5edd7a0df2a24bdc2af368bee3b5a59e3dc872 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Sun, 26 Oct 2025 20:54:10 +0100 Subject: [PATCH 16/29] Fixes Signed-off-by: Nils Bandener --- .../actionlevel/SubjectBasedActionPrivilegesTest.java | 4 ++-- .../security/systemindex/SystemIndexTests.java | 9 ++++++--- .../actionlevel/SubjectBasedActionPrivileges.java | 10 +++++++--- .../system_indices/AbstractSystemIndicesTests.java | 2 +- .../SystemIndexPermissionEnabledTests.java | 2 +- 5 files changed, 17 insertions(+), 10 deletions(-) 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 f1c01f8bb9..2380b6a28b 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java @@ -516,7 +516,7 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec) throws Exception config, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - false + true ); } @@ -785,7 +785,7 @@ public void hasExplicitIndexPrivilege_errors() throws Exception { 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/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/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java index 23d83d3d8b..f437300860 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java @@ -415,12 +415,16 @@ protected IntermediateResult checkWildcardIndexPrivilegesOnWellKnownActions( CheckTable checkTable ) { for (String action : actions) { - if (!this.actionsWithWildcardIndexPrivileges.contains(action)) { - return null; + if (this.actionsWithWildcardIndexPrivileges.contains(action)) { + checkTable.checkIf(index -> true, action); } } - return new IntermediateResult(checkTable); + if (checkTable.isComplete()) { + return new IntermediateResult(checkTable); + } else { + return null; + } } /** 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 914e798962..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, 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/SystemIndexPermissionEnabledTests.java b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java index e5f2f0a545..78b9bfda8e 100644 --- a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java +++ b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java @@ -432,7 +432,7 @@ public void testSnapshotSystemIndicesAsNormalUser() { normalUserHeader ); - validateForbiddenResponse(res, "cluster:admin/snapshot/restore", normalUser); + assertThat(res.getStatusCode(), is(RestStatus.FORBIDDEN.getStatus())); } } From ea2777f2e40a9cd957f5f18c8fdb472398a8fe26 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Mon, 27 Oct 2025 04:02:44 +0100 Subject: [PATCH 17/29] Temporarily raised log level Signed-off-by: Nils Bandener --- src/test/resources/log4j2-test.properties | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 From f8eeaf25516380ad55ef5713bf6fb00d791ff894 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Mon, 27 Oct 2025 04:03:20 +0100 Subject: [PATCH 18/29] Test fixes Signed-off-by: Nils Bandener --- .../RoleBasedActionPrivilegesTest.java | 128 +++++++++--------- .../SubjectBasedActionPrivilegesTest.java | 41 +----- .../security/privileges/IndexPattern.java | 8 +- .../multitenancy/test/MultitenancyTests.java | 18 +-- .../SystemIndexDisabledTests.java | 2 +- 5 files changed, 85 insertions(+), 112 deletions(-) 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 a4c693ebea..7f9f19fe47 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java @@ -551,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))); } @@ -572,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") @@ -584,16 +592,38 @@ public void positive_partial() throws Exception { assertThat( result, isPartiallyOk( - "data_stream_a11", - ".ds-data_stream_a11-000001", - ".ds-data_stream_a11-000002", - ".ds-data_stream_a11-000003" + "data_stream_a11" ) ); - } else if (covers(ctx, ".ds-data_stream_a11-000001")) { + } 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(".ds-data_stream_a11-000001", ".ds-data_stream_a11-000002", ".ds-data_stream_a11-000003") + 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))); @@ -603,14 +633,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))); } @@ -676,28 +706,33 @@ 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() - .put( - RoleBasedActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey(), - new ByteSizeValue(10, ByteSizeUnit.BYTES) - ) - .build(); + .put( + RoleBasedActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey(), + new ByteSizeValue(10, ByteSizeUnit.BYTES) + ) + .build(); } - this.subject = new RoleBasedActionPrivileges( - roles, - FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - settings, - false + 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 = // @@ -706,22 +741,6 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness stat static ResolvedIndices resolved(String... indices) { return ResolvedIndices.of(indices); - // TODO check - // 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); - // } - } } @@ -849,21 +868,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, i -> false) == metadata - ); - } - @Test public void relevantOnly_closed() throws Exception { Map metadata = indices("index_open_1", "index_open_2")// @@ -942,7 +946,7 @@ public void hasIndexPrivilege_errors() throws Exception { assertTrue( "Result mentions role_with_errors: " + result.getEvaluationExceptionInfo(), result.getEvaluationExceptionInfo() - .startsWith("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating") + .contains("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating") ); } @@ -1075,7 +1079,7 @@ public void hasExplicitIndexPrivilege_errors() throws Exception { assertTrue( "Result mentions role_with_errors: " + result.getEvaluationExceptionInfo(), result.getEvaluationExceptionInfo() - .startsWith("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating role role_with_errors") + .contains("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating role role_with_errors") ); } 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 2380b6a28b..c5362e45d6 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java @@ -404,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))); } @@ -429,17 +424,9 @@ public void positive_partial() throws Exception { assertThat( result, isPartiallyOk( - "data_stream_a11", - ".ds-data_stream_a11-000001", - ".ds-data_stream_a11-000002", - ".ds-data_stream_a11-000003" + "data_stream_a11" ) ); - } 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))); } @@ -516,7 +503,7 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec) throws Exception config, FlattenedActionGroups.EMPTY, RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - true + false // breakDownAliases = true is already sufficiently checked in RoleBasedActionPrivilegesTest ); } @@ -526,30 +513,6 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec) throws Exception static ResolvedIndices resolved(String... indices) { return ResolvedIndices.of(indices); - /* TODO CHECK - 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 - ); - - */ } } diff --git a/src/main/java/org/opensearch/security/privileges/IndexPattern.java b/src/main/java/org/opensearch/security/privileges/IndexPattern.java index 7197e7aea1..df296df0ea 100644 --- a/src/main/java/org/opensearch/security/privileges/IndexPattern.java +++ b/src/main/java/org/opensearch/security/privileges/IndexPattern.java @@ -53,9 +53,15 @@ public class IndexPattern { * Index patterns which contain date math (like ) */ private final ImmutableList dateMathExpressions; - private final int hashCode; + + /** + * 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, 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..f966ec5099 100644 --- a/src/test/java/org/opensearch/security/multitenancy/test/MultitenancyTests.java +++ b/src/test/java/org/opensearch/security/multitenancy/test/MultitenancyTests.java @@ -406,7 +406,7 @@ public void testMtMulti() throws Exception { Assert.assertTrue(res.getBody().contains(dashboardsIndex)); // get - assertThat( + assertThat(res.getBody(), HttpStatus.SC_OK, is( (res = rh.executeGetRequest( @@ -416,10 +416,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 +563,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 +638,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/system_indices/SystemIndexDisabledTests.java b/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java index 20ade6ae22..cfb08349ad 100644 --- a/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java +++ b/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java @@ -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)); } } } From b416fe7e56079c2ae81a75b1e34275ff69a86268 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Mon, 27 Oct 2025 10:18:21 +0100 Subject: [PATCH 19/29] API integration test refactoring Signed-off-by: Nils Bandener --- .../security/ConfigurationFiles.java | 11 - .../api/AbstractApiIntegrationTest.java | 244 ++-------- ...bstractConfigEntityApiIntegrationTest.java | 164 ++++--- .../api/AccountRestApiIntegrationTest.java | 151 ++++--- .../ActionGroupsRestApiIntegrationTest.java | 240 ++++++---- .../CertificatesRestApiIntegrationTest.java | 100 +++-- .../api/ConfigRestApiIntegrationTest.java | 110 +++-- .../security/api/DashboardsInfoTest.java | 24 +- .../api/DashboardsInfoWithSettingsTest.java | 36 +- ...DefaultApiAvailabilityIntegrationTest.java | 87 ++-- .../api/FlushCacheApiIntegrationTest.java | 36 +- ...xpPasswordRulesRestApiIntegrationTest.java | 89 ++-- .../InternalUsersRestApiIntegrationTest.java | 419 ++++++++++-------- ...edPasswordRulesRestApiIntegrationTest.java | 69 +-- .../RolesMappingRestApiIntegrationTest.java | 266 ++++++----- .../api/RolesRestApiIntegrationTest.java | 174 +++++--- .../RollbackVersionApiIntegrationTest.java | 52 +-- .../api/SslCertsRestApiIntegrationTest.java | 50 ++- .../api/TenantsRestApiIntegrationTest.java | 74 +++- .../api/ViewVersionApiIntegrationTest.java | 53 +-- .../test/framework/TestSecurityConfig.java | 52 +-- .../test/framework/cluster/LocalCluster.java | 5 + .../test/framework/matcher/RestMatchers.java | 8 + 23 files changed, 1313 insertions(+), 1201 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java b/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java index f871e131b9..4b0ef62b49 100644 --- a/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java +++ b/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java @@ -11,13 +11,10 @@ import java.io.IOException; import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import java.util.Objects; -import org.opensearch.core.common.Strings; import org.opensearch.security.securityconf.impl.CType; public class ConfigurationFiles { @@ -43,14 +40,6 @@ public static Path createConfigurationDirectory() { } } - public static void writeToConfig(final CType cType, final Path configFolder, final String content) throws IOException { - if (Strings.isNullOrEmpty(content)) return; - try (final var out = Files.newOutputStream(cType.configFile(configFolder), StandardOpenOption.APPEND)) { - out.write(content.getBytes(StandardCharsets.UTF_8)); - out.flush(); - } - } - public static void copyResourceToFile(String resource, Path destination) { try (InputStream input = ConfigurationFiles.class.getClassLoader().getResourceAsStream(resource)) { Objects.requireNonNull(input, "Cannot find source resource " + resource); diff --git a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java index 00c0cf4f07..b1dabff199 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java @@ -11,8 +11,6 @@ package org.opensearch.security.api; -import java.io.IOException; -import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -20,29 +18,17 @@ import com.carrotsearch.randomizedtesting.RandomizedTest; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import org.apache.commons.io.FileUtils; import org.apache.http.HttpStatus; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.awaitility.Awaitility; -import org.junit.AfterClass; -import org.junit.Before; import org.junit.runner.RunWith; -import org.opensearch.common.CheckedConsumer; import org.opensearch.common.CheckedSupplier; import org.opensearch.common.settings.Settings; -import org.opensearch.common.xcontent.XContentFactory; -import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.security.ConfigurationFiles; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.security.hasher.PasswordHasher; import org.opensearch.security.hasher.PasswordHasherFactory; -import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.certificate.CertificateData; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -52,7 +38,6 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.notNullValue; import static org.opensearch.security.CrossClusterSearchTests.PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED; import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; @@ -61,21 +46,28 @@ import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.RELOAD_CERTS_ACTION; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; -import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; -import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE; -import static org.opensearch.test.framework.TestSecurityConfig.REST_ADMIN_REST_API_ACCESS; @ThreadLeakScope(ThreadLeakScope.Scope.NONE) @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) public abstract class AbstractApiIntegrationTest extends RandomizedTest { - private static final Logger LOGGER = LogManager.getLogger(TestSecurityConfig.class); - - public static final String NEW_USER = "new-user"; + public static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles( + new TestSecurityConfig.Role("all_access").clusterPermissions("*").indexPermissions("*").on("*") + ); + public static final TestSecurityConfig.User REST_ADMIN_USER = new TestSecurityConfig.User("rest-api-admin").roles( + new TestSecurityConfig.Role("role").clusterPermissions(allRestAdminPermissions()) + ); - public static final String REST_ADMIN_USER = "rest-api-admin"; + public static final TestSecurityConfig.Role REST_ADMIN_REST_API_ACCESS_ROLE = new TestSecurityConfig.Role( + "rest_admin__rest_api_access" + ); + public static final TestSecurityConfig.Role EXAMPLE_ROLE = new TestSecurityConfig.Role("example_role").indexPermissions("crud") + .on("example_index"); - public static final String ADMIN_USER_NAME = "admin"; + /** + * A user without any privileges + */ + public static final TestSecurityConfig.User NEW_USER = new TestSecurityConfig.User("new-user"); public static final String DEFAULT_PASSWORD = "secret"; @@ -85,121 +77,23 @@ public abstract class AbstractApiIntegrationTest extends RandomizedTest { Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.BCRYPT).build() ); - public static Path configurationFolder; - - protected static TestSecurityConfig testSecurityConfig = new TestSecurityConfig(); - - public static LocalCluster localCluster; - - private Class testClass; - - @Before - public void startCluster() throws IOException { - if (this.getClass().equals(testClass)) { - return; - } - configurationFolder = ConfigurationFiles.createConfigurationDirectory(); - extendConfiguration(); - final var clusterManager = randomFrom(List.of(ClusterManager.THREE_CLUSTER_MANAGERS, ClusterManager.SINGLENODE)); - final var localClusterBuilder = new LocalCluster.Builder().clusterManager(clusterManager) + protected static LocalCluster.Builder clusterBuilder() { + return new LocalCluster.Builder().clusterManager(ClusterManager.DEFAULT) .nodeSettings(getClusterSettings()) - .defaultConfigurationInitDirectory(configurationFolder.toString()) - .loadConfigurationIntoIndex(false); - localCluster = localClusterBuilder.build(); - localCluster.before(); - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { - Awaitility.await() - .alias("Load default configuration") - .until(() -> client.securityHealth().getTextFromJsonBody("/status"), equalTo("UP")); - } - testClass = this.getClass(); + .authc(TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER, REST_ADMIN_USER, NEW_USER) + .roles(EXAMPLE_ROLE, REST_ADMIN_REST_API_ACCESS_ROLE); } - protected Map getClusterSettings() { + protected static Map getClusterSettings() { Map clusterSettings = new HashMap<>(); - clusterSettings.put(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true); - clusterSettings.put(PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_admin__all_access", REST_ADMIN_REST_API_ACCESS)); - clusterSettings.put(SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE, randomBoolean()); + clusterSettings.put( + PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED, + List.of("user_admin__all_access", REST_ADMIN_REST_API_ACCESS_ROLE.getName(), "user_rest-api-admin__role") + ); return clusterSettings; } - private static void extendConfiguration() throws IOException { - extendActionGroups(configurationFolder, testSecurityConfig.actionGroups()); - extendRoles(configurationFolder, testSecurityConfig.roles()); - extendRolesMapping(configurationFolder, testSecurityConfig.rolesMapping()); - extendUsers(configurationFolder, testSecurityConfig.getUsers()); - } - - private static void extendUsers(final Path configFolder, final List users) throws IOException { - if (users == null) return; - if (users.isEmpty()) return; - LOGGER.info("Adding users to the default configuration: "); - try (final var contentBuilder = XContentFactory.yamlBuilder()) { - contentBuilder.startObject(); - for (final var u : users) { - LOGGER.info("\t\t - {}", u.getName()); - contentBuilder.field(u.getName()); - u.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS); - } - contentBuilder.endObject(); - ConfigurationFiles.writeToConfig(CType.INTERNALUSERS, configFolder, removeDashes(contentBuilder.toString())); - } - } - - private static void extendActionGroups(final Path configFolder, final List actionGroups) - throws IOException { - if (actionGroups == null) return; - if (actionGroups.isEmpty()) return; - LOGGER.info("Adding action groups to the default configuration: "); - try (final var contentBuilder = XContentFactory.yamlBuilder()) { - contentBuilder.startObject(); - for (final var ag : actionGroups) { - LOGGER.info("\t\t - {}", ag.name()); - contentBuilder.field(ag.name()); - ag.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS); - } - contentBuilder.endObject(); - ConfigurationFiles.writeToConfig(CType.ACTIONGROUPS, configFolder, removeDashes(contentBuilder.toString())); - } - } - - private static void extendRoles(final Path configFolder, final List roles) throws IOException { - if (roles == null) return; - if (roles.isEmpty()) return; - LOGGER.info("Adding roles to the default configuration: "); - try (final var contentBuilder = XContentFactory.yamlBuilder()) { - contentBuilder.startObject(); - for (final var r : roles) { - LOGGER.info("\t\t - {}", r.getName()); - contentBuilder.field(r.getName()); - r.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS); - } - contentBuilder.endObject(); - ConfigurationFiles.writeToConfig(CType.ROLES, configFolder, removeDashes(contentBuilder.toString())); - } - } - - private static void extendRolesMapping(final Path configFolder, final List rolesMapping) - throws IOException { - if (rolesMapping == null) return; - if (rolesMapping.isEmpty()) return; - LOGGER.info("Adding roles mapping to the default configuration: "); - try (final var contentBuilder = XContentFactory.yamlBuilder()) { - contentBuilder.startObject(); - for (final var rm : rolesMapping) { - LOGGER.info("\t\t - {}", rm.name()); - contentBuilder.field(rm.name()); - rm.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS); - } - contentBuilder.endObject(); - ConfigurationFiles.writeToConfig(CType.ROLESMAPPING, configFolder, removeDashes(contentBuilder.toString())); - } - } - - private static String removeDashes(final String content) { - return content.replace("---", ""); - } - protected static String[] allRestAdminPermissions() { final var permissions = new String[ENDPOINTS_WITH_PERMISSIONS.size() + 1]; // 1 additional action for SSL update certs var counter = 0; @@ -233,42 +127,6 @@ protected String randomRestAdminPermission() { return randomFrom(permissions); } - @AfterClass - public static void stopCluster() throws IOException { - if (localCluster != null) localCluster.close(); - FileUtils.deleteDirectory(configurationFolder.toFile()); - } - - protected void withUser(final String user, final CheckedConsumer restClientHandler) throws Exception { - withUser(user, DEFAULT_PASSWORD, restClientHandler); - } - - protected void withUser(final String user, final String password, final CheckedConsumer restClientHandler) - throws Exception { - try (TestRestClient client = localCluster.getRestClient(user, password)) { - restClientHandler.accept(client); - } - } - - protected void withUser( - final String user, - final CertificateData certificateData, - final CheckedConsumer restClientHandler - ) throws Exception { - withUser(user, DEFAULT_PASSWORD, certificateData, restClientHandler); - } - - protected void withUser( - final String user, - final String password, - final CertificateData certificateData, - final CheckedConsumer restClientHandler - ) throws Exception { - try (final TestRestClient client = localCluster.getRestClient(user, password, certificateData)) { - restClientHandler.accept(client); - } - } - protected String apiPathPrefix() { return randomFrom(List.of(LEGACY_OPENDISTRO_PREFIX, PLUGINS_PREFIX)); } @@ -298,18 +156,6 @@ protected String apiPath(final String... path) { return fullPath.toString(); } - void badRequestWithReason(final CheckedSupplier endpointCallback, final String expectedMessage) - throws Exception { - final var response = badRequest(endpointCallback); - assertThat(response.getBody(), response.getTextFromJsonBody("/reason"), is(expectedMessage)); - } - - void badRequestWithMessage(final CheckedSupplier endpointCallback, final String expectedMessage) - throws Exception { - final var response = badRequest(endpointCallback); - assertThat(response.getBody(), response.getTextFromJsonBody("/message"), is(expectedMessage)); - } - public static TestRestClient.HttpResponse badRequest(final CheckedSupplier endpointCallback) throws Exception { final var response = endpointCallback.get(); @@ -318,14 +164,6 @@ public static TestRestClient.HttpResponse badRequest(final CheckedSupplier endpointCallback) throws Exception { - final var response = endpointCallback.get(); - assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_CREATED)); - assertResponseBody(response.getBody()); - assertThat(response.getBody(), response.getTextFromJsonBody("/status"), equalToIgnoringCase("created")); - return response; - } - public static void forbidden( final CheckedSupplier endpointCallback, final String expectedMessage @@ -342,14 +180,6 @@ public static TestRestClient.HttpResponse forbidden(final CheckedSupplier endpointCallback) - throws Exception { - final var response = endpointCallback.get(); - assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_METHOD_NOT_ALLOWED)); - assertResponseBody(response.getBody()); - return response; - } - public static TestRestClient.HttpResponse notImplemented(final CheckedSupplier endpointCallback) throws Exception { final var response = endpointCallback.get(); @@ -366,12 +196,6 @@ public static TestRestClient.HttpResponse notFound(final CheckedSupplier endpointCallback, final String expectedMessage) - throws Exception { - final var response = notFound(endpointCallback); - assertThat(response.getBody(), response.getTextFromJsonBody("/message"), is(expectedMessage)); - } - public static TestRestClient.HttpResponse ok(final CheckedSupplier endpointCallback) throws Exception { final var response = endpointCallback.get(); @@ -380,24 +204,6 @@ public static TestRestClient.HttpResponse ok(final CheckedSupplier endpointCallback, - final String expectedMessage - ) throws Exception { - final var response = endpointCallback.get(); - assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); - assertResponseBody(response.getBody(), expectedMessage); - return response; - } - - TestRestClient.HttpResponse unauthorized(final CheckedSupplier endpointCallback) - throws Exception { - final var response = endpointCallback.get(); - assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_UNAUTHORIZED)); - assertResponseBody(response.getBody()); - return response; - } - public static void assertResponseBody(final String responseBody) { assertThat(responseBody, notNullValue()); assertThat(responseBody, not(equalTo(""))); diff --git a/src/integrationTest/java/org/opensearch/security/api/AbstractConfigEntityApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AbstractConfigEntityApiIntegrationTest.java index a6d6902359..16031a8d42 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AbstractConfigEntityApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AbstractConfigEntityApiIntegrationTest.java @@ -16,10 +16,10 @@ import java.util.StringJoiner; import org.hamcrest.Matcher; -import org.junit.Test; import org.opensearch.common.CheckedSupplier; import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import com.nimbusds.jose.util.Pair; @@ -27,7 +27,6 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.oneOf; import static org.opensearch.security.api.PatchPayloadHelper.addOp; @@ -35,18 +34,16 @@ import static org.opensearch.security.api.PatchPayloadHelper.removeOp; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; +import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; +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.isNotFound; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public abstract class AbstractConfigEntityApiIntegrationTest extends AbstractApiIntegrationTest { - static { - testSecurityConfig.withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions()); - } - - @Override - protected Map getClusterSettings() { - Map clusterSettings = super.getClusterSettings(); - clusterSettings.put(SECURITY_RESTAPI_ADMIN_ENABLED, true); - return clusterSettings; + protected static LocalCluster.Builder clusterBuilder() { + return AbstractApiIntegrationTest.clusterBuilder().nodeSetting(SECURITY_RESTAPI_ADMIN_ENABLED, true); } interface TestDescriptor { @@ -99,26 +96,24 @@ protected String apiPath(String... paths) { return fullPath.toString(); } - @Test - public void forbiddenForRegularUsers() throws Exception { - withUser(NEW_USER, client -> { - forbidden(() -> client.putJson(apiPath("some_entity"), EMPTY_BODY)); - forbidden(() -> client.get(apiPath())); - forbidden(() -> client.get(apiPath("some_entity"))); - forbidden(() -> client.putJson(apiPath("some_entity"), EMPTY_BODY)); - forbidden(() -> client.patch(apiPath(), EMPTY_BODY)); - forbidden(() -> client.patch(apiPath("some_entity"), EMPTY_BODY)); - forbidden(() -> client.delete(apiPath("some_entity"))); - }); + public void forbiddenForRegularUsers(LocalCluster localCluster) throws Exception { + try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { + assertThat(client.putJson(apiPath("some_entity"), EMPTY_BODY), isForbidden()); + assertThat(client.get(apiPath()), isForbidden()); + assertThat(client.get(apiPath("some_entity")), isForbidden()); + assertThat(client.putJson(apiPath("some_entity"), EMPTY_BODY), isForbidden()); + assertThat(client.patch(apiPath(), EMPTY_BODY), isForbidden()); + assertThat(client.patch(apiPath("some_entity"), EMPTY_BODY), isForbidden()); + assertThat(client.delete(apiPath("some_entity")), isForbidden()); + } } - @Test - public void availableForAdminUser() throws Exception { - final var entitiesNames = predefinedHiddenAndReservedConfigEntities(); + public void availableForAdminUser(LocalCluster localCluster) throws Exception { + final var entitiesNames = predefinedHiddenAndReservedConfigEntities(localCluster); final var hiddenEntityName = entitiesNames.getLeft(); final var reservedEntityName = entitiesNames.getRight(); // can't see hidden resources - withUser(ADMIN_USER_NAME, client -> { + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { verifyNoHiddenEntities(() -> client.get(apiPath())); creationOfReadOnlyEntityForbidden( randomAsciiAlphanumOfLength(10), @@ -131,35 +126,28 @@ public void availableForAdminUser() throws Exception { verifyUpdateAndDeleteReservedConfigEntityForbidden(reservedEntityName, client); verifyCrudOperations(null, null, client); verifyBadRequestOperations(client); - }); + } } - Pair predefinedHiddenAndReservedConfigEntities() throws Exception { + Pair predefinedHiddenAndReservedConfigEntities(LocalCluster localCluster) throws Exception { final var hiddenEntityName = randomAsciiAlphanumOfLength(10); final var reservedEntityName = randomAsciiAlphanumOfLength(10); - withUser( - ADMIN_USER_NAME, - localCluster.getAdminCertificate(), - client -> created(() -> client.putJson(apiPath(hiddenEntityName), testDescriptor.hiddenEntityPayload())) - ); - withUser( - ADMIN_USER_NAME, - localCluster.getAdminCertificate(), - client -> created(() -> client.putJson(apiPath(reservedEntityName), testDescriptor.reservedEntityPayload())) - ); + try (TestRestClient client = localCluster.getAdminCertRestClient()) { + assertThat(client.putJson(apiPath(hiddenEntityName), testDescriptor.hiddenEntityPayload()), isCreated()); + assertThat(client.putJson(apiPath(reservedEntityName), testDescriptor.reservedEntityPayload()), isCreated()); + } return Pair.of(hiddenEntityName, reservedEntityName); } - @Test - public void availableForTLSAdminUser() throws Exception { - withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::availableForSuperAdminUser); + public void availableForTLSAdminUser(LocalCluster localCluster) throws Exception { + try (TestRestClient client = localCluster.getAdminCertRestClient()) { + availableForSuperAdminUser(client); + } } - @Test - public void availableForRESTAdminUser() throws Exception { - withUser(REST_ADMIN_USER, this::availableForSuperAdminUser); - if (testDescriptor.restAdminLimitedUser().isPresent()) { - withUser(testDescriptor.restAdminLimitedUser().get(), this::availableForSuperAdminUser); + public void availableForRESTAdminUser(LocalCluster localCluster) throws Exception { + try (TestRestClient client = localCluster.getRestClient(REST_ADMIN_USER)) { + availableForSuperAdminUser(client); } } @@ -178,7 +166,9 @@ void availableForSuperAdminUser(final TestRestClient client) throws Exception { } void verifyNoHiddenEntities(final CheckedSupplier endpointCallback) throws Exception { - final var body = ok(endpointCallback).bodyAsJsonNode(); + final var resp = endpointCallback.get(); + assertThat(resp, isOk()); + final var body = resp.bodyAsJsonNode(); final var pretty = body.toPrettyString(); final var it = body.elements(); while (it.hasNext()) { @@ -190,11 +180,11 @@ void verifyNoHiddenEntities(final CheckedSupplier client.putJson(apiPath(entityName), configEntity)), - is(oneOf("static", "hidden", "reserved")) - ); - badRequest(() -> client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(10), configEntity)))); + final var resp = client.putJson(apiPath(entityName), configEntity); + assertThat(resp, isBadRequest()); + assertInvalidKeys(resp, is(oneOf("static", "hidden", "reserved"))); + final var resp2 = client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(10), configEntity))); + assertThat(resp2, isBadRequest()); } } @@ -222,54 +212,60 @@ void assertWrongDataType(final TestRestClient.HttpResponse response, final Map client.putJson(apiPath(hiddenEntityName), testDescriptor.entityPayload()), expectedErrorMessage); - notFound( - () -> client.patch( + assertThat( + client.putJson(apiPath(hiddenEntityName), testDescriptor.entityPayload()), + isNotFound().withAttribute("/message", expectedErrorMessage) + ); + assertThat( + client.patch( apiPath(hiddenEntityName), patch(replaceOp(testDescriptor.entityJsonProperty(), testDescriptor.jsonPropertyPayload())) ), - expectedErrorMessage + isNotFound().withAttribute("/message", expectedErrorMessage) ); - notFound(() -> client.patch(apiPath(), patch(replaceOp(hiddenEntityName, testDescriptor.entityPayload()))), expectedErrorMessage); - notFound(() -> client.patch(apiPath(hiddenEntityName), patch(removeOp(testDescriptor.entityJsonProperty()))), expectedErrorMessage); - notFound(() -> client.patch(apiPath(), patch(removeOp(hiddenEntityName))), expectedErrorMessage); - notFound(() -> client.delete(apiPath(hiddenEntityName)), expectedErrorMessage); + assertThat( + client.patch(apiPath(), patch(replaceOp(hiddenEntityName, testDescriptor.entityPayload()))), + isNotFound().withAttribute("/message", expectedErrorMessage) + ); + assertThat( + client.patch(apiPath(hiddenEntityName), patch(removeOp(testDescriptor.entityJsonProperty()))), + isNotFound().withAttribute("/message", expectedErrorMessage) + ); + assertThat( + client.patch(apiPath(), patch(removeOp(hiddenEntityName))), + isNotFound().withAttribute("/message", expectedErrorMessage) + ); + assertThat(client.delete(apiPath(hiddenEntityName)), isNotFound().withAttribute("/message", expectedErrorMessage)); } void verifyUpdateAndDeleteReservedConfigEntityForbidden(final String reservedEntityName, final TestRestClient client) throws Exception { final var expectedErrorMessage = "Resource '" + reservedEntityName + "' is reserved."; - forbidden(() -> client.putJson(apiPath(reservedEntityName), testDescriptor.entityPayload()), expectedErrorMessage); - forbidden( - () -> client.patch( + assertThat( + client.putJson(apiPath(reservedEntityName), testDescriptor.entityPayload()), + isForbidden().withAttribute("/message", expectedErrorMessage) + ); + assertThat( + client.patch( apiPath(reservedEntityName), patch(replaceOp(testDescriptor.entityJsonProperty(), testDescriptor.entityJsonProperty())) ), - expectedErrorMessage + isForbidden().withAttribute("/message", expectedErrorMessage) ); - forbidden( - () -> client.patch(apiPath(), patch(replaceOp(reservedEntityName, testDescriptor.entityPayload()))), - expectedErrorMessage + assertThat( + client.patch(apiPath(), patch(replaceOp(reservedEntityName, testDescriptor.entityPayload()))), + isForbidden().withAttribute("/message", expectedErrorMessage) + ); + assertThat( + client.patch(apiPath(), patch(removeOp(reservedEntityName))), + isForbidden().withAttribute("/message", expectedErrorMessage) ); - forbidden(() -> client.patch(apiPath(), patch(removeOp(reservedEntityName))), expectedErrorMessage); - forbidden( - () -> client.patch(apiPath(reservedEntityName), patch(removeOp(testDescriptor.entityJsonProperty()))), - expectedErrorMessage + assertThat( + client.patch(apiPath(reservedEntityName), patch(removeOp(testDescriptor.entityJsonProperty()))), + isForbidden().withAttribute("/message", expectedErrorMessage) ); - forbidden(() -> client.delete(apiPath(reservedEntityName)), expectedErrorMessage); + assertThat(client.delete(apiPath(reservedEntityName)), isForbidden().withAttribute("/message", expectedErrorMessage)); } void forbiddenToCreateEntityWithRestAdminPermissions(final TestRestClient client) throws Exception {} diff --git a/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java index 65bec9f788..8af5910d19 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java @@ -10,16 +10,25 @@ package org.opensearch.security.api; +import org.junit.ClassRule; import org.junit.Test; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.not; +import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; +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.isNotFound; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; +import static org.opensearch.test.framework.matcher.RestMatchers.isUnauthorized; public class AccountRestApiIntegrationTest extends AbstractApiIntegrationTest { @@ -33,11 +42,12 @@ public class AccountRestApiIntegrationTest extends AbstractApiIntegrationTest { public final static String TEST_USER_NEW_PASSWORD = randomAlphabetic(10); - static { - testSecurityConfig.user(new TestSecurityConfig.User(TEST_USER).password(TEST_USER_PASSWORD)) - .user(new TestSecurityConfig.User(RESERVED_USER).reserved(true)) - .user(new TestSecurityConfig.User(HIDDEN_USERS).hidden(true)); - } + @ClassRule + public static LocalCluster localCluster = clusterBuilder().users( + new TestSecurityConfig.User(TEST_USER).password(TEST_USER_PASSWORD), + new TestSecurityConfig.User(RESERVED_USER).reserved(true), + new TestSecurityConfig.User(HIDDEN_USERS).hidden(true) + ).build(); private String accountPath() { return super.apiPath("account"); @@ -45,10 +55,11 @@ private String accountPath() { @Test public void accountInfo() throws Exception { - withUser(NEW_USER, client -> { - var response = ok(() -> client.get(accountPath())); + try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { + HttpResponse response = client.get(accountPath()); + assertThat(response, isOk()); final var account = response.bodyAsJsonNode(); - assertThat(response.getBody(), account.get("user_name").asText(), is(NEW_USER)); + assertThat(response.getBody(), account.get("user_name").asText(), is(NEW_USER.getName())); assertThat(response.getBody(), not(account.get("is_reserved").asBoolean())); assertThat(response.getBody(), not(account.get("is_hidden").asBoolean())); assertThat(response.getBody(), account.get("is_internal_user").asBoolean()); @@ -57,69 +68,77 @@ public void accountInfo() throws Exception { assertThat(response.getBody(), account.get("custom_attribute_names").isArray()); assertThat(response.getBody(), account.get("tenants").isObject()); assertThat(response.getBody(), account.get("roles").isArray()); - }); - withUser(NEW_USER, "a", client -> unauthorized(() -> client.get(accountPath()))); - withUser("a", "b", client -> unauthorized(() -> client.get(accountPath()))); + } + try (TestRestClient client = localCluster.getRestClient(NEW_USER.getName(), "a")) { + HttpResponse response = client.get(accountPath()); + assertThat(response, isUnauthorized()); + } + try (TestRestClient client = localCluster.getRestClient("a", "b")) { + HttpResponse response = client.get(accountPath()); + assertThat(response, isUnauthorized()); + } } @Test public void changeAccountPassword() throws Exception { - withUser(TEST_USER, TEST_USER_PASSWORD, this::verifyWrongPayload); + try (TestRestClient client = localCluster.getRestClient(TEST_USER, TEST_USER_PASSWORD)) { + verifyWrongPayload(client); + } verifyPasswordCanBeChanged(); - withUser(RESERVED_USER, client -> { - var response = ok(() -> client.get(accountPath())); - assertThat(response.getBody(), response.getBooleanFromJsonBody("/is_reserved")); - forbidden(() -> client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10)))); - }); - withUser(HIDDEN_USERS, client -> { - var response = ok(() -> client.get(accountPath())); - assertThat(response.getBody(), response.getBooleanFromJsonBody("/is_hidden")); - notFound(() -> client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10)))); - }); - withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { - ok(() -> client.get(accountPath())); - notFound(() -> client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10)))); - }); + try (TestRestClient client = localCluster.getRestClient(RESERVED_USER, DEFAULT_PASSWORD)) { + HttpResponse response = client.get(accountPath()); + assertThat(response, isOk()); + assertThat(response.getBooleanFromJsonBody("/is_reserved"), is(true)); + assertThat(client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10))), isForbidden()); + } + try (TestRestClient client = localCluster.getRestClient(HIDDEN_USERS, DEFAULT_PASSWORD)) { + HttpResponse response = client.get(accountPath()); + assertThat(response, isOk()); + assertThat(response.getBooleanFromJsonBody("/is_hidden"), is(true)); + assertThat(client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10))), isNotFound()); + } + try (TestRestClient client = localCluster.getAdminCertRestClient()) { + HttpResponse response = client.get(accountPath()); + assertThat(response, isOk()); + assertThat(client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10))), isNotFound()); + } } private void verifyWrongPayload(final TestRestClient client) throws Exception { - badRequest(() -> client.putJson(accountPath(), EMPTY_BODY)); - badRequest(() -> client.putJson(accountPath(), changePasswordPayload(null, "new_password"))); - badRequest(() -> client.putJson(accountPath(), changePasswordPayload("wrong-password", "some_new_pwd"))); - badRequest(() -> client.putJson(accountPath(), changePasswordPayload(TEST_USER_PASSWORD, null))); - badRequest(() -> client.putJson(accountPath(), changePasswordPayload(TEST_USER_PASSWORD, ""))); - badRequest(() -> client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, null))); - badRequest(() -> client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, ""))); - badRequest( - () -> client.putJson( + assertThat(client.putJson(accountPath(), EMPTY_BODY), isBadRequest()); + assertThat(client.putJson(accountPath(), changePasswordPayload(null, "new_password")), isBadRequest()); + assertThat(client.putJson(accountPath(), changePasswordPayload("wrong-password", "some_new_pwd")), isBadRequest()); + assertThat(client.putJson(accountPath(), changePasswordPayload(TEST_USER_PASSWORD, null)), isBadRequest()); + assertThat(client.putJson(accountPath(), changePasswordPayload(TEST_USER_PASSWORD, "")), isBadRequest()); + assertThat(client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, null)), isBadRequest()); + assertThat(client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, "")), isBadRequest()); + assertThat( + client.putJson( accountPath(), (builder, params) -> builder.startObject() .field("current_password", TEST_USER_PASSWORD) .startArray("backend_roles") .endArray() .endObject() - ) + ), + isBadRequest() ); } private void verifyPasswordCanBeChanged() throws Exception { final var newPassword = randomAlphabetic(10); - withUser( - TEST_USER, - TEST_USER_PASSWORD, - client -> ok( - () -> client.putJson( - accountPath(), - changePasswordWithHashPayload(TEST_USER_PASSWORD, passwordHasher.hash(newPassword.toCharArray())) - ) - ) - ); - withUser( - TEST_USER, - newPassword, - client -> ok(() -> client.putJson(accountPath(), changePasswordPayload(newPassword, TEST_USER_NEW_PASSWORD))) - ); + try (TestRestClient client = localCluster.getRestClient(TEST_USER, TEST_USER_PASSWORD)) { + HttpResponse resp = client.putJson( + accountPath(), + changePasswordWithHashPayload(TEST_USER_PASSWORD, passwordHasher.hash(newPassword.toCharArray())) + ); + assertThat(resp, isOk()); + } + try (TestRestClient client = localCluster.getRestClient(TEST_USER, newPassword)) { + HttpResponse resp = client.putJson(accountPath(), changePasswordPayload(newPassword, TEST_USER_NEW_PASSWORD)); + assertThat(resp, isOk()); + } } @Test @@ -127,10 +146,9 @@ public void testPutAccountRetainsAccountInformation() throws Exception { final var username = "test"; final String password = randomAlphabetic(10); final String newPassword = randomAlphabetic(10); - withUser( - ADMIN_USER_NAME, - client -> created( - () -> client.putJson( + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + assertThat( + client.putJson( apiPath("internalusers", username), (builder, params) -> builder.startObject() .field("password", password) @@ -140,24 +158,29 @@ public void testPutAccountRetainsAccountInformation() throws Exception { .endArray() .field("opendistro_security_roles") .startArray() - .value("user_limited-user__limited-role") + .value(EXAMPLE_ROLE.getName()) .endArray() .field("attributes") .startObject() .field("foo", "bar") .endObject() .endObject() - ) - ) - ); - withUser(username, password, client -> ok(() -> client.putJson(accountPath(), changePasswordPayload(password, newPassword)))); - withUser(ADMIN_USER_NAME, client -> { - final var response = ok(() -> client.get(apiPath("internalusers", username))); + ), + isCreated() + ); + } + try (TestRestClient client = localCluster.getRestClient(username, password)) { + HttpResponse resp = client.putJson(accountPath(), changePasswordPayload(password, newPassword)); + assertThat(resp, isOk()); + } + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + HttpResponse response = client.get(apiPath("internalusers", username)); + assertThat(response, isOk()); final var user = response.bodyAsJsonNode().get(username); assertThat(user.toPrettyString(), user.get("backend_roles").get(0).asText(), is("test-backend-role")); - assertThat(user.toPrettyString(), user.get("opendistro_security_roles").get(0).asText(), is("user_limited-user__limited-role")); + assertThat(user.toPrettyString(), user.get("opendistro_security_roles").get(0).asText(), is(EXAMPLE_ROLE.getName())); assertThat(user.toPrettyString(), user.get("attributes").get("foo").asText(), is("bar")); - }); + } } private ToXContentObject changePasswordPayload(final String currentPassword, final String newPassword) { diff --git a/src/integrationTest/java/org/opensearch/security/api/ActionGroupsRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/ActionGroupsRestApiIntegrationTest.java index 174c2b4ea6..bb47efefec 100644 --- a/src/integrationTest/java/org/opensearch/security/api/ActionGroupsRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/ActionGroupsRestApiIntegrationTest.java @@ -12,12 +12,15 @@ package org.opensearch.security.api; import java.util.List; -import java.util.Map; import java.util.Optional; +import org.junit.ClassRule; +import org.junit.Test; + import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.CoreMatchers.is; @@ -28,6 +31,11 @@ import static org.opensearch.security.api.PatchPayloadHelper.patch; import static org.opensearch.security.api.PatchPayloadHelper.removeOp; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; +import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; +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.isNotFound; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class ActionGroupsRestApiIntegrationTest extends AbstractConfigEntityApiIntegrationTest { @@ -35,16 +43,20 @@ public class ActionGroupsRestApiIntegrationTest extends AbstractConfigEntityApiI private final static String REST_ADMIN_PERMISSION_ACTION_GROUP = "rest-admin-permissions-action-group"; - static { - testSecurityConfig.withRestAdminUser(REST_API_ADMIN_ACTION_GROUPS_ONLY, restAdminPermission(Endpoint.ACTIONGROUPS)) - .actionGroups( - new TestSecurityConfig.ActionGroup( - REST_ADMIN_PERMISSION_ACTION_GROUP, - TestSecurityConfig.ActionGroup.Type.INDEX, - allRestAdminPermissions() - ) - ); - } + @ClassRule + public static LocalCluster localCluster = clusterBuilder().users( + new TestSecurityConfig.User(REST_API_ADMIN_ACTION_GROUPS_ONLY).roles( + new TestSecurityConfig.Role("rest_admin_role").clusterPermissions(restAdminPermission(Endpoint.ACTIONGROUPS)) + ) + ) + .actionGroups( + new TestSecurityConfig.ActionGroup( + REST_ADMIN_PERMISSION_ACTION_GROUP, + TestSecurityConfig.ActionGroup.Type.INDEX, + allRestAdminPermissions() + ) + ) + .build(); public ActionGroupsRestApiIntegrationTest() { super("actiongroups", new TestDescriptor() { @@ -111,74 +123,117 @@ static String randomType() { return randomFrom(List.of(TestSecurityConfig.ActionGroup.Type.CLUSTER.type(), TestSecurityConfig.ActionGroup.Type.INDEX.type())); } + @Test + public void forbiddenForRegularUsers() throws Exception { + super.forbiddenForRegularUsers(localCluster); + } + + @Test + public void availableForAdminUser() throws Exception { + super.availableForAdminUser(localCluster); + } + + @Test + public void availableForTLSAdminUser() throws Exception { + super.availableForTLSAdminUser(localCluster); + } + + @Test + public void availableForRESTAdminUser() throws Exception { + super.availableForRESTAdminUser(localCluster); + } + @Override void forbiddenToCreateEntityWithRestAdminPermissions(final TestRestClient client) throws Exception { - forbidden(() -> client.putJson(apiPath("new_rest_admin_action_group"), actionGroup(randomRestAdminPermission()))); - forbidden(() -> client.patch(apiPath(), patch(addOp("new_rest_admin_action_group", actionGroup(randomRestAdminPermission()))))); + assertThat(client.putJson(apiPath("new_rest_admin_action_group"), actionGroup(randomRestAdminPermission())), isForbidden()); + assertThat( + client.patch(apiPath(), patch(addOp("new_rest_admin_action_group", actionGroup(randomRestAdminPermission())))), + isForbidden() + ); } @Override void forbiddenToUpdateAndDeleteExistingEntityWithRestAdminPermissions(final TestRestClient client) throws Exception { // update - forbidden(() -> client.putJson(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP), actionGroup())); - forbidden(() -> client.patch(apiPath(), patch(replaceOp(REST_ADMIN_PERMISSION_ACTION_GROUP, actionGroup("a", "b"))))); - forbidden( - () -> client.patch(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP), patch(replaceOp("allowed_actions", configJsonArray("c", "d")))) + assertThat(client.putJson(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP), actionGroup()), isForbidden()); + assertThat(client.patch(apiPath(), patch(replaceOp(REST_ADMIN_PERMISSION_ACTION_GROUP, actionGroup("a", "b")))), isForbidden()); + assertThat( + client.patch(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP), patch(replaceOp("allowed_actions", configJsonArray("c", "d")))), + isForbidden() ); // remove - forbidden(() -> client.patch(apiPath(), patch(removeOp(REST_ADMIN_PERMISSION_ACTION_GROUP)))); - forbidden(() -> client.patch(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP), patch(removeOp("allowed_actions")))); - forbidden(() -> client.delete(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP))); + assertThat(client.patch(apiPath(), patch(removeOp(REST_ADMIN_PERMISSION_ACTION_GROUP))), isForbidden()); + assertThat(client.patch(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP), patch(removeOp("allowed_actions"))), isForbidden()); + assertThat(client.delete(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP)), isForbidden()); } @Override void verifyCrudOperations(final Boolean hidden, final Boolean reserved, final TestRestClient client) throws Exception { - created(() -> client.putJson(apiPath("new_action_group"), actionGroup(hidden, reserved, "a", "b"))); - assertActionGroup(ok(() -> client.get(apiPath("new_action_group"))), "new_action_group", List.of("a", "b")); - - ok(() -> client.putJson(apiPath("new_action_group"), actionGroup(hidden, reserved, "c", "d"))); - assertActionGroup(ok(() -> client.get(apiPath("new_action_group"))), "new_action_group", List.of("c", "d")); + // create + assertThat(client.putJson(apiPath("new_action_group"), actionGroup(hidden, reserved, "a", "b")), isCreated()); + var response = client.get(apiPath("new_action_group")); + assertThat(response, isOk()); + assertActionGroup(response, "new_action_group", List.of("a", "b")); - ok(() -> client.delete(apiPath("new_action_group"))); - notFound(() -> client.get(apiPath("new_action_group"))); - - ok(() -> client.patch(apiPath(), patch(addOp("new_action_group_for_patch", actionGroup(hidden, reserved, "e", "f"))))); - assertActionGroup(ok(() -> client.get(apiPath("new_action_group_for_patch"))), "new_action_group_for_patch", List.of("e", "f")); - - ok(() -> client.patch(apiPath("new_action_group_for_patch"), patch(replaceOp("allowed_actions", configJsonArray("g", "h"))))); - assertActionGroup(ok(() -> client.get(apiPath("new_action_group_for_patch"))), "new_action_group_for_patch", List.of("g", "h")); - - ok(() -> client.patch(apiPath(), patch(removeOp("new_action_group_for_patch")))); - notFound(() -> client.get(apiPath("new_action_group_for_patch"))); + // update + assertThat(client.putJson(apiPath("new_action_group"), actionGroup(hidden, reserved, "c", "d")), isOk()); + response = client.get(apiPath("new_action_group")); + assertThat(response, isOk()); + assertActionGroup(response, "new_action_group", List.of("c", "d")); + + // delete + assertThat(client.delete(apiPath("new_action_group")), isOk()); + response = client.get(apiPath("new_action_group")); + assertThat(response, isNotFound()); + + // patch add + assertThat(client.patch(apiPath(), patch(addOp("new_action_group_for_patch", actionGroup(hidden, reserved, "e", "f")))), isOk()); + response = client.get(apiPath("new_action_group_for_patch")); + assertThat(response, isOk()); + assertActionGroup(response, "new_action_group_for_patch", List.of("e", "f")); + + // patch replace + assertThat( + client.patch(apiPath("new_action_group_for_patch"), patch(replaceOp("allowed_actions", configJsonArray("g", "h")))), + isOk() + ); + response = client.get(apiPath("new_action_group_for_patch")); + assertThat(response, isOk()); + assertActionGroup(response, "new_action_group_for_patch", List.of("g", "h")); + + // patch remove + assertThat(client.patch(apiPath(), patch(removeOp("new_action_group_for_patch"))), isOk()); + response = client.get(apiPath("new_action_group_for_patch")); + assertThat(response, isNotFound()); } @Override void verifyBadRequestOperations(final TestRestClient client) throws Exception { // put - badRequest(() -> client.putJson(apiPath("some_action_group"), EMPTY_BODY)); - badRequestWithMessage( - () -> client.putJson(apiPath("kibana_user"), actionGroup("a", "b")), - "kibana_user is an existing role. A action group cannot be named with an existing role name." + assertThat(client.putJson(apiPath("some_action_group"), EMPTY_BODY), isBadRequest()); + assertThat( + client.putJson(apiPath("kibana_user"), actionGroup("a", "b")), + isBadRequest("/message", "kibana_user is an existing role. A action group cannot be named with an existing role name.") ); - badRequestWithMessage( - () -> client.putJson(apiPath("reference_itself"), actionGroup("reference_itself")), - "reference_itself cannot be an allowed_action of itself" + assertThat( + client.putJson(apiPath("reference_itself"), actionGroup("reference_itself")), + isBadRequest("/message", "reference_itself cannot be an allowed_action of itself") ); - - badRequestWithMessage(() -> client.putJson(apiPath("some_action_group"), (builder, params) -> { + assertThat(client.putJson(apiPath("some_action_group"), (builder, params) -> { builder.startObject().field("type", "asdasdsad").field("allowed_actions"); configJsonArray("g", "f").toXContent(builder, params); return builder.endObject(); - }), "Invalid action group type: asdasdsad. Supported types are: cluster, index."); + }), isBadRequest("/message", "Invalid action group type: asdasdsad. Supported types are: cluster, index.")); - assertMissingMandatoryKeys( - badRequest(() -> client.putJson(apiPath("some_action_group"), configJsonArray("a", "b", "c"))), - "allowed_actions" + assertThat( + client.putJson(apiPath("some_action_group"), configJsonArray("a", "b", "c")), + isBadRequest("/missing_mandatory_keys/keys", "allowed_actions") ); - assertMissingMandatoryKeys( - badRequest(() -> client.putJson(apiPath("some_action_group"), configJsonArray("a", "b", "c"))), - "allowed_actions" + // duplicate check retained from original + assertThat( + client.putJson(apiPath("some_action_group"), configJsonArray("a", "b", "c")), + isBadRequest("/missing_mandatory_keys/keys", "allowed_actions") ); final ToXContentObject unknownJsonFields = (builder, params) -> { @@ -186,68 +241,59 @@ void verifyBadRequestOperations(final TestRestClient client) throws Exception { configJsonArray("g", "h").toXContent(builder, params); return builder.endObject(); }; - assertInvalidKeys(badRequest(() -> client.putJson(apiPath("some_action_group"), unknownJsonFields)), "a,c"); + assertThat(client.putJson(apiPath("some_action_group"), unknownJsonFields), isBadRequest("/invalid_keys/keys", "a,c")); - assertNullValuesInArray(badRequest(() -> client.putJson(apiPath("some_action_group"), (builder, params) -> { + assertThat(client.putJson(apiPath("some_action_group"), (builder, params) -> { builder.startObject().field("type", randomType()).field("allowed_actions"); configJsonArray("g", null, "f").toXContent(builder, params); return builder.endObject(); - }))); - assertWrongDataType( - client.putJson( - apiPath("some_action_group"), - (builder, params) -> builder.startObject().field("allowed_actions", "a").endObject() - ), - Map.of("allowed_actions", "Array expected") - ); - // patch - badRequest(() -> client.patch(apiPath("some_action_group"), EMPTY_BODY)); - badRequest(() -> client.patch(apiPath(), patch(addOp("some_action_group", EMPTY_BODY)))); - badRequest(() -> client.patch(apiPath(), patch(replaceOp("some_action_group", EMPTY_BODY)))); + }), isBadRequest("/reason", "`null` or blank values are not allowed as json array elements")); - badRequestWithMessage( - () -> client.patch(apiPath(), patch(addOp("kibana_user", actionGroup("a")))), - "kibana_user is an existing role. A action group cannot be named with an existing role name." + // patch + assertThat(client.patch(apiPath("some_action_group"), EMPTY_BODY), isBadRequest()); + assertThat(client.patch(apiPath(), patch(addOp("some_action_group", EMPTY_BODY))), isBadRequest()); + assertThat(client.patch(apiPath(), patch(replaceOp("some_action_group", EMPTY_BODY))), isBadRequest()); + assertThat( + client.patch(apiPath(), patch(addOp("kibana_user", actionGroup("a")))), + isBadRequest("/message", "kibana_user is an existing role. A action group cannot be named with an existing role name.") ); - badRequestWithMessage( - () -> client.patch(apiPath(), patch(addOp("reference_itself", actionGroup("reference_itself")))), - "reference_itself cannot be an allowed_action of itself" + assertThat( + client.patch(apiPath(), patch(addOp("reference_itself", actionGroup("reference_itself")))), + isBadRequest("/message", "reference_itself cannot be an allowed_action of itself") ); - - assertMissingMandatoryKeys( - badRequest(() -> client.patch(apiPath(), patch(addOp("some_action_group", configJsonArray("a", "b", "c"))))), - "allowed_actions" + assertThat( + client.patch(apiPath(), patch(addOp("some_action_group", configJsonArray("a", "b", "c")))), + isBadRequest("/missing_mandatory_keys/keys", "allowed_actions") ); - badRequest(() -> client.patch(apiPath(), patch(addOp("some_action_group", (ToXContentObject) (builder, params) -> { + + assertThat(client.patch(apiPath(), patch(addOp("some_action_group", (ToXContentObject) (builder, params) -> { builder.startObject().field("type", "aaaa").field("allowed_actions"); configJsonArray("g", "f").toXContent(builder, params); return builder.endObject(); - })))); + }))), isBadRequest()); - badRequest(() -> client.patch(apiPath(), patch(addOp("some_action_group", (ToXContentObject) (builder, parameter) -> { + assertThat(client.patch(apiPath(), patch(addOp("some_action_group", (ToXContentObject) (builder, parameter) -> { builder.startObject(); unknownJsonFields.toXContent(builder, parameter); return builder.endObject(); - })))); - assertNullValuesInArray( - badRequest(() -> client.patch(apiPath(), patch(addOp("some_action_group", (ToXContentObject) (builder, params) -> { - builder.startObject().field("type", randomType()).field("allowed_actions"); - configJsonArray("g", null, "f").toXContent(builder, params); - return builder.endObject(); - })))) - ); - assertWrongDataType( - client.patch( - apiPath(), - patch( - addOp( - "some_action_group", - (ToXContentObject) (builder, params) -> builder.startObject().field("allowed_actions", "a").endObject() - ) + }))), isBadRequest()); + + assertThat(client.patch(apiPath(), patch(addOp("some_action_group", (ToXContentObject) (builder, params) -> { + builder.startObject().field("type", randomType()).field("allowed_actions"); + configJsonArray("g", null, "f").toXContent(builder, params); + return builder.endObject(); + }))), isBadRequest("/reason", "`null` or blank values are not allowed as json array elements")); + + var response = client.patch( + apiPath(), + patch( + addOp( + "some_action_group", + (ToXContentObject) (builder, params) -> builder.startObject().field("allowed_actions", "a").endObject() ) - ), - Map.of("allowed_actions", "Array expected") + ) ); + assertThat(response, isBadRequest().withAttribute("/status", "error").withAttribute("/allowed_actions", "Array expected")); } void assertActionGroup(final TestRestClient.HttpResponse response, final String actionGroupName, final List allowedActions) { diff --git a/src/integrationTest/java/org/opensearch/security/api/CertificatesRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/CertificatesRestApiIntegrationTest.java index 748b036d16..c1d6999fd8 100644 --- a/src/integrationTest/java/org/opensearch/security/api/CertificatesRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/CertificatesRestApiIntegrationTest.java @@ -14,19 +14,19 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.StringJoiner; import java.util.stream.Collectors; import com.carrotsearch.randomizedtesting.RandomizedContext; import com.fasterxml.jackson.databind.JsonNode; +import org.junit.ClassRule; import org.junit.Test; -import org.opensearch.common.CheckedConsumer; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.security.ssl.config.CertType; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.certificate.TestCertificates; +import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.LocalOpenSearchCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -36,6 +36,8 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; +import static org.opensearch.test.framework.matcher.RestMatchers.isForbidden; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; import static junit.framework.TestCase.fail; public class CertificatesRestApiIntegrationTest extends AbstractApiIntegrationTest { @@ -43,22 +45,18 @@ public class CertificatesRestApiIntegrationTest extends AbstractApiIntegrationTe final static String REGULAR_USER = "regular_user"; final static String ROOT_CA = "Root CA"; - static { - testSecurityConfig.roles( - new TestSecurityConfig.Role("simple_user_role").clusterPermissions("cluster:admin/security/certificates/info") + @ClassRule + public static LocalCluster localCluster = clusterBuilder().nodeSetting(SECURITY_RESTAPI_ADMIN_ENABLED, true) + .roles(new TestSecurityConfig.Role("simple_user_role").clusterPermissions("cluster:admin/security/certificates/info")) + .rolesMapping(new TestSecurityConfig.RoleMapping("simple_user_role").users(REGULAR_USER, ADMIN_USER.getName())) + .users( + new TestSecurityConfig.User(REGULAR_USER), + new TestSecurityConfig.User(REST_API_ADMIN_SSL_INFO).roles( + REST_ADMIN_REST_API_ACCESS_ROLE, + new TestSecurityConfig.Role("rest_admin_role").clusterPermissions(restAdminPermission(Endpoint.SSL, CERTS_INFO_ACTION)) + ) ) - .rolesMapping(new TestSecurityConfig.RoleMapping("simple_user_role").users(REGULAR_USER, ADMIN_USER_NAME)) - .user(new TestSecurityConfig.User(REGULAR_USER)) - .withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions()) - .withRestAdminUser(REST_API_ADMIN_SSL_INFO, restAdminPermission(Endpoint.SSL, CERTS_INFO_ACTION)); - } - - @Override - protected Map getClusterSettings() { - Map clusterSettings = super.getClusterSettings(); - clusterSettings.put(SECURITY_RESTAPI_ADMIN_ENABLED, true); - return clusterSettings; - } + .build(); @Override protected String apiPathPrefix() { @@ -78,59 +76,65 @@ protected String sslCertsPath(String... path) { @Test public void forbiddenForRegularUser() throws Exception { - withUser(REGULAR_USER, client -> forbidden(() -> client.get(sslCertsPath()))); + try (TestRestClient client = localCluster.getRestClient(REGULAR_USER, DEFAULT_PASSWORD)) { + assertThat(client.get(sslCertsPath()), isForbidden()); + } } @Test public void forbiddenForAdminUser() throws Exception { - withUser(ADMIN_USER_NAME, client -> forbidden(() -> client.get(sslCertsPath()))); + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + assertThat(client.get(sslCertsPath()), isForbidden()); + } } @Test public void availableForTlsAdmin() throws Exception { - withUser( - ADMIN_USER_NAME, - localCluster.getAdminCertificate(), - verifySSLCertsInfo(List.of(CertType.HTTP, CertType.TRANSPORT, CertType.TRANSPORT_CLIENT)) - ); + try (TestRestClient client = localCluster.getAdminCertRestClient()) { + verifySSLCertsInfo(client, List.of(CertType.HTTP, CertType.TRANSPORT, CertType.TRANSPORT_CLIENT)); + } } @Test public void availableForRestAdmin() throws Exception { - withUser(REST_ADMIN_USER, verifySSLCertsInfo(List.of(CertType.HTTP, CertType.TRANSPORT, CertType.TRANSPORT_CLIENT))); - withUser(REST_API_ADMIN_SSL_INFO, verifySSLCertsInfo(List.of(CertType.HTTP, CertType.TRANSPORT, CertType.TRANSPORT_CLIENT))); + try (TestRestClient client = localCluster.getRestClient(REST_ADMIN_USER)) { + verifySSLCertsInfo(client, List.of(CertType.HTTP, CertType.TRANSPORT, CertType.TRANSPORT_CLIENT)); + } + try (TestRestClient client = localCluster.getRestClient(REST_API_ADMIN_SSL_INFO, DEFAULT_PASSWORD)) { + verifySSLCertsInfo(client, List.of(CertType.HTTP, CertType.TRANSPORT, CertType.TRANSPORT_CLIENT)); + } } @Test public void timeoutTest() throws Exception { - withUser(REST_ADMIN_USER, this::verifyTimeoutRequest); + try (TestRestClient client = localCluster.getRestClient(REST_ADMIN_USER)) { + verifyTimeoutRequest(client); + } } private void verifyTimeoutRequest(final TestRestClient client) throws Exception { - ok(() -> client.get(sslCertsPath() + "?timeout=0")); + assertThat(client.get(sslCertsPath() + "?timeout=0"), isOk()); } - private CheckedConsumer verifySSLCertsInfo(List expectCerts) { - return testRestClient -> { - try { - assertSSLCertsInfo(localCluster.nodes(), expectCerts, ok(() -> testRestClient.get(sslCertsPath()))); - if (localCluster.nodes().size() > 1) { - final var randomNodes = randomNodes(); - final var nodeIds = randomNodes.stream() - .map(n -> n.esNode().getNodeEnvironment().nodeId()) - .collect(Collectors.joining(",")); - assertSSLCertsInfo(randomNodes, expectCerts, ok(() -> testRestClient.get(sslCertsPath(nodeIds)))); - } - final var randomCertType = randomFrom(expectCerts); - assertSSLCertsInfo( - localCluster.nodes(), - List.of(randomCertType), - ok(() -> testRestClient.get(String.format("%s?cert_type=%s", sslCertsPath(), randomCertType))) - ); - } catch (Exception e) { - fail("Verify SSLCerts info failed with exception: " + e.getMessage()); + private void verifySSLCertsInfo(final TestRestClient testRestClient, List expectCerts) { + try { + assertSSLCertsInfo(localCluster.nodes(), expectCerts, testRestClient.get(sslCertsPath())); + if (localCluster.nodes().size() > 1) { + final var randomNodes = randomNodes(); + final var nodeIds = randomNodes.stream() + .map(n -> n.esNode().getNodeEnvironment().nodeId()) + .collect(Collectors.joining(",")); + assertSSLCertsInfo(randomNodes, expectCerts, testRestClient.get(sslCertsPath(nodeIds))); } - }; + final var randomCertType = randomFrom(expectCerts); + assertSSLCertsInfo( + localCluster.nodes(), + List.of(randomCertType), + testRestClient.get(String.format("%s?cert_type=%s", sslCertsPath(), randomCertType)) + ); + } catch (Exception e) { + fail("Verify SSLCerts info failed with exception: " + e.getMessage()); + } } private void assertSSLCertsInfo( diff --git a/src/integrationTest/java/org/opensearch/security/api/ConfigRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/ConfigRestApiIntegrationTest.java index 16b089f99b..b69e271769 100644 --- a/src/integrationTest/java/org/opensearch/security/api/ConfigRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/ConfigRestApiIntegrationTest.java @@ -10,38 +10,48 @@ */ package org.opensearch.security.api; -import java.util.Map; import java.util.StringJoiner; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.ClassRule; import org.junit.Test; import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.dlic.rest.api.Endpoint; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; +import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.security.api.PatchPayloadHelper.patch; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION; +import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; +import static org.opensearch.test.framework.matcher.RestMatchers.isForbidden; +import static org.opensearch.test.framework.matcher.RestMatchers.isNotAllowed; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class ConfigRestApiIntegrationTest extends AbstractApiIntegrationTest { final static String REST_API_ADMIN_CONFIG_UPDATE = "rest-api-admin-config-update"; - static { - testSecurityConfig.withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions()) - .withRestAdminUser(REST_API_ADMIN_CONFIG_UPDATE, restAdminPermission(Endpoint.CONFIG, SECURITY_CONFIG_UPDATE)); - } - - @Override - protected Map getClusterSettings() { - Map clusterSettings = super.getClusterSettings(); - clusterSettings.put(SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true); - clusterSettings.put(SECURITY_RESTAPI_ADMIN_ENABLED, true); - return clusterSettings; - } + @ClassRule + public static LocalCluster localCluster = clusterBuilder().nodeSetting( + SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, + true + ) + .nodeSetting(SECURITY_RESTAPI_ADMIN_ENABLED, true) + .users( + new TestSecurityConfig.User(REST_API_ADMIN_CONFIG_UPDATE).roles( + REST_ADMIN_REST_API_ACCESS_ROLE, + new TestSecurityConfig.Role("rest_admin_role").clusterPermissions( + restAdminPermission(Endpoint.CONFIG, SECURITY_CONFIG_UPDATE) + ) + ) + ) + .build(); private String securityConfigPath(final String... path) { final var fullPath = new StringJoiner("/").add(super.apiPath("securityconfig")); @@ -52,44 +62,51 @@ private String securityConfigPath(final String... path) { @Test public void forbiddenForRegularUsers() throws Exception { - withUser(NEW_USER, client -> { - forbidden(() -> client.get(securityConfigPath())); - forbidden(() -> client.putJson(securityConfigPath("config"), EMPTY_BODY)); - forbidden(() -> client.patch(securityConfigPath(), EMPTY_BODY)); + try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { + assertThat(client.get(securityConfigPath()), isForbidden()); + assertThat(client.putJson(securityConfigPath("config"), EMPTY_BODY), isForbidden()); + assertThat(client.patch(securityConfigPath(), EMPTY_BODY), isForbidden()); verifyNotAllowedMethods(client); - }); + } } @Test public void partiallyAvailableForAdminUser() throws Exception { - withUser(ADMIN_USER_NAME, client -> ok(() -> client.get(securityConfigPath()))); - withUser(ADMIN_USER_NAME, client -> { - badRequest(() -> client.putJson(securityConfigPath("xxx"), EMPTY_BODY)); - forbidden(() -> client.putJson(securityConfigPath("config"), EMPTY_BODY)); - forbidden(() -> client.patch(securityConfigPath(), EMPTY_BODY)); - }); - withUser(ADMIN_USER_NAME, this::verifyNotAllowedMethods); + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + assertThat(client.get(securityConfigPath()), isOk()); + assertThat(client.putJson(securityConfigPath("xxx"), EMPTY_BODY), isBadRequest()); + assertThat(client.putJson(securityConfigPath("config"), EMPTY_BODY), isForbidden()); + assertThat(client.patch(securityConfigPath(), EMPTY_BODY), isForbidden()); + verifyNotAllowedMethods(client); + } } @Test public void availableForTlsAdminUser() throws Exception { - withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> ok(() -> client.get(securityConfigPath()))); - withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifyUpdate); + try (TestRestClient client = localCluster.getAdminCertRestClient()) { + assertThat(client.get(securityConfigPath()), isOk()); + verifyUpdate(client); + } } @Test public void availableForRestAdminUser() throws Exception { - withUser(REST_ADMIN_USER, client -> ok(() -> client.get(securityConfigPath()))); - withUser(REST_ADMIN_USER, this::verifyUpdate); - withUser(REST_API_ADMIN_CONFIG_UPDATE, this::verifyUpdate); + try (TestRestClient client = localCluster.getRestClient(REST_ADMIN_USER)) { + assertThat(client.get(securityConfigPath()), isOk()); + verifyUpdate(client); + } + try (TestRestClient client = localCluster.getRestClient(REST_API_ADMIN_CONFIG_UPDATE, DEFAULT_PASSWORD)) { + verifyUpdate(client); + } } void verifyUpdate(final TestRestClient client) throws Exception { - badRequest(() -> client.putJson(securityConfigPath("xxx"), EMPTY_BODY)); + assertThat(client.putJson(securityConfigPath("xxx"), EMPTY_BODY), isBadRequest()); verifyNotAllowedMethods(client); TestRestClient.HttpResponse resp = client.get(securityConfigPath()); - final var configJson = ok(() -> client.get(securityConfigPath())).bodyAsJsonNode(); + assertThat(resp, isOk()); + final var configJson = resp.bodyAsJsonNode(); final var authFailureListeners = DefaultObjectMapper.objectMapper.createObjectNode(); authFailureListeners.set( "ip_rate_limiting", @@ -114,20 +131,31 @@ void verifyUpdate(final TestRestClient client) throws Exception { ); final var dynamicConfigJson = (ObjectNode) configJson.get("config").get("dynamic"); dynamicConfigJson.set("auth_failure_listeners", authFailureListeners); - ok(() -> client.putJson(securityConfigPath("config"), DefaultObjectMapper.writeValueAsString(configJson.get("config"), false))); + assertThat( + client.putJson(securityConfigPath("config"), DefaultObjectMapper.writeValueAsString(configJson.get("config"), false)), + isOk() + ); String originalHostResolverMode = configJson.get("config").get("dynamic").get("hosts_resolver_mode").asText(); String nextOriginalHostResolverMode = originalHostResolverMode.equals("other") ? "ip-only" : "other"; - ok(() -> client.patch(securityConfigPath(), patch(replaceOp("/config/dynamic/hosts_resolver_mode", nextOriginalHostResolverMode)))); - ok(() -> client.patch(securityConfigPath(), patch(replaceOp("/config/dynamic/hosts_resolver_mode", originalHostResolverMode)))); - ok( - () -> client.patch(securityConfigPath(), patch(replaceOp("/config/dynamic/hosts_resolver_mode", originalHostResolverMode))), - "No updates required" + assertThat( + client.patch(securityConfigPath(), patch(replaceOp("/config/dynamic/hosts_resolver_mode", nextOriginalHostResolverMode))), + isOk() + ); + assertThat( + client.patch(securityConfigPath(), patch(replaceOp("/config/dynamic/hosts_resolver_mode", originalHostResolverMode))), + isOk() + ); + TestRestClient.HttpResponse last = client.patch( + securityConfigPath(), + patch(replaceOp("/config/dynamic/hosts_resolver_mode", originalHostResolverMode)) ); + assertThat(last, isOk()); + assertResponseBody(last.getBody(), "No updates required"); } - void verifyNotAllowedMethods(final TestRestClient client) throws Exception { - methodNotAllowed(() -> client.postJson(securityConfigPath(), EMPTY_BODY)); - methodNotAllowed(() -> client.delete(securityConfigPath())); + void verifyNotAllowedMethods(final TestRestClient client) { + assertThat(client.postJson(securityConfigPath(), EMPTY_BODY), isNotAllowed()); + assertThat(client.delete(securityConfigPath()), isNotAllowed()); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java b/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java index 635d9ecff4..d7cfe41311 100644 --- a/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java @@ -13,10 +13,13 @@ import java.util.List; +import org.junit.ClassRule; import org.junit.Test; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -24,16 +27,16 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; import static org.opensearch.security.rest.DashboardsInfoAction.DEFAULT_PASSWORD_MESSAGE; import static org.opensearch.security.rest.DashboardsInfoAction.DEFAULT_PASSWORD_REGEX; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class DashboardsInfoTest extends AbstractApiIntegrationTest { - static { - testSecurityConfig.user( - new TestSecurityConfig.User("dashboards_user").roles( - new Role("dashboards_role").indexPermissions("read").on("*").clusterPermissions("cluster_composite_ops") - ) - ); - } + static final TestSecurityConfig.User DASHBOARDS_USER = new TestSecurityConfig.User("dashboards_user").roles( + new Role("dashboards_role").indexPermissions("read").on("*").clusterPermissions("cluster_composite_ops") + ); + + @ClassRule + public static LocalCluster localCluster = clusterBuilder().users(DASHBOARDS_USER).build(); private String apiPath() { return randomFrom(List.of(PLUGINS_PREFIX + "/dashboardsinfo", LEGACY_OPENDISTRO_PREFIX + "/kibanainfo")); @@ -41,10 +44,11 @@ private String apiPath() { @Test public void testDashboardsInfoValidationMessage() throws Exception { - withUser("dashboards_user", client -> { - final var response = ok(() -> client.get(apiPath())); + try (TestRestClient client = localCluster.getRestClient(DASHBOARDS_USER)) { + final var response = client.get(apiPath()); + assertThat(response, isOk()); assertThat(response.getTextFromJsonBody("/password_validation_error_message"), equalTo(DEFAULT_PASSWORD_MESSAGE)); assertThat(response.getTextFromJsonBody("/password_validation_regex"), equalTo(DEFAULT_PASSWORD_REGEX)); - }); + } } } diff --git a/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoWithSettingsTest.java b/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoWithSettingsTest.java index af8eeb2c8a..8aba10fe35 100644 --- a/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoWithSettingsTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoWithSettingsTest.java @@ -12,18 +12,22 @@ package org.opensearch.security.api; import java.util.List; -import java.util.Map; +import org.junit.ClassRule; import org.junit.Test; -import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class DashboardsInfoWithSettingsTest extends AbstractApiIntegrationTest { @@ -32,21 +36,18 @@ public class DashboardsInfoWithSettingsTest extends AbstractApiIntegrationTest { private static final String CUSTOM_PASSWORD_MESSAGE = "Password must be minimum 5 characters long and must contain at least one uppercase letter, one lowercase letter, one digit, and one special character."; - static { - testSecurityConfig.user( + @ClassRule + public static LocalCluster localCluster = clusterBuilder().nodeSetting( + SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, + CUSTOM_PASSWORD_REGEX + ) + .nodeSetting(SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, CUSTOM_PASSWORD_MESSAGE) + .users( new TestSecurityConfig.User("dashboards_user").roles( new Role("dashboards_role").indexPermissions("read").on("*").clusterPermissions("cluster_composite_ops") ) - ); - } - - @Override - protected Map getClusterSettings() { - Map clusterSettings = super.getClusterSettings(); - clusterSettings.put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, CUSTOM_PASSWORD_REGEX); - clusterSettings.put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, CUSTOM_PASSWORD_MESSAGE); - return clusterSettings; - } + ) + .build(); private String apiPath() { return randomFrom(List.of(PLUGINS_PREFIX + "/dashboardsinfo", LEGACY_OPENDISTRO_PREFIX + "/kibanainfo")); @@ -55,10 +56,11 @@ private String apiPath() { @Test public void testDashboardsInfoValidationMessageWithCustomMessage() throws Exception { - withUser("dashboards_user", client -> { - final var response = ok(() -> client.get(apiPath())); + try (TestRestClient client = localCluster.getRestClient("dashboards_user", DEFAULT_PASSWORD)) { + final var response = client.get(apiPath()); + assertThat(response, isOk()); assertThat(response.getTextFromJsonBody("/password_validation_error_message"), equalTo(CUSTOM_PASSWORD_MESSAGE)); assertThat(response.getTextFromJsonBody("/password_validation_regex"), equalTo(CUSTOM_PASSWORD_REGEX)); - }); + } } } diff --git a/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java index 7ac2262899..568f7fea8d 100644 --- a/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java @@ -11,66 +11,87 @@ package org.opensearch.security.api; +import org.junit.ClassRule; import org.junit.Test; +import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.security.api.PatchPayloadHelper.patch; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; +import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; +import static org.opensearch.test.framework.matcher.RestMatchers.isForbidden; +import static org.opensearch.test.framework.matcher.RestMatchers.isNotAllowed; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class DefaultApiAvailabilityIntegrationTest extends AbstractApiIntegrationTest { + @ClassRule + public static LocalCluster localCluster = clusterBuilder().build(); + @Test public void nodesDnApiIsNotAvailableByDefault() throws Exception { - withUser(NEW_USER, this::verifyNodesDnApi); - withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifyNodesDnApi); - } - - private void verifyNodesDnApi(final TestRestClient client) throws Exception { - badRequest(() -> client.get(apiPath("nodesdn"))); - badRequest(() -> client.putJson(apiPath("nodesdn", "cluster_1"), EMPTY_BODY)); - badRequest(() -> client.delete(apiPath("nodesdn", "cluster_1"))); - badRequest(() -> client.patch(apiPath("nodesdn", "cluster_1"), EMPTY_BODY)); + try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { + assertThat(client.get(apiPath("nodesdn")), isBadRequest()); + assertThat(client.putJson(apiPath("nodesdn", "cluster_1"), EMPTY_BODY), isBadRequest()); + assertThat(client.delete(apiPath("nodesdn", "cluster_1")), isBadRequest()); + assertThat(client.patch(apiPath("nodesdn", "cluster_1"), EMPTY_BODY), isBadRequest()); + } + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + assertThat(client.get(apiPath("nodesdn")), isBadRequest()); + assertThat(client.putJson(apiPath("nodesdn", "cluster_1"), EMPTY_BODY), isBadRequest()); + assertThat(client.delete(apiPath("nodesdn", "cluster_1")), isBadRequest()); + assertThat(client.patch(apiPath("nodesdn", "cluster_1"), EMPTY_BODY), isBadRequest()); + } } @Test public void securityConfigIsNotAvailableByDefault() throws Exception { - withUser(NEW_USER, client -> { - forbidden(() -> client.get(apiPath("securityconfig"))); + try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { + assertThat(client.get(apiPath("securityconfig")), isForbidden()); verifySecurityConfigApi(client); - }); - withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { - ok(() -> client.get(apiPath("securityconfig"))); + } + try (TestRestClient client = localCluster.getAdminCertRestClient()) { + assertThat(client.get(apiPath("securityconfig")), isOk()); verifySecurityConfigApi(client); - }); + } } private void verifySecurityConfigApi(final TestRestClient client) throws Exception { - methodNotAllowed(() -> client.putJson(apiPath("securityconfig"), EMPTY_BODY)); - methodNotAllowed(() -> client.postJson(apiPath("securityconfig"), EMPTY_BODY)); - methodNotAllowed(() -> client.delete(apiPath("securityconfig"))); - forbidden(() -> client.patch(apiPath("securityconfig"), patch(replaceOp("/a/b/c", "other")))); + assertThat(client.putJson(apiPath("securityconfig"), EMPTY_BODY), isNotAllowed()); + assertThat(client.postJson(apiPath("securityconfig"), EMPTY_BODY), isNotAllowed()); + assertThat(client.delete(apiPath("securityconfig")), isNotAllowed()); + assertThat(client.patch(apiPath("securityconfig"), patch(replaceOp("/a/b/c", "other"))), isForbidden()); } @Test public void securityHealth() throws Exception { - withUser(NEW_USER, client -> ok(() -> client.get(securityPath("health")))); - withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> ok(() -> client.get(securityPath("health")))); + try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { + assertThat(client.get(securityPath("health")), isOk()); + } + try (TestRestClient client = localCluster.getAdminCertRestClient()) { + assertThat(client.get(securityPath("health")), isOk()); + } } @Test public void securityAuthInfo() throws Exception { - withUser(NEW_USER, this::verifyAuthInfoApi); - withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifyAuthInfoApi); + try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { + verifyAuthInfoApi(client); + } + try (TestRestClient client = localCluster.getAdminCertRestClient()) { + verifyAuthInfoApi(client); + } } private void verifyAuthInfoApi(final TestRestClient client) throws Exception { final var verbose = randomBoolean(); final TestRestClient.HttpResponse response; - if (verbose) response = ok(() -> client.get(securityPath("authinfo?verbose=" + verbose))); - else response = ok(() -> client.get(securityPath("authinfo"))); + if (verbose) response = client.get(securityPath("authinfo?verbose=" + verbose)); + else response = client.get(securityPath("authinfo")); + assertThat(response, isOk()); final var body = response.bodyAsJsonNode(); assertThat(response.getBody(), body.has("user")); assertThat(response.getBody(), body.has("user_name")); @@ -94,14 +115,14 @@ private void verifyAuthInfoApi(final TestRestClient client) throws Exception { @Test public void reloadSSLCertsNotAvailable() throws Exception { - withUser(NEW_USER, client -> { - forbidden(() -> client.putJson(apiPath("ssl", "http", "reloadcerts"), EMPTY_BODY)); - forbidden(() -> client.putJson(apiPath("ssl", "transport", "reloadcerts"), EMPTY_BODY)); - }); - withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { - badRequest(() -> client.putJson(apiPath("ssl", "http", "reloadcerts"), EMPTY_BODY)); - badRequest(() -> client.putJson(apiPath("ssl", "transport", "reloadcerts"), EMPTY_BODY)); - }); + try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { + assertThat(client.putJson(apiPath("ssl", "http", "reloadcerts"), EMPTY_BODY), isForbidden()); + assertThat(client.putJson(apiPath("ssl", "transport", "reloadcerts"), EMPTY_BODY), isForbidden()); + } + try (TestRestClient client = localCluster.getAdminCertRestClient()) { + assertThat(client.putJson(apiPath("ssl", "http", "reloadcerts"), EMPTY_BODY), isBadRequest()); + assertThat(client.putJson(apiPath("ssl", "transport", "reloadcerts"), EMPTY_BODY), isBadRequest()); + } } } diff --git a/src/integrationTest/java/org/opensearch/security/api/FlushCacheApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/FlushCacheApiIntegrationTest.java index 2879a43c93..58782b578d 100644 --- a/src/integrationTest/java/org/opensearch/security/api/FlushCacheApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/FlushCacheApiIntegrationTest.java @@ -11,15 +11,25 @@ package org.opensearch.security.api; +import org.junit.ClassRule; import org.junit.Test; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.security.dlic.rest.support.Utils.PLUGINS_PREFIX; +import static org.opensearch.test.framework.matcher.RestMatchers.isForbidden; +import static org.opensearch.test.framework.matcher.RestMatchers.isNotAllowed; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class FlushCacheApiIntegrationTest extends AbstractApiIntegrationTest { private final static String TEST_USER = "testuser"; + @ClassRule + public static LocalCluster localCluster = clusterBuilder().build(); + private String cachePath() { return super.apiPath("cache"); } @@ -35,26 +45,30 @@ protected String apiPathPrefix() { @Test public void testFlushCache() throws Exception { - withUser(NEW_USER, client -> { - forbidden(() -> client.delete(cachePath())); - forbidden(() -> client.delete(cachePath(TEST_USER))); - }); - withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { - methodNotAllowed(() -> client.get(cachePath())); - methodNotAllowed(() -> client.postJson(cachePath(), EMPTY_BODY)); - methodNotAllowed(() -> client.putJson(cachePath(), EMPTY_BODY)); - final var deleteAllCacheResponse = ok(() -> client.delete(cachePath())); + try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { + assertThat(client.delete(cachePath()), isForbidden()); + assertThat(client.delete(cachePath(TEST_USER)), isForbidden()); + } + try (TestRestClient client = localCluster.getAdminCertRestClient()) { + assertThat(client.get(cachePath()), isNotAllowed()); + assertThat(client.postJson(cachePath(), EMPTY_BODY), isNotAllowed()); + assertThat(client.putJson(cachePath(), EMPTY_BODY), isNotAllowed()); + + final var deleteAllCacheResponse = client.delete(cachePath()); + assertThat(deleteAllCacheResponse, isOk()); assertThat( deleteAllCacheResponse.getBody(), deleteAllCacheResponse.getTextFromJsonBody("/message"), is("Cache flushed successfully.") ); - final var deleteUserCacheResponse = ok(() -> client.delete(cachePath(TEST_USER))); + + final var deleteUserCacheResponse = client.delete(cachePath(TEST_USER)); + assertThat(deleteUserCacheResponse, isOk()); assertThat( deleteUserCacheResponse.getBody(), deleteUserCacheResponse.getTextFromJsonBody("/message"), is("Cache invalidated for user: " + TEST_USER) ); - }); + } } } diff --git a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java index 684f30e60b..76dd413454 100644 --- a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java @@ -11,37 +11,40 @@ package org.opensearch.security.api; -import java.util.Map; import java.util.StringJoiner; +import org.junit.ClassRule; import org.junit.Test; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.security.dlic.rest.validation.PasswordValidator; import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.security.api.PatchPayloadHelper.addOp; import static org.opensearch.security.api.PatchPayloadHelper.patch; +import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; +import static org.opensearch.test.framework.matcher.RestMatchers.isCreated; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class InternalUsersRegExpPasswordRulesRestApiIntegrationTest extends AbstractApiIntegrationTest { final static String PASSWORD_VALIDATION_ERROR_MESSAGE = "xxxxxxxx"; - @Override - protected Map getClusterSettings() { - Map clusterSettings = super.getClusterSettings(); - clusterSettings.put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, PASSWORD_VALIDATION_ERROR_MESSAGE); - clusterSettings.put( + @ClassRule + public static LocalCluster localCluster = clusterBuilder().nodeSetting( + ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, + PASSWORD_VALIDATION_ERROR_MESSAGE + ) + .nodeSetting( ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, "(?=.*[A-Z])(?=.*[^a-zA-Z\\\\d])(?=.*[0-9])(?=.*[a-z]).{8,}" - ); - clusterSettings.put( - ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH, - PasswordValidator.ScoreStrength.FAIR.name() - ); - return clusterSettings; - } + ) + .nodeSetting(ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH, PasswordValidator.ScoreStrength.FAIR.name()) + .build(); String internalUsers(String... path) { final var fullPath = new StringJoiner("/").add(super.apiPath("internalusers")); @@ -61,46 +64,42 @@ ToXContentObject internalUserWithPassword(final String password) { @Test public void canNotCreateUsersWithPassword() throws Exception { - withUser(ADMIN_USER_NAME, client -> { + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { // validate short passwords - badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword(""))); - badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("123"))); - badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("1234567"))); - badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("1Aa%"))); - badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("123456789"))); - badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("a123456789"))); - badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("A123456789"))); + assertThat(client.putJson(internalUsers("tooshoort"), internalUserWithPassword("")), isBadRequest()); + assertThat(client.putJson(internalUsers("tooshoort"), internalUserWithPassword("123")), isBadRequest()); + assertThat(client.putJson(internalUsers("tooshoort"), internalUserWithPassword("1234567")), isBadRequest()); + assertThat(client.putJson(internalUsers("tooshoort"), internalUserWithPassword("1Aa%")), isBadRequest()); + assertThat(client.putJson(internalUsers("tooshoort"), internalUserWithPassword("123456789")), isBadRequest()); + assertThat(client.putJson(internalUsers("tooshoort"), internalUserWithPassword("a123456789")), isBadRequest()); + assertThat(client.putJson(internalUsers("tooshoort"), internalUserWithPassword("A123456789")), isBadRequest()); // validate that password same as user - badRequest(() -> client.putJson(internalUsers("$1aAAAAAAAAC"), internalUserWithPassword("$1aAAAAAAAAC"))); - badRequest(() -> client.putJson(internalUsers("$1aAAAAAAAac"), internalUserWithPassword("$1aAAAAAAAAC"))); - badRequestWithReason( - () -> client.patch( - internalUsers(), - patch( - addOp("testuser1", internalUserWithPassword("$aA123456789")), - addOp("testuser2", internalUserWithPassword("testpassword2")) - ) - ), - PASSWORD_VALIDATION_ERROR_MESSAGE + assertThat(client.putJson(internalUsers("$1aAAAAAAAAC"), internalUserWithPassword("$1aAAAAAAAAC")), isBadRequest()); + assertThat(client.putJson(internalUsers("$1aAAAAAAAac"), internalUserWithPassword("$1aAAAAAAAAC")), isBadRequest()); + final var r = client.patch( + internalUsers(), + patch( + addOp("testuser1", internalUserWithPassword("$aA123456789")), + addOp("testuser2", internalUserWithPassword("testpassword2")) + ) ); + assertThat(r, isBadRequest("/reason", PASSWORD_VALIDATION_ERROR_MESSAGE)); // validate similarity - badRequestWithReason( - () -> client.putJson(internalUsers("some_user_name"), internalUserWithPassword("H3235,cc,some_User_Name")), - RequestContentValidator.ValidationError.SIMILAR_PASSWORD.message() - ); - }); + final var r2 = client.putJson(internalUsers("some_user_name"), internalUserWithPassword("H3235,cc,some_User_Name")); + assertThat(r2, isBadRequest("/reason", RequestContentValidator.ValidationError.SIMILAR_PASSWORD.message())); + } } @Test public void canCreateUsersWithPassword() throws Exception { - withUser(ADMIN_USER_NAME, client -> { - created(() -> client.putJson(internalUsers("ok1"), internalUserWithPassword("$aA123456789"))); - created(() -> client.putJson(internalUsers("ok2"), internalUserWithPassword("$Aa123456789"))); - created(() -> client.putJson(internalUsers("ok3"), internalUserWithPassword("$1aAAAAAAAAA"))); - ok(() -> client.putJson(internalUsers("ok3"), internalUserWithPassword("$1aAAAAAAAAC"))); - ok(() -> client.patch(internalUsers(), patch(addOp("ok3", internalUserWithPassword("$1aAAAAAAAAB"))))); - ok(() -> client.putJson(internalUsers("ok1"), internalUserWithPassword("Admin_123"))); - }); + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + assertThat(client.putJson(internalUsers("ok1"), internalUserWithPassword("$aA123456789")), isCreated()); + assertThat(client.putJson(internalUsers("ok2"), internalUserWithPassword("$Aa123456789")), isCreated()); + assertThat(client.putJson(internalUsers("ok3"), internalUserWithPassword("$1aAAAAAAAAA")), isCreated()); + assertThat(client.putJson(internalUsers("ok3"), internalUserWithPassword("$1aAAAAAAAAC")), isOk()); + assertThat(client.patch(internalUsers(), patch(addOp("ok3", internalUserWithPassword("$1aAAAAAAAAB")))), isOk()); + assertThat(client.putJson(internalUsers("ok1"), internalUserWithPassword("Admin_123")), isOk()); + } } } diff --git a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java index a87121297f..7cd184f590 100644 --- a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.JsonNode; import org.apache.http.HttpStatus; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import org.opensearch.common.xcontent.XContentType; @@ -33,17 +34,23 @@ import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; import static org.opensearch.security.api.PatchPayloadHelper.addOp; import static org.opensearch.security.api.PatchPayloadHelper.patch; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; import static org.opensearch.security.dlic.rest.api.InternalUsersApiAction.RESTRICTED_FROM_USERNAME; +import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; +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.isNotAllowed; +import static org.opensearch.test.framework.matcher.RestMatchers.isNotFound; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class InternalUsersRestApiIntegrationTest extends AbstractConfigEntityApiIntegrationTest { @@ -57,15 +64,20 @@ public class InternalUsersRestApiIntegrationTest extends AbstractConfigEntityApi private final static String SOME_ROLE = "some-role"; - static { - testSecurityConfig.withRestAdminUser(REST_API_ADMIN_INTERNAL_USERS_ONLY, restAdminPermission(Endpoint.INTERNALUSERS)) - .user(new TestSecurityConfig.User(SERVICE_ACCOUNT_USER).attr("service", "true").attr("enabled", "true")) - .roles( - new TestSecurityConfig.Role(HIDDEN_ROLE).hidden(true), - new TestSecurityConfig.Role(RESERVED_ROLE).reserved(true), - new TestSecurityConfig.Role(SOME_ROLE) - ); - } + @ClassRule + public static LocalCluster localCluster = clusterBuilder().users( + new TestSecurityConfig.User(SERVICE_ACCOUNT_USER).attr("service", "true").attr("enabled", "true"), + new TestSecurityConfig.User(REST_API_ADMIN_INTERNAL_USERS_ONLY).roles( + REST_ADMIN_REST_API_ACCESS_ROLE, + new TestSecurityConfig.Role("rest_admin_role").clusterPermissions(restAdminPermission(Endpoint.INTERNALUSERS)) + ) + ) + .roles( + new TestSecurityConfig.Role(HIDDEN_ROLE).hidden(true), + new TestSecurityConfig.Role(RESERVED_ROLE).reserved(true), + new TestSecurityConfig.Role(SOME_ROLE) + ) + .build(); public InternalUsersRestApiIntegrationTest() { super("internalusers", new TestDescriptor() { @@ -92,6 +104,26 @@ public Optional restAdminLimitedUser() { }); } + @Test + public void forbiddenForRegularUsers() throws Exception { + super.forbiddenForRegularUsers(localCluster); + } + + @Test + public void availableForAdminUser() throws Exception { + super.availableForAdminUser(localCluster); + } + + @Test + public void availableForTLSAdminUser() throws Exception { + super.availableForTLSAdminUser(localCluster); + } + + @Test + public void availableForRESTAdminUser() throws Exception { + super.availableForRESTAdminUser(localCluster); + } + static ToXContentObject internalUserWithPassword(final String password) { return internalUser(null, null, null, password, null, null, null); } @@ -193,60 +225,59 @@ static ToXContentObject serviceUser(final Boolean enabled, final String password @Override void verifyBadRequestOperations(TestRestClient client) throws Exception { // bad query string parameter name - badRequest(() -> client.get(apiPath() + "?aaaaa=bbbbb")); + assertThat(client.get(apiPath() + "?aaaaa=bbbbb"), isBadRequest()); final var predefinedUserName = randomAsciiAlphanumOfLength(4); - created( - () -> client.putJson( + assertThat( + client.putJson( apiPath(predefinedUserName), internalUser(randomAsciiAlphanumOfLength(10), configJsonArray(generateArrayValues(false)), null, null) - ) + ), + isCreated() ); invalidJson(client, predefinedUserName); } void invalidJson(final TestRestClient client, final String predefinedUserName) throws Exception { // put - badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY)); - badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { + assertThat(client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY), isBadRequest()); + assertThat(client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { builder.startObject(); builder.field("backend_roles"); randomConfigArray(false).toXContent(builder, params); builder.field("backend_roles"); randomConfigArray(false).toXContent(builder, params); return builder.endObject(); - })); - assertInvalidKeys(badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { + }), isBadRequest()); + HttpResponse response = client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { builder.startObject(); builder.field("unknown_json_property"); configJsonArray("a", "b").toXContent(builder, params); builder.field("backend_roles"); randomConfigArray(false).toXContent(builder, params); return builder.endObject(); - })), "unknown_json_property"); - assertWrongDataType( - client.putJson( - apiPath(randomAsciiAlphanumOfLength(10)), - (builder, params) -> builder.startObject() - .field("password", configJsonArray("a", "b")) - .field("hash") - .nullValue() - .field("backend_roles", "c") - .field("attributes", "d") - .field("opendistro_security_roles", "e") - .endObject() - ), - Map.of( - "password", - "String expected", - "hash", - "String expected", - "backend_roles", - "Array expected", - "attributes", - "Object expected", - "opendistro_security_roles", - "Array expected" - ) + }); + assertThat(response, isBadRequest()); + assertInvalidKeys(response, "unknown_json_property"); + + response = client.putJson( + apiPath(randomAsciiAlphanumOfLength(10)), + (builder, params) -> builder.startObject() + .field("password", configJsonArray("a", "b")) + .field("hash") + .nullValue() + .field("backend_roles", "c") + .field("attributes", "d") + .field("opendistro_security_roles", "e") + .endObject() + ); + assertThat( + response, + isBadRequest().withAttribute("/status", "error") + .withAttribute("/password", "String expected") + .withAttribute("/hash", "String expected") + .withAttribute("/backend_roles", "Array expected") + .withAttribute("/attributes", "Object expected") + .withAttribute("/opendistro_security_roles", "Array expected") ); assertNullValuesInArray( client.putJson( @@ -261,21 +292,22 @@ void invalidJson(final TestRestClient client, final String predefinedUserName) t ) ); // patch - badRequest(() -> client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(10), EMPTY_BODY)))); - badRequest( - () -> client.patch( + assertThat(client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(10), EMPTY_BODY))), isBadRequest()); + assertThat( + client.patch( apiPath(predefinedUserName), patch(replaceOp(randomFrom(List.of("opendistro_security_roles", "backend_roles", "attributes")), EMPTY_BODY)) - ) + ), + isBadRequest() ); - badRequest(() -> client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(5), (ToXContentObject) (builder, params) -> { + assertThat(client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(5), (ToXContentObject) (builder, params) -> { builder.startObject(); builder.field("unknown_json_property"); configJsonArray("a", "b").toXContent(builder, params); builder.field("backend_roles"); randomConfigArray(false).toXContent(builder, params); return builder.endObject(); - })))); + }))), isBadRequest()); assertWrongDataType( client.patch( apiPath(), @@ -338,7 +370,7 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien randomAttributes(), randomSecurityRoles() ); - created(() -> client.putJson(apiPath(usernamePut), newUserJsonPut)); + assertThat(client.putJson(apiPath(usernamePut), newUserJsonPut), isCreated()); assertInternalUser( ok(() -> client.get(apiPath(usernamePut))).bodyAsJsonNode().get(usernamePut), hidden, @@ -353,15 +385,15 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien randomAttributes(), randomSecurityRoles() ); - ok(() -> client.putJson(apiPath(usernamePut), updatedUserJsonPut)); + assertThat(client.putJson(apiPath(usernamePut), updatedUserJsonPut), isOk()); assertInternalUser( ok(() -> client.get(apiPath(usernamePut))).bodyAsJsonNode().get(usernamePut), hidden, reserved, Strings.toString(XContentType.JSON, updatedUserJsonPut) ); - ok(() -> client.delete(apiPath(usernamePut))); - notFound(() -> client.get(apiPath(usernamePut))); + assertThat(client.delete(apiPath(usernamePut)), isOk()); + assertThat(client.get(apiPath(usernamePut)), isNotFound()); // patch // TODO related to issue #4426 final var usernamePatch = randomAsciiAlphanumOfLength(10); @@ -373,22 +405,24 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien (builder, params) -> builder.startObject().endObject(), configJsonArray() ); - ok(() -> client.patch(apiPath(), patch(addOp(usernamePatch, newUserJsonPatch)))); + assertThat(client.patch(apiPath(), patch(addOp(usernamePatch, newUserJsonPatch))), isOk()); assertInternalUser( ok(() -> client.get(apiPath(usernamePatch))).bodyAsJsonNode().get(usernamePatch), hidden, reserved, Strings.toString(XContentType.JSON, newUserJsonPatch) ); - ok(() -> client.patch(apiPath(usernamePatch), patch(replaceOp("backend_roles", configJsonArray("c", "d"))))); - ok( - () -> client.patch( + assertThat(client.patch(apiPath(usernamePatch), patch(replaceOp("backend_roles", configJsonArray("c", "d")))), isOk()); + assertThat( + client.patch( apiPath(usernamePatch), patch(addOp("attributes", (ToXContentObject) (builder, params) -> builder.startObject().field("a", "b").endObject())) - ) + ), + isOk() ); - ok( - () -> client.patch(apiPath(usernamePatch), patch(addOp("opendistro_security_roles", configJsonArray(RESERVED_ROLE, SOME_ROLE)))) + assertThat( + client.patch(apiPath(usernamePatch), patch(addOp("opendistro_security_roles", configJsonArray(RESERVED_ROLE, SOME_ROLE)))), + isOk() ); } @@ -432,56 +466,58 @@ String filterBy(final String value) { @Test public void filters() throws Exception { - withUser(ADMIN_USER_NAME, client -> { + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { assertFilterByUsers(ok(() -> client.get(apiPath())), true, true); assertFilterByUsers(ok(() -> client.get(filterBy("any"))), true, true); assertFilterByUsers(ok(() -> client.get(filterBy("internal"))), false, true); assertFilterByUsers(ok(() -> client.get(filterBy("service"))), true, false); assertFilterByUsers(ok(() -> client.get(filterBy("something"))), true, true); - }); + } } void assertFilterByUsers(final HttpResponse response, final boolean hasServiceUser, final boolean hasInternalUser) { assertThat(response.getBody(), response.bodyAsJsonNode().has(SERVICE_ACCOUNT_USER), is(hasServiceUser)); - assertThat(response.getBody(), response.bodyAsJsonNode().has(NEW_USER), is(hasInternalUser)); + assertThat(response.getBody(), response.bodyAsJsonNode().has(NEW_USER.getName()), is(hasInternalUser)); } @Test public void verifyPOSTOnlyForAuthTokenEndpoint() throws Exception { - withUser(ADMIN_USER_NAME, client -> { - badRequest(() -> client.post(apiPath(ADMIN_USER_NAME, "authtoken"))); - ok(() -> client.post(apiPath(SERVICE_ACCOUNT_USER, "authtoken"))); + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + assertThat(client.post(apiPath(ADMIN_USER.getName(), "authtoken")), isBadRequest()); + assertThat(client.post(apiPath(SERVICE_ACCOUNT_USER, "authtoken")), isOk()); /* should be notImplement but the call doesn't reach {@link org.opensearch.security.dlic.rest.api.InternalUsersApiAction#withAuthTokenPath(RestRequest)} */ - methodNotAllowed(() -> client.post(apiPath("randomPath"))); - }); + assertThat(client.post(apiPath("randomPath")), isNotAllowed()); + } } @Test public void userApiWithDotsInName() throws Exception { - withUser(ADMIN_USER_NAME, client -> { + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { for (final var dottedUserName : List.of(".my.dotuser0", ".my.dot.user0")) { - created( - () -> client.putJson( + assertThat( + client.putJson( apiPath(dottedUserName), (builder, params) -> builder.startObject().field("password", randomAsciiAlphanumOfLength(10)).endObject() - ) + ), + isCreated() ); } for (final var dottedUserName : List.of(".my.dotuser1", ".my.dot.user1")) { - created( - () -> client.putJson( + assertThat( + client.putJson( apiPath(dottedUserName), (builder, params) -> builder.startObject() .field("hash", passwordHasher.hash(randomAsciiAlphanumOfLength(10).toCharArray())) .endObject() - ) + ), + isCreated() ); } for (final var dottedUserName : List.of(".my.dotuser2", ".my.dot.user2")) { - ok( - () -> client.patch( + assertThat( + client.patch( apiPath(), patch( addOp( @@ -491,12 +527,13 @@ public void userApiWithDotsInName() throws Exception { .endObject() ) ) - ) + ), + isOk() ); } for (final var dottedUserName : List.of(".my.dotuser3", ".my.dot.user3")) { - ok( - () -> client.patch( + assertThat( + client.patch( apiPath(), patch( addOp( @@ -506,91 +543,98 @@ public void userApiWithDotsInName() throws Exception { .endObject() ) ) - ) + ), + isOk() ); } - }); + } } @Test public void noPasswordChange() throws Exception { - withUser(ADMIN_USER_NAME, client -> { - created( - () -> client.putJson( + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + assertThat( + client.putJson( apiPath("user1"), (builder, params) -> builder.startObject() .field("hash", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m") .endObject() - ) + ), + isCreated() ); - badRequest( - () -> client.putJson( + assertThat( + client.putJson( apiPath("user1"), (builder, params) -> builder.startObject() .field("hash", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m") .field("password", "") .field("backend_roles", configJsonArray("admin", "role_a")) .endObject() - ) + ), + isBadRequest() ); - ok( - () -> client.putJson( + assertThat( + client.putJson( apiPath("user1"), (builder, params) -> builder.startObject() .field("hash", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m") .field("password", randomAsciiAlphanumOfLength(10)) .field("backend_roles", configJsonArray("admin", "role_a")) .endObject() - ) + ), + isOk() ); - created( - () -> client.putJson( + assertThat( + client.putJson( apiPath("user2"), (builder, params) -> builder.startObject() .field("hash", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m") .field("password", randomAsciiAlphanumOfLength(10)) .endObject() - ) + ), + isCreated() ); - badRequest( - () -> client.putJson( + assertThat( + client.putJson( apiPath("user2"), (builder, params) -> builder.startObject() .field("password", "") .field("backend_roles", configJsonArray("admin", "role_b")) .endObject() - ) + ), + isBadRequest() ); - ok( - () -> client.putJson( + assertThat( + client.putJson( apiPath("user2"), (builder, params) -> builder.startObject() .field("password", randomAsciiAlphanumOfLength(10)) .field("backend_roles", configJsonArray("admin", "role_b")) .endObject() - ) + ), + isOk() ); - }); + } } @Test public void securityRoles() throws Exception { final var userWithSecurityRoles = randomAsciiAlphanumOfLength(15); final var userWithSecurityRolesPassword = randomAsciiAlphanumOfLength(10); - withUser( - ADMIN_USER_NAME, - client -> ok( - () -> client.patch( - apiPath(), - patch(addOp(userWithSecurityRoles, internalUser(userWithSecurityRolesPassword, null, null, null))) - ) - ) - ); - withUser(userWithSecurityRoles, userWithSecurityRolesPassword, client -> forbidden(() -> client.get(apiPath()))); - withUser( - ADMIN_USER_NAME, - client -> ok( - () -> client.patch( + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + assertThat( + client.patch(apiPath(), patch(addOp(userWithSecurityRoles, internalUser(userWithSecurityRolesPassword, null, null, null)))), + isOk() + ); + } + + try (TestRestClient client = localCluster.getRestClient(userWithSecurityRoles, userWithSecurityRolesPassword)) { + assertThat(client.get(apiPath()), isForbidden()); + } + + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + assertThat( + client.patch( apiPath(), patch( replaceOp( @@ -603,38 +647,48 @@ public void securityRoles() throws Exception { ) ) ) - ) - ) - ); - withUser(userWithSecurityRoles, userWithSecurityRolesPassword, client -> ok(() -> client.get(apiPath()))); - withUser(ADMIN_USER_NAME, client -> impossibleToSetHiddenRoleIsNotAllowed(userWithSecurityRoles, client)); - withUser(ADMIN_USER_NAME, client -> settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client)); - - withUser( - ADMIN_USER_NAME, - localCluster.getAdminCertificate(), - client -> settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client) - ); - withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::canAssignedHiddenRole); + ), + isOk() + ); + } + + try (TestRestClient client = localCluster.getRestClient(userWithSecurityRoles, userWithSecurityRolesPassword)) { + assertThat(client.get(apiPath()), isOk()); + } + + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + impossibleToSetHiddenRoleIsNotAllowed(userWithSecurityRoles, client); + settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client); + } + + try (TestRestClient client = localCluster.getAdminCertRestClient()) { + settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client); + canAssignedHiddenRole(client); + } + + try (TestRestClient client = localCluster.getRestClient(REST_ADMIN_USER)) { + settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client); + canAssignedHiddenRole(client); + } - for (final var restAdminUser : List.of(REST_ADMIN_USER, REST_API_ADMIN_INTERNAL_USERS_ONLY)) { - withUser(restAdminUser, client -> settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client)); - withUser(restAdminUser, localCluster.getAdminCertificate(), this::canAssignedHiddenRole); + try (TestRestClient client = localCluster.getRestClient(REST_API_ADMIN_INTERNAL_USERS_ONLY, DEFAULT_PASSWORD)) { + settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client); + canAssignedHiddenRole(client); } } void impossibleToSetHiddenRoleIsNotAllowed(final String predefinedUserName, final TestRestClient client) throws Exception { // put - notFound( - () -> client.putJson( + assertThat( + client.putJson( apiPath(randomAsciiAlphanumOfLength(10)), internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray(HIDDEN_ROLE)) ), - "Resource 'hidden-role' is not available." + isNotFound().withAttribute("/message", "Resource 'hidden-role' is not available.") ); // patch - notFound( - () -> client.patch( + assertThat( + client.patch( apiPath(), patch( addOp( @@ -642,35 +696,34 @@ void impossibleToSetHiddenRoleIsNotAllowed(final String predefinedUserName, fina internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray(HIDDEN_ROLE)) ) ) - ) + ), + isNotFound() ); // TODO related to issue #4426 - notFound( - () -> client.patch(apiPath(predefinedUserName), patch(addOp("opendistro_security_roles", configJsonArray(HIDDEN_ROLE)))) - + assertThat( + client.patch(apiPath(predefinedUserName), patch(addOp("opendistro_security_roles", configJsonArray(HIDDEN_ROLE)))), + isNotFound() ); } void canAssignedHiddenRole(final TestRestClient client) throws Exception { final var userNamePut = randomAsciiAlphanumOfLength(4); - created( - () -> client.putJson( - apiPath(userNamePut), - internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray(HIDDEN_ROLE)) - ) + assertThat( + client.putJson(apiPath(userNamePut), internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray(HIDDEN_ROLE))), + isCreated() ); } void settingOfUnknownRoleIsNotAllowed(final String predefinedUserName, final TestRestClient client) throws Exception { - notFound( - () -> client.putJson( + assertThat( + client.putJson( apiPath(randomAsciiAlphanumOfLength(10)), internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray("unknown-role")) ), - "role 'unknown-role' not found." + isNotFound().withAttribute("/message", "role 'unknown-role' not found.") ); - notFound( - () -> client.patch( + assertThat( + client.patch( apiPath(), patch( addOp( @@ -678,16 +731,18 @@ void settingOfUnknownRoleIsNotAllowed(final String predefinedUserName, final Tes internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray("unknown-role")) ) ) - ) + ), + isNotFound() ); - notFound( - () -> client.patch(apiPath(predefinedUserName), patch(addOp("opendistro_security_roles", configJsonArray("unknown-role")))) + assertThat( + client.patch(apiPath(predefinedUserName), patch(addOp("opendistro_security_roles", configJsonArray("unknown-role")))), + isNotFound() ); } @Test public void parallelPutRequests() throws Exception { - withUser(ADMIN_USER_NAME, client -> { + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { final var userName = randomAsciiAlphanumOfLength(10); final var httpResponses = new HttpResponse[10]; @@ -722,40 +777,41 @@ public void parallelPutRequests() throws Exception { break; } } - }); + } } @Test public void restrictedUsernameContents() throws Exception { - withUser(ADMIN_USER_NAME, client -> { + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { { for (final var restrictedTerm : RESTRICTED_FROM_USERNAME) { for (final var username : List.of( randomAsciiAlphanumOfLength(2) + restrictedTerm + randomAsciiAlphanumOfLength(3), URLEncoder.encode(randomAsciiAlphanumOfLength(4) + ":" + randomAsciiAlphanumOfLength(3), StandardCharsets.UTF_8) )) { - final var putResponse = badRequest( - () -> client.putJson(apiPath(username), internalUserWithPassword(randomAsciiAlphanumOfLength(10))) + assertThat( + client.putJson(apiPath(username), internalUserWithPassword(randomAsciiAlphanumOfLength(10))), + isBadRequest("/message", restrictedTerm) ); - assertThat(putResponse.getBody(), containsString(restrictedTerm)); - final var patchResponse = badRequest( - () -> client.patch(apiPath(), patch(addOp(username, internalUserWithPassword(randomAsciiAlphanumOfLength(10))))) + assertThat( + client.patch(apiPath(), patch(addOp(username, internalUserWithPassword(randomAsciiAlphanumOfLength(10))))), + isBadRequest("/message", restrictedTerm) ); - assertThat(patchResponse.getBody(), containsString(restrictedTerm)); } } } - }); + } } @Test public void serviceUsers() throws Exception { - withUser(ADMIN_USER_NAME, client -> { + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { // Add enabled service account then get it // TODO related to issue #4426 add default behave when enabled is true final var happyServiceLiveUserName = randomAsciiAlphanumOfLength(10); - created(() -> client.putJson(apiPath(happyServiceLiveUserName), serviceUser(true))); - final var serviceLiveResponse = ok(() -> client.get(apiPath(happyServiceLiveUserName))); + assertThat(client.putJson(apiPath(happyServiceLiveUserName), serviceUser(true)), isCreated()); + final var serviceLiveResponse = client.get(apiPath(happyServiceLiveUserName)); + assertThat(serviceLiveResponse, isOk()); assertThat( serviceLiveResponse.getBody(), serviceLiveResponse.getBooleanFromJsonBody("/" + happyServiceLiveUserName + "/attributes/service") @@ -767,8 +823,9 @@ public void serviceUsers() throws Exception { // Add disabled service account final var happyServiceDeadUserName = randomAsciiAlphanumOfLength(10); - created(() -> client.putJson(apiPath(happyServiceDeadUserName), serviceUser(false))); - final var serviceDeadResponse = ok(() -> client.get(apiPath(happyServiceDeadUserName))); + assertThat(client.putJson(apiPath(happyServiceDeadUserName), serviceUser(false)), isCreated()); + final var serviceDeadResponse = client.get(apiPath(happyServiceDeadUserName)); + assertThat(serviceDeadResponse, isOk()); assertThat( serviceDeadResponse.getBody(), serviceDeadResponse.getBooleanFromJsonBody("/" + happyServiceDeadUserName + "/attributes/service") @@ -778,27 +835,27 @@ public void serviceUsers() throws Exception { not(serviceDeadResponse.getBooleanFromJsonBody("/" + happyServiceDeadUserName + "/attributes/enabled")) ); // Add service account with password -- Should Fail - badRequest( - () -> client.putJson( - apiPath(randomAsciiAlphanumOfLength(10)), - serviceUserWithPassword(true, randomAsciiAlphanumOfLength(10)) - ) + assertThat( + client.putJson(apiPath(randomAsciiAlphanumOfLength(10)), serviceUserWithPassword(true, randomAsciiAlphanumOfLength(10))), + isBadRequest() ); // Add service with hash -- should fail - badRequest( - () -> client.putJson( + assertThat( + client.putJson( apiPath(randomAsciiAlphanumOfLength(10)), serviceUserWithHash(true, passwordHasher.hash(randomAsciiAlphanumOfLength(10).toCharArray())) - ) + ), + isBadRequest() ); // Add Service account with password & Hash -- should fail final var password = randomAsciiAlphanumOfLength(10); - badRequest( - () -> client.putJson( + assertThat( + client.putJson( apiPath(randomAsciiAlphanumOfLength(10)), serviceUser(true, password, passwordHasher.hash(password.toCharArray())) - ) + ), + isBadRequest() ); - }); + } } } diff --git a/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java index b18a0c6fd6..8939b24301 100644 --- a/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java @@ -11,26 +11,28 @@ package org.opensearch.security.api; -import java.util.Map; import java.util.StringJoiner; +import org.junit.ClassRule; import org.junit.Test; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.security.api.PatchPayloadHelper.addOp; import static org.opensearch.security.api.PatchPayloadHelper.patch; +import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; +import static org.opensearch.test.framework.matcher.RestMatchers.isCreated; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest extends AbstractApiIntegrationTest { - @Override - protected Map getClusterSettings() { - Map clusterSettings = super.getClusterSettings(); - clusterSettings.put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, 9); - return clusterSettings; - } + @ClassRule + public static LocalCluster localCluster = clusterBuilder().nodeSetting(ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, 9).build(); String internalUsers(String... path) { final var fullPath = new StringJoiner("/").add(super.apiPath("internalusers")); @@ -50,38 +52,45 @@ ToXContentObject internalUserWithPassword(final String password) { @Test public void canNotCreateUsersWithPassword() throws Exception { - withUser(ADMIN_USER_NAME, client -> { - badRequestWithReason( - () -> client.putJson(internalUsers("admin"), internalUserWithPassword("password89")), - RequestContentValidator.ValidationError.WEAK_PASSWORD.message() + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + final var r1 = client.putJson(internalUsers("admin"), internalUserWithPassword("password89")); + assertThat(r1, isBadRequest()); + assertThat( + r1.getTextFromJsonBody("/reason"), + org.hamcrest.Matchers.containsString(RequestContentValidator.ValidationError.WEAK_PASSWORD.message()) ); - badRequestWithReason( - () -> client.putJson(internalUsers("admin"), internalUserWithPassword("A123456789")), - RequestContentValidator.ValidationError.WEAK_PASSWORD.message() + + final var r2 = client.putJson(internalUsers("admin"), internalUserWithPassword("A123456789")); + assertThat(r2, isBadRequest()); + assertThat( + r2.getTextFromJsonBody("/reason"), + org.hamcrest.Matchers.containsString(RequestContentValidator.ValidationError.WEAK_PASSWORD.message()) ); - badRequestWithReason( - () -> client.putJson(internalUsers("admin"), internalUserWithPassword(randomAsciiAlphanumOfLengthBetween(2, 8))), - RequestContentValidator.ValidationError.INVALID_PASSWORD_TOO_SHORT.message() + + final var r3 = client.putJson(internalUsers("admin"), internalUserWithPassword(randomAsciiAlphanumOfLengthBetween(2, 8))); + assertThat(r3, isBadRequest()); + assertThat( + r3.getTextFromJsonBody("/reason"), + org.hamcrest.Matchers.containsString(RequestContentValidator.ValidationError.INVALID_PASSWORD_TOO_SHORT.message()) ); - }); + } } @Test public void canCreateUserWithPassword() throws Exception { - withUser(ADMIN_USER_NAME, client -> { - created( - () -> client.putJson( - internalUsers(randomAsciiAlphanumOfLength(10)), - internalUserWithPassword(randomAsciiAlphanumOfLength(9)) - ) + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + final var createdResp = client.putJson( + internalUsers(randomAsciiAlphanumOfLength(10)), + internalUserWithPassword(randomAsciiAlphanumOfLength(9)) ); - ok( - () -> client.patch( - internalUsers(), - patch(addOp(randomAsciiAlphanumOfLength(10), internalUserWithPassword(randomAsciiAlphanumOfLength(9)))) - ) + assertThat(createdResp, isCreated()); + + final var patchResp = client.patch( + internalUsers(), + patch(addOp(randomAsciiAlphanumOfLength(10), internalUserWithPassword(randomAsciiAlphanumOfLength(9)))) ); - }); + assertThat(patchResp, isOk()); + } } } diff --git a/src/integrationTest/java/org/opensearch/security/api/RolesMappingRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/RolesMappingRestApiIntegrationTest.java index 7255007271..7cb08d645a 100644 --- a/src/integrationTest/java/org/opensearch/security/api/RolesMappingRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/RolesMappingRestApiIntegrationTest.java @@ -13,11 +13,12 @@ import java.io.IOException; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.StringJoiner; import com.fasterxml.jackson.databind.JsonNode; +import org.junit.ClassRule; +import org.junit.Test; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.common.Strings; @@ -25,7 +26,9 @@ import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import com.nimbusds.jose.util.Pair; @@ -36,23 +39,33 @@ import static org.opensearch.security.api.PatchPayloadHelper.removeOp; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; import static org.opensearch.test.framework.TestSecurityConfig.Role; +import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; +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.isNotFound; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class RolesMappingRestApiIntegrationTest extends AbstractConfigEntityApiIntegrationTest { - final static String REST_API_ADMIN_ROLES_MAPPING_ONLY = "rest-api-admin-roles-mapping-only"; + final static TestSecurityConfig.User REST_API_ADMIN_ROLES_MAPPING_ONLY = new TestSecurityConfig.User( + "rest-api-admin-roles-mapping-only" + ).roles( + REST_ADMIN_REST_API_ACCESS_ROLE, + new TestSecurityConfig.Role("rest_admin_role").clusterPermissions(restAdminPermission(Endpoint.ROLESMAPPING)) + ); final static String REST_ADMIN_ROLE = "rest-admin-role"; final static String REST_ADMIN_ROLE_WITH_MAPPING = "rest-admin-role-with-mapping"; - static { - testSecurityConfig.withRestAdminUser(REST_API_ADMIN_ROLES_MAPPING_ONLY, restAdminPermission(Endpoint.ROLESMAPPING)) - .roles( - new Role(REST_ADMIN_ROLE).reserved(true).clusterPermissions(allRestAdminPermissions()), - new Role(REST_ADMIN_ROLE_WITH_MAPPING).clusterPermissions(allRestAdminPermissions()) - ) - .rolesMapping(new TestSecurityConfig.RoleMapping(REST_ADMIN_ROLE_WITH_MAPPING)); - } + @ClassRule + public static LocalCluster localCluster = clusterBuilder().users(REST_API_ADMIN_ROLES_MAPPING_ONLY) + .roles( + new Role(REST_ADMIN_ROLE).reserved(true).clusterPermissions(allRestAdminPermissions()), + new Role(REST_ADMIN_ROLE_WITH_MAPPING).clusterPermissions(allRestAdminPermissions()) + ) + .rolesMapping(new TestSecurityConfig.RoleMapping(REST_ADMIN_ROLE_WITH_MAPPING)) + .build(); public RolesMappingRestApiIntegrationTest() { super("rolesmapping", new TestDescriptor() { @@ -81,11 +94,31 @@ public ToXContentObject jsonPropertyPayload() { @Override public Optional restAdminLimitedUser() { - return Optional.of(REST_API_ADMIN_ROLES_MAPPING_ONLY); + return Optional.of(REST_API_ADMIN_ROLES_MAPPING_ONLY.getName()); } }); } + @Test + public void forbiddenForRegularUsers() throws Exception { + super.forbiddenForRegularUsers(localCluster); + } + + @Test + public void availableForAdminUser() throws Exception { + super.availableForAdminUser(localCluster); + } + + @Test + public void availableForTLSAdminUser() throws Exception { + super.availableForTLSAdminUser(localCluster); + } + + @Test + public void availableForRESTAdminUser() throws Exception { + super.availableForRESTAdminUser(localCluster); + } + static ToXContentObject roleMappingWithUsers(ToXContentObject users) { return roleMapping(null, null, null, null, null, users, null); } @@ -168,52 +201,45 @@ String rolesApiPath(final String roleName) { } @Override - Pair predefinedHiddenAndReservedConfigEntities() throws Exception { + Pair predefinedHiddenAndReservedConfigEntities(LocalCluster localCluster) throws Exception { final var hiddenEntityName = randomAsciiAlphanumOfLength(10); final var reservedEntityName = randomAsciiAlphanumOfLength(10); - withUser( - ADMIN_USER_NAME, - localCluster.getAdminCertificate(), - client -> created(() -> client.putJson(rolesApiPath(hiddenEntityName), roleJson(true, null))) - ); - withUser( - ADMIN_USER_NAME, - localCluster.getAdminCertificate(), - client -> created( - () -> client.putJson( + try (TestRestClient client = localCluster.getAdminCertRestClient()) { + assertThat(client.putJson(rolesApiPath(hiddenEntityName), roleJson(true, null)), isCreated()); + assertThat( + client.putJson( apiPath(hiddenEntityName), roleMapping(true, null, null, configJsonArray("a", "b"), configJsonArray(), configJsonArray(), configJsonArray()) - ) - ) - ); - withUser( - ADMIN_USER_NAME, - localCluster.getAdminCertificate(), - client -> created(() -> client.putJson(rolesApiPath(reservedEntityName), roleJson(null, true))) - ); - withUser( - ADMIN_USER_NAME, - localCluster.getAdminCertificate(), - client -> created( - () -> client.putJson( + ), + isCreated() + ); + assertThat(client.putJson(rolesApiPath(reservedEntityName), roleJson(null, true)), isCreated()); + assertThat( + client.putJson( apiPath(reservedEntityName), roleMapping(null, true, null, configJsonArray("a", "b"), configJsonArray(), configJsonArray(), configJsonArray()) - ) - ) - ); + ), + isCreated() + ); + + } + return Pair.of(hiddenEntityName, reservedEntityName); } @Override void creationOfReadOnlyEntityForbidden(String entityName, TestRestClient client, ToXContentObject... entities) throws Exception { - withUser(ADMIN_USER_NAME, adminClient -> created(() -> adminClient.putJson(rolesApiPath(entityName), roleJson()))); + try (TestRestClient adminClient = localCluster.getRestClient(ADMIN_USER)) { + assertThat(adminClient.putJson(rolesApiPath(entityName), roleJson()), isCreated()); + } + super.creationOfReadOnlyEntityForbidden(entityName, client, entities); } @Override void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient client) throws Exception { final String roleName = randomAsciiAlphanumOfLength(10); - created(() -> client.putJson(rolesApiPath(roleName), roleJson())); + assertThat(client.putJson(rolesApiPath(roleName), roleJson()), isCreated()); // put final var newPutRoleMappingJson = roleMapping( hidden, @@ -223,7 +249,7 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien randomArray(false), randomArray(false) ); - created(() -> client.putJson(apiPath(roleName), newPutRoleMappingJson)); + assertThat(client.putJson(apiPath(roleName), newPutRoleMappingJson), isCreated()); assertRoleMapping( ok(() -> client.get(apiPath(roleName))).bodyAsJsonNode().get(roleName), hidden, @@ -238,7 +264,7 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien randomArray(false), randomArray(false) ); - ok(() -> client.putJson(apiPath(roleName), updatePutRoleMappingJson)); + assertThat(client.putJson(apiPath(roleName), updatePutRoleMappingJson), isOk()); assertRoleMapping( ok(() -> client.get(apiPath(roleName))).bodyAsJsonNode().get(roleName), hidden, @@ -246,8 +272,8 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien Strings.toString(XContentType.JSON, updatePutRoleMappingJson) ); - ok(() -> client.delete(apiPath(roleName))); - notFound(() -> client.get(apiPath(roleName))); + assertThat(client.delete(apiPath(roleName)), isOk()); + assertThat(client.get(apiPath(roleName)), isNotFound()); // patch // TODO related to issue #4426 final var newPatchRoleMappingJson = roleMapping( @@ -258,22 +284,23 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien configJsonArray(), configJsonArray() ); - ok(() -> client.patch(apiPath(), patch(addOp(roleName, newPatchRoleMappingJson)))); + assertThat(client.patch(apiPath(), patch(addOp(roleName, newPatchRoleMappingJson))), isOk()); assertRoleMapping( ok(() -> client.get(apiPath(roleName))).bodyAsJsonNode().get(roleName), hidden, reserved, Strings.toString(XContentType.JSON, newPatchRoleMappingJson) ); - ok(() -> client.patch(apiPath(roleName), patch(replaceOp("backend_roles", configJsonArray("c", "d"))))); - ok(() -> client.patch(apiPath(roleName), patch(addOp("hosts", configJsonArray("e", "f"))))); - ok(() -> client.patch(apiPath(roleName), patch(addOp("users", configJsonArray("g", "h"))))); - ok(() -> client.patch(apiPath(roleName), patch(addOp("and_backend_roles", configJsonArray("i", "j"))))); - ok(() -> client.patch(apiPath(roleName), patch(addOp("and_backend_roles", configJsonArray("i", "j")))), "No updates required"); - badRequest(() -> client.patch(apiPath(roleName), patch(replaceOp("backend_roles", configJsonArray("c", ""))))); - - ok(() -> client.patch(apiPath(), patch(removeOp(roleName)))); - notFound(() -> client.get(apiPath(roleName))); + assertThat(client.patch(apiPath(roleName), patch(replaceOp("backend_roles", configJsonArray("c", "d")))), isOk()); + assertThat(client.patch(apiPath(roleName), patch(addOp("hosts", configJsonArray("e", "f")))), isOk()); + assertThat(client.patch(apiPath(roleName), patch(addOp("users", configJsonArray("g", "h")))), isOk()); + assertThat(client.patch(apiPath(roleName), patch(addOp("and_backend_roles", configJsonArray("i", "j")))), isOk()); + // second identical update should still be OK; message assertion omitted + assertThat(client.patch(apiPath(roleName), patch(addOp("and_backend_roles", configJsonArray("i", "j")))), isOk()); + assertThat(client.patch(apiPath(roleName), patch(replaceOp("backend_roles", configJsonArray("c", "")))), isBadRequest()); + + assertThat(client.patch(apiPath(), patch(removeOp(roleName))), isOk()); + assertThat(client.get(apiPath(roleName)), isNotFound()); } void assertRoleMapping(final JsonNode actualObjectNode, final Boolean hidden, final Boolean reserved, final String expectedRoleJson) @@ -305,38 +332,36 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { return builder.endObject(); }; - notFound( - () -> client.putJson( + assertThat( + client.putJson( apiPath("unknown_role"), roleMapping(configJsonArray(), configJsonArray(), configJsonArray(), configJsonArray()) ), - "role 'unknown_role' not found." + isNotFound().withAttribute("/message", "role 'unknown_role' not found.") ); // put - badRequestWithReason( - () -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY), - "Request body required for this action." + assertThat( + client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY), + isBadRequest().withAttribute("/reason", "Request body required for this action.") ); - badRequestWithReason( - () -> client.putJson( + assertThat( + client.putJson( apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> builder.startObject().field("users", configJsonArray()).field("users", configJsonArray()).endObject() ), - "Could not parse content of request." - ); - assertInvalidKeys( - badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), unparseableJsonRequest)), - "unknown_json_property" + isBadRequest().withAttribute("/reason", "Could not parse content of request.") ); + HttpResponse response = client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), unparseableJsonRequest); + assertThat(response, isBadRequest()); + assertInvalidKeys(response, "unknown_json_property"); final var randomPropertyForPut = randomJsonProperty(); - assertWrongDataType( - client.putJson( - apiPath(randomAsciiAlphanumOfLength(5)), - (builder, params) -> builder.startObject().field(randomPropertyForPut).value("something").endObject() - ), - Map.of(randomPropertyForPut, "Array expected") + + response = client.putJson( + apiPath(randomAsciiAlphanumOfLength(5)), + (builder, params) -> builder.startObject().field(randomPropertyForPut).value("something").endObject() ); + assertThat(response, isBadRequest().withAttribute("/status", "error").withAttribute("/" + randomPropertyForPut, "Array expected")); assertNullValuesInArray( client.putJson( apiPath(randomAsciiAlphanumOfLength(5)), @@ -350,52 +375,49 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { ); // patch final var predefinedRole = randomAsciiAlphanumOfLength(5); - created(() -> client.putJson(rolesApiPath(predefinedRole), roleJson())); - created( - () -> client.putJson( + assertThat(client.putJson(rolesApiPath(predefinedRole), roleJson()), isCreated()); + assertThat( + client.putJson( apiPath(predefinedRole), roleMapping(configJsonArray("a", "b"), configJsonArray(), configJsonArray(), configJsonArray()) - ) + ), + isCreated() ); - badRequest(() -> client.patch(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY)); - badRequest( - () -> client.patch( + assertThat(client.patch(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY), isBadRequest()); + assertThat( + client.patch( apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> builder.startObject().field("users", configJsonArray()).field("users", configJsonArray()).endObject() - ) - ); - assertInvalidKeys( - badRequest(() -> client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(5), unparseableJsonRequest)))), - "unknown_json_property" + ), + isBadRequest() ); - badRequest(() -> client.patch(apiPath(predefinedRole), patch(replaceOp("users", unparseableJsonRequest)))); + response = client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(5), unparseableJsonRequest))); + assertThat(response, isBadRequest()); + assertInvalidKeys(response, "unknown_json_property"); + assertThat(client.patch(apiPath(predefinedRole), patch(replaceOp("users", unparseableJsonRequest))), isBadRequest()); final var randomPropertyForPatch = randomJsonProperty(); - assertWrongDataType( - client.patch( - apiPath(), - patch( - addOp( - randomAsciiAlphanumOfLength(5), - (ToXContentObject) (builder, params) -> builder.startObject() - .field(randomPropertyForPatch) - .value("something") - .endObject() - ) + var resp2 = client.patch( + apiPath(), + patch( + addOp( + randomAsciiAlphanumOfLength(5), + (ToXContentObject) (builder, params) -> builder.startObject() + .field(randomPropertyForPatch) + .value("something") + .endObject() ) - ), - Map.of(randomPropertyForPatch, "Array expected") + ) ); + assertThat(resp2, isBadRequest().withAttribute("/status", "error").withAttribute("/" + randomPropertyForPatch, "Array expected")); // TODO related to issue #4426 - assertWrongDataType( - client.patch(apiPath(predefinedRole), patch(replaceOp("backend_roles", "something"))), - Map.of("backend_roles", "Array expected") - ); - assertWrongDataType(client.patch(apiPath(predefinedRole), patch(addOp("hosts", "something"))), Map.of("hosts", "Array expected")); - assertWrongDataType(client.patch(apiPath(predefinedRole), patch(addOp("users", "something"))), Map.of("users", "Array expected")); - assertWrongDataType( - client.patch(apiPath(predefinedRole), patch(addOp("and_backend_roles", "something"))), - Map.of("and_backend_roles", "Array expected") - ); + var resp3 = client.patch(apiPath(predefinedRole), patch(replaceOp("backend_roles", "something"))); + assertThat(resp3, isBadRequest().withAttribute("/status", "error").withAttribute("/backend_roles", "Array expected")); + var resp4 = client.patch(apiPath(predefinedRole), patch(addOp("hosts", "something"))); + assertThat(resp4, isBadRequest().withAttribute("/status", "error").withAttribute("/hosts", "Array expected")); + var resp5 = client.patch(apiPath(predefinedRole), patch(addOp("users", "something"))); + assertThat(resp5, isBadRequest().withAttribute("/status", "error").withAttribute("/users", "Array expected")); + var resp6 = client.patch(apiPath(predefinedRole), patch(addOp("and_backend_roles", "something"))); + assertThat(resp6, isBadRequest().withAttribute("/status", "error").withAttribute("/and_backend_roles", "Array expected")); assertNullValuesInArray( client.patch( apiPath(), @@ -420,22 +442,23 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { @Override void forbiddenToCreateEntityWithRestAdminPermissions(TestRestClient client) throws Exception { - forbidden(() -> client.putJson(apiPath(REST_ADMIN_ROLE), roleMappingWithUsers(randomArray(false)))); - forbidden(() -> client.patch(apiPath(), patch(addOp(REST_ADMIN_ROLE, roleMappingWithUsers(randomArray(false)))))); + assertThat(client.putJson(apiPath(REST_ADMIN_ROLE), roleMappingWithUsers(randomArray(false))), isForbidden()); + assertThat(client.patch(apiPath(), patch(addOp(REST_ADMIN_ROLE, roleMappingWithUsers(randomArray(false))))), isForbidden()); } @Override void forbiddenToUpdateAndDeleteExistingEntityWithRestAdminPermissions(TestRestClient client) throws Exception { // update - forbidden( - () -> client.putJson( + assertThat( + client.putJson( apiPath(REST_ADMIN_ROLE_WITH_MAPPING), roleMapping(randomArray(false), randomArray(false), randomArray(false), randomArray(false)) - ) + ), + isForbidden() ); - forbidden( - () -> client.patch( + assertThat( + client.patch( apiPath(), patch( replaceOp( @@ -443,13 +466,14 @@ void forbiddenToUpdateAndDeleteExistingEntityWithRestAdminPermissions(TestRestCl roleMapping(randomArray(false), randomArray(false), randomArray(false), randomArray(false)) ) ) - ) + ), + isForbidden() ); - forbidden(() -> client.patch(apiPath(REST_ADMIN_ROLE_WITH_MAPPING), patch(replaceOp("users", randomArray(false))))); + assertThat(client.patch(apiPath(REST_ADMIN_ROLE_WITH_MAPPING), patch(replaceOp("users", randomArray(false)))), isForbidden()); // remove - forbidden(() -> client.patch(apiPath(), patch(removeOp(REST_ADMIN_ROLE_WITH_MAPPING)))); - forbidden(() -> client.patch(apiPath(REST_ADMIN_ROLE_WITH_MAPPING), patch(removeOp("users")))); - forbidden(() -> client.delete(apiPath(REST_ADMIN_ROLE_WITH_MAPPING))); + assertThat(client.patch(apiPath(), patch(removeOp(REST_ADMIN_ROLE_WITH_MAPPING))), isForbidden()); + assertThat(client.patch(apiPath(REST_ADMIN_ROLE_WITH_MAPPING), patch(removeOp("users"))), isForbidden()); + assertThat(client.delete(apiPath(REST_ADMIN_ROLE_WITH_MAPPING)), isForbidden()); } String randomJsonProperty() { diff --git a/src/integrationTest/java/org/opensearch/security/api/RolesRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/RolesRestApiIntegrationTest.java index f52fe5fbfd..a0b8234fc4 100644 --- a/src/integrationTest/java/org/opensearch/security/api/RolesRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/RolesRestApiIntegrationTest.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.ClassRule; +import org.junit.Test; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.common.Strings; @@ -26,6 +28,7 @@ import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.CoreMatchers.is; @@ -35,6 +38,11 @@ import static org.opensearch.security.api.PatchPayloadHelper.patch; import static org.opensearch.security.api.PatchPayloadHelper.removeOp; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; +import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; +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.isNotFound; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class RolesRestApiIntegrationTest extends AbstractConfigEntityApiIntegrationTest { @@ -42,10 +50,12 @@ public class RolesRestApiIntegrationTest extends AbstractConfigEntityApiIntegrat private final static String REST_ADMIN_PERMISSION_ROLE = "rest-admin-permission-role"; - static { - testSecurityConfig.withRestAdminUser(REST_API_ADMIN_ACTION_ROLES_ONLY, restAdminPermission(Endpoint.ROLES)) - .roles(new TestSecurityConfig.Role(REST_ADMIN_PERMISSION_ROLE).clusterPermissions(allRestAdminPermissions())); - } + @ClassRule + public static LocalCluster localCluster = clusterBuilder().users( + new TestSecurityConfig.User(REST_API_ADMIN_ACTION_ROLES_ONLY).roles( + new TestSecurityConfig.Role("rest_admin_role").clusterPermissions(restAdminPermission(Endpoint.ROLES)) + ) + ).roles(new TestSecurityConfig.Role(REST_ADMIN_PERMISSION_ROLE).clusterPermissions(allRestAdminPermissions())).build(); public RolesRestApiIntegrationTest() { super("roles", new TestDescriptor() { @@ -71,27 +81,47 @@ public Optional restAdminLimitedUser() { }); } + @Test + public void forbiddenForRegularUsers() throws Exception { + super.forbiddenForRegularUsers(localCluster); + } + + @Test + public void availableForAdminUser() throws Exception { + super.availableForAdminUser(localCluster); + } + + @Test + public void availableForTLSAdminUser() throws Exception { + super.availableForTLSAdminUser(localCluster); + } + + @Test + public void availableForRESTAdminUser() throws Exception { + super.availableForRESTAdminUser(localCluster); + } + @Override void verifyCrudOperations(final Boolean hidden, final Boolean reserved, final TestRestClient client) throws Exception { final var newRoleJson = Strings.toString( XContentType.JSON, role(hidden, reserved, randomClusterPermissions(false), randomIndexPermissions(false), randomTenantPermissions(false)) ); - created(() -> client.putJson(apiPath("new_role"), newRoleJson)); + assertThat(client.putJson(apiPath("new_role"), newRoleJson), isCreated()); assertRole(ok(() -> client.get(apiPath("new_role"))), "new_role", hidden, reserved, newRoleJson); final var updatedRoleJson = Strings.toString( XContentType.JSON, role(hidden, reserved, randomClusterPermissions(false), randomIndexPermissions(false), randomTenantPermissions(false)) ); - ok(() -> client.putJson(apiPath("new_role"), updatedRoleJson)); + assertThat(client.putJson(apiPath("new_role"), updatedRoleJson), isOk()); assertRole(ok(() -> client.get(apiPath("new_role"))), "new_role", hidden, reserved, updatedRoleJson); - ok(() -> client.delete(apiPath("new_role"))); - notFound(() -> client.get(apiPath("new_role"))); + assertThat(client.delete(apiPath("new_role")), isOk()); + assertThat(client.get(apiPath("new_role")), isNotFound()); final var roleForPatch = role(hidden, reserved, configJsonArray("a", "b"), configJsonArray(), configJsonArray()); - ok(() -> client.patch(apiPath(), patch(addOp("new_role_for_patch", roleForPatch)))); + assertThat(client.patch(apiPath(), patch(addOp("new_role_for_patch", roleForPatch))), isOk()); assertRole( ok(() -> client.get(apiPath("new_role_for_patch"))), "new_role_for_patch", @@ -101,47 +131,43 @@ void verifyCrudOperations(final Boolean hidden, final Boolean reserved, final Te ); // TODO related to issue #4426 - ok( - () -> client.patch(apiPath("new_role_for_patch"), patch(replaceOp("cluster_permissions", configJsonArray("a", "b")))), - "No updates required" - ); - ok( - () -> client.patch(apiPath("new_role_for_patch"), patch(replaceOp("cluster_permissions", configJsonArray("a", "b", "c")))), - "'new_role_for_patch' updated." + assertThat(client.patch(apiPath("new_role_for_patch"), patch(replaceOp("cluster_permissions", configJsonArray("a", "b")))), isOk()); + assertThat( + client.patch(apiPath("new_role_for_patch"), patch(replaceOp("cluster_permissions", configJsonArray("a", "b", "c")))), + isOk() ); - ok(() -> client.patch(apiPath("new_role_for_patch"), patch(addOp("index_permissions", randomIndexPermissions(false))))); - ok(() -> client.patch(apiPath("new_role_for_patch"), patch(addOp("tenant_permissions", randomTenantPermissions(false))))); - - ok(() -> client.patch(apiPath(), patch(removeOp("new_role_for_patch")))); - notFound(() -> client.get(apiPath("new_role_for_patch"))); + assertThat(client.patch(apiPath("new_role_for_patch"), patch(addOp("index_permissions", randomIndexPermissions(false)))), isOk()); + assertThat(client.patch(apiPath("new_role_for_patch"), patch(addOp("tenant_permissions", randomTenantPermissions(false)))), isOk()); + assertThat(client.patch(apiPath(), patch(removeOp("new_role_for_patch"))), isOk()); + assertThat(client.get(apiPath("new_role_for_patch")), isNotFound()); } @Override void verifyBadRequestOperations(TestRestClient client) throws Exception { // put - badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY)); - badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { + assertThat(client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY), isBadRequest()); + assertThat(client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { builder.startObject(); builder.field("cluster_permissions"); randomClusterPermissions(false).toXContent(builder, params); builder.field("cluster_permissions"); randomClusterPermissions(false).toXContent(builder, params); return builder.endObject(); - })); - assertInvalidKeys(badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { + }), isBadRequest()); + assertInvalidKeys(client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { builder.startObject(); builder.field("unknown_json_property"); configJsonArray("a", "b").toXContent(builder, params); builder.field("cluster_permissions"); randomClusterPermissions(false).toXContent(builder, params); return builder.endObject(); - })), "unknown_json_property"); - assertWrongDataType(badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { + }), "unknown_json_property"); + assertWrongDataType(client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { builder.startObject(); builder.field("cluster_permissions").value("a"); builder.field("index_permissions").value("b"); return builder.endObject(); - })), Map.of("cluster_permissions", "Array expected", "index_permissions", "Array expected")); + }), Map.of("cluster_permissions", "Array expected", "index_permissions", "Array expected")); assertNullValuesInArray( client.putJson( apiPath(randomAsciiAlphanumOfLength(5)), @@ -150,18 +176,23 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { ); // patch final var predefinedRoleName = randomAsciiAlphanumOfLength(4); - created(() -> client.putJson(apiPath(predefinedRoleName), role(configJsonArray("a", "b"), configJsonArray(), configJsonArray()))); + assertThat( + client.putJson(apiPath(predefinedRoleName), role(configJsonArray("a", "b"), configJsonArray(), configJsonArray())), + isCreated() + ); - badRequest(() -> client.patch(apiPath(), patch(addOp("some_new_role", EMPTY_BODY)))); - badRequest( - () -> client.patch( + assertThat(client.patch(apiPath(), patch(addOp("some_new_role", EMPTY_BODY))), isBadRequest()); + + assertThat( + client.patch( apiPath(predefinedRoleName), patch(replaceOp(randomFrom(List.of("cluster_permissions", "index_permissions", "tenant_permissions")), EMPTY_BODY)) - ) + ), + isBadRequest() ); - badRequest( - () -> client.patch( + assertThat( + client.patch( apiPath(randomAsciiAlphanumOfLength(5)), patch(addOp(randomAsciiAlphanumOfLength(5), (ToXContentObject) (builder, params) -> { builder.startObject(); @@ -171,29 +202,34 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { randomClusterPermissions(false).toXContent(builder, params); return builder.endObject(); })) - ) + ), + isBadRequest() ); - badRequest(() -> client.patch(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { + + assertThat(client.patch(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { builder.startObject(); builder.field("unknown_json_property"); configJsonArray("a", "b").toXContent(builder, params); builder.field("cluster_permissions"); randomClusterPermissions(false).toXContent(builder, params); return builder.endObject(); - })); - assertWrongDataType( - badRequest(() -> client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(5), (ToXContentObject) (builder, params) -> { - builder.startObject(); - builder.field("cluster_permissions").value("a"); - builder.field("index_permissions").value("b"); - return builder.endObject(); - })))), - Map.of("cluster_permissions", "Array expected", "index_permissions", "Array expected") - ); - assertWrongDataType( - badRequest(() -> client.patch(apiPath(predefinedRoleName), patch(replaceOp("cluster_permissions", "true")))), - Map.of("cluster_permissions", "Array expected") + }), isBadRequest()); + + var response = client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(5), (ToXContentObject) (builder, params) -> { + builder.startObject(); + builder.field("cluster_permissions").value("a"); + builder.field("index_permissions").value("b"); + return builder.endObject(); + }))); + assertThat( + response, + isBadRequest().withAttribute("/status", "error") + .withAttribute("/cluster_permissions", "Array expected") + .withAttribute("/index_permissions", "Array expected") ); + + response = badRequest(() -> client.patch(apiPath(predefinedRoleName), patch(replaceOp("cluster_permissions", "true")))); + assertThat(response, isBadRequest().withAttribute("/status", "error").withAttribute("/cluster_permissions", "Array expected")); assertNullValuesInArray( client.patch( apiPath(), @@ -213,26 +249,25 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { @Override void forbiddenToCreateEntityWithRestAdminPermissions(final TestRestClient client) throws Exception { - forbidden(() -> client.putJson(apiPath("new_rest_admin_role"), roleWithClusterPermissions(randomRestAdminPermission()))); - forbidden( - () -> client.patch( - apiPath(), - patch(addOp("new_rest_admin_action_group", roleWithClusterPermissions(randomRestAdminPermission()))) - ) + assertThat(client.putJson(apiPath("new_rest_admin_role"), roleWithClusterPermissions(randomRestAdminPermission())), isForbidden()); + assertThat( + client.patch(apiPath(), patch(addOp("new_rest_admin_action_group", roleWithClusterPermissions(randomRestAdminPermission())))), + isForbidden() ); } @Override void forbiddenToUpdateAndDeleteExistingEntityWithRestAdminPermissions(final TestRestClient client) throws Exception { // update - forbidden( - () -> client.putJson( + assertThat( + client.putJson( apiPath(REST_ADMIN_PERMISSION_ROLE), role(randomClusterPermissions(false), randomIndexPermissions(false), randomTenantPermissions(false)) - ) + ), + isForbidden() ); - forbidden( - () -> client.patch( + assertThat( + client.patch( apiPath(), patch( replaceOp( @@ -240,18 +275,17 @@ void forbiddenToUpdateAndDeleteExistingEntityWithRestAdminPermissions(final Test role(randomClusterPermissions(false), randomIndexPermissions(false), randomTenantPermissions(false)) ) ) - ) + ), + isForbidden() ); - forbidden( - () -> client.patch( - apiPath(REST_ADMIN_PERMISSION_ROLE), - patch(replaceOp("cluster_permissions", randomClusterPermissions(false))) - ) + assertThat( + client.patch(apiPath(REST_ADMIN_PERMISSION_ROLE), patch(replaceOp("cluster_permissions", randomClusterPermissions(false)))), + isForbidden() ); // remove - forbidden(() -> client.patch(apiPath(), patch(removeOp(REST_ADMIN_PERMISSION_ROLE)))); - forbidden(() -> client.patch(apiPath(REST_ADMIN_PERMISSION_ROLE), patch(removeOp("cluster_permissions")))); - forbidden(() -> client.delete(apiPath(REST_ADMIN_PERMISSION_ROLE))); + assertThat(client.patch(apiPath(), patch(removeOp(REST_ADMIN_PERMISSION_ROLE))), isForbidden()); + assertThat(client.patch(apiPath(REST_ADMIN_PERMISSION_ROLE), patch(removeOp("cluster_permissions"))), isForbidden()); + assertThat(client.delete(apiPath(REST_ADMIN_PERMISSION_ROLE)), isForbidden()); } void assertRole( diff --git a/src/integrationTest/java/org/opensearch/security/api/RollbackVersionApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/RollbackVersionApiIntegrationTest.java index 330d6baf72..82fda7c18f 100644 --- a/src/integrationTest/java/org/opensearch/security/api/RollbackVersionApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/RollbackVersionApiIntegrationTest.java @@ -11,22 +11,26 @@ package org.opensearch.security.api; -import java.util.Map; - -import org.apache.http.HttpStatus; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isOneOf; import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; import static org.opensearch.security.support.ConfigConstants.EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED; +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.isNotFound; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; +import static org.opensearch.test.framework.matcher.RestMatchers.isUnauthorized; public class RollbackVersionApiIntegrationTest extends AbstractApiIntegrationTest { @@ -38,61 +42,57 @@ private String RollbackVersion(String versionId) { return ROLLBACK_BASE + "/" + versionId; } - @Override - protected Map getClusterSettings() { - Map settings = super.getClusterSettings(); - settings.put(EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED, true); - return settings; - } + @Rule + public LocalCluster localCluster = clusterBuilder().nodeSetting(EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED, true).build(); @Before public void setupConfigVersionsIndex() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { - client.createUser(USER.getName(), USER).assertStatusCode(201); + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + assertThat(client.createUser(USER.getName(), USER), anyOf(isOk(), isCreated())); } } @Test public void testRollbackToPreviousVersion_success() throws Exception { - withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> { + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { var response = client.post(ROLLBACK_BASE); - assertThat(response.getStatusCode(), is(HttpStatus.SC_OK)); + assertThat(response, isOk()); assertThat(response.getTextFromJsonBody("/status"), equalTo("OK")); assertThat(response.getTextFromJsonBody("/message"), containsString("config rolled back to version")); - }); + } } @Test public void testRollbackToSpecificVersion_success() throws Exception { String versionId = "v1"; - withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> { + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { var response = client.post(RollbackVersion(versionId)); - assertThat(response.getStatusCode(), is(HttpStatus.SC_OK)); + assertThat(response, isOk()); assertThat(response.getTextFromJsonBody("/status"), equalTo("OK")); assertThat(response.getTextFromJsonBody("/message"), containsString("config rolled back to version " + versionId)); - }); + } } @Test public void testRollbackWithNonAdmin_shouldBeUnauthorized() throws Exception { - withUser(NEW_USER, DEFAULT_PASSWORD, client -> { + try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { var response = client.post(ROLLBACK_BASE); - assertThat(response.getStatusCode(), isOneOf(HttpStatus.SC_FORBIDDEN, HttpStatus.SC_UNAUTHORIZED)); - }); + assertThat(response, anyOf(isForbidden(), isUnauthorized())); + } } @Test public void testRollbackToInvalidVersion_shouldReturnNotFound() throws Exception { - withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> { + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { var response = client.post(RollbackVersion("does-not-exist")); - assertThat(response.getStatusCode(), is(HttpStatus.SC_NOT_FOUND)); + assertThat(response, isNotFound()); assertThat(response.getTextFromJsonBody("/message"), containsString("not found")); - }); + } } @Test public void testRollbackWhenOnlyOneVersion_shouldFail() throws Exception { - withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> { + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { // To perform below test, delete all entries in .opensearch_security_config_versions index String deleteQuery = """ { @@ -116,7 +116,7 @@ public void testRollbackWhenOnlyOneVersion_shouldFail() throws Exception { var response = client.post(ROLLBACK_BASE); assertThat(response.getStatusCode(), is(404)); assertThat(response.getBody(), containsString("No previous version available to rollback")); - }); + } } } diff --git a/src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java index bbdd9ff793..8c911d3161 100644 --- a/src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java @@ -10,34 +10,35 @@ */ package org.opensearch.security.api; -import java.util.Map; - import com.fasterxml.jackson.databind.JsonNode; +import org.junit.ClassRule; import org.junit.Test; import org.opensearch.security.dlic.rest.api.Endpoint; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; +import static org.opensearch.test.framework.matcher.RestMatchers.isForbidden; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; @Deprecated public class SslCertsRestApiIntegrationTest extends AbstractApiIntegrationTest { final static String REST_API_ADMIN_SSL_INFO = "rest-api-admin-ssl-info"; - static { - testSecurityConfig.withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions()) - .withRestAdminUser(REST_API_ADMIN_SSL_INFO, restAdminPermission(Endpoint.SSL, CERTS_INFO_ACTION)); - } - - @Override - protected Map getClusterSettings() { - Map clusterSettings = super.getClusterSettings(); - clusterSettings.put(SECURITY_RESTAPI_ADMIN_ENABLED, true); - return clusterSettings; - } + @ClassRule + public static LocalCluster localCluster = clusterBuilder().nodeSetting(SECURITY_RESTAPI_ADMIN_ENABLED, true) + .users( + new TestSecurityConfig.User(REST_API_ADMIN_SSL_INFO).roles( + REST_ADMIN_REST_API_ACCESS_ROLE, + new TestSecurityConfig.Role("rest_admin_role").clusterPermissions(restAdminPermission(Endpoint.SSL, CERTS_INFO_ACTION)) + ) + ) + .build(); protected String sslCertsPath() { return super.apiPath("ssl", "certs"); @@ -45,27 +46,38 @@ protected String sslCertsPath() { @Test public void certsInfoForbiddenForRegularUser() throws Exception { - withUser(NEW_USER, client -> forbidden(() -> client.get(sslCertsPath()))); + try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { + assertThat(client.get(sslCertsPath()), isForbidden()); + } } @Test public void certsInfoForbiddenForAdminUser() throws Exception { - withUser(NEW_USER, client -> forbidden(() -> client.get(sslCertsPath()))); + try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { + assertThat(client.get(sslCertsPath()), isForbidden()); + } } @Test public void certsInfoAvailableForTlsAdmin() throws Exception { - withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifySSLCertsInfo); + try (TestRestClient client = localCluster.getAdminCertRestClient()) { + verifySSLCertsInfo(client); + } } @Test public void certsInfoAvailableForRestAdmin() throws Exception { - withUser(REST_ADMIN_USER, this::verifySSLCertsInfo); - withUser(REST_API_ADMIN_SSL_INFO, this::verifySSLCertsInfo); + try (TestRestClient client = localCluster.getRestClient(REST_ADMIN_USER)) { + verifySSLCertsInfo(client); + } + try (TestRestClient client = localCluster.getRestClient(REST_API_ADMIN_SSL_INFO, DEFAULT_PASSWORD)) { + verifySSLCertsInfo(client); + } } private void verifySSLCertsInfo(final TestRestClient client) throws Exception { - final var response = ok(() -> client.get(sslCertsPath())); + final var response = client.get(sslCertsPath()); + assertThat(response, isOk()); final var body = response.bodyAsJsonNode(); assertThat(response.getBody(), body.has("http_certificates_list")); diff --git a/src/integrationTest/java/org/opensearch/security/api/TenantsRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/TenantsRestApiIntegrationTest.java index cb3431be79..7dbf74781d 100644 --- a/src/integrationTest/java/org/opensearch/security/api/TenantsRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/TenantsRestApiIntegrationTest.java @@ -14,9 +14,13 @@ import java.util.Optional; import com.fasterxml.jackson.databind.JsonNode; +import org.junit.ClassRule; +import org.junit.Test; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.security.dlic.rest.api.Endpoint; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.CoreMatchers.is; @@ -25,14 +29,22 @@ import static org.opensearch.security.api.PatchPayloadHelper.patch; import static org.opensearch.security.api.PatchPayloadHelper.removeOp; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; +import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; +import static org.opensearch.test.framework.matcher.RestMatchers.isCreated; +import static org.opensearch.test.framework.matcher.RestMatchers.isNotFound; +import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class TenantsRestApiIntegrationTest extends AbstractConfigEntityApiIntegrationTest { private final static String REST_API_ADMIN_TENANTS_ONLY = "rest_api_admin_tenants_only"; - static { - testSecurityConfig.withRestAdminUser(REST_API_ADMIN_TENANTS_ONLY, restAdminPermission(Endpoint.TENANTS)); - } + @ClassRule + public static LocalCluster localCluster = clusterBuilder().users( + new TestSecurityConfig.User(REST_API_ADMIN_TENANTS_ONLY).roles( + REST_ADMIN_REST_API_ACCESS_ROLE, + new TestSecurityConfig.Role("rest_admin_role").clusterPermissions(restAdminPermission(Endpoint.TENANTS)) + ) + ).build(); public TenantsRestApiIntegrationTest() { super("tenants", new TestDescriptor() { @@ -83,15 +95,36 @@ static ToXContentObject tenant(final Boolean hidden, final Boolean reserved, fin }; } + @Test + public void forbiddenForRegularUsers() throws Exception { + super.forbiddenForRegularUsers(localCluster); + } + + @Test + public void availableForAdminUser() throws Exception { + super.availableForAdminUser(localCluster); + } + + @Test + public void availableForTLSAdminUser() throws Exception { + super.availableForTLSAdminUser(localCluster); + } + + @Test + public void availableForRESTAdminUser() throws Exception { + super.availableForRESTAdminUser(localCluster); + } + @Override void verifyBadRequestOperations(TestRestClient client) throws Exception { // put - badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(4)), EMPTY_BODY)); - badRequest( - () -> client.putJson( + assertThat(client.putJson(apiPath(randomAsciiAlphanumOfLength(4)), EMPTY_BODY), isBadRequest()); + assertThat( + client.putJson( apiPath(randomAsciiAlphanumOfLength(4)), (builder, params) -> builder.startObject().field("description", "a").field("description", "b").endObject() - ) + ), + isBadRequest() ); assertInvalidKeys( client.putJson( @@ -101,9 +134,9 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { "a,c" ); // patch - badRequest(() -> client.patch(apiPath(), EMPTY_BODY)); - badRequest( - () -> client.patch( + assertThat(client.patch(apiPath(), EMPTY_BODY), isBadRequest()); + assertThat( + client.patch( apiPath(), patch( addOp( @@ -114,7 +147,8 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { .endObject() ) ) - ) + ), + isBadRequest() ); assertInvalidKeys( client.patch( @@ -140,23 +174,23 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien // put final var putDescription = randomAsciiAlphanumOfLength(10); final var putTenantName = randomAsciiAlphanumOfLength(4); - created(() -> client.putJson(apiPath(putTenantName), tenant(hidden, reserved, putDescription))); - assertTenant(ok(() -> client.get(apiPath(putTenantName))).bodyAsJsonNode().get(putTenantName), hidden, reserved, putDescription); + assertThat(client.putJson(apiPath(putTenantName), tenant(hidden, reserved, putDescription)), isCreated()); + assertTenant(client.get(apiPath(putTenantName)).bodyAsJsonNode().get(putTenantName), hidden, reserved, putDescription); final var putUpdatedDescription = randomAsciiAlphanumOfLength(10); - ok(() -> client.putJson(apiPath(putTenantName), tenant(hidden, reserved, putUpdatedDescription))); + assertThat(client.putJson(apiPath(putTenantName), tenant(hidden, reserved, putUpdatedDescription)), isOk()); assertTenant( ok(() -> client.get(apiPath(putTenantName))).bodyAsJsonNode().get(putTenantName), hidden, reserved, putUpdatedDescription ); - ok(() -> client.delete(apiPath(putTenantName))); - notFound(() -> client.get(apiPath(putTenantName))); + assertThat(client.delete(apiPath(putTenantName)), isOk()); + assertThat(client.get(apiPath(putTenantName)), isNotFound()); // patch final var patchTenantName = randomAsciiAlphanumOfLength(4); final var patchDescription = randomAsciiAlphanumOfLength(10); - ok(() -> client.patch(apiPath(), patch(addOp(patchTenantName, tenant(hidden, reserved, patchDescription))))); + assertThat(client.patch(apiPath(), patch(addOp(patchTenantName, tenant(hidden, reserved, patchDescription)))), isOk()); assertTenant( ok(() -> client.get(apiPath(patchTenantName))).bodyAsJsonNode().get(patchTenantName), hidden, @@ -165,7 +199,7 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien ); final var patchUpdatedDescription = randomAsciiAlphanumOfLength(10); - ok(() -> client.patch(apiPath(patchTenantName), patch(replaceOp("description", patchUpdatedDescription)))); + assertThat(client.patch(apiPath(patchTenantName), patch(replaceOp("description", patchUpdatedDescription))), isOk()); assertTenant( ok(() -> client.get(apiPath(patchTenantName))).bodyAsJsonNode().get(patchTenantName), hidden, @@ -173,8 +207,8 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien patchUpdatedDescription ); - ok(() -> client.patch(apiPath(), patch(removeOp(patchTenantName)))); - notFound(() -> client.get(apiPath(patchTenantName))); + assertThat(client.patch(apiPath(), patch(removeOp(patchTenantName))), isOk()); + assertThat(client.get(apiPath(patchTenantName)), isNotFound()); } void assertTenant(final JsonNode actualJson, final Boolean hidden, final Boolean reserved, final String expectedDescription) { diff --git a/src/integrationTest/java/org/opensearch/security/api/ViewVersionApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/ViewVersionApiIntegrationTest.java index 67a7627549..13274cb20c 100644 --- a/src/integrationTest/java/org/opensearch/security/api/ViewVersionApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/ViewVersionApiIntegrationTest.java @@ -11,28 +11,33 @@ package org.opensearch.security.api; -import java.util.Map; - import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isOneOf; import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; import static org.opensearch.security.support.ConfigConstants.EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED; +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.opensearch.test.framework.matcher.RestMatchers.isUnauthorized; public class ViewVersionApiIntegrationTest extends AbstractApiIntegrationTest { - static { - testSecurityConfig.user(new TestSecurityConfig.User("limitedUser").password("limitedPass")); - } + @Rule + public LocalCluster localCluster = clusterBuilder().users(new TestSecurityConfig.User("limitedUser").password("limitedPass")) + .nodeSetting(EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED, true) + .build(); private static final TestSecurityConfig.User USER = new TestSecurityConfig.User("user"); @@ -48,37 +53,32 @@ private String viewVersion(String versionId) { return endpointPrefix() + "/version/" + versionId; } - @Override - protected Map getClusterSettings() { - Map settings = super.getClusterSettings(); - settings.put(EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED, true); - return settings; - } - @Before public void setupIndexAndCerts() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { client.createUser(USER.getName(), USER).assertStatusCode(201); } } @Test public void testViewAllVersions() throws Exception { - withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> { - var response = ok(() -> client.get(viewVersionBase())); + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + var response = client.get(viewVersionBase()); + assertThat(response, isOk()); var json = response.bodyAsJsonNode(); assertThat(json.has("versions"), is(true)); var versions = json.get("versions"); assertThat(versions.isArray(), is(true)); assertThat(versions.size(), greaterThan(0)); - }); + } } @Test public void testViewSpecificVersionFound() throws Exception { - withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> { - var response = ok(() -> client.get(viewVersion("v1"))); + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + var response = client.get(viewVersion("v1")); + assertThat(response, isOk()); var json = response.bodyAsJsonNode(); assertThat(json.has("versions"), is(true)); @@ -88,13 +88,14 @@ public void testViewSpecificVersionFound() throws Exception { var ver = versions.get(0); assertThat(ver.get("version_id").asText(), equalTo("v1")); - }); + } } @Test public void testViewSpecificVersionNotFound() throws Exception { - withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> { - var response = notFound(() -> client.get(viewVersion("does-not-exist"))); + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + var response = client.get(viewVersion("does-not-exist")); + assertThat(response, isNotFound()); var json = response.bodyAsJsonNode(); assertThat(json.has("status"), is(true)); @@ -102,14 +103,14 @@ public void testViewSpecificVersionNotFound() throws Exception { assertThat(json.has("message"), is(true)); assertThat(json.get("message").asText(), containsString("not found")); - }); + } } @Test public void testViewAllVersions_forbiddenWithoutAdminCert() throws Exception { - withUser("limitedUser", "limitedPass", client -> { + try (TestRestClient client = localCluster.getRestClient("limitedUser", "limitedPass")) { var response = client.get(viewVersionBase()); - assertThat(response.getStatusCode(), isOneOf(401, 403)); - }); + assertThat(response, anyOf(isUnauthorized(), isForbidden())); + } } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 23527744cf..8ea82be44d 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -74,7 +74,6 @@ import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.cluster.OpenSearchClientProvider.UserCredentialsHolder; import org.opensearch.test.framework.data.TestIndex; -import org.opensearch.test.framework.matcher.RestIndexMatchers; import org.opensearch.transport.client.Client; import static org.apache.http.HttpHeaders.AUTHORIZATION; @@ -93,8 +92,6 @@ */ public class TestSecurityConfig { - public static final String REST_ADMIN_REST_API_ACCESS = "rest_admin__rest_api_access"; - private static final Logger log = LogManager.getLogger(TestSecurityConfig.class); private static final PasswordHasher passwordHasher = PasswordHasherFactory.createPasswordHasher( @@ -180,18 +177,6 @@ public TestSecurityConfig users(User... users) { return this; } - public TestSecurityConfig withRestAdminUser(final String name, final String... permissions) { - if (!internalUsers.containsKey(name)) { - user(new User(name).description("REST Admin with permissions: " + Arrays.toString(permissions)).reserved(true)); - final var roleName = name + "__rest_admin_role"; - roles(new Role(roleName).clusterPermissions(permissions)); - - rolesMapping.computeIfAbsent(roleName, RoleMapping::new).users(name); - rolesMapping.computeIfAbsent(REST_ADMIN_REST_API_ACCESS, RoleMapping::new).users(name); - } - return this; - } - public List getUsers() { return new ArrayList<>(internalUsers.values()); } @@ -476,7 +461,6 @@ public static final class User implements UserCredentialsHolder, ToXContentObjec String requestedTenant; private Map attributes = new HashMap<>(); private Map, Object> matchers = new HashMap<>(); - private Map indexMatchers = new HashMap<>(); private boolean adminCertUser = false; private Boolean hidden = null; @@ -550,6 +534,9 @@ public String getPassword() { } public Set getRoleNames() { + if (roleNames == null) { + this.aggregateRoles(); + } return roleNames; } @@ -653,6 +640,25 @@ public MetadataKey(String name, Class type) { this.type = type; } } + + void aggregateRoles() { + if (this.roleNames == null) { + this.roleNames = new HashSet<>(); + } + + for (Role role : this.roles) { + if (role.addedIndependentlyOfUser) { + // This is a globally defined role, we just use this + this.roleNames.add(role.name); + } else { + // This is role that is locally defined for the user; let's scope the name + if (!role.name.startsWith("user_" + this.name)) { + role.name = "user_" + this.name + "__" + role.name; + } + this.roleNames.add(role.name); + } + } + } } public static class Role implements ToXContentObject { @@ -1135,20 +1141,10 @@ private Map aggregateRoles() { Map result = new HashMap<>(this.roles); for (User user : this.internalUsers.values()) { - if (user.roleNames == null) { - user.roleNames = new HashSet<>(); - } + user.aggregateRoles(); for (Role role : user.roles) { - if (role.addedIndependentlyOfUser) { - // This is a globally defined role, we just use this - user.roleNames.add(role.name); - } else { - // This is role that is locally defined for the user; let's scope the name - if (!role.name.startsWith("user_" + user.name)) { - role.name = "user_" + user.name + "__" + role.name; - } - user.roleNames.add(role.name); + if (!role.addedIndependentlyOfUser) { result.put(role.name, role); } } 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 20d27016fd..1e1c611604 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -450,6 +450,11 @@ public Builder nodeSettings(Map settings) { return this; } + public Builder nodeSetting(String key, Object value) { + nodeOverrideSettingsBuilder.put(key, String.valueOf(value)); + return this; + } + public Builder nodeSpecificSettings(int nodeNumber, Map settings) { if (!nodeSpecificOverrideSettingsBuilder.containsKey(nodeNumber)) { Settings.Builder builderCopy = Settings.builder(); diff --git a/src/integrationTest/java/org/opensearch/test/framework/matcher/RestMatchers.java b/src/integrationTest/java/org/opensearch/test/framework/matcher/RestMatchers.java index 96faab57c1..20dc5b59d5 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/matcher/RestMatchers.java +++ b/src/integrationTest/java/org/opensearch/test/framework/matcher/RestMatchers.java @@ -66,6 +66,14 @@ public static OpenSearchErrorHttpResponseMatcher isNotFound() { return new OpenSearchErrorHttpResponseMatcher(404, "Not Found"); } + public static OpenSearchErrorHttpResponseMatcher isNotAllowed() { + return new OpenSearchErrorHttpResponseMatcher(405, "Not Allowed"); + } + + public static OpenSearchErrorHttpResponseMatcher isUnauthorized() { + return new OpenSearchErrorHttpResponseMatcher(401, "Unauthorized"); + } + public static class HttpResponseMatcher extends DiagnosingMatcher { final int statusCode; final String statusName; From 196b373ef85046fabdf97535101a1b55ef7f1b13 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Mon, 27 Oct 2025 10:58:59 +0100 Subject: [PATCH 20/29] Test fixes Signed-off-by: Nils Bandener --- .../RoleBasedActionPrivilegesTest.java | 47 +++++++------------ .../SubjectBasedActionPrivilegesTest.java | 7 +-- .../resources/log4j2-test.properties | 5 ++ .../multitenancy/test/MultitenancyTests.java | 11 +++-- .../SecuritySettingsConfigurerTests.java | 2 - 5 files changed, 28 insertions(+), 44 deletions(-) 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 7f9f19fe47..b1e586c68e 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java @@ -589,12 +589,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" - ) - ); + assertThat(result, isPartiallyOk("data_stream_a11")); } else { assertThat(result, isForbidden(missingPrivileges(requiredActions))); } @@ -604,27 +599,17 @@ public void positive_partial() throws Exception { 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") + 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" - ) - ); + assertThat(result, isPartiallyOk("data_stream_a11")); } else if (covers(ctx, ".ds-data_stream_a11")) { - assertThat( - result, - isPartiallyOk( - "data_stream_a11" - ) - ); + assertThat(result, isPartiallyOk("data_stream_a11")); } else { assertThat(result, isForbidden(missingPrivileges(requiredActions))); } @@ -713,19 +698,19 @@ private RoleBasedActionPrivileges subject(boolean breakDownAliases) { Settings settings = Settings.EMPTY; if (statefulness == Statefulness.STATEFUL_LIMITED) { settings = Settings.builder() - .put( - RoleBasedActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey(), - new ByteSizeValue(10, ByteSizeUnit.BYTES) - ) - .build(); + .put( + RoleBasedActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey(), + new ByteSizeValue(10, ByteSizeUnit.BYTES) + ) + .build(); } RoleBasedActionPrivileges result = new RoleBasedActionPrivileges( - roles, - FlattenedActionGroups.EMPTY, - RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, - settings, - breakDownAliases + roles, + FlattenedActionGroups.EMPTY, + RuntimeOptimizedActionPrivileges.SpecialIndexProtection.NONE, + settings, + breakDownAliases ); if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { 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 c5362e45d6..e345ffa2c8 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java @@ -421,12 +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" - ) - ); + assertThat(result, isPartiallyOk("data_stream_a11")); } else { assertThat(result, isForbidden(missingPrivileges(requiredActions))); } diff --git a/src/integrationTest/resources/log4j2-test.properties b/src/integrationTest/resources/log4j2-test.properties index 9609611808..21eb271446 100644 --- a/src/integrationTest/resources/log4j2-test.properties +++ b/src/integrationTest/resources/log4j2-test.properties @@ -55,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/test/java/org/opensearch/security/multitenancy/test/MultitenancyTests.java b/src/test/java/org/opensearch/security/multitenancy/test/MultitenancyTests.java index f966ec5099..c49f771856 100644 --- a/src/test/java/org/opensearch/security/multitenancy/test/MultitenancyTests.java +++ b/src/test/java/org/opensearch/security/multitenancy/test/MultitenancyTests.java @@ -406,7 +406,8 @@ public void testMtMulti() throws Exception { Assert.assertTrue(res.getBody().contains(dashboardsIndex)); // get - assertThat(res.getBody(), + assertThat( + res.getBody(), HttpStatus.SC_OK, is( (res = rh.executeGetRequest( @@ -416,10 +417,10 @@ public void testMtMulti() throws Exception { )).getStatusCode() ) ); - 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)); + 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\"}]}"; 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..5105202c82 100644 --- a/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java +++ b/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java @@ -28,12 +28,10 @@ import java.util.List; import java.util.Map; -import com.carrotsearch.randomizedtesting.RandomizedRunner; import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.tools.Hasher; From 8b474fb96e4335e02b0761eda420111ec5faac63 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 28 Oct 2025 13:44:05 +0100 Subject: [PATCH 21/29] Test fixes Signed-off-by: Nils Bandener --- .../RoleBasedActionPrivilegesTest.java | 5 +- .../test/framework/TestSecurityConfig.java | 88 ++++++++++--------- .../RuntimeOptimizedActionPrivileges.java | 2 +- 3 files changed, 48 insertions(+), 47 deletions(-) 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 b1e586c68e..280f7f15de 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java @@ -931,7 +931,7 @@ public void hasIndexPrivilege_errors() throws Exception { assertTrue( "Result mentions role_with_errors: " + result.getEvaluationExceptionInfo(), result.getEvaluationExceptionInfo() - .contains("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating") + .contains("Error while evaluating dynamic index pattern: /invalid_regex_with_attr${user.name}\\/") ); } @@ -1063,8 +1063,7 @@ public void hasExplicitIndexPrivilege_errors() throws Exception { assertTrue(result.hasEvaluationExceptions()); assertTrue( "Result mentions role_with_errors: " + result.getEvaluationExceptionInfo(), - result.getEvaluationExceptionInfo() - .contains("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating role role_with_errors") + result.getEvaluationExceptionInfo().contains("Error while evaluating role role_with_errors") ); } diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 8ea82be44d..7a10360cd7 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -35,7 +35,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -45,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; @@ -166,7 +166,9 @@ public TestSecurityConfig authz(AuthzDomain authzDomain) { public TestSecurityConfig user(User user) { this.internalUsers.put(user.name, user); - // The user's roles will be collected by aggregateRoles() when the configuration is written + for (Role role : user.roles) { + this.roles.put(role.name, role); + } return this; } @@ -453,11 +455,8 @@ 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<>(); - /** - * This will be initialized by aggregateRoles() - */ - Set roleNames; String requestedTenant; private Map attributes = new HashMap<>(); private Map, Object> matchers = new HashMap<>(); @@ -486,8 +485,31 @@ 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) { - this.roles.addAll(Arrays.asList(roles)); + // We scope the role names by user to keep tests free of potential side effects + String roleNamePrefix = "user_" + this.getName() + "__"; + + for (Role role : roles) { + Role copy = role.clone(); + if (!copy.name.startsWith(roleNamePrefix)) { + copy.name = roleNamePrefix + role.name; + } + this.roles.add(copy); + } + + 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; } @@ -534,10 +556,7 @@ public String getPassword() { } public Set getRoleNames() { - if (roleNames == null) { - this.aggregateRoles(); - } - return roleNames; + return Streams.concat(roles.stream(), referencedRoles.stream()).map(Role::getName).collect(Collectors.toSet()); } public String getDescription() { @@ -640,25 +659,6 @@ public MetadataKey(String name, Class type) { this.type = type; } } - - void aggregateRoles() { - if (this.roleNames == null) { - this.roleNames = new HashSet<>(); - } - - for (Role role : this.roles) { - if (role.addedIndependentlyOfUser) { - // This is a globally defined role, we just use this - this.roleNames.add(role.name); - } else { - // This is role that is locally defined for the user; let's scope the name - if (!role.name.startsWith("user_" + this.name)) { - role.name = "user_" + this.name + "__" + role.name; - } - this.roleNames.add(role.name); - } - } - } } public static class Role implements ToXContentObject { @@ -1115,11 +1115,13 @@ 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); } - writeConfigToIndex(client, CType.ROLES, aggregateRoles()); + writeConfigToIndex(client, CType.ROLES, roles); writeConfigToIndex(client, CType.INTERNALUSERS, internalUsers); writeConfigToIndex(client, CType.ROLESMAPPING, rolesMapping); writeConfigToIndex(client, CType.ACTIONGROUPS, actionGroups); @@ -1134,23 +1136,23 @@ public void initIndex(Client client) { } /** - * Merges the globally defined roles with the roles defined by user. Roles defined by user will be scoped - * so that user definitions cannot interfere with others. + * Does a sanity check on the user's referenced roles; these must actually match the globally defined roles. */ - private Map aggregateRoles() { - Map result = new HashMap<>(this.roles); - + private void checkReferencedRoles() { for (User user : this.internalUsers.values()) { - user.aggregateRoles(); - - for (Role role : user.roles) { - if (!role.addedIndependentlyOfUser) { - result.put(role.name, role); + 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) + ); } } } - - return result; } public void updateInternalUsersConfiguration(Client client, List users) { 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 80ffc66693..ac4ab665eb 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java @@ -452,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)); } } } From 700befa20805e642159991a7e57bd24cd75e198c Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 28 Oct 2025 16:11:15 +0100 Subject: [PATCH 22/29] Support for alias based DLS/FLS evaluation Signed-off-by: Nils Bandener --- .../dlsfls/AbstractRuleBasedPrivileges.java | 91 ++++++++++++++++--- .../security/filter/SecurityFilterTests.java | 1 - 2 files changed, 79 insertions(+), 13 deletions(-) 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 0ab44e4b55..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,6 +25,7 @@ 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; @@ -230,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); @@ -246,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; - } /** @@ -283,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; } } @@ -294,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. diff --git a/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java b/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java index 6a8f662e05..431b7316e7 100644 --- a/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java +++ b/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java @@ -34,7 +34,6 @@ import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.ResourceAccessEvaluator; import org.opensearch.security.privileges.RoleMapper; -import org.opensearch.security.privileges.ResourceAccessEvaluator; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.threadpool.ThreadPool; From 59322bc9f0d6895d0eb6973069fd4154b0648496 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 28 Oct 2025 16:17:43 +0100 Subject: [PATCH 23/29] Fixed OBO Signed-off-by: Nils Bandener --- .../security/OpenSearchSecurityPlugin.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 8c57dd66bb..63a587b786 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -1167,16 +1167,16 @@ public Collection createComponents( backendRegistry = new BackendRegistry(settings, adminDns, xffResolver, auditLog, threadPool, cih); backendRegistry.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); cr.subscribeOnChange(configMap -> { backendRegistry.invalidateCache(); }); + 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); - RoleMapper roleMapper = new RolesInjector.InjectedRoleMapper( - new ConfigurableRoleMapper(cr, settings), - threadPool.getThreadContext() - ); - this.roleMapper = roleMapper; + PrivilegesConfiguration privilegesConfiguration = new PrivilegesConfiguration( cr, clusterService, From 34ad1368302c0ee5fd724afb27272823614979ee Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 28 Oct 2025 23:30:32 +0100 Subject: [PATCH 24/29] Int tests for MT Signed-off-by: Nils Bandener --- .../DashboardMultiTenancyIntTests.java | 714 ++++++++++++++++++ .../test/framework/TestSecurityConfig.java | 116 ++- .../test/framework/cluster/LocalCluster.java | 5 + 3 files changed, 820 insertions(+), 15 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/privileges/int_tests/DashboardMultiTenancyIntTests.java 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..87c671d92f --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DashboardMultiTenancyIntTests.java @@ -0,0 +1,714 @@ +/* + * 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/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 7a10360cd7..d320c0fc43 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -59,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 +77,7 @@ import org.opensearch.test.framework.data.TestIndex; 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; @@ -103,8 +105,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. @@ -167,7 +169,9 @@ 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; } @@ -188,7 +192,6 @@ public TestSecurityConfig roles(Role... roles) { if (this.roles.containsKey(role.name)) { throw new IllegalStateException("Role with name " + role.name + " is already defined"); } - role.addedIndependentlyOfUser = true; this.roles.put(role.name, role); } @@ -230,6 +233,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 @@ -494,11 +508,16 @@ public User roles(Role... roles) { String roleNamePrefix = "user_" + this.getName() + "__"; for (Role role : roles) { - Role copy = role.clone(); - if (!copy.name.startsWith(roleNamePrefix)) { - copy.name = roleNamePrefix + role.name; + 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); } - this.roles.add(copy); } return this; @@ -663,23 +682,22 @@ 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; /** - * This will be set to true, if this was added using the roles() method on TestSecurityConfig. - * Then, we will consider this a role which is shared between users and we won't scope its name. + * 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 addedIndependentlyOfUser = false; + private boolean isPredefined = false; public Role(String name) { this(name, null); @@ -699,6 +717,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; @@ -718,10 +740,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; } @@ -732,10 +764,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); } @@ -943,6 +977,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 = @@ -1125,7 +1211,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 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 1e1c611604..dcf7ab322a 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -608,6 +608,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; From ef4ef8f7a8f294beb48c75b35735c978186d5d97 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 28 Oct 2025 23:30:40 +0100 Subject: [PATCH 25/29] formatting Signed-off-by: Nils Bandener --- .../org/opensearch/security/OpenSearchSecurityPlugin.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 63a587b786..694d5aff0c 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -1168,8 +1168,8 @@ public Collection createComponents( backendRegistry.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); cr.subscribeOnChange(configMap -> { backendRegistry.invalidateCache(); }); RoleMapper roleMapper = new RolesInjector.InjectedRoleMapper( - new ConfigurableRoleMapper(cr, settings), - threadPool.getThreadContext() + new ConfigurableRoleMapper(cr, settings), + threadPool.getThreadContext() ); this.roleMapper = roleMapper; tokenManager = new SecurityTokenManager(cs, threadPool, userService, roleMapper); From 4ab1082650422f50736aa2b9895c6e4c248a14d1 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Wed, 29 Oct 2025 11:03:59 +0100 Subject: [PATCH 26/29] Revert "API integration test refactoring" This reverts commit 4742abd6 --- .../security/ConfigurationFiles.java | 11 + .../api/AbstractApiIntegrationTest.java | 244 ++++++++-- ...bstractConfigEntityApiIntegrationTest.java | 164 +++---- .../api/AccountRestApiIntegrationTest.java | 151 +++---- .../ActionGroupsRestApiIntegrationTest.java | 240 ++++------ .../CertificatesRestApiIntegrationTest.java | 100 ++--- .../api/ConfigRestApiIntegrationTest.java | 110 ++--- .../security/api/DashboardsInfoTest.java | 24 +- .../api/DashboardsInfoWithSettingsTest.java | 36 +- ...DefaultApiAvailabilityIntegrationTest.java | 87 ++-- .../api/FlushCacheApiIntegrationTest.java | 36 +- ...xpPasswordRulesRestApiIntegrationTest.java | 89 ++-- .../InternalUsersRestApiIntegrationTest.java | 419 ++++++++---------- ...edPasswordRulesRestApiIntegrationTest.java | 69 ++- .../RolesMappingRestApiIntegrationTest.java | 266 +++++------ .../api/RolesRestApiIntegrationTest.java | 174 +++----- .../RollbackVersionApiIntegrationTest.java | 52 +-- .../api/SslCertsRestApiIntegrationTest.java | 50 +-- .../api/TenantsRestApiIntegrationTest.java | 74 +--- .../api/ViewVersionApiIntegrationTest.java | 53 ++- .../test/framework/TestSecurityConfig.java | 16 + .../test/framework/cluster/LocalCluster.java | 5 - .../test/framework/matcher/RestMatchers.java | 8 - 23 files changed, 1189 insertions(+), 1289 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java b/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java index 4b0ef62b49..f871e131b9 100644 --- a/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java +++ b/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java @@ -11,10 +11,13 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.Objects; +import org.opensearch.core.common.Strings; import org.opensearch.security.securityconf.impl.CType; public class ConfigurationFiles { @@ -40,6 +43,14 @@ public static Path createConfigurationDirectory() { } } + public static void writeToConfig(final CType cType, final Path configFolder, final String content) throws IOException { + if (Strings.isNullOrEmpty(content)) return; + try (final var out = Files.newOutputStream(cType.configFile(configFolder), StandardOpenOption.APPEND)) { + out.write(content.getBytes(StandardCharsets.UTF_8)); + out.flush(); + } + } + public static void copyResourceToFile(String resource, Path destination) { try (InputStream input = ConfigurationFiles.class.getClassLoader().getResourceAsStream(resource)) { Objects.requireNonNull(input, "Cannot find source resource " + resource); diff --git a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java index b1dabff199..00c0cf4f07 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java @@ -11,6 +11,8 @@ package org.opensearch.security.api; +import java.io.IOException; +import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -18,17 +20,29 @@ import com.carrotsearch.randomizedtesting.RandomizedTest; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.commons.io.FileUtils; import org.apache.http.HttpStatus; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.awaitility.Awaitility; +import org.junit.AfterClass; +import org.junit.Before; import org.junit.runner.RunWith; +import org.opensearch.common.CheckedConsumer; import org.opensearch.common.CheckedSupplier; import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.security.ConfigurationFiles; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.security.hasher.PasswordHasher; import org.opensearch.security.hasher.PasswordHasherFactory; +import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.certificate.CertificateData; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -38,6 +52,7 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.notNullValue; import static org.opensearch.security.CrossClusterSearchTests.PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED; import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; @@ -46,28 +61,21 @@ import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.RELOAD_CERTS_ACTION; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE; +import static org.opensearch.test.framework.TestSecurityConfig.REST_ADMIN_REST_API_ACCESS; @ThreadLeakScope(ThreadLeakScope.Scope.NONE) @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) public abstract class AbstractApiIntegrationTest extends RandomizedTest { - public static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles( - new TestSecurityConfig.Role("all_access").clusterPermissions("*").indexPermissions("*").on("*") - ); - public static final TestSecurityConfig.User REST_ADMIN_USER = new TestSecurityConfig.User("rest-api-admin").roles( - new TestSecurityConfig.Role("role").clusterPermissions(allRestAdminPermissions()) - ); + private static final Logger LOGGER = LogManager.getLogger(TestSecurityConfig.class); - public static final TestSecurityConfig.Role REST_ADMIN_REST_API_ACCESS_ROLE = new TestSecurityConfig.Role( - "rest_admin__rest_api_access" - ); - public static final TestSecurityConfig.Role EXAMPLE_ROLE = new TestSecurityConfig.Role("example_role").indexPermissions("crud") - .on("example_index"); + public static final String NEW_USER = "new-user"; - /** - * A user without any privileges - */ - public static final TestSecurityConfig.User NEW_USER = new TestSecurityConfig.User("new-user"); + public static final String REST_ADMIN_USER = "rest-api-admin"; + + public static final String ADMIN_USER_NAME = "admin"; public static final String DEFAULT_PASSWORD = "secret"; @@ -77,23 +85,121 @@ public abstract class AbstractApiIntegrationTest extends RandomizedTest { Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.BCRYPT).build() ); - protected static LocalCluster.Builder clusterBuilder() { - return new LocalCluster.Builder().clusterManager(ClusterManager.DEFAULT) + public static Path configurationFolder; + + protected static TestSecurityConfig testSecurityConfig = new TestSecurityConfig(); + + public static LocalCluster localCluster; + + private Class testClass; + + @Before + public void startCluster() throws IOException { + if (this.getClass().equals(testClass)) { + return; + } + configurationFolder = ConfigurationFiles.createConfigurationDirectory(); + extendConfiguration(); + final var clusterManager = randomFrom(List.of(ClusterManager.THREE_CLUSTER_MANAGERS, ClusterManager.SINGLENODE)); + final var localClusterBuilder = new LocalCluster.Builder().clusterManager(clusterManager) .nodeSettings(getClusterSettings()) - .authc(TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL) - .users(ADMIN_USER, REST_ADMIN_USER, NEW_USER) - .roles(EXAMPLE_ROLE, REST_ADMIN_REST_API_ACCESS_ROLE); + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .loadConfigurationIntoIndex(false); + localCluster = localClusterBuilder.build(); + localCluster.before(); + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { + Awaitility.await() + .alias("Load default configuration") + .until(() -> client.securityHealth().getTextFromJsonBody("/status"), equalTo("UP")); + } + testClass = this.getClass(); } - protected static Map getClusterSettings() { + protected Map getClusterSettings() { Map clusterSettings = new HashMap<>(); - clusterSettings.put( - PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED, - List.of("user_admin__all_access", REST_ADMIN_REST_API_ACCESS_ROLE.getName(), "user_rest-api-admin__role") - ); + clusterSettings.put(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true); + clusterSettings.put(PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_admin__all_access", REST_ADMIN_REST_API_ACCESS)); + clusterSettings.put(SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE, randomBoolean()); return clusterSettings; } + private static void extendConfiguration() throws IOException { + extendActionGroups(configurationFolder, testSecurityConfig.actionGroups()); + extendRoles(configurationFolder, testSecurityConfig.roles()); + extendRolesMapping(configurationFolder, testSecurityConfig.rolesMapping()); + extendUsers(configurationFolder, testSecurityConfig.getUsers()); + } + + private static void extendUsers(final Path configFolder, final List users) throws IOException { + if (users == null) return; + if (users.isEmpty()) return; + LOGGER.info("Adding users to the default configuration: "); + try (final var contentBuilder = XContentFactory.yamlBuilder()) { + contentBuilder.startObject(); + for (final var u : users) { + LOGGER.info("\t\t - {}", u.getName()); + contentBuilder.field(u.getName()); + u.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS); + } + contentBuilder.endObject(); + ConfigurationFiles.writeToConfig(CType.INTERNALUSERS, configFolder, removeDashes(contentBuilder.toString())); + } + } + + private static void extendActionGroups(final Path configFolder, final List actionGroups) + throws IOException { + if (actionGroups == null) return; + if (actionGroups.isEmpty()) return; + LOGGER.info("Adding action groups to the default configuration: "); + try (final var contentBuilder = XContentFactory.yamlBuilder()) { + contentBuilder.startObject(); + for (final var ag : actionGroups) { + LOGGER.info("\t\t - {}", ag.name()); + contentBuilder.field(ag.name()); + ag.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS); + } + contentBuilder.endObject(); + ConfigurationFiles.writeToConfig(CType.ACTIONGROUPS, configFolder, removeDashes(contentBuilder.toString())); + } + } + + private static void extendRoles(final Path configFolder, final List roles) throws IOException { + if (roles == null) return; + if (roles.isEmpty()) return; + LOGGER.info("Adding roles to the default configuration: "); + try (final var contentBuilder = XContentFactory.yamlBuilder()) { + contentBuilder.startObject(); + for (final var r : roles) { + LOGGER.info("\t\t - {}", r.getName()); + contentBuilder.field(r.getName()); + r.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS); + } + contentBuilder.endObject(); + ConfigurationFiles.writeToConfig(CType.ROLES, configFolder, removeDashes(contentBuilder.toString())); + } + } + + private static void extendRolesMapping(final Path configFolder, final List rolesMapping) + throws IOException { + if (rolesMapping == null) return; + if (rolesMapping.isEmpty()) return; + LOGGER.info("Adding roles mapping to the default configuration: "); + try (final var contentBuilder = XContentFactory.yamlBuilder()) { + contentBuilder.startObject(); + for (final var rm : rolesMapping) { + LOGGER.info("\t\t - {}", rm.name()); + contentBuilder.field(rm.name()); + rm.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS); + } + contentBuilder.endObject(); + ConfigurationFiles.writeToConfig(CType.ROLESMAPPING, configFolder, removeDashes(contentBuilder.toString())); + } + } + + private static String removeDashes(final String content) { + return content.replace("---", ""); + } + protected static String[] allRestAdminPermissions() { final var permissions = new String[ENDPOINTS_WITH_PERMISSIONS.size() + 1]; // 1 additional action for SSL update certs var counter = 0; @@ -127,6 +233,42 @@ protected String randomRestAdminPermission() { return randomFrom(permissions); } + @AfterClass + public static void stopCluster() throws IOException { + if (localCluster != null) localCluster.close(); + FileUtils.deleteDirectory(configurationFolder.toFile()); + } + + protected void withUser(final String user, final CheckedConsumer restClientHandler) throws Exception { + withUser(user, DEFAULT_PASSWORD, restClientHandler); + } + + protected void withUser(final String user, final String password, final CheckedConsumer restClientHandler) + throws Exception { + try (TestRestClient client = localCluster.getRestClient(user, password)) { + restClientHandler.accept(client); + } + } + + protected void withUser( + final String user, + final CertificateData certificateData, + final CheckedConsumer restClientHandler + ) throws Exception { + withUser(user, DEFAULT_PASSWORD, certificateData, restClientHandler); + } + + protected void withUser( + final String user, + final String password, + final CertificateData certificateData, + final CheckedConsumer restClientHandler + ) throws Exception { + try (final TestRestClient client = localCluster.getRestClient(user, password, certificateData)) { + restClientHandler.accept(client); + } + } + protected String apiPathPrefix() { return randomFrom(List.of(LEGACY_OPENDISTRO_PREFIX, PLUGINS_PREFIX)); } @@ -156,6 +298,18 @@ protected String apiPath(final String... path) { return fullPath.toString(); } + void badRequestWithReason(final CheckedSupplier endpointCallback, final String expectedMessage) + throws Exception { + final var response = badRequest(endpointCallback); + assertThat(response.getBody(), response.getTextFromJsonBody("/reason"), is(expectedMessage)); + } + + void badRequestWithMessage(final CheckedSupplier endpointCallback, final String expectedMessage) + throws Exception { + final var response = badRequest(endpointCallback); + assertThat(response.getBody(), response.getTextFromJsonBody("/message"), is(expectedMessage)); + } + public static TestRestClient.HttpResponse badRequest(final CheckedSupplier endpointCallback) throws Exception { final var response = endpointCallback.get(); @@ -164,6 +318,14 @@ public static TestRestClient.HttpResponse badRequest(final CheckedSupplier endpointCallback) throws Exception { + final var response = endpointCallback.get(); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_CREATED)); + assertResponseBody(response.getBody()); + assertThat(response.getBody(), response.getTextFromJsonBody("/status"), equalToIgnoringCase("created")); + return response; + } + public static void forbidden( final CheckedSupplier endpointCallback, final String expectedMessage @@ -180,6 +342,14 @@ public static TestRestClient.HttpResponse forbidden(final CheckedSupplier endpointCallback) + throws Exception { + final var response = endpointCallback.get(); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_METHOD_NOT_ALLOWED)); + assertResponseBody(response.getBody()); + return response; + } + public static TestRestClient.HttpResponse notImplemented(final CheckedSupplier endpointCallback) throws Exception { final var response = endpointCallback.get(); @@ -196,6 +366,12 @@ public static TestRestClient.HttpResponse notFound(final CheckedSupplier endpointCallback, final String expectedMessage) + throws Exception { + final var response = notFound(endpointCallback); + assertThat(response.getBody(), response.getTextFromJsonBody("/message"), is(expectedMessage)); + } + public static TestRestClient.HttpResponse ok(final CheckedSupplier endpointCallback) throws Exception { final var response = endpointCallback.get(); @@ -204,6 +380,24 @@ public static TestRestClient.HttpResponse ok(final CheckedSupplier endpointCallback, + final String expectedMessage + ) throws Exception { + final var response = endpointCallback.get(); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + assertResponseBody(response.getBody(), expectedMessage); + return response; + } + + TestRestClient.HttpResponse unauthorized(final CheckedSupplier endpointCallback) + throws Exception { + final var response = endpointCallback.get(); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_UNAUTHORIZED)); + assertResponseBody(response.getBody()); + return response; + } + public static void assertResponseBody(final String responseBody) { assertThat(responseBody, notNullValue()); assertThat(responseBody, not(equalTo(""))); diff --git a/src/integrationTest/java/org/opensearch/security/api/AbstractConfigEntityApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AbstractConfigEntityApiIntegrationTest.java index 16031a8d42..a6d6902359 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AbstractConfigEntityApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AbstractConfigEntityApiIntegrationTest.java @@ -16,10 +16,10 @@ import java.util.StringJoiner; import org.hamcrest.Matcher; +import org.junit.Test; import org.opensearch.common.CheckedSupplier; import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import com.nimbusds.jose.util.Pair; @@ -27,6 +27,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.oneOf; import static org.opensearch.security.api.PatchPayloadHelper.addOp; @@ -34,16 +35,18 @@ import static org.opensearch.security.api.PatchPayloadHelper.removeOp; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; -import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; -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.isNotFound; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public abstract class AbstractConfigEntityApiIntegrationTest extends AbstractApiIntegrationTest { - protected static LocalCluster.Builder clusterBuilder() { - return AbstractApiIntegrationTest.clusterBuilder().nodeSetting(SECURITY_RESTAPI_ADMIN_ENABLED, true); + static { + testSecurityConfig.withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions()); + } + + @Override + protected Map getClusterSettings() { + Map clusterSettings = super.getClusterSettings(); + clusterSettings.put(SECURITY_RESTAPI_ADMIN_ENABLED, true); + return clusterSettings; } interface TestDescriptor { @@ -96,24 +99,26 @@ protected String apiPath(String... paths) { return fullPath.toString(); } - public void forbiddenForRegularUsers(LocalCluster localCluster) throws Exception { - try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { - assertThat(client.putJson(apiPath("some_entity"), EMPTY_BODY), isForbidden()); - assertThat(client.get(apiPath()), isForbidden()); - assertThat(client.get(apiPath("some_entity")), isForbidden()); - assertThat(client.putJson(apiPath("some_entity"), EMPTY_BODY), isForbidden()); - assertThat(client.patch(apiPath(), EMPTY_BODY), isForbidden()); - assertThat(client.patch(apiPath("some_entity"), EMPTY_BODY), isForbidden()); - assertThat(client.delete(apiPath("some_entity")), isForbidden()); - } + @Test + public void forbiddenForRegularUsers() throws Exception { + withUser(NEW_USER, client -> { + forbidden(() -> client.putJson(apiPath("some_entity"), EMPTY_BODY)); + forbidden(() -> client.get(apiPath())); + forbidden(() -> client.get(apiPath("some_entity"))); + forbidden(() -> client.putJson(apiPath("some_entity"), EMPTY_BODY)); + forbidden(() -> client.patch(apiPath(), EMPTY_BODY)); + forbidden(() -> client.patch(apiPath("some_entity"), EMPTY_BODY)); + forbidden(() -> client.delete(apiPath("some_entity"))); + }); } - public void availableForAdminUser(LocalCluster localCluster) throws Exception { - final var entitiesNames = predefinedHiddenAndReservedConfigEntities(localCluster); + @Test + public void availableForAdminUser() throws Exception { + final var entitiesNames = predefinedHiddenAndReservedConfigEntities(); final var hiddenEntityName = entitiesNames.getLeft(); final var reservedEntityName = entitiesNames.getRight(); // can't see hidden resources - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + withUser(ADMIN_USER_NAME, client -> { verifyNoHiddenEntities(() -> client.get(apiPath())); creationOfReadOnlyEntityForbidden( randomAsciiAlphanumOfLength(10), @@ -126,28 +131,35 @@ public void availableForAdminUser(LocalCluster localCluster) throws Exception { verifyUpdateAndDeleteReservedConfigEntityForbidden(reservedEntityName, client); verifyCrudOperations(null, null, client); verifyBadRequestOperations(client); - } + }); } - Pair predefinedHiddenAndReservedConfigEntities(LocalCluster localCluster) throws Exception { + Pair predefinedHiddenAndReservedConfigEntities() throws Exception { final var hiddenEntityName = randomAsciiAlphanumOfLength(10); final var reservedEntityName = randomAsciiAlphanumOfLength(10); - try (TestRestClient client = localCluster.getAdminCertRestClient()) { - assertThat(client.putJson(apiPath(hiddenEntityName), testDescriptor.hiddenEntityPayload()), isCreated()); - assertThat(client.putJson(apiPath(reservedEntityName), testDescriptor.reservedEntityPayload()), isCreated()); - } + withUser( + ADMIN_USER_NAME, + localCluster.getAdminCertificate(), + client -> created(() -> client.putJson(apiPath(hiddenEntityName), testDescriptor.hiddenEntityPayload())) + ); + withUser( + ADMIN_USER_NAME, + localCluster.getAdminCertificate(), + client -> created(() -> client.putJson(apiPath(reservedEntityName), testDescriptor.reservedEntityPayload())) + ); return Pair.of(hiddenEntityName, reservedEntityName); } - public void availableForTLSAdminUser(LocalCluster localCluster) throws Exception { - try (TestRestClient client = localCluster.getAdminCertRestClient()) { - availableForSuperAdminUser(client); - } + @Test + public void availableForTLSAdminUser() throws Exception { + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::availableForSuperAdminUser); } - public void availableForRESTAdminUser(LocalCluster localCluster) throws Exception { - try (TestRestClient client = localCluster.getRestClient(REST_ADMIN_USER)) { - availableForSuperAdminUser(client); + @Test + public void availableForRESTAdminUser() throws Exception { + withUser(REST_ADMIN_USER, this::availableForSuperAdminUser); + if (testDescriptor.restAdminLimitedUser().isPresent()) { + withUser(testDescriptor.restAdminLimitedUser().get(), this::availableForSuperAdminUser); } } @@ -166,9 +178,7 @@ void availableForSuperAdminUser(final TestRestClient client) throws Exception { } void verifyNoHiddenEntities(final CheckedSupplier endpointCallback) throws Exception { - final var resp = endpointCallback.get(); - assertThat(resp, isOk()); - final var body = resp.bodyAsJsonNode(); + final var body = ok(endpointCallback).bodyAsJsonNode(); final var pretty = body.toPrettyString(); final var it = body.elements(); while (it.hasNext()) { @@ -180,11 +190,11 @@ void verifyNoHiddenEntities(final CheckedSupplier client.putJson(apiPath(entityName), configEntity)), + is(oneOf("static", "hidden", "reserved")) + ); + badRequest(() -> client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(10), configEntity)))); } } @@ -212,60 +222,54 @@ void assertWrongDataType(final TestRestClient.HttpResponse response, final Map client.putJson(apiPath(hiddenEntityName), testDescriptor.entityPayload()), expectedErrorMessage); + notFound( + () -> client.patch( apiPath(hiddenEntityName), patch(replaceOp(testDescriptor.entityJsonProperty(), testDescriptor.jsonPropertyPayload())) ), - isNotFound().withAttribute("/message", expectedErrorMessage) + expectedErrorMessage ); - assertThat( - client.patch(apiPath(), patch(replaceOp(hiddenEntityName, testDescriptor.entityPayload()))), - isNotFound().withAttribute("/message", expectedErrorMessage) - ); - assertThat( - client.patch(apiPath(hiddenEntityName), patch(removeOp(testDescriptor.entityJsonProperty()))), - isNotFound().withAttribute("/message", expectedErrorMessage) - ); - assertThat( - client.patch(apiPath(), patch(removeOp(hiddenEntityName))), - isNotFound().withAttribute("/message", expectedErrorMessage) - ); - assertThat(client.delete(apiPath(hiddenEntityName)), isNotFound().withAttribute("/message", expectedErrorMessage)); + notFound(() -> client.patch(apiPath(), patch(replaceOp(hiddenEntityName, testDescriptor.entityPayload()))), expectedErrorMessage); + notFound(() -> client.patch(apiPath(hiddenEntityName), patch(removeOp(testDescriptor.entityJsonProperty()))), expectedErrorMessage); + notFound(() -> client.patch(apiPath(), patch(removeOp(hiddenEntityName))), expectedErrorMessage); + notFound(() -> client.delete(apiPath(hiddenEntityName)), expectedErrorMessage); } void verifyUpdateAndDeleteReservedConfigEntityForbidden(final String reservedEntityName, final TestRestClient client) throws Exception { final var expectedErrorMessage = "Resource '" + reservedEntityName + "' is reserved."; - assertThat( - client.putJson(apiPath(reservedEntityName), testDescriptor.entityPayload()), - isForbidden().withAttribute("/message", expectedErrorMessage) - ); - assertThat( - client.patch( + forbidden(() -> client.putJson(apiPath(reservedEntityName), testDescriptor.entityPayload()), expectedErrorMessage); + forbidden( + () -> client.patch( apiPath(reservedEntityName), patch(replaceOp(testDescriptor.entityJsonProperty(), testDescriptor.entityJsonProperty())) ), - isForbidden().withAttribute("/message", expectedErrorMessage) + expectedErrorMessage ); - assertThat( - client.patch(apiPath(), patch(replaceOp(reservedEntityName, testDescriptor.entityPayload()))), - isForbidden().withAttribute("/message", expectedErrorMessage) - ); - assertThat( - client.patch(apiPath(), patch(removeOp(reservedEntityName))), - isForbidden().withAttribute("/message", expectedErrorMessage) + forbidden( + () -> client.patch(apiPath(), patch(replaceOp(reservedEntityName, testDescriptor.entityPayload()))), + expectedErrorMessage ); - assertThat( - client.patch(apiPath(reservedEntityName), patch(removeOp(testDescriptor.entityJsonProperty()))), - isForbidden().withAttribute("/message", expectedErrorMessage) + forbidden(() -> client.patch(apiPath(), patch(removeOp(reservedEntityName))), expectedErrorMessage); + forbidden( + () -> client.patch(apiPath(reservedEntityName), patch(removeOp(testDescriptor.entityJsonProperty()))), + expectedErrorMessage ); - assertThat(client.delete(apiPath(reservedEntityName)), isForbidden().withAttribute("/message", expectedErrorMessage)); + forbidden(() -> client.delete(apiPath(reservedEntityName)), expectedErrorMessage); } void forbiddenToCreateEntityWithRestAdminPermissions(final TestRestClient client) throws Exception {} diff --git a/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java index 8af5910d19..65bec9f788 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java @@ -10,25 +10,16 @@ package org.opensearch.security.api; -import org.junit.ClassRule; import org.junit.Test; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; -import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.not; -import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; -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.isNotFound; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; -import static org.opensearch.test.framework.matcher.RestMatchers.isUnauthorized; public class AccountRestApiIntegrationTest extends AbstractApiIntegrationTest { @@ -42,12 +33,11 @@ public class AccountRestApiIntegrationTest extends AbstractApiIntegrationTest { public final static String TEST_USER_NEW_PASSWORD = randomAlphabetic(10); - @ClassRule - public static LocalCluster localCluster = clusterBuilder().users( - new TestSecurityConfig.User(TEST_USER).password(TEST_USER_PASSWORD), - new TestSecurityConfig.User(RESERVED_USER).reserved(true), - new TestSecurityConfig.User(HIDDEN_USERS).hidden(true) - ).build(); + static { + testSecurityConfig.user(new TestSecurityConfig.User(TEST_USER).password(TEST_USER_PASSWORD)) + .user(new TestSecurityConfig.User(RESERVED_USER).reserved(true)) + .user(new TestSecurityConfig.User(HIDDEN_USERS).hidden(true)); + } private String accountPath() { return super.apiPath("account"); @@ -55,11 +45,10 @@ private String accountPath() { @Test public void accountInfo() throws Exception { - try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { - HttpResponse response = client.get(accountPath()); - assertThat(response, isOk()); + withUser(NEW_USER, client -> { + var response = ok(() -> client.get(accountPath())); final var account = response.bodyAsJsonNode(); - assertThat(response.getBody(), account.get("user_name").asText(), is(NEW_USER.getName())); + assertThat(response.getBody(), account.get("user_name").asText(), is(NEW_USER)); assertThat(response.getBody(), not(account.get("is_reserved").asBoolean())); assertThat(response.getBody(), not(account.get("is_hidden").asBoolean())); assertThat(response.getBody(), account.get("is_internal_user").asBoolean()); @@ -68,77 +57,69 @@ public void accountInfo() throws Exception { assertThat(response.getBody(), account.get("custom_attribute_names").isArray()); assertThat(response.getBody(), account.get("tenants").isObject()); assertThat(response.getBody(), account.get("roles").isArray()); - } - try (TestRestClient client = localCluster.getRestClient(NEW_USER.getName(), "a")) { - HttpResponse response = client.get(accountPath()); - assertThat(response, isUnauthorized()); - } - try (TestRestClient client = localCluster.getRestClient("a", "b")) { - HttpResponse response = client.get(accountPath()); - assertThat(response, isUnauthorized()); - } + }); + withUser(NEW_USER, "a", client -> unauthorized(() -> client.get(accountPath()))); + withUser("a", "b", client -> unauthorized(() -> client.get(accountPath()))); } @Test public void changeAccountPassword() throws Exception { - try (TestRestClient client = localCluster.getRestClient(TEST_USER, TEST_USER_PASSWORD)) { - verifyWrongPayload(client); - } + withUser(TEST_USER, TEST_USER_PASSWORD, this::verifyWrongPayload); verifyPasswordCanBeChanged(); - try (TestRestClient client = localCluster.getRestClient(RESERVED_USER, DEFAULT_PASSWORD)) { - HttpResponse response = client.get(accountPath()); - assertThat(response, isOk()); - assertThat(response.getBooleanFromJsonBody("/is_reserved"), is(true)); - assertThat(client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10))), isForbidden()); - } - try (TestRestClient client = localCluster.getRestClient(HIDDEN_USERS, DEFAULT_PASSWORD)) { - HttpResponse response = client.get(accountPath()); - assertThat(response, isOk()); - assertThat(response.getBooleanFromJsonBody("/is_hidden"), is(true)); - assertThat(client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10))), isNotFound()); - } - try (TestRestClient client = localCluster.getAdminCertRestClient()) { - HttpResponse response = client.get(accountPath()); - assertThat(response, isOk()); - assertThat(client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10))), isNotFound()); - } + withUser(RESERVED_USER, client -> { + var response = ok(() -> client.get(accountPath())); + assertThat(response.getBody(), response.getBooleanFromJsonBody("/is_reserved")); + forbidden(() -> client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10)))); + }); + withUser(HIDDEN_USERS, client -> { + var response = ok(() -> client.get(accountPath())); + assertThat(response.getBody(), response.getBooleanFromJsonBody("/is_hidden")); + notFound(() -> client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10)))); + }); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { + ok(() -> client.get(accountPath())); + notFound(() -> client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10)))); + }); } private void verifyWrongPayload(final TestRestClient client) throws Exception { - assertThat(client.putJson(accountPath(), EMPTY_BODY), isBadRequest()); - assertThat(client.putJson(accountPath(), changePasswordPayload(null, "new_password")), isBadRequest()); - assertThat(client.putJson(accountPath(), changePasswordPayload("wrong-password", "some_new_pwd")), isBadRequest()); - assertThat(client.putJson(accountPath(), changePasswordPayload(TEST_USER_PASSWORD, null)), isBadRequest()); - assertThat(client.putJson(accountPath(), changePasswordPayload(TEST_USER_PASSWORD, "")), isBadRequest()); - assertThat(client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, null)), isBadRequest()); - assertThat(client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, "")), isBadRequest()); - assertThat( - client.putJson( + badRequest(() -> client.putJson(accountPath(), EMPTY_BODY)); + badRequest(() -> client.putJson(accountPath(), changePasswordPayload(null, "new_password"))); + badRequest(() -> client.putJson(accountPath(), changePasswordPayload("wrong-password", "some_new_pwd"))); + badRequest(() -> client.putJson(accountPath(), changePasswordPayload(TEST_USER_PASSWORD, null))); + badRequest(() -> client.putJson(accountPath(), changePasswordPayload(TEST_USER_PASSWORD, ""))); + badRequest(() -> client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, null))); + badRequest(() -> client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, ""))); + badRequest( + () -> client.putJson( accountPath(), (builder, params) -> builder.startObject() .field("current_password", TEST_USER_PASSWORD) .startArray("backend_roles") .endArray() .endObject() - ), - isBadRequest() + ) ); } private void verifyPasswordCanBeChanged() throws Exception { final var newPassword = randomAlphabetic(10); - try (TestRestClient client = localCluster.getRestClient(TEST_USER, TEST_USER_PASSWORD)) { - HttpResponse resp = client.putJson( - accountPath(), - changePasswordWithHashPayload(TEST_USER_PASSWORD, passwordHasher.hash(newPassword.toCharArray())) - ); - assertThat(resp, isOk()); - } - try (TestRestClient client = localCluster.getRestClient(TEST_USER, newPassword)) { - HttpResponse resp = client.putJson(accountPath(), changePasswordPayload(newPassword, TEST_USER_NEW_PASSWORD)); - assertThat(resp, isOk()); - } + withUser( + TEST_USER, + TEST_USER_PASSWORD, + client -> ok( + () -> client.putJson( + accountPath(), + changePasswordWithHashPayload(TEST_USER_PASSWORD, passwordHasher.hash(newPassword.toCharArray())) + ) + ) + ); + withUser( + TEST_USER, + newPassword, + client -> ok(() -> client.putJson(accountPath(), changePasswordPayload(newPassword, TEST_USER_NEW_PASSWORD))) + ); } @Test @@ -146,9 +127,10 @@ public void testPutAccountRetainsAccountInformation() throws Exception { final var username = "test"; final String password = randomAlphabetic(10); final String newPassword = randomAlphabetic(10); - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - assertThat( - client.putJson( + withUser( + ADMIN_USER_NAME, + client -> created( + () -> client.putJson( apiPath("internalusers", username), (builder, params) -> builder.startObject() .field("password", password) @@ -158,29 +140,24 @@ public void testPutAccountRetainsAccountInformation() throws Exception { .endArray() .field("opendistro_security_roles") .startArray() - .value(EXAMPLE_ROLE.getName()) + .value("user_limited-user__limited-role") .endArray() .field("attributes") .startObject() .field("foo", "bar") .endObject() .endObject() - ), - isCreated() - ); - } - try (TestRestClient client = localCluster.getRestClient(username, password)) { - HttpResponse resp = client.putJson(accountPath(), changePasswordPayload(password, newPassword)); - assertThat(resp, isOk()); - } - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - HttpResponse response = client.get(apiPath("internalusers", username)); - assertThat(response, isOk()); + ) + ) + ); + withUser(username, password, client -> ok(() -> client.putJson(accountPath(), changePasswordPayload(password, newPassword)))); + withUser(ADMIN_USER_NAME, client -> { + final var response = ok(() -> client.get(apiPath("internalusers", username))); final var user = response.bodyAsJsonNode().get(username); assertThat(user.toPrettyString(), user.get("backend_roles").get(0).asText(), is("test-backend-role")); - assertThat(user.toPrettyString(), user.get("opendistro_security_roles").get(0).asText(), is(EXAMPLE_ROLE.getName())); + assertThat(user.toPrettyString(), user.get("opendistro_security_roles").get(0).asText(), is("user_limited-user__limited-role")); assertThat(user.toPrettyString(), user.get("attributes").get("foo").asText(), is("bar")); - } + }); } private ToXContentObject changePasswordPayload(final String currentPassword, final String newPassword) { diff --git a/src/integrationTest/java/org/opensearch/security/api/ActionGroupsRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/ActionGroupsRestApiIntegrationTest.java index bb47efefec..174c2b4ea6 100644 --- a/src/integrationTest/java/org/opensearch/security/api/ActionGroupsRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/ActionGroupsRestApiIntegrationTest.java @@ -12,15 +12,12 @@ package org.opensearch.security.api; import java.util.List; +import java.util.Map; import java.util.Optional; -import org.junit.ClassRule; -import org.junit.Test; - import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.CoreMatchers.is; @@ -31,11 +28,6 @@ import static org.opensearch.security.api.PatchPayloadHelper.patch; import static org.opensearch.security.api.PatchPayloadHelper.removeOp; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; -import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; -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.isNotFound; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class ActionGroupsRestApiIntegrationTest extends AbstractConfigEntityApiIntegrationTest { @@ -43,20 +35,16 @@ public class ActionGroupsRestApiIntegrationTest extends AbstractConfigEntityApiI private final static String REST_ADMIN_PERMISSION_ACTION_GROUP = "rest-admin-permissions-action-group"; - @ClassRule - public static LocalCluster localCluster = clusterBuilder().users( - new TestSecurityConfig.User(REST_API_ADMIN_ACTION_GROUPS_ONLY).roles( - new TestSecurityConfig.Role("rest_admin_role").clusterPermissions(restAdminPermission(Endpoint.ACTIONGROUPS)) - ) - ) - .actionGroups( - new TestSecurityConfig.ActionGroup( - REST_ADMIN_PERMISSION_ACTION_GROUP, - TestSecurityConfig.ActionGroup.Type.INDEX, - allRestAdminPermissions() - ) - ) - .build(); + static { + testSecurityConfig.withRestAdminUser(REST_API_ADMIN_ACTION_GROUPS_ONLY, restAdminPermission(Endpoint.ACTIONGROUPS)) + .actionGroups( + new TestSecurityConfig.ActionGroup( + REST_ADMIN_PERMISSION_ACTION_GROUP, + TestSecurityConfig.ActionGroup.Type.INDEX, + allRestAdminPermissions() + ) + ); + } public ActionGroupsRestApiIntegrationTest() { super("actiongroups", new TestDescriptor() { @@ -123,117 +111,74 @@ static String randomType() { return randomFrom(List.of(TestSecurityConfig.ActionGroup.Type.CLUSTER.type(), TestSecurityConfig.ActionGroup.Type.INDEX.type())); } - @Test - public void forbiddenForRegularUsers() throws Exception { - super.forbiddenForRegularUsers(localCluster); - } - - @Test - public void availableForAdminUser() throws Exception { - super.availableForAdminUser(localCluster); - } - - @Test - public void availableForTLSAdminUser() throws Exception { - super.availableForTLSAdminUser(localCluster); - } - - @Test - public void availableForRESTAdminUser() throws Exception { - super.availableForRESTAdminUser(localCluster); - } - @Override void forbiddenToCreateEntityWithRestAdminPermissions(final TestRestClient client) throws Exception { - assertThat(client.putJson(apiPath("new_rest_admin_action_group"), actionGroup(randomRestAdminPermission())), isForbidden()); - assertThat( - client.patch(apiPath(), patch(addOp("new_rest_admin_action_group", actionGroup(randomRestAdminPermission())))), - isForbidden() - ); + forbidden(() -> client.putJson(apiPath("new_rest_admin_action_group"), actionGroup(randomRestAdminPermission()))); + forbidden(() -> client.patch(apiPath(), patch(addOp("new_rest_admin_action_group", actionGroup(randomRestAdminPermission()))))); } @Override void forbiddenToUpdateAndDeleteExistingEntityWithRestAdminPermissions(final TestRestClient client) throws Exception { // update - assertThat(client.putJson(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP), actionGroup()), isForbidden()); - assertThat(client.patch(apiPath(), patch(replaceOp(REST_ADMIN_PERMISSION_ACTION_GROUP, actionGroup("a", "b")))), isForbidden()); - assertThat( - client.patch(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP), patch(replaceOp("allowed_actions", configJsonArray("c", "d")))), - isForbidden() + forbidden(() -> client.putJson(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP), actionGroup())); + forbidden(() -> client.patch(apiPath(), patch(replaceOp(REST_ADMIN_PERMISSION_ACTION_GROUP, actionGroup("a", "b"))))); + forbidden( + () -> client.patch(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP), patch(replaceOp("allowed_actions", configJsonArray("c", "d")))) ); // remove - assertThat(client.patch(apiPath(), patch(removeOp(REST_ADMIN_PERMISSION_ACTION_GROUP))), isForbidden()); - assertThat(client.patch(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP), patch(removeOp("allowed_actions"))), isForbidden()); - assertThat(client.delete(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP)), isForbidden()); + forbidden(() -> client.patch(apiPath(), patch(removeOp(REST_ADMIN_PERMISSION_ACTION_GROUP)))); + forbidden(() -> client.patch(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP), patch(removeOp("allowed_actions")))); + forbidden(() -> client.delete(apiPath(REST_ADMIN_PERMISSION_ACTION_GROUP))); } @Override void verifyCrudOperations(final Boolean hidden, final Boolean reserved, final TestRestClient client) throws Exception { - // create - assertThat(client.putJson(apiPath("new_action_group"), actionGroup(hidden, reserved, "a", "b")), isCreated()); - var response = client.get(apiPath("new_action_group")); - assertThat(response, isOk()); - assertActionGroup(response, "new_action_group", List.of("a", "b")); + created(() -> client.putJson(apiPath("new_action_group"), actionGroup(hidden, reserved, "a", "b"))); + assertActionGroup(ok(() -> client.get(apiPath("new_action_group"))), "new_action_group", List.of("a", "b")); - // update - assertThat(client.putJson(apiPath("new_action_group"), actionGroup(hidden, reserved, "c", "d")), isOk()); - response = client.get(apiPath("new_action_group")); - assertThat(response, isOk()); - assertActionGroup(response, "new_action_group", List.of("c", "d")); - - // delete - assertThat(client.delete(apiPath("new_action_group")), isOk()); - response = client.get(apiPath("new_action_group")); - assertThat(response, isNotFound()); - - // patch add - assertThat(client.patch(apiPath(), patch(addOp("new_action_group_for_patch", actionGroup(hidden, reserved, "e", "f")))), isOk()); - response = client.get(apiPath("new_action_group_for_patch")); - assertThat(response, isOk()); - assertActionGroup(response, "new_action_group_for_patch", List.of("e", "f")); - - // patch replace - assertThat( - client.patch(apiPath("new_action_group_for_patch"), patch(replaceOp("allowed_actions", configJsonArray("g", "h")))), - isOk() - ); - response = client.get(apiPath("new_action_group_for_patch")); - assertThat(response, isOk()); - assertActionGroup(response, "new_action_group_for_patch", List.of("g", "h")); - - // patch remove - assertThat(client.patch(apiPath(), patch(removeOp("new_action_group_for_patch"))), isOk()); - response = client.get(apiPath("new_action_group_for_patch")); - assertThat(response, isNotFound()); + ok(() -> client.putJson(apiPath("new_action_group"), actionGroup(hidden, reserved, "c", "d"))); + assertActionGroup(ok(() -> client.get(apiPath("new_action_group"))), "new_action_group", List.of("c", "d")); + + ok(() -> client.delete(apiPath("new_action_group"))); + notFound(() -> client.get(apiPath("new_action_group"))); + + ok(() -> client.patch(apiPath(), patch(addOp("new_action_group_for_patch", actionGroup(hidden, reserved, "e", "f"))))); + assertActionGroup(ok(() -> client.get(apiPath("new_action_group_for_patch"))), "new_action_group_for_patch", List.of("e", "f")); + + ok(() -> client.patch(apiPath("new_action_group_for_patch"), patch(replaceOp("allowed_actions", configJsonArray("g", "h"))))); + assertActionGroup(ok(() -> client.get(apiPath("new_action_group_for_patch"))), "new_action_group_for_patch", List.of("g", "h")); + + ok(() -> client.patch(apiPath(), patch(removeOp("new_action_group_for_patch")))); + notFound(() -> client.get(apiPath("new_action_group_for_patch"))); } @Override void verifyBadRequestOperations(final TestRestClient client) throws Exception { // put - assertThat(client.putJson(apiPath("some_action_group"), EMPTY_BODY), isBadRequest()); - assertThat( - client.putJson(apiPath("kibana_user"), actionGroup("a", "b")), - isBadRequest("/message", "kibana_user is an existing role. A action group cannot be named with an existing role name.") + badRequest(() -> client.putJson(apiPath("some_action_group"), EMPTY_BODY)); + badRequestWithMessage( + () -> client.putJson(apiPath("kibana_user"), actionGroup("a", "b")), + "kibana_user is an existing role. A action group cannot be named with an existing role name." ); - assertThat( - client.putJson(apiPath("reference_itself"), actionGroup("reference_itself")), - isBadRequest("/message", "reference_itself cannot be an allowed_action of itself") + badRequestWithMessage( + () -> client.putJson(apiPath("reference_itself"), actionGroup("reference_itself")), + "reference_itself cannot be an allowed_action of itself" ); - assertThat(client.putJson(apiPath("some_action_group"), (builder, params) -> { + + badRequestWithMessage(() -> client.putJson(apiPath("some_action_group"), (builder, params) -> { builder.startObject().field("type", "asdasdsad").field("allowed_actions"); configJsonArray("g", "f").toXContent(builder, params); return builder.endObject(); - }), isBadRequest("/message", "Invalid action group type: asdasdsad. Supported types are: cluster, index.")); + }), "Invalid action group type: asdasdsad. Supported types are: cluster, index."); - assertThat( - client.putJson(apiPath("some_action_group"), configJsonArray("a", "b", "c")), - isBadRequest("/missing_mandatory_keys/keys", "allowed_actions") + assertMissingMandatoryKeys( + badRequest(() -> client.putJson(apiPath("some_action_group"), configJsonArray("a", "b", "c"))), + "allowed_actions" ); - // duplicate check retained from original - assertThat( - client.putJson(apiPath("some_action_group"), configJsonArray("a", "b", "c")), - isBadRequest("/missing_mandatory_keys/keys", "allowed_actions") + assertMissingMandatoryKeys( + badRequest(() -> client.putJson(apiPath("some_action_group"), configJsonArray("a", "b", "c"))), + "allowed_actions" ); final ToXContentObject unknownJsonFields = (builder, params) -> { @@ -241,59 +186,68 @@ void verifyBadRequestOperations(final TestRestClient client) throws Exception { configJsonArray("g", "h").toXContent(builder, params); return builder.endObject(); }; - assertThat(client.putJson(apiPath("some_action_group"), unknownJsonFields), isBadRequest("/invalid_keys/keys", "a,c")); + assertInvalidKeys(badRequest(() -> client.putJson(apiPath("some_action_group"), unknownJsonFields)), "a,c"); - assertThat(client.putJson(apiPath("some_action_group"), (builder, params) -> { + assertNullValuesInArray(badRequest(() -> client.putJson(apiPath("some_action_group"), (builder, params) -> { builder.startObject().field("type", randomType()).field("allowed_actions"); configJsonArray("g", null, "f").toXContent(builder, params); return builder.endObject(); - }), isBadRequest("/reason", "`null` or blank values are not allowed as json array elements")); - - // patch - assertThat(client.patch(apiPath("some_action_group"), EMPTY_BODY), isBadRequest()); - assertThat(client.patch(apiPath(), patch(addOp("some_action_group", EMPTY_BODY))), isBadRequest()); - assertThat(client.patch(apiPath(), patch(replaceOp("some_action_group", EMPTY_BODY))), isBadRequest()); - assertThat( - client.patch(apiPath(), patch(addOp("kibana_user", actionGroup("a")))), - isBadRequest("/message", "kibana_user is an existing role. A action group cannot be named with an existing role name.") + }))); + assertWrongDataType( + client.putJson( + apiPath("some_action_group"), + (builder, params) -> builder.startObject().field("allowed_actions", "a").endObject() + ), + Map.of("allowed_actions", "Array expected") ); - assertThat( - client.patch(apiPath(), patch(addOp("reference_itself", actionGroup("reference_itself")))), - isBadRequest("/message", "reference_itself cannot be an allowed_action of itself") + // patch + badRequest(() -> client.patch(apiPath("some_action_group"), EMPTY_BODY)); + badRequest(() -> client.patch(apiPath(), patch(addOp("some_action_group", EMPTY_BODY)))); + badRequest(() -> client.patch(apiPath(), patch(replaceOp("some_action_group", EMPTY_BODY)))); + + badRequestWithMessage( + () -> client.patch(apiPath(), patch(addOp("kibana_user", actionGroup("a")))), + "kibana_user is an existing role. A action group cannot be named with an existing role name." ); - assertThat( - client.patch(apiPath(), patch(addOp("some_action_group", configJsonArray("a", "b", "c")))), - isBadRequest("/missing_mandatory_keys/keys", "allowed_actions") + badRequestWithMessage( + () -> client.patch(apiPath(), patch(addOp("reference_itself", actionGroup("reference_itself")))), + "reference_itself cannot be an allowed_action of itself" ); - assertThat(client.patch(apiPath(), patch(addOp("some_action_group", (ToXContentObject) (builder, params) -> { + assertMissingMandatoryKeys( + badRequest(() -> client.patch(apiPath(), patch(addOp("some_action_group", configJsonArray("a", "b", "c"))))), + "allowed_actions" + ); + badRequest(() -> client.patch(apiPath(), patch(addOp("some_action_group", (ToXContentObject) (builder, params) -> { builder.startObject().field("type", "aaaa").field("allowed_actions"); configJsonArray("g", "f").toXContent(builder, params); return builder.endObject(); - }))), isBadRequest()); + })))); - assertThat(client.patch(apiPath(), patch(addOp("some_action_group", (ToXContentObject) (builder, parameter) -> { + badRequest(() -> client.patch(apiPath(), patch(addOp("some_action_group", (ToXContentObject) (builder, parameter) -> { builder.startObject(); unknownJsonFields.toXContent(builder, parameter); return builder.endObject(); - }))), isBadRequest()); - - assertThat(client.patch(apiPath(), patch(addOp("some_action_group", (ToXContentObject) (builder, params) -> { - builder.startObject().field("type", randomType()).field("allowed_actions"); - configJsonArray("g", null, "f").toXContent(builder, params); - return builder.endObject(); - }))), isBadRequest("/reason", "`null` or blank values are not allowed as json array elements")); - - var response = client.patch( - apiPath(), - patch( - addOp( - "some_action_group", - (ToXContentObject) (builder, params) -> builder.startObject().field("allowed_actions", "a").endObject() + })))); + assertNullValuesInArray( + badRequest(() -> client.patch(apiPath(), patch(addOp("some_action_group", (ToXContentObject) (builder, params) -> { + builder.startObject().field("type", randomType()).field("allowed_actions"); + configJsonArray("g", null, "f").toXContent(builder, params); + return builder.endObject(); + })))) + ); + assertWrongDataType( + client.patch( + apiPath(), + patch( + addOp( + "some_action_group", + (ToXContentObject) (builder, params) -> builder.startObject().field("allowed_actions", "a").endObject() + ) ) - ) + ), + Map.of("allowed_actions", "Array expected") ); - assertThat(response, isBadRequest().withAttribute("/status", "error").withAttribute("/allowed_actions", "Array expected")); } void assertActionGroup(final TestRestClient.HttpResponse response, final String actionGroupName, final List allowedActions) { diff --git a/src/integrationTest/java/org/opensearch/security/api/CertificatesRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/CertificatesRestApiIntegrationTest.java index c1d6999fd8..748b036d16 100644 --- a/src/integrationTest/java/org/opensearch/security/api/CertificatesRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/CertificatesRestApiIntegrationTest.java @@ -14,19 +14,19 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.StringJoiner; import java.util.stream.Collectors; import com.carrotsearch.randomizedtesting.RandomizedContext; import com.fasterxml.jackson.databind.JsonNode; -import org.junit.ClassRule; import org.junit.Test; +import org.opensearch.common.CheckedConsumer; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.security.ssl.config.CertType; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.certificate.TestCertificates; -import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.LocalOpenSearchCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -36,8 +36,6 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; -import static org.opensearch.test.framework.matcher.RestMatchers.isForbidden; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; import static junit.framework.TestCase.fail; public class CertificatesRestApiIntegrationTest extends AbstractApiIntegrationTest { @@ -45,18 +43,22 @@ public class CertificatesRestApiIntegrationTest extends AbstractApiIntegrationTe final static String REGULAR_USER = "regular_user"; final static String ROOT_CA = "Root CA"; - @ClassRule - public static LocalCluster localCluster = clusterBuilder().nodeSetting(SECURITY_RESTAPI_ADMIN_ENABLED, true) - .roles(new TestSecurityConfig.Role("simple_user_role").clusterPermissions("cluster:admin/security/certificates/info")) - .rolesMapping(new TestSecurityConfig.RoleMapping("simple_user_role").users(REGULAR_USER, ADMIN_USER.getName())) - .users( - new TestSecurityConfig.User(REGULAR_USER), - new TestSecurityConfig.User(REST_API_ADMIN_SSL_INFO).roles( - REST_ADMIN_REST_API_ACCESS_ROLE, - new TestSecurityConfig.Role("rest_admin_role").clusterPermissions(restAdminPermission(Endpoint.SSL, CERTS_INFO_ACTION)) - ) + static { + testSecurityConfig.roles( + new TestSecurityConfig.Role("simple_user_role").clusterPermissions("cluster:admin/security/certificates/info") ) - .build(); + .rolesMapping(new TestSecurityConfig.RoleMapping("simple_user_role").users(REGULAR_USER, ADMIN_USER_NAME)) + .user(new TestSecurityConfig.User(REGULAR_USER)) + .withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions()) + .withRestAdminUser(REST_API_ADMIN_SSL_INFO, restAdminPermission(Endpoint.SSL, CERTS_INFO_ACTION)); + } + + @Override + protected Map getClusterSettings() { + Map clusterSettings = super.getClusterSettings(); + clusterSettings.put(SECURITY_RESTAPI_ADMIN_ENABLED, true); + return clusterSettings; + } @Override protected String apiPathPrefix() { @@ -76,65 +78,59 @@ protected String sslCertsPath(String... path) { @Test public void forbiddenForRegularUser() throws Exception { - try (TestRestClient client = localCluster.getRestClient(REGULAR_USER, DEFAULT_PASSWORD)) { - assertThat(client.get(sslCertsPath()), isForbidden()); - } + withUser(REGULAR_USER, client -> forbidden(() -> client.get(sslCertsPath()))); } @Test public void forbiddenForAdminUser() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - assertThat(client.get(sslCertsPath()), isForbidden()); - } + withUser(ADMIN_USER_NAME, client -> forbidden(() -> client.get(sslCertsPath()))); } @Test public void availableForTlsAdmin() throws Exception { - try (TestRestClient client = localCluster.getAdminCertRestClient()) { - verifySSLCertsInfo(client, List.of(CertType.HTTP, CertType.TRANSPORT, CertType.TRANSPORT_CLIENT)); - } + withUser( + ADMIN_USER_NAME, + localCluster.getAdminCertificate(), + verifySSLCertsInfo(List.of(CertType.HTTP, CertType.TRANSPORT, CertType.TRANSPORT_CLIENT)) + ); } @Test public void availableForRestAdmin() throws Exception { - try (TestRestClient client = localCluster.getRestClient(REST_ADMIN_USER)) { - verifySSLCertsInfo(client, List.of(CertType.HTTP, CertType.TRANSPORT, CertType.TRANSPORT_CLIENT)); - } - try (TestRestClient client = localCluster.getRestClient(REST_API_ADMIN_SSL_INFO, DEFAULT_PASSWORD)) { - verifySSLCertsInfo(client, List.of(CertType.HTTP, CertType.TRANSPORT, CertType.TRANSPORT_CLIENT)); - } + withUser(REST_ADMIN_USER, verifySSLCertsInfo(List.of(CertType.HTTP, CertType.TRANSPORT, CertType.TRANSPORT_CLIENT))); + withUser(REST_API_ADMIN_SSL_INFO, verifySSLCertsInfo(List.of(CertType.HTTP, CertType.TRANSPORT, CertType.TRANSPORT_CLIENT))); } @Test public void timeoutTest() throws Exception { - try (TestRestClient client = localCluster.getRestClient(REST_ADMIN_USER)) { - verifyTimeoutRequest(client); - } + withUser(REST_ADMIN_USER, this::verifyTimeoutRequest); } private void verifyTimeoutRequest(final TestRestClient client) throws Exception { - assertThat(client.get(sslCertsPath() + "?timeout=0"), isOk()); + ok(() -> client.get(sslCertsPath() + "?timeout=0")); } - private void verifySSLCertsInfo(final TestRestClient testRestClient, List expectCerts) { - try { - assertSSLCertsInfo(localCluster.nodes(), expectCerts, testRestClient.get(sslCertsPath())); - if (localCluster.nodes().size() > 1) { - final var randomNodes = randomNodes(); - final var nodeIds = randomNodes.stream() - .map(n -> n.esNode().getNodeEnvironment().nodeId()) - .collect(Collectors.joining(",")); - assertSSLCertsInfo(randomNodes, expectCerts, testRestClient.get(sslCertsPath(nodeIds))); + private CheckedConsumer verifySSLCertsInfo(List expectCerts) { + return testRestClient -> { + try { + assertSSLCertsInfo(localCluster.nodes(), expectCerts, ok(() -> testRestClient.get(sslCertsPath()))); + if (localCluster.nodes().size() > 1) { + final var randomNodes = randomNodes(); + final var nodeIds = randomNodes.stream() + .map(n -> n.esNode().getNodeEnvironment().nodeId()) + .collect(Collectors.joining(",")); + assertSSLCertsInfo(randomNodes, expectCerts, ok(() -> testRestClient.get(sslCertsPath(nodeIds)))); + } + final var randomCertType = randomFrom(expectCerts); + assertSSLCertsInfo( + localCluster.nodes(), + List.of(randomCertType), + ok(() -> testRestClient.get(String.format("%s?cert_type=%s", sslCertsPath(), randomCertType))) + ); + } catch (Exception e) { + fail("Verify SSLCerts info failed with exception: " + e.getMessage()); } - final var randomCertType = randomFrom(expectCerts); - assertSSLCertsInfo( - localCluster.nodes(), - List.of(randomCertType), - testRestClient.get(String.format("%s?cert_type=%s", sslCertsPath(), randomCertType)) - ); - } catch (Exception e) { - fail("Verify SSLCerts info failed with exception: " + e.getMessage()); - } + }; } private void assertSSLCertsInfo( diff --git a/src/integrationTest/java/org/opensearch/security/api/ConfigRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/ConfigRestApiIntegrationTest.java index b69e271769..16b089f99b 100644 --- a/src/integrationTest/java/org/opensearch/security/api/ConfigRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/ConfigRestApiIntegrationTest.java @@ -10,48 +10,38 @@ */ package org.opensearch.security.api; +import java.util.Map; import java.util.StringJoiner; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.junit.ClassRule; import org.junit.Test; import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.dlic.rest.api.Endpoint; -import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; -import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.security.api.PatchPayloadHelper.patch; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION; -import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; -import static org.opensearch.test.framework.matcher.RestMatchers.isForbidden; -import static org.opensearch.test.framework.matcher.RestMatchers.isNotAllowed; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class ConfigRestApiIntegrationTest extends AbstractApiIntegrationTest { final static String REST_API_ADMIN_CONFIG_UPDATE = "rest-api-admin-config-update"; - @ClassRule - public static LocalCluster localCluster = clusterBuilder().nodeSetting( - SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, - true - ) - .nodeSetting(SECURITY_RESTAPI_ADMIN_ENABLED, true) - .users( - new TestSecurityConfig.User(REST_API_ADMIN_CONFIG_UPDATE).roles( - REST_ADMIN_REST_API_ACCESS_ROLE, - new TestSecurityConfig.Role("rest_admin_role").clusterPermissions( - restAdminPermission(Endpoint.CONFIG, SECURITY_CONFIG_UPDATE) - ) - ) - ) - .build(); + static { + testSecurityConfig.withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions()) + .withRestAdminUser(REST_API_ADMIN_CONFIG_UPDATE, restAdminPermission(Endpoint.CONFIG, SECURITY_CONFIG_UPDATE)); + } + + @Override + protected Map getClusterSettings() { + Map clusterSettings = super.getClusterSettings(); + clusterSettings.put(SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true); + clusterSettings.put(SECURITY_RESTAPI_ADMIN_ENABLED, true); + return clusterSettings; + } private String securityConfigPath(final String... path) { final var fullPath = new StringJoiner("/").add(super.apiPath("securityconfig")); @@ -62,51 +52,44 @@ private String securityConfigPath(final String... path) { @Test public void forbiddenForRegularUsers() throws Exception { - try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { - assertThat(client.get(securityConfigPath()), isForbidden()); - assertThat(client.putJson(securityConfigPath("config"), EMPTY_BODY), isForbidden()); - assertThat(client.patch(securityConfigPath(), EMPTY_BODY), isForbidden()); + withUser(NEW_USER, client -> { + forbidden(() -> client.get(securityConfigPath())); + forbidden(() -> client.putJson(securityConfigPath("config"), EMPTY_BODY)); + forbidden(() -> client.patch(securityConfigPath(), EMPTY_BODY)); verifyNotAllowedMethods(client); - } + }); } @Test public void partiallyAvailableForAdminUser() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - assertThat(client.get(securityConfigPath()), isOk()); - assertThat(client.putJson(securityConfigPath("xxx"), EMPTY_BODY), isBadRequest()); - assertThat(client.putJson(securityConfigPath("config"), EMPTY_BODY), isForbidden()); - assertThat(client.patch(securityConfigPath(), EMPTY_BODY), isForbidden()); - verifyNotAllowedMethods(client); - } + withUser(ADMIN_USER_NAME, client -> ok(() -> client.get(securityConfigPath()))); + withUser(ADMIN_USER_NAME, client -> { + badRequest(() -> client.putJson(securityConfigPath("xxx"), EMPTY_BODY)); + forbidden(() -> client.putJson(securityConfigPath("config"), EMPTY_BODY)); + forbidden(() -> client.patch(securityConfigPath(), EMPTY_BODY)); + }); + withUser(ADMIN_USER_NAME, this::verifyNotAllowedMethods); } @Test public void availableForTlsAdminUser() throws Exception { - try (TestRestClient client = localCluster.getAdminCertRestClient()) { - assertThat(client.get(securityConfigPath()), isOk()); - verifyUpdate(client); - } + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> ok(() -> client.get(securityConfigPath()))); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifyUpdate); } @Test public void availableForRestAdminUser() throws Exception { - try (TestRestClient client = localCluster.getRestClient(REST_ADMIN_USER)) { - assertThat(client.get(securityConfigPath()), isOk()); - verifyUpdate(client); - } - try (TestRestClient client = localCluster.getRestClient(REST_API_ADMIN_CONFIG_UPDATE, DEFAULT_PASSWORD)) { - verifyUpdate(client); - } + withUser(REST_ADMIN_USER, client -> ok(() -> client.get(securityConfigPath()))); + withUser(REST_ADMIN_USER, this::verifyUpdate); + withUser(REST_API_ADMIN_CONFIG_UPDATE, this::verifyUpdate); } void verifyUpdate(final TestRestClient client) throws Exception { - assertThat(client.putJson(securityConfigPath("xxx"), EMPTY_BODY), isBadRequest()); + badRequest(() -> client.putJson(securityConfigPath("xxx"), EMPTY_BODY)); verifyNotAllowedMethods(client); TestRestClient.HttpResponse resp = client.get(securityConfigPath()); - assertThat(resp, isOk()); - final var configJson = resp.bodyAsJsonNode(); + final var configJson = ok(() -> client.get(securityConfigPath())).bodyAsJsonNode(); final var authFailureListeners = DefaultObjectMapper.objectMapper.createObjectNode(); authFailureListeners.set( "ip_rate_limiting", @@ -131,31 +114,20 @@ void verifyUpdate(final TestRestClient client) throws Exception { ); final var dynamicConfigJson = (ObjectNode) configJson.get("config").get("dynamic"); dynamicConfigJson.set("auth_failure_listeners", authFailureListeners); - assertThat( - client.putJson(securityConfigPath("config"), DefaultObjectMapper.writeValueAsString(configJson.get("config"), false)), - isOk() - ); + ok(() -> client.putJson(securityConfigPath("config"), DefaultObjectMapper.writeValueAsString(configJson.get("config"), false))); String originalHostResolverMode = configJson.get("config").get("dynamic").get("hosts_resolver_mode").asText(); String nextOriginalHostResolverMode = originalHostResolverMode.equals("other") ? "ip-only" : "other"; - assertThat( - client.patch(securityConfigPath(), patch(replaceOp("/config/dynamic/hosts_resolver_mode", nextOriginalHostResolverMode))), - isOk() - ); - assertThat( - client.patch(securityConfigPath(), patch(replaceOp("/config/dynamic/hosts_resolver_mode", originalHostResolverMode))), - isOk() - ); - TestRestClient.HttpResponse last = client.patch( - securityConfigPath(), - patch(replaceOp("/config/dynamic/hosts_resolver_mode", originalHostResolverMode)) + ok(() -> client.patch(securityConfigPath(), patch(replaceOp("/config/dynamic/hosts_resolver_mode", nextOriginalHostResolverMode)))); + ok(() -> client.patch(securityConfigPath(), patch(replaceOp("/config/dynamic/hosts_resolver_mode", originalHostResolverMode)))); + ok( + () -> client.patch(securityConfigPath(), patch(replaceOp("/config/dynamic/hosts_resolver_mode", originalHostResolverMode))), + "No updates required" ); - assertThat(last, isOk()); - assertResponseBody(last.getBody(), "No updates required"); } - void verifyNotAllowedMethods(final TestRestClient client) { - assertThat(client.postJson(securityConfigPath(), EMPTY_BODY), isNotAllowed()); - assertThat(client.delete(securityConfigPath()), isNotAllowed()); + void verifyNotAllowedMethods(final TestRestClient client) throws Exception { + methodNotAllowed(() -> client.postJson(securityConfigPath(), EMPTY_BODY)); + methodNotAllowed(() -> client.delete(securityConfigPath())); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java b/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java index d7cfe41311..635d9ecff4 100644 --- a/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java @@ -13,13 +13,10 @@ import java.util.List; -import org.junit.ClassRule; import org.junit.Test; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; -import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -27,16 +24,16 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; import static org.opensearch.security.rest.DashboardsInfoAction.DEFAULT_PASSWORD_MESSAGE; import static org.opensearch.security.rest.DashboardsInfoAction.DEFAULT_PASSWORD_REGEX; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class DashboardsInfoTest extends AbstractApiIntegrationTest { - static final TestSecurityConfig.User DASHBOARDS_USER = new TestSecurityConfig.User("dashboards_user").roles( - new Role("dashboards_role").indexPermissions("read").on("*").clusterPermissions("cluster_composite_ops") - ); - - @ClassRule - public static LocalCluster localCluster = clusterBuilder().users(DASHBOARDS_USER).build(); + static { + testSecurityConfig.user( + new TestSecurityConfig.User("dashboards_user").roles( + new Role("dashboards_role").indexPermissions("read").on("*").clusterPermissions("cluster_composite_ops") + ) + ); + } private String apiPath() { return randomFrom(List.of(PLUGINS_PREFIX + "/dashboardsinfo", LEGACY_OPENDISTRO_PREFIX + "/kibanainfo")); @@ -44,11 +41,10 @@ private String apiPath() { @Test public void testDashboardsInfoValidationMessage() throws Exception { - try (TestRestClient client = localCluster.getRestClient(DASHBOARDS_USER)) { - final var response = client.get(apiPath()); - assertThat(response, isOk()); + withUser("dashboards_user", client -> { + final var response = ok(() -> client.get(apiPath())); assertThat(response.getTextFromJsonBody("/password_validation_error_message"), equalTo(DEFAULT_PASSWORD_MESSAGE)); assertThat(response.getTextFromJsonBody("/password_validation_regex"), equalTo(DEFAULT_PASSWORD_REGEX)); - } + }); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoWithSettingsTest.java b/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoWithSettingsTest.java index 8aba10fe35..af8eeb2c8a 100644 --- a/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoWithSettingsTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoWithSettingsTest.java @@ -12,22 +12,18 @@ package org.opensearch.security.api; import java.util.List; +import java.util.Map; -import org.junit.ClassRule; import org.junit.Test; +import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; -import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; -import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE; -import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class DashboardsInfoWithSettingsTest extends AbstractApiIntegrationTest { @@ -36,18 +32,21 @@ public class DashboardsInfoWithSettingsTest extends AbstractApiIntegrationTest { private static final String CUSTOM_PASSWORD_MESSAGE = "Password must be minimum 5 characters long and must contain at least one uppercase letter, one lowercase letter, one digit, and one special character."; - @ClassRule - public static LocalCluster localCluster = clusterBuilder().nodeSetting( - SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, - CUSTOM_PASSWORD_REGEX - ) - .nodeSetting(SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, CUSTOM_PASSWORD_MESSAGE) - .users( + static { + testSecurityConfig.user( new TestSecurityConfig.User("dashboards_user").roles( new Role("dashboards_role").indexPermissions("read").on("*").clusterPermissions("cluster_composite_ops") ) - ) - .build(); + ); + } + + @Override + protected Map getClusterSettings() { + Map clusterSettings = super.getClusterSettings(); + clusterSettings.put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, CUSTOM_PASSWORD_REGEX); + clusterSettings.put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, CUSTOM_PASSWORD_MESSAGE); + return clusterSettings; + } private String apiPath() { return randomFrom(List.of(PLUGINS_PREFIX + "/dashboardsinfo", LEGACY_OPENDISTRO_PREFIX + "/kibanainfo")); @@ -56,11 +55,10 @@ private String apiPath() { @Test public void testDashboardsInfoValidationMessageWithCustomMessage() throws Exception { - try (TestRestClient client = localCluster.getRestClient("dashboards_user", DEFAULT_PASSWORD)) { - final var response = client.get(apiPath()); - assertThat(response, isOk()); + withUser("dashboards_user", client -> { + final var response = ok(() -> client.get(apiPath())); assertThat(response.getTextFromJsonBody("/password_validation_error_message"), equalTo(CUSTOM_PASSWORD_MESSAGE)); assertThat(response.getTextFromJsonBody("/password_validation_regex"), equalTo(CUSTOM_PASSWORD_REGEX)); - } + }); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java index 568f7fea8d..7ac2262899 100644 --- a/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java @@ -11,87 +11,66 @@ package org.opensearch.security.api; -import org.junit.ClassRule; import org.junit.Test; -import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.security.api.PatchPayloadHelper.patch; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; -import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; -import static org.opensearch.test.framework.matcher.RestMatchers.isForbidden; -import static org.opensearch.test.framework.matcher.RestMatchers.isNotAllowed; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class DefaultApiAvailabilityIntegrationTest extends AbstractApiIntegrationTest { - @ClassRule - public static LocalCluster localCluster = clusterBuilder().build(); - @Test public void nodesDnApiIsNotAvailableByDefault() throws Exception { - try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { - assertThat(client.get(apiPath("nodesdn")), isBadRequest()); - assertThat(client.putJson(apiPath("nodesdn", "cluster_1"), EMPTY_BODY), isBadRequest()); - assertThat(client.delete(apiPath("nodesdn", "cluster_1")), isBadRequest()); - assertThat(client.patch(apiPath("nodesdn", "cluster_1"), EMPTY_BODY), isBadRequest()); - } - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - assertThat(client.get(apiPath("nodesdn")), isBadRequest()); - assertThat(client.putJson(apiPath("nodesdn", "cluster_1"), EMPTY_BODY), isBadRequest()); - assertThat(client.delete(apiPath("nodesdn", "cluster_1")), isBadRequest()); - assertThat(client.patch(apiPath("nodesdn", "cluster_1"), EMPTY_BODY), isBadRequest()); - } + withUser(NEW_USER, this::verifyNodesDnApi); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifyNodesDnApi); + } + + private void verifyNodesDnApi(final TestRestClient client) throws Exception { + badRequest(() -> client.get(apiPath("nodesdn"))); + badRequest(() -> client.putJson(apiPath("nodesdn", "cluster_1"), EMPTY_BODY)); + badRequest(() -> client.delete(apiPath("nodesdn", "cluster_1"))); + badRequest(() -> client.patch(apiPath("nodesdn", "cluster_1"), EMPTY_BODY)); } @Test public void securityConfigIsNotAvailableByDefault() throws Exception { - try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { - assertThat(client.get(apiPath("securityconfig")), isForbidden()); + withUser(NEW_USER, client -> { + forbidden(() -> client.get(apiPath("securityconfig"))); verifySecurityConfigApi(client); - } - try (TestRestClient client = localCluster.getAdminCertRestClient()) { - assertThat(client.get(apiPath("securityconfig")), isOk()); + }); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { + ok(() -> client.get(apiPath("securityconfig"))); verifySecurityConfigApi(client); - } + }); } private void verifySecurityConfigApi(final TestRestClient client) throws Exception { - assertThat(client.putJson(apiPath("securityconfig"), EMPTY_BODY), isNotAllowed()); - assertThat(client.postJson(apiPath("securityconfig"), EMPTY_BODY), isNotAllowed()); - assertThat(client.delete(apiPath("securityconfig")), isNotAllowed()); - assertThat(client.patch(apiPath("securityconfig"), patch(replaceOp("/a/b/c", "other"))), isForbidden()); + methodNotAllowed(() -> client.putJson(apiPath("securityconfig"), EMPTY_BODY)); + methodNotAllowed(() -> client.postJson(apiPath("securityconfig"), EMPTY_BODY)); + methodNotAllowed(() -> client.delete(apiPath("securityconfig"))); + forbidden(() -> client.patch(apiPath("securityconfig"), patch(replaceOp("/a/b/c", "other")))); } @Test public void securityHealth() throws Exception { - try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { - assertThat(client.get(securityPath("health")), isOk()); - } - try (TestRestClient client = localCluster.getAdminCertRestClient()) { - assertThat(client.get(securityPath("health")), isOk()); - } + withUser(NEW_USER, client -> ok(() -> client.get(securityPath("health")))); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> ok(() -> client.get(securityPath("health")))); } @Test public void securityAuthInfo() throws Exception { - try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { - verifyAuthInfoApi(client); - } - try (TestRestClient client = localCluster.getAdminCertRestClient()) { - verifyAuthInfoApi(client); - } + withUser(NEW_USER, this::verifyAuthInfoApi); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifyAuthInfoApi); } private void verifyAuthInfoApi(final TestRestClient client) throws Exception { final var verbose = randomBoolean(); final TestRestClient.HttpResponse response; - if (verbose) response = client.get(securityPath("authinfo?verbose=" + verbose)); - else response = client.get(securityPath("authinfo")); - assertThat(response, isOk()); + if (verbose) response = ok(() -> client.get(securityPath("authinfo?verbose=" + verbose))); + else response = ok(() -> client.get(securityPath("authinfo"))); final var body = response.bodyAsJsonNode(); assertThat(response.getBody(), body.has("user")); assertThat(response.getBody(), body.has("user_name")); @@ -115,14 +94,14 @@ private void verifyAuthInfoApi(final TestRestClient client) throws Exception { @Test public void reloadSSLCertsNotAvailable() throws Exception { - try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { - assertThat(client.putJson(apiPath("ssl", "http", "reloadcerts"), EMPTY_BODY), isForbidden()); - assertThat(client.putJson(apiPath("ssl", "transport", "reloadcerts"), EMPTY_BODY), isForbidden()); - } - try (TestRestClient client = localCluster.getAdminCertRestClient()) { - assertThat(client.putJson(apiPath("ssl", "http", "reloadcerts"), EMPTY_BODY), isBadRequest()); - assertThat(client.putJson(apiPath("ssl", "transport", "reloadcerts"), EMPTY_BODY), isBadRequest()); - } + withUser(NEW_USER, client -> { + forbidden(() -> client.putJson(apiPath("ssl", "http", "reloadcerts"), EMPTY_BODY)); + forbidden(() -> client.putJson(apiPath("ssl", "transport", "reloadcerts"), EMPTY_BODY)); + }); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { + badRequest(() -> client.putJson(apiPath("ssl", "http", "reloadcerts"), EMPTY_BODY)); + badRequest(() -> client.putJson(apiPath("ssl", "transport", "reloadcerts"), EMPTY_BODY)); + }); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/FlushCacheApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/FlushCacheApiIntegrationTest.java index 58782b578d..2879a43c93 100644 --- a/src/integrationTest/java/org/opensearch/security/api/FlushCacheApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/FlushCacheApiIntegrationTest.java @@ -11,25 +11,15 @@ package org.opensearch.security.api; -import org.junit.ClassRule; import org.junit.Test; -import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.test.framework.cluster.TestRestClient; - import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.security.dlic.rest.support.Utils.PLUGINS_PREFIX; -import static org.opensearch.test.framework.matcher.RestMatchers.isForbidden; -import static org.opensearch.test.framework.matcher.RestMatchers.isNotAllowed; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class FlushCacheApiIntegrationTest extends AbstractApiIntegrationTest { private final static String TEST_USER = "testuser"; - @ClassRule - public static LocalCluster localCluster = clusterBuilder().build(); - private String cachePath() { return super.apiPath("cache"); } @@ -45,30 +35,26 @@ protected String apiPathPrefix() { @Test public void testFlushCache() throws Exception { - try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { - assertThat(client.delete(cachePath()), isForbidden()); - assertThat(client.delete(cachePath(TEST_USER)), isForbidden()); - } - try (TestRestClient client = localCluster.getAdminCertRestClient()) { - assertThat(client.get(cachePath()), isNotAllowed()); - assertThat(client.postJson(cachePath(), EMPTY_BODY), isNotAllowed()); - assertThat(client.putJson(cachePath(), EMPTY_BODY), isNotAllowed()); - - final var deleteAllCacheResponse = client.delete(cachePath()); - assertThat(deleteAllCacheResponse, isOk()); + withUser(NEW_USER, client -> { + forbidden(() -> client.delete(cachePath())); + forbidden(() -> client.delete(cachePath(TEST_USER))); + }); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { + methodNotAllowed(() -> client.get(cachePath())); + methodNotAllowed(() -> client.postJson(cachePath(), EMPTY_BODY)); + methodNotAllowed(() -> client.putJson(cachePath(), EMPTY_BODY)); + final var deleteAllCacheResponse = ok(() -> client.delete(cachePath())); assertThat( deleteAllCacheResponse.getBody(), deleteAllCacheResponse.getTextFromJsonBody("/message"), is("Cache flushed successfully.") ); - - final var deleteUserCacheResponse = client.delete(cachePath(TEST_USER)); - assertThat(deleteUserCacheResponse, isOk()); + final var deleteUserCacheResponse = ok(() -> client.delete(cachePath(TEST_USER))); assertThat( deleteUserCacheResponse.getBody(), deleteUserCacheResponse.getTextFromJsonBody("/message"), is("Cache invalidated for user: " + TEST_USER) ); - } + }); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java index 76dd413454..684f30e60b 100644 --- a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java @@ -11,40 +11,37 @@ package org.opensearch.security.api; +import java.util.Map; import java.util.StringJoiner; -import org.junit.ClassRule; import org.junit.Test; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.security.dlic.rest.validation.PasswordValidator; import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.support.ConfigConstants; -import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.test.framework.cluster.TestRestClient; -import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.security.api.PatchPayloadHelper.addOp; import static org.opensearch.security.api.PatchPayloadHelper.patch; -import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; -import static org.opensearch.test.framework.matcher.RestMatchers.isCreated; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class InternalUsersRegExpPasswordRulesRestApiIntegrationTest extends AbstractApiIntegrationTest { final static String PASSWORD_VALIDATION_ERROR_MESSAGE = "xxxxxxxx"; - @ClassRule - public static LocalCluster localCluster = clusterBuilder().nodeSetting( - ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, - PASSWORD_VALIDATION_ERROR_MESSAGE - ) - .nodeSetting( + @Override + protected Map getClusterSettings() { + Map clusterSettings = super.getClusterSettings(); + clusterSettings.put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, PASSWORD_VALIDATION_ERROR_MESSAGE); + clusterSettings.put( ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, "(?=.*[A-Z])(?=.*[^a-zA-Z\\\\d])(?=.*[0-9])(?=.*[a-z]).{8,}" - ) - .nodeSetting(ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH, PasswordValidator.ScoreStrength.FAIR.name()) - .build(); + ); + clusterSettings.put( + ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH, + PasswordValidator.ScoreStrength.FAIR.name() + ); + return clusterSettings; + } String internalUsers(String... path) { final var fullPath = new StringJoiner("/").add(super.apiPath("internalusers")); @@ -64,42 +61,46 @@ ToXContentObject internalUserWithPassword(final String password) { @Test public void canNotCreateUsersWithPassword() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + withUser(ADMIN_USER_NAME, client -> { // validate short passwords - assertThat(client.putJson(internalUsers("tooshoort"), internalUserWithPassword("")), isBadRequest()); - assertThat(client.putJson(internalUsers("tooshoort"), internalUserWithPassword("123")), isBadRequest()); - assertThat(client.putJson(internalUsers("tooshoort"), internalUserWithPassword("1234567")), isBadRequest()); - assertThat(client.putJson(internalUsers("tooshoort"), internalUserWithPassword("1Aa%")), isBadRequest()); - assertThat(client.putJson(internalUsers("tooshoort"), internalUserWithPassword("123456789")), isBadRequest()); - assertThat(client.putJson(internalUsers("tooshoort"), internalUserWithPassword("a123456789")), isBadRequest()); - assertThat(client.putJson(internalUsers("tooshoort"), internalUserWithPassword("A123456789")), isBadRequest()); + badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword(""))); + badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("123"))); + badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("1234567"))); + badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("1Aa%"))); + badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("123456789"))); + badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("a123456789"))); + badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("A123456789"))); // validate that password same as user - assertThat(client.putJson(internalUsers("$1aAAAAAAAAC"), internalUserWithPassword("$1aAAAAAAAAC")), isBadRequest()); - assertThat(client.putJson(internalUsers("$1aAAAAAAAac"), internalUserWithPassword("$1aAAAAAAAAC")), isBadRequest()); - final var r = client.patch( - internalUsers(), - patch( - addOp("testuser1", internalUserWithPassword("$aA123456789")), - addOp("testuser2", internalUserWithPassword("testpassword2")) - ) + badRequest(() -> client.putJson(internalUsers("$1aAAAAAAAAC"), internalUserWithPassword("$1aAAAAAAAAC"))); + badRequest(() -> client.putJson(internalUsers("$1aAAAAAAAac"), internalUserWithPassword("$1aAAAAAAAAC"))); + badRequestWithReason( + () -> client.patch( + internalUsers(), + patch( + addOp("testuser1", internalUserWithPassword("$aA123456789")), + addOp("testuser2", internalUserWithPassword("testpassword2")) + ) + ), + PASSWORD_VALIDATION_ERROR_MESSAGE ); - assertThat(r, isBadRequest("/reason", PASSWORD_VALIDATION_ERROR_MESSAGE)); // validate similarity - final var r2 = client.putJson(internalUsers("some_user_name"), internalUserWithPassword("H3235,cc,some_User_Name")); - assertThat(r2, isBadRequest("/reason", RequestContentValidator.ValidationError.SIMILAR_PASSWORD.message())); - } + badRequestWithReason( + () -> client.putJson(internalUsers("some_user_name"), internalUserWithPassword("H3235,cc,some_User_Name")), + RequestContentValidator.ValidationError.SIMILAR_PASSWORD.message() + ); + }); } @Test public void canCreateUsersWithPassword() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - assertThat(client.putJson(internalUsers("ok1"), internalUserWithPassword("$aA123456789")), isCreated()); - assertThat(client.putJson(internalUsers("ok2"), internalUserWithPassword("$Aa123456789")), isCreated()); - assertThat(client.putJson(internalUsers("ok3"), internalUserWithPassword("$1aAAAAAAAAA")), isCreated()); - assertThat(client.putJson(internalUsers("ok3"), internalUserWithPassword("$1aAAAAAAAAC")), isOk()); - assertThat(client.patch(internalUsers(), patch(addOp("ok3", internalUserWithPassword("$1aAAAAAAAAB")))), isOk()); - assertThat(client.putJson(internalUsers("ok1"), internalUserWithPassword("Admin_123")), isOk()); - } + withUser(ADMIN_USER_NAME, client -> { + created(() -> client.putJson(internalUsers("ok1"), internalUserWithPassword("$aA123456789"))); + created(() -> client.putJson(internalUsers("ok2"), internalUserWithPassword("$Aa123456789"))); + created(() -> client.putJson(internalUsers("ok3"), internalUserWithPassword("$1aAAAAAAAAA"))); + ok(() -> client.putJson(internalUsers("ok3"), internalUserWithPassword("$1aAAAAAAAAC"))); + ok(() -> client.patch(internalUsers(), patch(addOp("ok3", internalUserWithPassword("$1aAAAAAAAAB"))))); + ok(() -> client.putJson(internalUsers("ok1"), internalUserWithPassword("Admin_123"))); + }); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java index 7cd184f590..a87121297f 100644 --- a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java @@ -25,7 +25,6 @@ import com.fasterxml.jackson.databind.JsonNode; import org.apache.http.HttpStatus; import org.junit.Assert; -import org.junit.ClassRule; import org.junit.Test; import org.opensearch.common.xcontent.XContentType; @@ -34,23 +33,17 @@ import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.opensearch.security.api.PatchPayloadHelper.addOp; import static org.opensearch.security.api.PatchPayloadHelper.patch; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; import static org.opensearch.security.dlic.rest.api.InternalUsersApiAction.RESTRICTED_FROM_USERNAME; -import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; -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.isNotAllowed; -import static org.opensearch.test.framework.matcher.RestMatchers.isNotFound; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class InternalUsersRestApiIntegrationTest extends AbstractConfigEntityApiIntegrationTest { @@ -64,20 +57,15 @@ public class InternalUsersRestApiIntegrationTest extends AbstractConfigEntityApi private final static String SOME_ROLE = "some-role"; - @ClassRule - public static LocalCluster localCluster = clusterBuilder().users( - new TestSecurityConfig.User(SERVICE_ACCOUNT_USER).attr("service", "true").attr("enabled", "true"), - new TestSecurityConfig.User(REST_API_ADMIN_INTERNAL_USERS_ONLY).roles( - REST_ADMIN_REST_API_ACCESS_ROLE, - new TestSecurityConfig.Role("rest_admin_role").clusterPermissions(restAdminPermission(Endpoint.INTERNALUSERS)) - ) - ) - .roles( - new TestSecurityConfig.Role(HIDDEN_ROLE).hidden(true), - new TestSecurityConfig.Role(RESERVED_ROLE).reserved(true), - new TestSecurityConfig.Role(SOME_ROLE) - ) - .build(); + static { + testSecurityConfig.withRestAdminUser(REST_API_ADMIN_INTERNAL_USERS_ONLY, restAdminPermission(Endpoint.INTERNALUSERS)) + .user(new TestSecurityConfig.User(SERVICE_ACCOUNT_USER).attr("service", "true").attr("enabled", "true")) + .roles( + new TestSecurityConfig.Role(HIDDEN_ROLE).hidden(true), + new TestSecurityConfig.Role(RESERVED_ROLE).reserved(true), + new TestSecurityConfig.Role(SOME_ROLE) + ); + } public InternalUsersRestApiIntegrationTest() { super("internalusers", new TestDescriptor() { @@ -104,26 +92,6 @@ public Optional restAdminLimitedUser() { }); } - @Test - public void forbiddenForRegularUsers() throws Exception { - super.forbiddenForRegularUsers(localCluster); - } - - @Test - public void availableForAdminUser() throws Exception { - super.availableForAdminUser(localCluster); - } - - @Test - public void availableForTLSAdminUser() throws Exception { - super.availableForTLSAdminUser(localCluster); - } - - @Test - public void availableForRESTAdminUser() throws Exception { - super.availableForRESTAdminUser(localCluster); - } - static ToXContentObject internalUserWithPassword(final String password) { return internalUser(null, null, null, password, null, null, null); } @@ -225,59 +193,60 @@ static ToXContentObject serviceUser(final Boolean enabled, final String password @Override void verifyBadRequestOperations(TestRestClient client) throws Exception { // bad query string parameter name - assertThat(client.get(apiPath() + "?aaaaa=bbbbb"), isBadRequest()); + badRequest(() -> client.get(apiPath() + "?aaaaa=bbbbb")); final var predefinedUserName = randomAsciiAlphanumOfLength(4); - assertThat( - client.putJson( + created( + () -> client.putJson( apiPath(predefinedUserName), internalUser(randomAsciiAlphanumOfLength(10), configJsonArray(generateArrayValues(false)), null, null) - ), - isCreated() + ) ); invalidJson(client, predefinedUserName); } void invalidJson(final TestRestClient client, final String predefinedUserName) throws Exception { // put - assertThat(client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY), isBadRequest()); - assertThat(client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { + badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY)); + badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { builder.startObject(); builder.field("backend_roles"); randomConfigArray(false).toXContent(builder, params); builder.field("backend_roles"); randomConfigArray(false).toXContent(builder, params); return builder.endObject(); - }), isBadRequest()); - HttpResponse response = client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { + })); + assertInvalidKeys(badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { builder.startObject(); builder.field("unknown_json_property"); configJsonArray("a", "b").toXContent(builder, params); builder.field("backend_roles"); randomConfigArray(false).toXContent(builder, params); return builder.endObject(); - }); - assertThat(response, isBadRequest()); - assertInvalidKeys(response, "unknown_json_property"); - - response = client.putJson( - apiPath(randomAsciiAlphanumOfLength(10)), - (builder, params) -> builder.startObject() - .field("password", configJsonArray("a", "b")) - .field("hash") - .nullValue() - .field("backend_roles", "c") - .field("attributes", "d") - .field("opendistro_security_roles", "e") - .endObject() - ); - assertThat( - response, - isBadRequest().withAttribute("/status", "error") - .withAttribute("/password", "String expected") - .withAttribute("/hash", "String expected") - .withAttribute("/backend_roles", "Array expected") - .withAttribute("/attributes", "Object expected") - .withAttribute("/opendistro_security_roles", "Array expected") + })), "unknown_json_property"); + assertWrongDataType( + client.putJson( + apiPath(randomAsciiAlphanumOfLength(10)), + (builder, params) -> builder.startObject() + .field("password", configJsonArray("a", "b")) + .field("hash") + .nullValue() + .field("backend_roles", "c") + .field("attributes", "d") + .field("opendistro_security_roles", "e") + .endObject() + ), + Map.of( + "password", + "String expected", + "hash", + "String expected", + "backend_roles", + "Array expected", + "attributes", + "Object expected", + "opendistro_security_roles", + "Array expected" + ) ); assertNullValuesInArray( client.putJson( @@ -292,22 +261,21 @@ void invalidJson(final TestRestClient client, final String predefinedUserName) t ) ); // patch - assertThat(client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(10), EMPTY_BODY))), isBadRequest()); - assertThat( - client.patch( + badRequest(() -> client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(10), EMPTY_BODY)))); + badRequest( + () -> client.patch( apiPath(predefinedUserName), patch(replaceOp(randomFrom(List.of("opendistro_security_roles", "backend_roles", "attributes")), EMPTY_BODY)) - ), - isBadRequest() + ) ); - assertThat(client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(5), (ToXContentObject) (builder, params) -> { + badRequest(() -> client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(5), (ToXContentObject) (builder, params) -> { builder.startObject(); builder.field("unknown_json_property"); configJsonArray("a", "b").toXContent(builder, params); builder.field("backend_roles"); randomConfigArray(false).toXContent(builder, params); return builder.endObject(); - }))), isBadRequest()); + })))); assertWrongDataType( client.patch( apiPath(), @@ -370,7 +338,7 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien randomAttributes(), randomSecurityRoles() ); - assertThat(client.putJson(apiPath(usernamePut), newUserJsonPut), isCreated()); + created(() -> client.putJson(apiPath(usernamePut), newUserJsonPut)); assertInternalUser( ok(() -> client.get(apiPath(usernamePut))).bodyAsJsonNode().get(usernamePut), hidden, @@ -385,15 +353,15 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien randomAttributes(), randomSecurityRoles() ); - assertThat(client.putJson(apiPath(usernamePut), updatedUserJsonPut), isOk()); + ok(() -> client.putJson(apiPath(usernamePut), updatedUserJsonPut)); assertInternalUser( ok(() -> client.get(apiPath(usernamePut))).bodyAsJsonNode().get(usernamePut), hidden, reserved, Strings.toString(XContentType.JSON, updatedUserJsonPut) ); - assertThat(client.delete(apiPath(usernamePut)), isOk()); - assertThat(client.get(apiPath(usernamePut)), isNotFound()); + ok(() -> client.delete(apiPath(usernamePut))); + notFound(() -> client.get(apiPath(usernamePut))); // patch // TODO related to issue #4426 final var usernamePatch = randomAsciiAlphanumOfLength(10); @@ -405,24 +373,22 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien (builder, params) -> builder.startObject().endObject(), configJsonArray() ); - assertThat(client.patch(apiPath(), patch(addOp(usernamePatch, newUserJsonPatch))), isOk()); + ok(() -> client.patch(apiPath(), patch(addOp(usernamePatch, newUserJsonPatch)))); assertInternalUser( ok(() -> client.get(apiPath(usernamePatch))).bodyAsJsonNode().get(usernamePatch), hidden, reserved, Strings.toString(XContentType.JSON, newUserJsonPatch) ); - assertThat(client.patch(apiPath(usernamePatch), patch(replaceOp("backend_roles", configJsonArray("c", "d")))), isOk()); - assertThat( - client.patch( + ok(() -> client.patch(apiPath(usernamePatch), patch(replaceOp("backend_roles", configJsonArray("c", "d"))))); + ok( + () -> client.patch( apiPath(usernamePatch), patch(addOp("attributes", (ToXContentObject) (builder, params) -> builder.startObject().field("a", "b").endObject())) - ), - isOk() + ) ); - assertThat( - client.patch(apiPath(usernamePatch), patch(addOp("opendistro_security_roles", configJsonArray(RESERVED_ROLE, SOME_ROLE)))), - isOk() + ok( + () -> client.patch(apiPath(usernamePatch), patch(addOp("opendistro_security_roles", configJsonArray(RESERVED_ROLE, SOME_ROLE)))) ); } @@ -466,58 +432,56 @@ String filterBy(final String value) { @Test public void filters() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + withUser(ADMIN_USER_NAME, client -> { assertFilterByUsers(ok(() -> client.get(apiPath())), true, true); assertFilterByUsers(ok(() -> client.get(filterBy("any"))), true, true); assertFilterByUsers(ok(() -> client.get(filterBy("internal"))), false, true); assertFilterByUsers(ok(() -> client.get(filterBy("service"))), true, false); assertFilterByUsers(ok(() -> client.get(filterBy("something"))), true, true); - } + }); } void assertFilterByUsers(final HttpResponse response, final boolean hasServiceUser, final boolean hasInternalUser) { assertThat(response.getBody(), response.bodyAsJsonNode().has(SERVICE_ACCOUNT_USER), is(hasServiceUser)); - assertThat(response.getBody(), response.bodyAsJsonNode().has(NEW_USER.getName()), is(hasInternalUser)); + assertThat(response.getBody(), response.bodyAsJsonNode().has(NEW_USER), is(hasInternalUser)); } @Test public void verifyPOSTOnlyForAuthTokenEndpoint() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - assertThat(client.post(apiPath(ADMIN_USER.getName(), "authtoken")), isBadRequest()); - assertThat(client.post(apiPath(SERVICE_ACCOUNT_USER, "authtoken")), isOk()); + withUser(ADMIN_USER_NAME, client -> { + badRequest(() -> client.post(apiPath(ADMIN_USER_NAME, "authtoken"))); + ok(() -> client.post(apiPath(SERVICE_ACCOUNT_USER, "authtoken"))); /* should be notImplement but the call doesn't reach {@link org.opensearch.security.dlic.rest.api.InternalUsersApiAction#withAuthTokenPath(RestRequest)} */ - assertThat(client.post(apiPath("randomPath")), isNotAllowed()); - } + methodNotAllowed(() -> client.post(apiPath("randomPath"))); + }); } @Test public void userApiWithDotsInName() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + withUser(ADMIN_USER_NAME, client -> { for (final var dottedUserName : List.of(".my.dotuser0", ".my.dot.user0")) { - assertThat( - client.putJson( + created( + () -> client.putJson( apiPath(dottedUserName), (builder, params) -> builder.startObject().field("password", randomAsciiAlphanumOfLength(10)).endObject() - ), - isCreated() + ) ); } for (final var dottedUserName : List.of(".my.dotuser1", ".my.dot.user1")) { - assertThat( - client.putJson( + created( + () -> client.putJson( apiPath(dottedUserName), (builder, params) -> builder.startObject() .field("hash", passwordHasher.hash(randomAsciiAlphanumOfLength(10).toCharArray())) .endObject() - ), - isCreated() + ) ); } for (final var dottedUserName : List.of(".my.dotuser2", ".my.dot.user2")) { - assertThat( - client.patch( + ok( + () -> client.patch( apiPath(), patch( addOp( @@ -527,13 +491,12 @@ public void userApiWithDotsInName() throws Exception { .endObject() ) ) - ), - isOk() + ) ); } for (final var dottedUserName : List.of(".my.dotuser3", ".my.dot.user3")) { - assertThat( - client.patch( + ok( + () -> client.patch( apiPath(), patch( addOp( @@ -543,98 +506,91 @@ public void userApiWithDotsInName() throws Exception { .endObject() ) ) - ), - isOk() + ) ); } - } + }); } @Test public void noPasswordChange() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - assertThat( - client.putJson( + withUser(ADMIN_USER_NAME, client -> { + created( + () -> client.putJson( apiPath("user1"), (builder, params) -> builder.startObject() .field("hash", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m") .endObject() - ), - isCreated() + ) ); - assertThat( - client.putJson( + badRequest( + () -> client.putJson( apiPath("user1"), (builder, params) -> builder.startObject() .field("hash", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m") .field("password", "") .field("backend_roles", configJsonArray("admin", "role_a")) .endObject() - ), - isBadRequest() + ) ); - assertThat( - client.putJson( + ok( + () -> client.putJson( apiPath("user1"), (builder, params) -> builder.startObject() .field("hash", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m") .field("password", randomAsciiAlphanumOfLength(10)) .field("backend_roles", configJsonArray("admin", "role_a")) .endObject() - ), - isOk() + ) ); - assertThat( - client.putJson( + created( + () -> client.putJson( apiPath("user2"), (builder, params) -> builder.startObject() .field("hash", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m") .field("password", randomAsciiAlphanumOfLength(10)) .endObject() - ), - isCreated() + ) ); - assertThat( - client.putJson( + badRequest( + () -> client.putJson( apiPath("user2"), (builder, params) -> builder.startObject() .field("password", "") .field("backend_roles", configJsonArray("admin", "role_b")) .endObject() - ), - isBadRequest() + ) ); - assertThat( - client.putJson( + ok( + () -> client.putJson( apiPath("user2"), (builder, params) -> builder.startObject() .field("password", randomAsciiAlphanumOfLength(10)) .field("backend_roles", configJsonArray("admin", "role_b")) .endObject() - ), - isOk() + ) ); - } + }); } @Test public void securityRoles() throws Exception { final var userWithSecurityRoles = randomAsciiAlphanumOfLength(15); final var userWithSecurityRolesPassword = randomAsciiAlphanumOfLength(10); - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - assertThat( - client.patch(apiPath(), patch(addOp(userWithSecurityRoles, internalUser(userWithSecurityRolesPassword, null, null, null)))), - isOk() - ); - } - - try (TestRestClient client = localCluster.getRestClient(userWithSecurityRoles, userWithSecurityRolesPassword)) { - assertThat(client.get(apiPath()), isForbidden()); - } - - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - assertThat( - client.patch( + withUser( + ADMIN_USER_NAME, + client -> ok( + () -> client.patch( + apiPath(), + patch(addOp(userWithSecurityRoles, internalUser(userWithSecurityRolesPassword, null, null, null))) + ) + ) + ); + withUser(userWithSecurityRoles, userWithSecurityRolesPassword, client -> forbidden(() -> client.get(apiPath()))); + withUser( + ADMIN_USER_NAME, + client -> ok( + () -> client.patch( apiPath(), patch( replaceOp( @@ -647,48 +603,38 @@ public void securityRoles() throws Exception { ) ) ) - ), - isOk() - ); - } - - try (TestRestClient client = localCluster.getRestClient(userWithSecurityRoles, userWithSecurityRolesPassword)) { - assertThat(client.get(apiPath()), isOk()); - } - - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - impossibleToSetHiddenRoleIsNotAllowed(userWithSecurityRoles, client); - settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client); - } - - try (TestRestClient client = localCluster.getAdminCertRestClient()) { - settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client); - canAssignedHiddenRole(client); - } - - try (TestRestClient client = localCluster.getRestClient(REST_ADMIN_USER)) { - settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client); - canAssignedHiddenRole(client); - } + ) + ) + ); + withUser(userWithSecurityRoles, userWithSecurityRolesPassword, client -> ok(() -> client.get(apiPath()))); + withUser(ADMIN_USER_NAME, client -> impossibleToSetHiddenRoleIsNotAllowed(userWithSecurityRoles, client)); + withUser(ADMIN_USER_NAME, client -> settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client)); + + withUser( + ADMIN_USER_NAME, + localCluster.getAdminCertificate(), + client -> settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client) + ); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::canAssignedHiddenRole); - try (TestRestClient client = localCluster.getRestClient(REST_API_ADMIN_INTERNAL_USERS_ONLY, DEFAULT_PASSWORD)) { - settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client); - canAssignedHiddenRole(client); + for (final var restAdminUser : List.of(REST_ADMIN_USER, REST_API_ADMIN_INTERNAL_USERS_ONLY)) { + withUser(restAdminUser, client -> settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client)); + withUser(restAdminUser, localCluster.getAdminCertificate(), this::canAssignedHiddenRole); } } void impossibleToSetHiddenRoleIsNotAllowed(final String predefinedUserName, final TestRestClient client) throws Exception { // put - assertThat( - client.putJson( + notFound( + () -> client.putJson( apiPath(randomAsciiAlphanumOfLength(10)), internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray(HIDDEN_ROLE)) ), - isNotFound().withAttribute("/message", "Resource 'hidden-role' is not available.") + "Resource 'hidden-role' is not available." ); // patch - assertThat( - client.patch( + notFound( + () -> client.patch( apiPath(), patch( addOp( @@ -696,34 +642,35 @@ void impossibleToSetHiddenRoleIsNotAllowed(final String predefinedUserName, fina internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray(HIDDEN_ROLE)) ) ) - ), - isNotFound() + ) ); // TODO related to issue #4426 - assertThat( - client.patch(apiPath(predefinedUserName), patch(addOp("opendistro_security_roles", configJsonArray(HIDDEN_ROLE)))), - isNotFound() + notFound( + () -> client.patch(apiPath(predefinedUserName), patch(addOp("opendistro_security_roles", configJsonArray(HIDDEN_ROLE)))) + ); } void canAssignedHiddenRole(final TestRestClient client) throws Exception { final var userNamePut = randomAsciiAlphanumOfLength(4); - assertThat( - client.putJson(apiPath(userNamePut), internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray(HIDDEN_ROLE))), - isCreated() + created( + () -> client.putJson( + apiPath(userNamePut), + internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray(HIDDEN_ROLE)) + ) ); } void settingOfUnknownRoleIsNotAllowed(final String predefinedUserName, final TestRestClient client) throws Exception { - assertThat( - client.putJson( + notFound( + () -> client.putJson( apiPath(randomAsciiAlphanumOfLength(10)), internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray("unknown-role")) ), - isNotFound().withAttribute("/message", "role 'unknown-role' not found.") + "role 'unknown-role' not found." ); - assertThat( - client.patch( + notFound( + () -> client.patch( apiPath(), patch( addOp( @@ -731,18 +678,16 @@ void settingOfUnknownRoleIsNotAllowed(final String predefinedUserName, final Tes internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray("unknown-role")) ) ) - ), - isNotFound() + ) ); - assertThat( - client.patch(apiPath(predefinedUserName), patch(addOp("opendistro_security_roles", configJsonArray("unknown-role")))), - isNotFound() + notFound( + () -> client.patch(apiPath(predefinedUserName), patch(addOp("opendistro_security_roles", configJsonArray("unknown-role")))) ); } @Test public void parallelPutRequests() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + withUser(ADMIN_USER_NAME, client -> { final var userName = randomAsciiAlphanumOfLength(10); final var httpResponses = new HttpResponse[10]; @@ -777,41 +722,40 @@ public void parallelPutRequests() throws Exception { break; } } - } + }); } @Test public void restrictedUsernameContents() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + withUser(ADMIN_USER_NAME, client -> { { for (final var restrictedTerm : RESTRICTED_FROM_USERNAME) { for (final var username : List.of( randomAsciiAlphanumOfLength(2) + restrictedTerm + randomAsciiAlphanumOfLength(3), URLEncoder.encode(randomAsciiAlphanumOfLength(4) + ":" + randomAsciiAlphanumOfLength(3), StandardCharsets.UTF_8) )) { - assertThat( - client.putJson(apiPath(username), internalUserWithPassword(randomAsciiAlphanumOfLength(10))), - isBadRequest("/message", restrictedTerm) + final var putResponse = badRequest( + () -> client.putJson(apiPath(username), internalUserWithPassword(randomAsciiAlphanumOfLength(10))) ); - assertThat( - client.patch(apiPath(), patch(addOp(username, internalUserWithPassword(randomAsciiAlphanumOfLength(10))))), - isBadRequest("/message", restrictedTerm) + assertThat(putResponse.getBody(), containsString(restrictedTerm)); + final var patchResponse = badRequest( + () -> client.patch(apiPath(), patch(addOp(username, internalUserWithPassword(randomAsciiAlphanumOfLength(10))))) ); + assertThat(patchResponse.getBody(), containsString(restrictedTerm)); } } } - } + }); } @Test public void serviceUsers() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + withUser(ADMIN_USER_NAME, client -> { // Add enabled service account then get it // TODO related to issue #4426 add default behave when enabled is true final var happyServiceLiveUserName = randomAsciiAlphanumOfLength(10); - assertThat(client.putJson(apiPath(happyServiceLiveUserName), serviceUser(true)), isCreated()); - final var serviceLiveResponse = client.get(apiPath(happyServiceLiveUserName)); - assertThat(serviceLiveResponse, isOk()); + created(() -> client.putJson(apiPath(happyServiceLiveUserName), serviceUser(true))); + final var serviceLiveResponse = ok(() -> client.get(apiPath(happyServiceLiveUserName))); assertThat( serviceLiveResponse.getBody(), serviceLiveResponse.getBooleanFromJsonBody("/" + happyServiceLiveUserName + "/attributes/service") @@ -823,9 +767,8 @@ public void serviceUsers() throws Exception { // Add disabled service account final var happyServiceDeadUserName = randomAsciiAlphanumOfLength(10); - assertThat(client.putJson(apiPath(happyServiceDeadUserName), serviceUser(false)), isCreated()); - final var serviceDeadResponse = client.get(apiPath(happyServiceDeadUserName)); - assertThat(serviceDeadResponse, isOk()); + created(() -> client.putJson(apiPath(happyServiceDeadUserName), serviceUser(false))); + final var serviceDeadResponse = ok(() -> client.get(apiPath(happyServiceDeadUserName))); assertThat( serviceDeadResponse.getBody(), serviceDeadResponse.getBooleanFromJsonBody("/" + happyServiceDeadUserName + "/attributes/service") @@ -835,27 +778,27 @@ public void serviceUsers() throws Exception { not(serviceDeadResponse.getBooleanFromJsonBody("/" + happyServiceDeadUserName + "/attributes/enabled")) ); // Add service account with password -- Should Fail - assertThat( - client.putJson(apiPath(randomAsciiAlphanumOfLength(10)), serviceUserWithPassword(true, randomAsciiAlphanumOfLength(10))), - isBadRequest() + badRequest( + () -> client.putJson( + apiPath(randomAsciiAlphanumOfLength(10)), + serviceUserWithPassword(true, randomAsciiAlphanumOfLength(10)) + ) ); // Add service with hash -- should fail - assertThat( - client.putJson( + badRequest( + () -> client.putJson( apiPath(randomAsciiAlphanumOfLength(10)), serviceUserWithHash(true, passwordHasher.hash(randomAsciiAlphanumOfLength(10).toCharArray())) - ), - isBadRequest() + ) ); // Add Service account with password & Hash -- should fail final var password = randomAsciiAlphanumOfLength(10); - assertThat( - client.putJson( + badRequest( + () -> client.putJson( apiPath(randomAsciiAlphanumOfLength(10)), serviceUser(true, password, passwordHasher.hash(password.toCharArray())) - ), - isBadRequest() + ) ); - } + }); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java index 8939b24301..b18a0c6fd6 100644 --- a/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java @@ -11,28 +11,26 @@ package org.opensearch.security.api; +import java.util.Map; import java.util.StringJoiner; -import org.junit.ClassRule; import org.junit.Test; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.support.ConfigConstants; -import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.test.framework.cluster.TestRestClient; -import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.security.api.PatchPayloadHelper.addOp; import static org.opensearch.security.api.PatchPayloadHelper.patch; -import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; -import static org.opensearch.test.framework.matcher.RestMatchers.isCreated; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest extends AbstractApiIntegrationTest { - @ClassRule - public static LocalCluster localCluster = clusterBuilder().nodeSetting(ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, 9).build(); + @Override + protected Map getClusterSettings() { + Map clusterSettings = super.getClusterSettings(); + clusterSettings.put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, 9); + return clusterSettings; + } String internalUsers(String... path) { final var fullPath = new StringJoiner("/").add(super.apiPath("internalusers")); @@ -52,45 +50,38 @@ ToXContentObject internalUserWithPassword(final String password) { @Test public void canNotCreateUsersWithPassword() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - final var r1 = client.putJson(internalUsers("admin"), internalUserWithPassword("password89")); - assertThat(r1, isBadRequest()); - assertThat( - r1.getTextFromJsonBody("/reason"), - org.hamcrest.Matchers.containsString(RequestContentValidator.ValidationError.WEAK_PASSWORD.message()) + withUser(ADMIN_USER_NAME, client -> { + badRequestWithReason( + () -> client.putJson(internalUsers("admin"), internalUserWithPassword("password89")), + RequestContentValidator.ValidationError.WEAK_PASSWORD.message() ); - - final var r2 = client.putJson(internalUsers("admin"), internalUserWithPassword("A123456789")); - assertThat(r2, isBadRequest()); - assertThat( - r2.getTextFromJsonBody("/reason"), - org.hamcrest.Matchers.containsString(RequestContentValidator.ValidationError.WEAK_PASSWORD.message()) + badRequestWithReason( + () -> client.putJson(internalUsers("admin"), internalUserWithPassword("A123456789")), + RequestContentValidator.ValidationError.WEAK_PASSWORD.message() ); - - final var r3 = client.putJson(internalUsers("admin"), internalUserWithPassword(randomAsciiAlphanumOfLengthBetween(2, 8))); - assertThat(r3, isBadRequest()); - assertThat( - r3.getTextFromJsonBody("/reason"), - org.hamcrest.Matchers.containsString(RequestContentValidator.ValidationError.INVALID_PASSWORD_TOO_SHORT.message()) + badRequestWithReason( + () -> client.putJson(internalUsers("admin"), internalUserWithPassword(randomAsciiAlphanumOfLengthBetween(2, 8))), + RequestContentValidator.ValidationError.INVALID_PASSWORD_TOO_SHORT.message() ); - } + }); } @Test public void canCreateUserWithPassword() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - final var createdResp = client.putJson( - internalUsers(randomAsciiAlphanumOfLength(10)), - internalUserWithPassword(randomAsciiAlphanumOfLength(9)) + withUser(ADMIN_USER_NAME, client -> { + created( + () -> client.putJson( + internalUsers(randomAsciiAlphanumOfLength(10)), + internalUserWithPassword(randomAsciiAlphanumOfLength(9)) + ) ); - assertThat(createdResp, isCreated()); - - final var patchResp = client.patch( - internalUsers(), - patch(addOp(randomAsciiAlphanumOfLength(10), internalUserWithPassword(randomAsciiAlphanumOfLength(9)))) + ok( + () -> client.patch( + internalUsers(), + patch(addOp(randomAsciiAlphanumOfLength(10), internalUserWithPassword(randomAsciiAlphanumOfLength(9)))) + ) ); - assertThat(patchResp, isOk()); - } + }); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/RolesMappingRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/RolesMappingRestApiIntegrationTest.java index 7cb08d645a..7255007271 100644 --- a/src/integrationTest/java/org/opensearch/security/api/RolesMappingRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/RolesMappingRestApiIntegrationTest.java @@ -13,12 +13,11 @@ import java.io.IOException; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.StringJoiner; import com.fasterxml.jackson.databind.JsonNode; -import org.junit.ClassRule; -import org.junit.Test; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.common.Strings; @@ -26,9 +25,7 @@ import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; -import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import com.nimbusds.jose.util.Pair; @@ -39,33 +36,23 @@ import static org.opensearch.security.api.PatchPayloadHelper.removeOp; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; import static org.opensearch.test.framework.TestSecurityConfig.Role; -import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; -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.isNotFound; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class RolesMappingRestApiIntegrationTest extends AbstractConfigEntityApiIntegrationTest { - final static TestSecurityConfig.User REST_API_ADMIN_ROLES_MAPPING_ONLY = new TestSecurityConfig.User( - "rest-api-admin-roles-mapping-only" - ).roles( - REST_ADMIN_REST_API_ACCESS_ROLE, - new TestSecurityConfig.Role("rest_admin_role").clusterPermissions(restAdminPermission(Endpoint.ROLESMAPPING)) - ); + final static String REST_API_ADMIN_ROLES_MAPPING_ONLY = "rest-api-admin-roles-mapping-only"; final static String REST_ADMIN_ROLE = "rest-admin-role"; final static String REST_ADMIN_ROLE_WITH_MAPPING = "rest-admin-role-with-mapping"; - @ClassRule - public static LocalCluster localCluster = clusterBuilder().users(REST_API_ADMIN_ROLES_MAPPING_ONLY) - .roles( - new Role(REST_ADMIN_ROLE).reserved(true).clusterPermissions(allRestAdminPermissions()), - new Role(REST_ADMIN_ROLE_WITH_MAPPING).clusterPermissions(allRestAdminPermissions()) - ) - .rolesMapping(new TestSecurityConfig.RoleMapping(REST_ADMIN_ROLE_WITH_MAPPING)) - .build(); + static { + testSecurityConfig.withRestAdminUser(REST_API_ADMIN_ROLES_MAPPING_ONLY, restAdminPermission(Endpoint.ROLESMAPPING)) + .roles( + new Role(REST_ADMIN_ROLE).reserved(true).clusterPermissions(allRestAdminPermissions()), + new Role(REST_ADMIN_ROLE_WITH_MAPPING).clusterPermissions(allRestAdminPermissions()) + ) + .rolesMapping(new TestSecurityConfig.RoleMapping(REST_ADMIN_ROLE_WITH_MAPPING)); + } public RolesMappingRestApiIntegrationTest() { super("rolesmapping", new TestDescriptor() { @@ -94,31 +81,11 @@ public ToXContentObject jsonPropertyPayload() { @Override public Optional restAdminLimitedUser() { - return Optional.of(REST_API_ADMIN_ROLES_MAPPING_ONLY.getName()); + return Optional.of(REST_API_ADMIN_ROLES_MAPPING_ONLY); } }); } - @Test - public void forbiddenForRegularUsers() throws Exception { - super.forbiddenForRegularUsers(localCluster); - } - - @Test - public void availableForAdminUser() throws Exception { - super.availableForAdminUser(localCluster); - } - - @Test - public void availableForTLSAdminUser() throws Exception { - super.availableForTLSAdminUser(localCluster); - } - - @Test - public void availableForRESTAdminUser() throws Exception { - super.availableForRESTAdminUser(localCluster); - } - static ToXContentObject roleMappingWithUsers(ToXContentObject users) { return roleMapping(null, null, null, null, null, users, null); } @@ -201,45 +168,52 @@ String rolesApiPath(final String roleName) { } @Override - Pair predefinedHiddenAndReservedConfigEntities(LocalCluster localCluster) throws Exception { + Pair predefinedHiddenAndReservedConfigEntities() throws Exception { final var hiddenEntityName = randomAsciiAlphanumOfLength(10); final var reservedEntityName = randomAsciiAlphanumOfLength(10); - try (TestRestClient client = localCluster.getAdminCertRestClient()) { - assertThat(client.putJson(rolesApiPath(hiddenEntityName), roleJson(true, null)), isCreated()); - assertThat( - client.putJson( + withUser( + ADMIN_USER_NAME, + localCluster.getAdminCertificate(), + client -> created(() -> client.putJson(rolesApiPath(hiddenEntityName), roleJson(true, null))) + ); + withUser( + ADMIN_USER_NAME, + localCluster.getAdminCertificate(), + client -> created( + () -> client.putJson( apiPath(hiddenEntityName), roleMapping(true, null, null, configJsonArray("a", "b"), configJsonArray(), configJsonArray(), configJsonArray()) - ), - isCreated() - ); - assertThat(client.putJson(rolesApiPath(reservedEntityName), roleJson(null, true)), isCreated()); - assertThat( - client.putJson( + ) + ) + ); + withUser( + ADMIN_USER_NAME, + localCluster.getAdminCertificate(), + client -> created(() -> client.putJson(rolesApiPath(reservedEntityName), roleJson(null, true))) + ); + withUser( + ADMIN_USER_NAME, + localCluster.getAdminCertificate(), + client -> created( + () -> client.putJson( apiPath(reservedEntityName), roleMapping(null, true, null, configJsonArray("a", "b"), configJsonArray(), configJsonArray(), configJsonArray()) - ), - isCreated() - ); - - } - + ) + ) + ); return Pair.of(hiddenEntityName, reservedEntityName); } @Override void creationOfReadOnlyEntityForbidden(String entityName, TestRestClient client, ToXContentObject... entities) throws Exception { - try (TestRestClient adminClient = localCluster.getRestClient(ADMIN_USER)) { - assertThat(adminClient.putJson(rolesApiPath(entityName), roleJson()), isCreated()); - } - + withUser(ADMIN_USER_NAME, adminClient -> created(() -> adminClient.putJson(rolesApiPath(entityName), roleJson()))); super.creationOfReadOnlyEntityForbidden(entityName, client, entities); } @Override void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient client) throws Exception { final String roleName = randomAsciiAlphanumOfLength(10); - assertThat(client.putJson(rolesApiPath(roleName), roleJson()), isCreated()); + created(() -> client.putJson(rolesApiPath(roleName), roleJson())); // put final var newPutRoleMappingJson = roleMapping( hidden, @@ -249,7 +223,7 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien randomArray(false), randomArray(false) ); - assertThat(client.putJson(apiPath(roleName), newPutRoleMappingJson), isCreated()); + created(() -> client.putJson(apiPath(roleName), newPutRoleMappingJson)); assertRoleMapping( ok(() -> client.get(apiPath(roleName))).bodyAsJsonNode().get(roleName), hidden, @@ -264,7 +238,7 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien randomArray(false), randomArray(false) ); - assertThat(client.putJson(apiPath(roleName), updatePutRoleMappingJson), isOk()); + ok(() -> client.putJson(apiPath(roleName), updatePutRoleMappingJson)); assertRoleMapping( ok(() -> client.get(apiPath(roleName))).bodyAsJsonNode().get(roleName), hidden, @@ -272,8 +246,8 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien Strings.toString(XContentType.JSON, updatePutRoleMappingJson) ); - assertThat(client.delete(apiPath(roleName)), isOk()); - assertThat(client.get(apiPath(roleName)), isNotFound()); + ok(() -> client.delete(apiPath(roleName))); + notFound(() -> client.get(apiPath(roleName))); // patch // TODO related to issue #4426 final var newPatchRoleMappingJson = roleMapping( @@ -284,23 +258,22 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien configJsonArray(), configJsonArray() ); - assertThat(client.patch(apiPath(), patch(addOp(roleName, newPatchRoleMappingJson))), isOk()); + ok(() -> client.patch(apiPath(), patch(addOp(roleName, newPatchRoleMappingJson)))); assertRoleMapping( ok(() -> client.get(apiPath(roleName))).bodyAsJsonNode().get(roleName), hidden, reserved, Strings.toString(XContentType.JSON, newPatchRoleMappingJson) ); - assertThat(client.patch(apiPath(roleName), patch(replaceOp("backend_roles", configJsonArray("c", "d")))), isOk()); - assertThat(client.patch(apiPath(roleName), patch(addOp("hosts", configJsonArray("e", "f")))), isOk()); - assertThat(client.patch(apiPath(roleName), patch(addOp("users", configJsonArray("g", "h")))), isOk()); - assertThat(client.patch(apiPath(roleName), patch(addOp("and_backend_roles", configJsonArray("i", "j")))), isOk()); - // second identical update should still be OK; message assertion omitted - assertThat(client.patch(apiPath(roleName), patch(addOp("and_backend_roles", configJsonArray("i", "j")))), isOk()); - assertThat(client.patch(apiPath(roleName), patch(replaceOp("backend_roles", configJsonArray("c", "")))), isBadRequest()); - - assertThat(client.patch(apiPath(), patch(removeOp(roleName))), isOk()); - assertThat(client.get(apiPath(roleName)), isNotFound()); + ok(() -> client.patch(apiPath(roleName), patch(replaceOp("backend_roles", configJsonArray("c", "d"))))); + ok(() -> client.patch(apiPath(roleName), patch(addOp("hosts", configJsonArray("e", "f"))))); + ok(() -> client.patch(apiPath(roleName), patch(addOp("users", configJsonArray("g", "h"))))); + ok(() -> client.patch(apiPath(roleName), patch(addOp("and_backend_roles", configJsonArray("i", "j"))))); + ok(() -> client.patch(apiPath(roleName), patch(addOp("and_backend_roles", configJsonArray("i", "j")))), "No updates required"); + badRequest(() -> client.patch(apiPath(roleName), patch(replaceOp("backend_roles", configJsonArray("c", ""))))); + + ok(() -> client.patch(apiPath(), patch(removeOp(roleName)))); + notFound(() -> client.get(apiPath(roleName))); } void assertRoleMapping(final JsonNode actualObjectNode, final Boolean hidden, final Boolean reserved, final String expectedRoleJson) @@ -332,36 +305,38 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { return builder.endObject(); }; - assertThat( - client.putJson( + notFound( + () -> client.putJson( apiPath("unknown_role"), roleMapping(configJsonArray(), configJsonArray(), configJsonArray(), configJsonArray()) ), - isNotFound().withAttribute("/message", "role 'unknown_role' not found.") + "role 'unknown_role' not found." ); // put - assertThat( - client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY), - isBadRequest().withAttribute("/reason", "Request body required for this action.") + badRequestWithReason( + () -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY), + "Request body required for this action." ); - assertThat( - client.putJson( + badRequestWithReason( + () -> client.putJson( apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> builder.startObject().field("users", configJsonArray()).field("users", configJsonArray()).endObject() ), - isBadRequest().withAttribute("/reason", "Could not parse content of request.") + "Could not parse content of request." + ); + assertInvalidKeys( + badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), unparseableJsonRequest)), + "unknown_json_property" ); - HttpResponse response = client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), unparseableJsonRequest); - assertThat(response, isBadRequest()); - assertInvalidKeys(response, "unknown_json_property"); final var randomPropertyForPut = randomJsonProperty(); - - response = client.putJson( - apiPath(randomAsciiAlphanumOfLength(5)), - (builder, params) -> builder.startObject().field(randomPropertyForPut).value("something").endObject() + assertWrongDataType( + client.putJson( + apiPath(randomAsciiAlphanumOfLength(5)), + (builder, params) -> builder.startObject().field(randomPropertyForPut).value("something").endObject() + ), + Map.of(randomPropertyForPut, "Array expected") ); - assertThat(response, isBadRequest().withAttribute("/status", "error").withAttribute("/" + randomPropertyForPut, "Array expected")); assertNullValuesInArray( client.putJson( apiPath(randomAsciiAlphanumOfLength(5)), @@ -375,49 +350,52 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { ); // patch final var predefinedRole = randomAsciiAlphanumOfLength(5); - assertThat(client.putJson(rolesApiPath(predefinedRole), roleJson()), isCreated()); - assertThat( - client.putJson( + created(() -> client.putJson(rolesApiPath(predefinedRole), roleJson())); + created( + () -> client.putJson( apiPath(predefinedRole), roleMapping(configJsonArray("a", "b"), configJsonArray(), configJsonArray(), configJsonArray()) - ), - isCreated() + ) ); - assertThat(client.patch(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY), isBadRequest()); - assertThat( - client.patch( + badRequest(() -> client.patch(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY)); + badRequest( + () -> client.patch( apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> builder.startObject().field("users", configJsonArray()).field("users", configJsonArray()).endObject() - ), - isBadRequest() + ) ); - response = client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(5), unparseableJsonRequest))); - assertThat(response, isBadRequest()); - assertInvalidKeys(response, "unknown_json_property"); - assertThat(client.patch(apiPath(predefinedRole), patch(replaceOp("users", unparseableJsonRequest))), isBadRequest()); + assertInvalidKeys( + badRequest(() -> client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(5), unparseableJsonRequest)))), + "unknown_json_property" + ); + badRequest(() -> client.patch(apiPath(predefinedRole), patch(replaceOp("users", unparseableJsonRequest)))); final var randomPropertyForPatch = randomJsonProperty(); - var resp2 = client.patch( - apiPath(), - patch( - addOp( - randomAsciiAlphanumOfLength(5), - (ToXContentObject) (builder, params) -> builder.startObject() - .field(randomPropertyForPatch) - .value("something") - .endObject() + assertWrongDataType( + client.patch( + apiPath(), + patch( + addOp( + randomAsciiAlphanumOfLength(5), + (ToXContentObject) (builder, params) -> builder.startObject() + .field(randomPropertyForPatch) + .value("something") + .endObject() + ) ) - ) + ), + Map.of(randomPropertyForPatch, "Array expected") ); - assertThat(resp2, isBadRequest().withAttribute("/status", "error").withAttribute("/" + randomPropertyForPatch, "Array expected")); // TODO related to issue #4426 - var resp3 = client.patch(apiPath(predefinedRole), patch(replaceOp("backend_roles", "something"))); - assertThat(resp3, isBadRequest().withAttribute("/status", "error").withAttribute("/backend_roles", "Array expected")); - var resp4 = client.patch(apiPath(predefinedRole), patch(addOp("hosts", "something"))); - assertThat(resp4, isBadRequest().withAttribute("/status", "error").withAttribute("/hosts", "Array expected")); - var resp5 = client.patch(apiPath(predefinedRole), patch(addOp("users", "something"))); - assertThat(resp5, isBadRequest().withAttribute("/status", "error").withAttribute("/users", "Array expected")); - var resp6 = client.patch(apiPath(predefinedRole), patch(addOp("and_backend_roles", "something"))); - assertThat(resp6, isBadRequest().withAttribute("/status", "error").withAttribute("/and_backend_roles", "Array expected")); + assertWrongDataType( + client.patch(apiPath(predefinedRole), patch(replaceOp("backend_roles", "something"))), + Map.of("backend_roles", "Array expected") + ); + assertWrongDataType(client.patch(apiPath(predefinedRole), patch(addOp("hosts", "something"))), Map.of("hosts", "Array expected")); + assertWrongDataType(client.patch(apiPath(predefinedRole), patch(addOp("users", "something"))), Map.of("users", "Array expected")); + assertWrongDataType( + client.patch(apiPath(predefinedRole), patch(addOp("and_backend_roles", "something"))), + Map.of("and_backend_roles", "Array expected") + ); assertNullValuesInArray( client.patch( apiPath(), @@ -442,23 +420,22 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { @Override void forbiddenToCreateEntityWithRestAdminPermissions(TestRestClient client) throws Exception { - assertThat(client.putJson(apiPath(REST_ADMIN_ROLE), roleMappingWithUsers(randomArray(false))), isForbidden()); - assertThat(client.patch(apiPath(), patch(addOp(REST_ADMIN_ROLE, roleMappingWithUsers(randomArray(false))))), isForbidden()); + forbidden(() -> client.putJson(apiPath(REST_ADMIN_ROLE), roleMappingWithUsers(randomArray(false)))); + forbidden(() -> client.patch(apiPath(), patch(addOp(REST_ADMIN_ROLE, roleMappingWithUsers(randomArray(false)))))); } @Override void forbiddenToUpdateAndDeleteExistingEntityWithRestAdminPermissions(TestRestClient client) throws Exception { // update - assertThat( - client.putJson( + forbidden( + () -> client.putJson( apiPath(REST_ADMIN_ROLE_WITH_MAPPING), roleMapping(randomArray(false), randomArray(false), randomArray(false), randomArray(false)) - ), - isForbidden() + ) ); - assertThat( - client.patch( + forbidden( + () -> client.patch( apiPath(), patch( replaceOp( @@ -466,14 +443,13 @@ void forbiddenToUpdateAndDeleteExistingEntityWithRestAdminPermissions(TestRestCl roleMapping(randomArray(false), randomArray(false), randomArray(false), randomArray(false)) ) ) - ), - isForbidden() + ) ); - assertThat(client.patch(apiPath(REST_ADMIN_ROLE_WITH_MAPPING), patch(replaceOp("users", randomArray(false)))), isForbidden()); + forbidden(() -> client.patch(apiPath(REST_ADMIN_ROLE_WITH_MAPPING), patch(replaceOp("users", randomArray(false))))); // remove - assertThat(client.patch(apiPath(), patch(removeOp(REST_ADMIN_ROLE_WITH_MAPPING))), isForbidden()); - assertThat(client.patch(apiPath(REST_ADMIN_ROLE_WITH_MAPPING), patch(removeOp("users"))), isForbidden()); - assertThat(client.delete(apiPath(REST_ADMIN_ROLE_WITH_MAPPING)), isForbidden()); + forbidden(() -> client.patch(apiPath(), patch(removeOp(REST_ADMIN_ROLE_WITH_MAPPING)))); + forbidden(() -> client.patch(apiPath(REST_ADMIN_ROLE_WITH_MAPPING), patch(removeOp("users")))); + forbidden(() -> client.delete(apiPath(REST_ADMIN_ROLE_WITH_MAPPING))); } String randomJsonProperty() { diff --git a/src/integrationTest/java/org/opensearch/security/api/RolesRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/RolesRestApiIntegrationTest.java index a0b8234fc4..f52fe5fbfd 100644 --- a/src/integrationTest/java/org/opensearch/security/api/RolesRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/RolesRestApiIntegrationTest.java @@ -19,8 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.junit.ClassRule; -import org.junit.Test; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.common.Strings; @@ -28,7 +26,6 @@ import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.dlic.rest.api.Endpoint; import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.CoreMatchers.is; @@ -38,11 +35,6 @@ import static org.opensearch.security.api.PatchPayloadHelper.patch; import static org.opensearch.security.api.PatchPayloadHelper.removeOp; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; -import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; -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.isNotFound; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class RolesRestApiIntegrationTest extends AbstractConfigEntityApiIntegrationTest { @@ -50,12 +42,10 @@ public class RolesRestApiIntegrationTest extends AbstractConfigEntityApiIntegrat private final static String REST_ADMIN_PERMISSION_ROLE = "rest-admin-permission-role"; - @ClassRule - public static LocalCluster localCluster = clusterBuilder().users( - new TestSecurityConfig.User(REST_API_ADMIN_ACTION_ROLES_ONLY).roles( - new TestSecurityConfig.Role("rest_admin_role").clusterPermissions(restAdminPermission(Endpoint.ROLES)) - ) - ).roles(new TestSecurityConfig.Role(REST_ADMIN_PERMISSION_ROLE).clusterPermissions(allRestAdminPermissions())).build(); + static { + testSecurityConfig.withRestAdminUser(REST_API_ADMIN_ACTION_ROLES_ONLY, restAdminPermission(Endpoint.ROLES)) + .roles(new TestSecurityConfig.Role(REST_ADMIN_PERMISSION_ROLE).clusterPermissions(allRestAdminPermissions())); + } public RolesRestApiIntegrationTest() { super("roles", new TestDescriptor() { @@ -81,47 +71,27 @@ public Optional restAdminLimitedUser() { }); } - @Test - public void forbiddenForRegularUsers() throws Exception { - super.forbiddenForRegularUsers(localCluster); - } - - @Test - public void availableForAdminUser() throws Exception { - super.availableForAdminUser(localCluster); - } - - @Test - public void availableForTLSAdminUser() throws Exception { - super.availableForTLSAdminUser(localCluster); - } - - @Test - public void availableForRESTAdminUser() throws Exception { - super.availableForRESTAdminUser(localCluster); - } - @Override void verifyCrudOperations(final Boolean hidden, final Boolean reserved, final TestRestClient client) throws Exception { final var newRoleJson = Strings.toString( XContentType.JSON, role(hidden, reserved, randomClusterPermissions(false), randomIndexPermissions(false), randomTenantPermissions(false)) ); - assertThat(client.putJson(apiPath("new_role"), newRoleJson), isCreated()); + created(() -> client.putJson(apiPath("new_role"), newRoleJson)); assertRole(ok(() -> client.get(apiPath("new_role"))), "new_role", hidden, reserved, newRoleJson); final var updatedRoleJson = Strings.toString( XContentType.JSON, role(hidden, reserved, randomClusterPermissions(false), randomIndexPermissions(false), randomTenantPermissions(false)) ); - assertThat(client.putJson(apiPath("new_role"), updatedRoleJson), isOk()); + ok(() -> client.putJson(apiPath("new_role"), updatedRoleJson)); assertRole(ok(() -> client.get(apiPath("new_role"))), "new_role", hidden, reserved, updatedRoleJson); - assertThat(client.delete(apiPath("new_role")), isOk()); - assertThat(client.get(apiPath("new_role")), isNotFound()); + ok(() -> client.delete(apiPath("new_role"))); + notFound(() -> client.get(apiPath("new_role"))); final var roleForPatch = role(hidden, reserved, configJsonArray("a", "b"), configJsonArray(), configJsonArray()); - assertThat(client.patch(apiPath(), patch(addOp("new_role_for_patch", roleForPatch))), isOk()); + ok(() -> client.patch(apiPath(), patch(addOp("new_role_for_patch", roleForPatch)))); assertRole( ok(() -> client.get(apiPath("new_role_for_patch"))), "new_role_for_patch", @@ -131,43 +101,47 @@ void verifyCrudOperations(final Boolean hidden, final Boolean reserved, final Te ); // TODO related to issue #4426 - assertThat(client.patch(apiPath("new_role_for_patch"), patch(replaceOp("cluster_permissions", configJsonArray("a", "b")))), isOk()); - assertThat( - client.patch(apiPath("new_role_for_patch"), patch(replaceOp("cluster_permissions", configJsonArray("a", "b", "c")))), - isOk() + ok( + () -> client.patch(apiPath("new_role_for_patch"), patch(replaceOp("cluster_permissions", configJsonArray("a", "b")))), + "No updates required" + ); + ok( + () -> client.patch(apiPath("new_role_for_patch"), patch(replaceOp("cluster_permissions", configJsonArray("a", "b", "c")))), + "'new_role_for_patch' updated." ); - assertThat(client.patch(apiPath("new_role_for_patch"), patch(addOp("index_permissions", randomIndexPermissions(false)))), isOk()); - assertThat(client.patch(apiPath("new_role_for_patch"), patch(addOp("tenant_permissions", randomTenantPermissions(false)))), isOk()); - assertThat(client.patch(apiPath(), patch(removeOp("new_role_for_patch"))), isOk()); - assertThat(client.get(apiPath("new_role_for_patch")), isNotFound()); + ok(() -> client.patch(apiPath("new_role_for_patch"), patch(addOp("index_permissions", randomIndexPermissions(false))))); + ok(() -> client.patch(apiPath("new_role_for_patch"), patch(addOp("tenant_permissions", randomTenantPermissions(false))))); + + ok(() -> client.patch(apiPath(), patch(removeOp("new_role_for_patch")))); + notFound(() -> client.get(apiPath("new_role_for_patch"))); } @Override void verifyBadRequestOperations(TestRestClient client) throws Exception { // put - assertThat(client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY), isBadRequest()); - assertThat(client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { + badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY)); + badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { builder.startObject(); builder.field("cluster_permissions"); randomClusterPermissions(false).toXContent(builder, params); builder.field("cluster_permissions"); randomClusterPermissions(false).toXContent(builder, params); return builder.endObject(); - }), isBadRequest()); - assertInvalidKeys(client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { + })); + assertInvalidKeys(badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { builder.startObject(); builder.field("unknown_json_property"); configJsonArray("a", "b").toXContent(builder, params); builder.field("cluster_permissions"); randomClusterPermissions(false).toXContent(builder, params); return builder.endObject(); - }), "unknown_json_property"); - assertWrongDataType(client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { + })), "unknown_json_property"); + assertWrongDataType(badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { builder.startObject(); builder.field("cluster_permissions").value("a"); builder.field("index_permissions").value("b"); return builder.endObject(); - }), Map.of("cluster_permissions", "Array expected", "index_permissions", "Array expected")); + })), Map.of("cluster_permissions", "Array expected", "index_permissions", "Array expected")); assertNullValuesInArray( client.putJson( apiPath(randomAsciiAlphanumOfLength(5)), @@ -176,23 +150,18 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { ); // patch final var predefinedRoleName = randomAsciiAlphanumOfLength(4); - assertThat( - client.putJson(apiPath(predefinedRoleName), role(configJsonArray("a", "b"), configJsonArray(), configJsonArray())), - isCreated() - ); + created(() -> client.putJson(apiPath(predefinedRoleName), role(configJsonArray("a", "b"), configJsonArray(), configJsonArray()))); - assertThat(client.patch(apiPath(), patch(addOp("some_new_role", EMPTY_BODY))), isBadRequest()); - - assertThat( - client.patch( + badRequest(() -> client.patch(apiPath(), patch(addOp("some_new_role", EMPTY_BODY)))); + badRequest( + () -> client.patch( apiPath(predefinedRoleName), patch(replaceOp(randomFrom(List.of("cluster_permissions", "index_permissions", "tenant_permissions")), EMPTY_BODY)) - ), - isBadRequest() + ) ); - assertThat( - client.patch( + badRequest( + () -> client.patch( apiPath(randomAsciiAlphanumOfLength(5)), patch(addOp(randomAsciiAlphanumOfLength(5), (ToXContentObject) (builder, params) -> { builder.startObject(); @@ -202,34 +171,29 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { randomClusterPermissions(false).toXContent(builder, params); return builder.endObject(); })) - ), - isBadRequest() + ) ); - - assertThat(client.patch(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { + badRequest(() -> client.patch(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { builder.startObject(); builder.field("unknown_json_property"); configJsonArray("a", "b").toXContent(builder, params); builder.field("cluster_permissions"); randomClusterPermissions(false).toXContent(builder, params); return builder.endObject(); - }), isBadRequest()); - - var response = client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(5), (ToXContentObject) (builder, params) -> { - builder.startObject(); - builder.field("cluster_permissions").value("a"); - builder.field("index_permissions").value("b"); - return builder.endObject(); - }))); - assertThat( - response, - isBadRequest().withAttribute("/status", "error") - .withAttribute("/cluster_permissions", "Array expected") - .withAttribute("/index_permissions", "Array expected") + })); + assertWrongDataType( + badRequest(() -> client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(5), (ToXContentObject) (builder, params) -> { + builder.startObject(); + builder.field("cluster_permissions").value("a"); + builder.field("index_permissions").value("b"); + return builder.endObject(); + })))), + Map.of("cluster_permissions", "Array expected", "index_permissions", "Array expected") + ); + assertWrongDataType( + badRequest(() -> client.patch(apiPath(predefinedRoleName), patch(replaceOp("cluster_permissions", "true")))), + Map.of("cluster_permissions", "Array expected") ); - - response = badRequest(() -> client.patch(apiPath(predefinedRoleName), patch(replaceOp("cluster_permissions", "true")))); - assertThat(response, isBadRequest().withAttribute("/status", "error").withAttribute("/cluster_permissions", "Array expected")); assertNullValuesInArray( client.patch( apiPath(), @@ -249,25 +213,26 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { @Override void forbiddenToCreateEntityWithRestAdminPermissions(final TestRestClient client) throws Exception { - assertThat(client.putJson(apiPath("new_rest_admin_role"), roleWithClusterPermissions(randomRestAdminPermission())), isForbidden()); - assertThat( - client.patch(apiPath(), patch(addOp("new_rest_admin_action_group", roleWithClusterPermissions(randomRestAdminPermission())))), - isForbidden() + forbidden(() -> client.putJson(apiPath("new_rest_admin_role"), roleWithClusterPermissions(randomRestAdminPermission()))); + forbidden( + () -> client.patch( + apiPath(), + patch(addOp("new_rest_admin_action_group", roleWithClusterPermissions(randomRestAdminPermission()))) + ) ); } @Override void forbiddenToUpdateAndDeleteExistingEntityWithRestAdminPermissions(final TestRestClient client) throws Exception { // update - assertThat( - client.putJson( + forbidden( + () -> client.putJson( apiPath(REST_ADMIN_PERMISSION_ROLE), role(randomClusterPermissions(false), randomIndexPermissions(false), randomTenantPermissions(false)) - ), - isForbidden() + ) ); - assertThat( - client.patch( + forbidden( + () -> client.patch( apiPath(), patch( replaceOp( @@ -275,17 +240,18 @@ void forbiddenToUpdateAndDeleteExistingEntityWithRestAdminPermissions(final Test role(randomClusterPermissions(false), randomIndexPermissions(false), randomTenantPermissions(false)) ) ) - ), - isForbidden() + ) ); - assertThat( - client.patch(apiPath(REST_ADMIN_PERMISSION_ROLE), patch(replaceOp("cluster_permissions", randomClusterPermissions(false)))), - isForbidden() + forbidden( + () -> client.patch( + apiPath(REST_ADMIN_PERMISSION_ROLE), + patch(replaceOp("cluster_permissions", randomClusterPermissions(false))) + ) ); // remove - assertThat(client.patch(apiPath(), patch(removeOp(REST_ADMIN_PERMISSION_ROLE))), isForbidden()); - assertThat(client.patch(apiPath(REST_ADMIN_PERMISSION_ROLE), patch(removeOp("cluster_permissions"))), isForbidden()); - assertThat(client.delete(apiPath(REST_ADMIN_PERMISSION_ROLE)), isForbidden()); + forbidden(() -> client.patch(apiPath(), patch(removeOp(REST_ADMIN_PERMISSION_ROLE)))); + forbidden(() -> client.patch(apiPath(REST_ADMIN_PERMISSION_ROLE), patch(removeOp("cluster_permissions")))); + forbidden(() -> client.delete(apiPath(REST_ADMIN_PERMISSION_ROLE))); } void assertRole( diff --git a/src/integrationTest/java/org/opensearch/security/api/RollbackVersionApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/RollbackVersionApiIntegrationTest.java index 82fda7c18f..330d6baf72 100644 --- a/src/integrationTest/java/org/opensearch/security/api/RollbackVersionApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/RollbackVersionApiIntegrationTest.java @@ -11,26 +11,22 @@ package org.opensearch.security.api; +import java.util.Map; + +import org.apache.http.HttpStatus; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isOneOf; import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; import static org.opensearch.security.support.ConfigConstants.EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED; -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.isNotFound; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; -import static org.opensearch.test.framework.matcher.RestMatchers.isUnauthorized; public class RollbackVersionApiIntegrationTest extends AbstractApiIntegrationTest { @@ -42,57 +38,61 @@ private String RollbackVersion(String versionId) { return ROLLBACK_BASE + "/" + versionId; } - @Rule - public LocalCluster localCluster = clusterBuilder().nodeSetting(EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED, true).build(); + @Override + protected Map getClusterSettings() { + Map settings = super.getClusterSettings(); + settings.put(EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED, true); + return settings; + } @Before public void setupConfigVersionsIndex() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - assertThat(client.createUser(USER.getName(), USER), anyOf(isOk(), isCreated())); + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { + client.createUser(USER.getName(), USER).assertStatusCode(201); } } @Test public void testRollbackToPreviousVersion_success() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> { var response = client.post(ROLLBACK_BASE); - assertThat(response, isOk()); + assertThat(response.getStatusCode(), is(HttpStatus.SC_OK)); assertThat(response.getTextFromJsonBody("/status"), equalTo("OK")); assertThat(response.getTextFromJsonBody("/message"), containsString("config rolled back to version")); - } + }); } @Test public void testRollbackToSpecificVersion_success() throws Exception { String versionId = "v1"; - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> { var response = client.post(RollbackVersion(versionId)); - assertThat(response, isOk()); + assertThat(response.getStatusCode(), is(HttpStatus.SC_OK)); assertThat(response.getTextFromJsonBody("/status"), equalTo("OK")); assertThat(response.getTextFromJsonBody("/message"), containsString("config rolled back to version " + versionId)); - } + }); } @Test public void testRollbackWithNonAdmin_shouldBeUnauthorized() throws Exception { - try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { + withUser(NEW_USER, DEFAULT_PASSWORD, client -> { var response = client.post(ROLLBACK_BASE); - assertThat(response, anyOf(isForbidden(), isUnauthorized())); - } + assertThat(response.getStatusCode(), isOneOf(HttpStatus.SC_FORBIDDEN, HttpStatus.SC_UNAUTHORIZED)); + }); } @Test public void testRollbackToInvalidVersion_shouldReturnNotFound() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> { var response = client.post(RollbackVersion("does-not-exist")); - assertThat(response, isNotFound()); + assertThat(response.getStatusCode(), is(HttpStatus.SC_NOT_FOUND)); assertThat(response.getTextFromJsonBody("/message"), containsString("not found")); - } + }); } @Test public void testRollbackWhenOnlyOneVersion_shouldFail() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> { // To perform below test, delete all entries in .opensearch_security_config_versions index String deleteQuery = """ { @@ -116,7 +116,7 @@ public void testRollbackWhenOnlyOneVersion_shouldFail() throws Exception { var response = client.post(ROLLBACK_BASE); assertThat(response.getStatusCode(), is(404)); assertThat(response.getBody(), containsString("No previous version available to rollback")); - } + }); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java index 8c911d3161..bbdd9ff793 100644 --- a/src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java @@ -10,35 +10,34 @@ */ package org.opensearch.security.api; +import java.util.Map; + import com.fasterxml.jackson.databind.JsonNode; -import org.junit.ClassRule; import org.junit.Test; import org.opensearch.security.dlic.rest.api.Endpoint; -import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; -import static org.opensearch.test.framework.matcher.RestMatchers.isForbidden; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; @Deprecated public class SslCertsRestApiIntegrationTest extends AbstractApiIntegrationTest { final static String REST_API_ADMIN_SSL_INFO = "rest-api-admin-ssl-info"; - @ClassRule - public static LocalCluster localCluster = clusterBuilder().nodeSetting(SECURITY_RESTAPI_ADMIN_ENABLED, true) - .users( - new TestSecurityConfig.User(REST_API_ADMIN_SSL_INFO).roles( - REST_ADMIN_REST_API_ACCESS_ROLE, - new TestSecurityConfig.Role("rest_admin_role").clusterPermissions(restAdminPermission(Endpoint.SSL, CERTS_INFO_ACTION)) - ) - ) - .build(); + static { + testSecurityConfig.withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions()) + .withRestAdminUser(REST_API_ADMIN_SSL_INFO, restAdminPermission(Endpoint.SSL, CERTS_INFO_ACTION)); + } + + @Override + protected Map getClusterSettings() { + Map clusterSettings = super.getClusterSettings(); + clusterSettings.put(SECURITY_RESTAPI_ADMIN_ENABLED, true); + return clusterSettings; + } protected String sslCertsPath() { return super.apiPath("ssl", "certs"); @@ -46,38 +45,27 @@ protected String sslCertsPath() { @Test public void certsInfoForbiddenForRegularUser() throws Exception { - try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { - assertThat(client.get(sslCertsPath()), isForbidden()); - } + withUser(NEW_USER, client -> forbidden(() -> client.get(sslCertsPath()))); } @Test public void certsInfoForbiddenForAdminUser() throws Exception { - try (TestRestClient client = localCluster.getRestClient(NEW_USER)) { - assertThat(client.get(sslCertsPath()), isForbidden()); - } + withUser(NEW_USER, client -> forbidden(() -> client.get(sslCertsPath()))); } @Test public void certsInfoAvailableForTlsAdmin() throws Exception { - try (TestRestClient client = localCluster.getAdminCertRestClient()) { - verifySSLCertsInfo(client); - } + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifySSLCertsInfo); } @Test public void certsInfoAvailableForRestAdmin() throws Exception { - try (TestRestClient client = localCluster.getRestClient(REST_ADMIN_USER)) { - verifySSLCertsInfo(client); - } - try (TestRestClient client = localCluster.getRestClient(REST_API_ADMIN_SSL_INFO, DEFAULT_PASSWORD)) { - verifySSLCertsInfo(client); - } + withUser(REST_ADMIN_USER, this::verifySSLCertsInfo); + withUser(REST_API_ADMIN_SSL_INFO, this::verifySSLCertsInfo); } private void verifySSLCertsInfo(final TestRestClient client) throws Exception { - final var response = client.get(sslCertsPath()); - assertThat(response, isOk()); + final var response = ok(() -> client.get(sslCertsPath())); final var body = response.bodyAsJsonNode(); assertThat(response.getBody(), body.has("http_certificates_list")); diff --git a/src/integrationTest/java/org/opensearch/security/api/TenantsRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/TenantsRestApiIntegrationTest.java index 7dbf74781d..cb3431be79 100644 --- a/src/integrationTest/java/org/opensearch/security/api/TenantsRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/TenantsRestApiIntegrationTest.java @@ -14,13 +14,9 @@ import java.util.Optional; import com.fasterxml.jackson.databind.JsonNode; -import org.junit.ClassRule; -import org.junit.Test; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.security.dlic.rest.api.Endpoint; -import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.CoreMatchers.is; @@ -29,22 +25,14 @@ import static org.opensearch.security.api.PatchPayloadHelper.patch; import static org.opensearch.security.api.PatchPayloadHelper.removeOp; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; -import static org.opensearch.test.framework.matcher.RestMatchers.isBadRequest; -import static org.opensearch.test.framework.matcher.RestMatchers.isCreated; -import static org.opensearch.test.framework.matcher.RestMatchers.isNotFound; -import static org.opensearch.test.framework.matcher.RestMatchers.isOk; public class TenantsRestApiIntegrationTest extends AbstractConfigEntityApiIntegrationTest { private final static String REST_API_ADMIN_TENANTS_ONLY = "rest_api_admin_tenants_only"; - @ClassRule - public static LocalCluster localCluster = clusterBuilder().users( - new TestSecurityConfig.User(REST_API_ADMIN_TENANTS_ONLY).roles( - REST_ADMIN_REST_API_ACCESS_ROLE, - new TestSecurityConfig.Role("rest_admin_role").clusterPermissions(restAdminPermission(Endpoint.TENANTS)) - ) - ).build(); + static { + testSecurityConfig.withRestAdminUser(REST_API_ADMIN_TENANTS_ONLY, restAdminPermission(Endpoint.TENANTS)); + } public TenantsRestApiIntegrationTest() { super("tenants", new TestDescriptor() { @@ -95,36 +83,15 @@ static ToXContentObject tenant(final Boolean hidden, final Boolean reserved, fin }; } - @Test - public void forbiddenForRegularUsers() throws Exception { - super.forbiddenForRegularUsers(localCluster); - } - - @Test - public void availableForAdminUser() throws Exception { - super.availableForAdminUser(localCluster); - } - - @Test - public void availableForTLSAdminUser() throws Exception { - super.availableForTLSAdminUser(localCluster); - } - - @Test - public void availableForRESTAdminUser() throws Exception { - super.availableForRESTAdminUser(localCluster); - } - @Override void verifyBadRequestOperations(TestRestClient client) throws Exception { // put - assertThat(client.putJson(apiPath(randomAsciiAlphanumOfLength(4)), EMPTY_BODY), isBadRequest()); - assertThat( - client.putJson( + badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(4)), EMPTY_BODY)); + badRequest( + () -> client.putJson( apiPath(randomAsciiAlphanumOfLength(4)), (builder, params) -> builder.startObject().field("description", "a").field("description", "b").endObject() - ), - isBadRequest() + ) ); assertInvalidKeys( client.putJson( @@ -134,9 +101,9 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { "a,c" ); // patch - assertThat(client.patch(apiPath(), EMPTY_BODY), isBadRequest()); - assertThat( - client.patch( + badRequest(() -> client.patch(apiPath(), EMPTY_BODY)); + badRequest( + () -> client.patch( apiPath(), patch( addOp( @@ -147,8 +114,7 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { .endObject() ) ) - ), - isBadRequest() + ) ); assertInvalidKeys( client.patch( @@ -174,23 +140,23 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien // put final var putDescription = randomAsciiAlphanumOfLength(10); final var putTenantName = randomAsciiAlphanumOfLength(4); - assertThat(client.putJson(apiPath(putTenantName), tenant(hidden, reserved, putDescription)), isCreated()); - assertTenant(client.get(apiPath(putTenantName)).bodyAsJsonNode().get(putTenantName), hidden, reserved, putDescription); + created(() -> client.putJson(apiPath(putTenantName), tenant(hidden, reserved, putDescription))); + assertTenant(ok(() -> client.get(apiPath(putTenantName))).bodyAsJsonNode().get(putTenantName), hidden, reserved, putDescription); final var putUpdatedDescription = randomAsciiAlphanumOfLength(10); - assertThat(client.putJson(apiPath(putTenantName), tenant(hidden, reserved, putUpdatedDescription)), isOk()); + ok(() -> client.putJson(apiPath(putTenantName), tenant(hidden, reserved, putUpdatedDescription))); assertTenant( ok(() -> client.get(apiPath(putTenantName))).bodyAsJsonNode().get(putTenantName), hidden, reserved, putUpdatedDescription ); - assertThat(client.delete(apiPath(putTenantName)), isOk()); - assertThat(client.get(apiPath(putTenantName)), isNotFound()); + ok(() -> client.delete(apiPath(putTenantName))); + notFound(() -> client.get(apiPath(putTenantName))); // patch final var patchTenantName = randomAsciiAlphanumOfLength(4); final var patchDescription = randomAsciiAlphanumOfLength(10); - assertThat(client.patch(apiPath(), patch(addOp(patchTenantName, tenant(hidden, reserved, patchDescription)))), isOk()); + ok(() -> client.patch(apiPath(), patch(addOp(patchTenantName, tenant(hidden, reserved, patchDescription))))); assertTenant( ok(() -> client.get(apiPath(patchTenantName))).bodyAsJsonNode().get(patchTenantName), hidden, @@ -199,7 +165,7 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien ); final var patchUpdatedDescription = randomAsciiAlphanumOfLength(10); - assertThat(client.patch(apiPath(patchTenantName), patch(replaceOp("description", patchUpdatedDescription))), isOk()); + ok(() -> client.patch(apiPath(patchTenantName), patch(replaceOp("description", patchUpdatedDescription)))); assertTenant( ok(() -> client.get(apiPath(patchTenantName))).bodyAsJsonNode().get(patchTenantName), hidden, @@ -207,8 +173,8 @@ void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient clien patchUpdatedDescription ); - assertThat(client.patch(apiPath(), patch(removeOp(patchTenantName))), isOk()); - assertThat(client.get(apiPath(patchTenantName)), isNotFound()); + ok(() -> client.patch(apiPath(), patch(removeOp(patchTenantName)))); + notFound(() -> client.get(apiPath(patchTenantName))); } void assertTenant(final JsonNode actualJson, final Boolean hidden, final Boolean reserved, final String expectedDescription) { diff --git a/src/integrationTest/java/org/opensearch/security/api/ViewVersionApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/ViewVersionApiIntegrationTest.java index 13274cb20c..67a7627549 100644 --- a/src/integrationTest/java/org/opensearch/security/api/ViewVersionApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/ViewVersionApiIntegrationTest.java @@ -11,33 +11,28 @@ package org.opensearch.security.api; +import java.util.Map; + import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.opensearch.test.framework.TestSecurityConfig; -import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isOneOf; import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; import static org.opensearch.security.support.ConfigConstants.EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED; -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.opensearch.test.framework.matcher.RestMatchers.isUnauthorized; public class ViewVersionApiIntegrationTest extends AbstractApiIntegrationTest { - @Rule - public LocalCluster localCluster = clusterBuilder().users(new TestSecurityConfig.User("limitedUser").password("limitedPass")) - .nodeSetting(EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED, true) - .build(); + static { + testSecurityConfig.user(new TestSecurityConfig.User("limitedUser").password("limitedPass")); + } private static final TestSecurityConfig.User USER = new TestSecurityConfig.User("user"); @@ -53,32 +48,37 @@ private String viewVersion(String versionId) { return endpointPrefix() + "/version/" + versionId; } + @Override + protected Map getClusterSettings() { + Map settings = super.getClusterSettings(); + settings.put(EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED, true); + return settings; + } + @Before public void setupIndexAndCerts() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { client.createUser(USER.getName(), USER).assertStatusCode(201); } } @Test public void testViewAllVersions() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - var response = client.get(viewVersionBase()); - assertThat(response, isOk()); + withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> { + var response = ok(() -> client.get(viewVersionBase())); var json = response.bodyAsJsonNode(); assertThat(json.has("versions"), is(true)); var versions = json.get("versions"); assertThat(versions.isArray(), is(true)); assertThat(versions.size(), greaterThan(0)); - } + }); } @Test public void testViewSpecificVersionFound() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - var response = client.get(viewVersion("v1")); - assertThat(response, isOk()); + withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> { + var response = ok(() -> client.get(viewVersion("v1"))); var json = response.bodyAsJsonNode(); assertThat(json.has("versions"), is(true)); @@ -88,14 +88,13 @@ public void testViewSpecificVersionFound() throws Exception { var ver = versions.get(0); assertThat(ver.get("version_id").asText(), equalTo("v1")); - } + }); } @Test public void testViewSpecificVersionNotFound() throws Exception { - try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - var response = client.get(viewVersion("does-not-exist")); - assertThat(response, isNotFound()); + withUser(ADMIN_USER_NAME, DEFAULT_PASSWORD, client -> { + var response = notFound(() -> client.get(viewVersion("does-not-exist"))); var json = response.bodyAsJsonNode(); assertThat(json.has("status"), is(true)); @@ -103,14 +102,14 @@ public void testViewSpecificVersionNotFound() throws Exception { assertThat(json.has("message"), is(true)); assertThat(json.get("message").asText(), containsString("not found")); - } + }); } @Test public void testViewAllVersions_forbiddenWithoutAdminCert() throws Exception { - try (TestRestClient client = localCluster.getRestClient("limitedUser", "limitedPass")) { + withUser("limitedUser", "limitedPass", client -> { var response = client.get(viewVersionBase()); - assertThat(response, anyOf(isUnauthorized(), isForbidden())); - } + assertThat(response.getStatusCode(), isOneOf(401, 403)); + }); } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index d320c0fc43..b18b8d3f97 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -75,6 +75,7 @@ import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.cluster.OpenSearchClientProvider.UserCredentialsHolder; import org.opensearch.test.framework.data.TestIndex; +import org.opensearch.test.framework.matcher.RestIndexMatchers; import org.opensearch.transport.client.Client; import static java.util.Arrays.asList; @@ -94,6 +95,8 @@ */ public class TestSecurityConfig { + public static final String REST_ADMIN_REST_API_ACCESS = "rest_admin__rest_api_access"; + private static final Logger log = LogManager.getLogger(TestSecurityConfig.class); private static final PasswordHasher passwordHasher = PasswordHasherFactory.createPasswordHasher( @@ -183,6 +186,18 @@ public TestSecurityConfig users(User... users) { return this; } + public TestSecurityConfig withRestAdminUser(final String name, final String... permissions) { + if (!internalUsers.containsKey(name)) { + user(new User(name).description("REST Admin with permissions: " + Arrays.toString(permissions)).reserved(true)); + final var roleName = name + "__rest_admin_role"; + roles(new Role(roleName).clusterPermissions(permissions)); + + rolesMapping.computeIfAbsent(roleName, RoleMapping::new).users(name); + rolesMapping.computeIfAbsent(REST_ADMIN_REST_API_ACCESS, RoleMapping::new).users(name); + } + return this; + } + public List getUsers() { return new ArrayList<>(internalUsers.values()); } @@ -474,6 +489,7 @@ public static final class User implements UserCredentialsHolder, ToXContentObjec String requestedTenant; private Map attributes = new HashMap<>(); private Map, Object> matchers = new HashMap<>(); + private Map indexMatchers = new HashMap<>(); private boolean adminCertUser = false; private Boolean hidden = null; 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 dcf7ab322a..2b86d21549 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -450,11 +450,6 @@ public Builder nodeSettings(Map settings) { return this; } - public Builder nodeSetting(String key, Object value) { - nodeOverrideSettingsBuilder.put(key, String.valueOf(value)); - return this; - } - public Builder nodeSpecificSettings(int nodeNumber, Map settings) { if (!nodeSpecificOverrideSettingsBuilder.containsKey(nodeNumber)) { Settings.Builder builderCopy = Settings.builder(); diff --git a/src/integrationTest/java/org/opensearch/test/framework/matcher/RestMatchers.java b/src/integrationTest/java/org/opensearch/test/framework/matcher/RestMatchers.java index 20dc5b59d5..96faab57c1 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/matcher/RestMatchers.java +++ b/src/integrationTest/java/org/opensearch/test/framework/matcher/RestMatchers.java @@ -66,14 +66,6 @@ public static OpenSearchErrorHttpResponseMatcher isNotFound() { return new OpenSearchErrorHttpResponseMatcher(404, "Not Found"); } - public static OpenSearchErrorHttpResponseMatcher isNotAllowed() { - return new OpenSearchErrorHttpResponseMatcher(405, "Not Allowed"); - } - - public static OpenSearchErrorHttpResponseMatcher isUnauthorized() { - return new OpenSearchErrorHttpResponseMatcher(401, "Unauthorized"); - } - public static class HttpResponseMatcher extends DiagnosingMatcher { final int statusCode; final String statusName; From 4169f06deca70c5775c884172045f73464db93bd Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Wed, 29 Oct 2025 12:20:02 +0100 Subject: [PATCH 27/29] Test fix --- .../int_tests/IndexAuthorizationReadOnlyIntTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5bb1b69a1a..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 @@ -1915,7 +1915,7 @@ public void pit_list_all() throws Exception { 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 && user != LIMITED_USER_OTHER_PRIVILEGES) { + if (user != LIMITED_USER_NONE) { assertThat(httpResponse, isOk()); } else { assertThat(httpResponse, isForbidden()); From ceb36dbf2a210805a180efdb063a084631659b7b Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Thu, 30 Oct 2025 05:37:35 +0100 Subject: [PATCH 28/29] Test fix --- .../IndicesRequestResolverTest.java | 50 ++++++++++++------ .../DashboardMultiTenancyIntTests.java | 51 +++++++++---------- .../privileges/IndicesRequestResolver.java | 5 +- 3 files changed, 64 insertions(+), 42 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/privileges/IndicesRequestResolverTest.java b/src/integrationTest/java/org/opensearch/security/privileges/IndicesRequestResolverTest.java index 22e70d2eea..b1e6af1a1a 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/IndicesRequestResolverTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndicesRequestResolverTest.java @@ -11,29 +11,45 @@ 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("index1", "index2", "index3").build(); + 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(Optional.of(resolvedIndices)); + when(actionRequestMetadata.resolvedIndices()).thenReturn(resolvedIndices); - ResolvedIndices returnedResolvedIndices = subject.resolve(request, actionRequestMetadata, () -> clusterState); + OptionallyResolvedIndices returnedResolvedIndices = subject.resolve(request, actionRequestMetadata, () -> clusterState); assertEquals(resolvedIndices, returnedResolvedIndices); } @@ -41,30 +57,34 @@ public void resolve_normal() { public void resolve_fallback() { SearchRequest request = new SearchRequest("index1"); ActionRequestMetadata actionRequestMetadata = mock(); - when(actionRequestMetadata.resolvedIndices()).thenReturn(Optional.empty()); + when(actionRequestMetadata.resolvedIndices()).thenReturn(OptionallyResolvedIndices.unknown()); - ResolvedIndices returnedResolvedIndices = subject.resolve(request, actionRequestMetadata, () -> clusterState); - assertEquals(Set.of("index1"), returnedResolvedIndices.local().names()); + 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(Optional.empty()); + when(actionRequestMetadata.resolvedIndices()).thenReturn(OptionallyResolvedIndices.unknown()); - ResolvedIndices returnedResolvedIndices = subject.resolve(request, actionRequestMetadata, () -> clusterState); - assertTrue("Expected isAll(), got: " + returnedResolvedIndices, returnedResolvedIndices.local().isAll()); + OptionallyResolvedIndices returnedResolvedIndices = subject.resolve(request, actionRequestMetadata, () -> clusterState); + assertFalse(returnedResolvedIndices instanceof ResolvedIndices); } @Test public void resolve_withPrivilegesEvaluationContext() { - SearchRequest request = new SearchRequest("index*"); + SearchRequest request = new SearchRequest("index_a*"); ActionRequestMetadata actionRequestMetadata = mock(); - when(actionRequestMetadata.resolvedIndices()).thenReturn(Optional.empty()); + when(actionRequestMetadata.resolvedIndices()).thenReturn(OptionallyResolvedIndices.unknown()); PrivilegesEvaluationContext context = MockPrivilegeEvaluationContextBuilder.ctx().clusterState(clusterState).get(); - ResolvedIndices returnedResolvedIndices = subject.resolve(request, actionRequestMetadata, context); - assertEquals(Set.of("index1", "index2", "index3"), returnedResolvedIndices.local().names()); - }*/ + 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/int_tests/DashboardMultiTenancyIntTests.java b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DashboardMultiTenancyIntTests.java index 87c671d92f..388d572cee 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DashboardMultiTenancyIntTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/int_tests/DashboardMultiTenancyIntTests.java @@ -595,20 +595,20 @@ public void bulk_withTenantHeader_humanResources() { """; TestRestClient.HttpResponse response = restClient.postJson( - "_bulk?pretty", - bulkBody, - new BasicHeader("securitytenant", "human_resources") + "_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)) + 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" + dashboards_index_human_resources.name() + "/_doc/mt_bulk_doc_1", + dashboards_index_human_resources.name() + "/_doc/mt_bulk_doc_2" ); } } @@ -624,20 +624,21 @@ public void bulk_withTenantHeader_direct_humanResources_alias() { """; TestRestClient.HttpResponse response = restClient.postJson( - "_bulk?pretty", - bulkBody, - new BasicHeader("securitytenant", "human_resources") + "_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()) + 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" + dashboards_index_human_resources.name() + "/_doc/mt_bulk_doc_1", + dashboards_index_human_resources.name() + "/_doc/mt_bulk_doc_2" ); } } @@ -653,21 +654,19 @@ public void bulk_withTenantHeader_direct_global_index() { """; TestRestClient.HttpResponse response = restClient.postJson( - "_bulk?pretty", - bulkBody, - new BasicHeader("securitytenant", "human_resources") + "_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()) + 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" - ); + delete(dashboards_index_global.name() + "/_doc/mt_bulk_doc_1", dashboards_index_global.name() + "/_doc/mt_bulk_doc_2"); } } diff --git a/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java b/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java index da18b4ea06..971b9b7a8c 100644 --- a/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java +++ b/src/main/java/org/opensearch/security/privileges/IndicesRequestResolver.java @@ -20,6 +20,10 @@ 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; @@ -56,5 +60,4 @@ private OptionallyResolvedIndices resolveFallback(ActionRequest request, Cluster return ResolvedIndices.unknown(); } } - } From 8e209b4f25df54a35b6e2e8fb599e9d3ffbfe915 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Thu, 30 Oct 2025 09:42:45 +0100 Subject: [PATCH 29/29] Test fix --- .../tools/democonfig/SecuritySettingsConfigurerTests.java | 2 ++ 1 file changed, 2 insertions(+) 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 5105202c82..69353c5b97 100644 --- a/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java +++ b/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java @@ -28,11 +28,13 @@ import java.util.List; import java.util.Map; +import com.carrotsearch.randomizedtesting.RandomizedRunner; import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.tools.Hasher;