From 1c5496603b404f1929c98973645a8db45bd86390 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Wed, 28 May 2025 09:40:02 +0200 Subject: [PATCH 01/10] Extracted ActionPrivileges interface and created RoleBasedActionPrivileges implementation and SubjectBasedActionPrivileges implementation Signed-off-by: Nils Bandener --- .../security/privileges/IndexPatternTest.java | 3 +- .../RestEndpointPermissionTests.java | 18 +- ...ava => RoleBasedActionPrivilegesTest.java} | 117 +- .../privileges/TenantPrivilegesTest.java | 6 +- .../dlsfls/DlsFlsLegacyHeadersTest.java | 7 +- .../dlsfls/DocumentPrivilegesTest.java | 24 +- .../privileges/dlsfls/FieldMaskingTest.java | 4 +- .../dlsfls/FieldPrivilegesTest.java | 4 +- .../security/OpenSearchSecurityPlugin.java | 4 +- .../SystemIndexSearcherWrapper.java | 2 +- .../security/privileges/ActionPrivileges.java | 1193 +---------------- .../PrivilegesEvaluationContext.java | 18 +- .../privileges/PrivilegesEvaluator.java | 71 +- .../RestLayerPrivilegesEvaluator.java | 2 +- .../privileges/RoleBasedActionPrivileges.java | 1185 ++++++++++++++++ .../SubjectBasedActionPrivileges.java | 589 ++++++++ .../SystemIndexAccessEvaluatorTest.java | 12 +- 17 files changed, 2029 insertions(+), 1230 deletions(-) rename src/integrationTest/java/org/opensearch/security/privileges/{ActionPrivilegesTest.java => RoleBasedActionPrivilegesTest.java} (92%) create mode 100644 src/main/java/org/opensearch/security/privileges/RoleBasedActionPrivileges.java create mode 100644 src/main/java/org/opensearch/security/privileges/SubjectBasedActionPrivileges.java diff --git a/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java b/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java index cc6766c2ea..e2924bc45d 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java @@ -243,7 +243,8 @@ private static PrivilegesEvaluationContext ctx() { null, indexResolverReplacer, indexNameExpressionResolver, - () -> CLUSTER_STATE + () -> CLUSTER_STATE, + ActionPrivileges.EMPTY ); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java index 1e61aa0206..33841b9fd8 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java @@ -113,10 +113,10 @@ static String[] allRestApiPermissions() { }).toArray(String[]::new); } - final ActionPrivileges actionPrivileges; + final RoleBasedActionPrivileges actionPrivileges; public RestEndpointPermissionTests() throws IOException { - this.actionPrivileges = new ActionPrivileges(createRolesConfig(), FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + this.actionPrivileges = new RoleBasedActionPrivileges(createRolesConfig(), FlattenedActionGroups.EMPTY, null, Settings.EMPTY); } @Test @@ -250,8 +250,18 @@ static SecurityDynamicConfiguration createRolesConfig() throws IOExcepti return SecurityDynamicConfiguration.fromNode(rolesNode, CType.ROLES, 2, 0, 0); } - static PrivilegesEvaluationContext ctx(String... roles) { - return new PrivilegesEvaluationContext(new User("test_user"), ImmutableSet.copyOf(roles), null, null, null, null, null, null); + PrivilegesEvaluationContext ctx(String... roles) { + return new PrivilegesEvaluationContext( + new User("test_user"), + ImmutableSet.copyOf(roles), + null, + null, + null, + null, + null, + null, + actionPrivileges + ); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/RoleBasedActionPrivilegesTest.java similarity index 92% rename from src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java rename to src/integrationTest/java/org/opensearch/security/privileges/RoleBasedActionPrivilegesTest.java index e2830246da..17b11606ed 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/RoleBasedActionPrivilegesTest.java @@ -66,12 +66,23 @@ */ @RunWith(Suite.class) @Suite.SuiteClasses({ - ActionPrivilegesTest.ClusterPrivileges.class, - ActionPrivilegesTest.IndexPrivileges.IndicesAndAliases.class, - ActionPrivilegesTest.IndexPrivileges.DataStreams.class, - ActionPrivilegesTest.Misc.class, - ActionPrivilegesTest.StatefulIndexPrivilegesHeapSize.class }) -public class ActionPrivilegesTest { + RoleBasedActionPrivilegesTest.ClusterPrivileges.class, + RoleBasedActionPrivilegesTest.IndexPrivileges.IndicesAndAliases.class, + RoleBasedActionPrivilegesTest.IndexPrivileges.DataStreams.class, + RoleBasedActionPrivilegesTest.Misc.class, + RoleBasedActionPrivilegesTest.StatefulIndexPrivilegesHeapSize.class }) +public class RoleBasedActionPrivilegesTest { + // TODO Create unlimited role statically here + private static final RoleV7 UNLIMITED_ROLE = RoleV7.fromYamlStringUnchecked(""" + cluster_permissions: + - "*" + index_permissions: + - index_patterns: + - "*" + allowed_actions: + - "*" + """); + public static class ClusterPrivileges { @Test public void wellKnown() throws Exception { @@ -79,7 +90,7 @@ public void wellKnown() throws Exception { " cluster_permissions:\n" + // " - cluster:monitor/nodes/stats*", CType.ROLES); - ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/stats"), isAllowed()); assertThat( @@ -98,7 +109,7 @@ public void notWellKnown() throws Exception { " cluster_permissions:\n" + // " - cluster:monitor/nodes/stats*", CType.ROLES); - ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/stats/somethingnotwellknown"), isAllowed()); assertThat( @@ -117,7 +128,7 @@ public void wildcard() throws Exception { " cluster_permissions:\n" + // " - '*'", CType.ROLES); - ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:whatever"), isAllowed()); assertThat( @@ -126,28 +137,6 @@ public void wildcard() throws Exception { ); } - @Test - public void wildcardByUsername() throws Exception { - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.empty(CType.ROLES); - - ActionPrivileges subject = new ActionPrivileges( - roles, - FlattenedActionGroups.EMPTY, - null, - Settings.EMPTY, - Map.of("plugin:org.opensearch.sample.SamplePlugin", Set.of("*")) - ); - - assertThat( - subject.hasClusterPrivilege(ctxByUsername("plugin:org.opensearch.sample.SamplePlugin"), "cluster:whatever"), - isAllowed() - ); - assertThat( - subject.hasClusterPrivilege(ctx("plugin:org.opensearch.other.OtherPlugin"), "cluster:whatever"), - isForbidden(missingPrivileges("cluster:whatever")) - ); - } - @Test public void explicit_wellKnown() throws Exception { SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("non_explicit_role:\n" + // @@ -162,7 +151,7 @@ public void explicit_wellKnown() throws Exception { CType.ROLES ); - ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); assertThat(subject.hasExplicitClusterPrivilege(ctx("explicit_role"), "cluster:monitor/nodes/stats"), isAllowed()); assertThat(subject.hasExplicitClusterPrivilege(ctx("semi_explicit_role"), "cluster:monitor/nodes/stats"), isAllowed()); @@ -190,7 +179,7 @@ public void explicit_notWellKnown() throws Exception { CType.ROLES ); - ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); assertThat(subject.hasExplicitClusterPrivilege(ctx("explicit_role"), "cluster:monitor/nodes/notwellknown"), isAllowed()); assertThat(subject.hasExplicitClusterPrivilege(ctx("semi_explicit_role"), "cluster:monitor/nodes/notwellknown"), isAllowed()); @@ -210,7 +199,7 @@ public void hasAny_wellKnown() throws Exception { " cluster_permissions:\n" + // " - cluster:monitor/nodes/stats*", CType.ROLES); - ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); assertThat(subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/nodes/stats")), isAllowed()); assertThat( @@ -237,7 +226,7 @@ public void hasAny_notWellKnown() throws Exception { " cluster_permissions:\n" + // " - cluster:monitor/nodes/*", CType.ROLES); - ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); assertThat( subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/nodes/notwellknown")), @@ -271,7 +260,7 @@ public void hasAny_wildcard() throws Exception { " cluster_permissions:\n" + // " - '*'", CType.ROLES); - ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); assertThat(subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:whatever")), isAllowed()); @@ -306,7 +295,7 @@ public static class IndicesAndAliases { final String primaryAction; final ImmutableSet requiredActions; final ImmutableSet otherActions; - final ActionPrivileges subject; + final RoleBasedActionPrivileges subject; @Test public void positive_full() throws Exception { @@ -466,19 +455,21 @@ public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec, Statefulnes Settings settings = Settings.EMPTY; if (statefulness == Statefulness.STATEFUL_LIMITED) { settings = Settings.builder() - .put(ActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey(), new ByteSizeValue(10, ByteSizeUnit.BYTES)) + .put( + RoleBasedActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey(), + new ByteSizeValue(10, ByteSizeUnit.BYTES) + ) .build(); } - this.subject = new ActionPrivileges( + this.subject = new RoleBasedActionPrivileges( roles, FlattenedActionGroups.EMPTY, () -> INDEX_METADATA, settings, WellKnownActions.CLUSTER_ACTIONS, WellKnownActions.INDEX_ACTIONS, - WellKnownActions.INDEX_ACTIONS, - Map.of() + WellKnownActions.INDEX_ACTIONS ); if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { @@ -518,7 +509,7 @@ public static class DataStreams { final String primaryAction; final ImmutableSet requiredActions; final ImmutableSet otherActions; - final ActionPrivileges subject; + final RoleBasedActionPrivileges subject; @Test public void positive_full() throws Exception { @@ -647,11 +638,14 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness stat Settings settings = Settings.EMPTY; if (statefulness == Statefulness.STATEFUL_LIMITED) { settings = Settings.builder() - .put(ActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey(), new ByteSizeValue(10, ByteSizeUnit.BYTES)) + .put( + RoleBasedActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey(), + new ByteSizeValue(10, ByteSizeUnit.BYTES) + ) .build(); } - this.subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, () -> INDEX_METADATA, settings); + this.subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, () -> INDEX_METADATA, settings); if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { this.subject.updateStatefulIndexPrivileges(INDEX_METADATA, 1); @@ -823,7 +817,7 @@ public void relevantOnly_identity() throws Exception { assertTrue( "relevantOnly() returned identical object", - ActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata) == metadata + RoleBasedActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata) == metadata ); } @@ -837,7 +831,7 @@ 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 = ActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata); + Map filteredMetadata = RoleBasedActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata); assertNotNull("Filtered metadata contains index_open_1", filteredMetadata.get("index_open_1")); assertNull("Filtered metadata does not contain index_closed", filteredMetadata.get("index_closed")); @@ -850,7 +844,7 @@ 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 = ActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata); + Map filteredMetadata = RoleBasedActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata); 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")); @@ -860,12 +854,15 @@ public void relevantOnly_dataStreamBackingIndices() throws Exception { public void backingIndexToDataStream() { Map metadata = indices("index").dataStream("data_stream").build().getIndicesLookup(); - assertEquals("index", ActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream("index", metadata)); + assertEquals("index", RoleBasedActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream("index", metadata)); assertEquals( "data_stream", - ActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream(".ds-data_stream-000001", metadata) + RoleBasedActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream(".ds-data_stream-000001", metadata) + ); + assertEquals( + "non_existing", + RoleBasedActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream("non_existing", metadata) ); - assertEquals("non_existing", ActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream("non_existing", metadata)); } @Test @@ -878,7 +875,7 @@ public void hasIndexPrivilege_errors() throws Exception { CType.ROLES ); - ActionPrivileges subject = new ActionPrivileges( + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( roles, FlattenedActionGroups.EMPTY, () -> Collections.emptyMap(), @@ -909,7 +906,7 @@ public void hasExplicitIndexPrivilege_errors() throws Exception { CType.ROLES ); - ActionPrivileges subject = new ActionPrivileges( + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( roles, FlattenedActionGroups.EMPTY, () -> Collections.emptyMap(), @@ -943,7 +940,12 @@ public void aliasesOnDataStreamBackingIndices() throws Exception { + " allowed_actions: ['indices:data/write/index']", CType.ROLES ); - ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, () -> metadata, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + () -> metadata, + Settings.EMPTY + ); subject.updateStatefulIndexPrivileges(metadata, 2); PrivilegesEvaluatorResponse resultForIndexCoveredByAlias = subject.hasIndexPrivilege( @@ -974,7 +976,12 @@ public static class StatefulIndexPrivilegesHeapSize { @Test public void estimatedSize() throws Exception { - ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, () -> indices, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + () -> indices, + Settings.EMPTY + ); subject.updateStatefulIndexPrivileges(indices, 1); @@ -1080,6 +1087,7 @@ static PrivilegesEvaluationContext ctx(String... roles) { null, null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), + null, null ); } @@ -1094,6 +1102,7 @@ static PrivilegesEvaluationContext ctxByUsername(String username) { null, null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), + null, null ); } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/TenantPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/TenantPrivilegesTest.java index 6c549a911d..315e738bc1 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/TenantPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/TenantPrivilegesTest.java @@ -386,7 +386,8 @@ static PrivilegesEvaluationContext ctx(String... roles) { null, null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - null + null, + ActionPrivileges.EMPTY ); } @@ -400,7 +401,8 @@ static PrivilegesEvaluationContext ctxWithDifferentUserAttr(String... roles) { null, null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - null + null, + ActionPrivileges.EMPTY ); } 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 2c8e6de587..5fe2837d19 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DlsFlsLegacyHeadersTest.java @@ -35,6 +35,7 @@ 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; @@ -345,7 +346,8 @@ public void prepare_ccs() throws Exception { null, null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - () -> clusterState + () -> clusterState, + ActionPrivileges.EMPTY ); DlsFlsLegacyHeaders.prepare(threadContext, ctx, dlsFlsProcessedConfig(exampleRolesConfig(), metadata), metadata, false); @@ -364,7 +366,8 @@ static PrivilegesEvaluationContext ctx(Metadata metadata, String... roles) { null, null, new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - () -> clusterState + () -> clusterState, + ActionPrivileges.EMPTY ); } 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 1ffa4e7ad8..8814bc10e1 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java @@ -51,6 +51,7 @@ import org.opensearch.index.query.QueryBuilder; import org.opensearch.index.query.QueryBuilders; import org.opensearch.index.query.TermQueryBuilder; +import org.opensearch.security.privileges.ActionPrivileges; import org.opensearch.security.privileges.PrivilegesConfigurationValidationException; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluationException; @@ -526,7 +527,8 @@ public IndicesAndAliases_getRestriction( null, null, null, - () -> CLUSTER_STATE + () -> CLUSTER_STATE, + ActionPrivileges.EMPTY ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -841,7 +843,8 @@ public IndicesRequest indices(String... strings) { null, RESOLVER_REPLACER, INDEX_NAME_EXPRESSION_RESOLVER, - () -> CLUSTER_STATE + () -> CLUSTER_STATE, + ActionPrivileges.EMPTY ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -1126,7 +1129,8 @@ public DataStreams_getRestriction( null, null, null, - () -> CLUSTER_STATE + () -> CLUSTER_STATE, + ActionPrivileges.EMPTY ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -1146,7 +1150,19 @@ public void invalidQuery() throws Exception { @Test(expected = PrivilegesEvaluationException.class) public void invalidTemplatedQuery() throws Exception { DocumentPrivileges.DlsQuery.create("{\"invalid\": \"totally ${attr.foo}\"}", xContentRegistry) - .evaluate(new PrivilegesEvaluationContext(new User("test_user"), ImmutableSet.of(), null, null, null, null, null, null)); + .evaluate( + new PrivilegesEvaluationContext( + new User("test_user"), + ImmutableSet.of(), + null, + null, + null, + null, + null, + null, + ActionPrivileges.EMPTY + ) + ); } @Test 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 a7ce8b0c1d..75d05e1fae 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldMaskingTest.java @@ -22,6 +22,7 @@ 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; @@ -124,7 +125,8 @@ static PrivilegesEvaluationContext ctx(String... roles) { null, null, null, - () -> CLUSTER_STATE + () -> CLUSTER_STATE, + ActionPrivileges.EMPTY ); } } 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 ad2e19eb8d..084b90d0ff 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/FieldPrivilegesTest.java @@ -21,6 +21,7 @@ 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; @@ -161,7 +162,8 @@ static PrivilegesEvaluationContext ctx(String... roles) { null, null, null, - () -> CLUSTER_STATE + () -> CLUSTER_STATE, + ActionPrivileges.EMPTY ); } } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 781a80795f..f6701d6dc6 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -168,11 +168,11 @@ import org.opensearch.security.http.XFFResolver; import org.opensearch.security.identity.ContextProvidingPluginSubject; import org.opensearch.security.identity.SecurityTokenManager; -import org.opensearch.security.privileges.ActionPrivileges; import org.opensearch.security.privileges.PrivilegesEvaluationException; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; +import org.opensearch.security.privileges.RoleBasedActionPrivileges; import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resources.ResourceAccessControlClient; @@ -2135,7 +2135,7 @@ public List> getSettings() { ); // Privileges evaluation - settings.add(ActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE); + settings.add(RoleBasedActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE); // Resource Sharing settings.add( diff --git a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java index 080e2a8a08..447f134877 100644 --- a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java @@ -170,7 +170,7 @@ protected final boolean isBlockedSystemIndexRequest() { String permission = ConfigConstants.SYSTEM_INDEX_PERMISSION; PrivilegesEvaluationContext context = evaluator.createContext(user, permission); - PrivilegesEvaluatorResponse result = evaluator.getActionPrivileges() + PrivilegesEvaluatorResponse result = context.getActionPrivileges() .hasExplicitIndexPrivilege(context, Set.of(permission), IndexResolverReplacer.Resolved.ofIndex(index.getName())); return !result.isAllowed(); diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java index f095d31b2c..9ee104246f 100644 --- a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -11,160 +11,62 @@ package org.opensearch.security.privileges; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.cluster.metadata.DataStream; -import org.opensearch.cluster.metadata.IndexAbstraction; -import org.opensearch.cluster.metadata.IndexMetadata; -import org.opensearch.cluster.metadata.Metadata; -import org.opensearch.common.settings.Setting; -import org.opensearch.common.settings.Settings; -import org.opensearch.core.common.unit.ByteSizeUnit; -import org.opensearch.core.common.unit.ByteSizeValue; 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; -import org.opensearch.security.support.WildcardMatcher; - -import com.selectivem.collections.CheckTable; -import com.selectivem.collections.CompactMapGroupBuilder; -import com.selectivem.collections.DeduplicatingCompactSubSetBuilder; -import com.selectivem.collections.ImmutableCompactSubSet; /** - * This class converts role configuration into pre-computed, optimized data structures for checking privileges. + * Defines the general interface for evaluating privileges on actions. References to ActionPrivileges instances + * should be usually obtained by PrivilegesEvaluator.createContext().getActionPrivileges(). *

- * With the exception of the statefulIndex property, instances of this class are immutable. The life-cycle of an - * instance of this class corresponds to the life-cycle of the role and action group configuration. If the role or - * action group configuration is changed, a new instance needs to be built. + * Different ActionPrivileges implementations might consider different data from the PrivilegeEvaluationContext + * for privilege evaluation. Some (like RoleBasedActionPrivileges) might consider the mapped roles. Others might + * be completely self-sufficient because the PrivilegesEvaluator.createContext() method already checked all + * pre-conditions to choose the correct instance (e.g. for plugins or API tokens). */ -public class ActionPrivileges extends ClusterStateMetadataDependentPrivileges { +public interface ActionPrivileges { /** - * This setting controls the allowed heap size of the precomputed index privileges (in the inner class StatefulIndexPrivileges). - * If the size of the indices exceed the amount of bytes configured here, it will be truncated. Privileges evaluation will - * continue to work correctly, but it will be slower. - *

- * This settings defaults to 10 MB. This is a generous limit. Experiments have shown that an example setup with - * 10,000 indices and 1,000 roles requires about 1 MB of heap. 100,000 indices and 100 roles require about 9 MB of heap. - * (Of course, these numbers can vary widely based on the actual role configuration). + * Checks whether this instance provides privileges for the provided action. + * + * @param context The context of the privilege evaluation. Depending on the ActionPrivileges implementation, + * the mapped role from the context might be used (RoleBasedActionPrivileges) or not. + * @param action The name of the OpenSearch action to be evaluated. + * @return Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. */ - public static Setting PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE = Setting.memorySizeSetting( - "plugins.security.privileges_evaluation.precomputed_privileges.max_heap_size", - new ByteSizeValue(10, ByteSizeUnit.MB), - Setting.Property.NodeScope - ); - - private static final Logger log = LogManager.getLogger(ActionPrivileges.class); - - private final ClusterPrivileges cluster; - private final IndexPrivileges index; - private final SecurityDynamicConfiguration roles; - private final FlattenedActionGroups actionGroups; - private final ImmutableSet wellKnownClusterActions; - private final ImmutableSet wellKnownIndexActions; - private final Supplier> indexMetadataSupplier; - private final ByteSizeValue statefulIndexMaxHeapSize; + PrivilegesEvaluatorResponse hasClusterPrivilege(PrivilegesEvaluationContext context, String action); - private final AtomicReference statefulIndex = new AtomicReference<>(); - - public ActionPrivileges( - SecurityDynamicConfiguration roles, - FlattenedActionGroups actionGroups, - Supplier> indexMetadataSupplier, - Settings settings, - ImmutableSet wellKnownClusterActions, - ImmutableSet wellKnownIndexActions, - ImmutableSet explicitlyRequiredIndexActions, - Map> pluginToClusterActions - ) { - this.cluster = new ClusterPrivileges(roles, actionGroups, wellKnownClusterActions, pluginToClusterActions); - this.index = new IndexPrivileges(roles, actionGroups, wellKnownIndexActions, explicitlyRequiredIndexActions); - this.roles = roles; - this.actionGroups = actionGroups; - this.wellKnownClusterActions = wellKnownClusterActions; - this.wellKnownIndexActions = wellKnownIndexActions; - this.indexMetadataSupplier = indexMetadataSupplier; - this.statefulIndexMaxHeapSize = PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.get(settings); - } - - public ActionPrivileges( - SecurityDynamicConfiguration roles, - FlattenedActionGroups actionGroups, - Supplier> indexMetadataSupplier, - Settings settings - ) { - this( - roles, - actionGroups, - indexMetadataSupplier, - settings, - WellKnownActions.CLUSTER_ACTIONS, - WellKnownActions.INDEX_ACTIONS, - WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS, - Map.of() - ); - } - - public ActionPrivileges( - SecurityDynamicConfiguration roles, - FlattenedActionGroups actionGroups, - Supplier> indexMetadataSupplier, - Settings settings, - Map> pluginToClusterActions - ) { - this( - roles, - actionGroups, - indexMetadataSupplier, - settings, - WellKnownActions.CLUSTER_ACTIONS, - WellKnownActions.INDEX_ACTIONS, - WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS, - pluginToClusterActions - ); - } - - public PrivilegesEvaluatorResponse hasClusterPrivilege(PrivilegesEvaluationContext context, String action) { - return cluster.providesPrivilege(context, action, context.getMappedRoles()); - } + /** + * Checks whether this instance provides privileges for any of the provided actions. + * + * @param context The context of the privilege evaluation. Depending on the ActionPrivileges implementation, + * the mapped role from the context might be used (RoleBasedActionPrivileges) or not. + * @param actions The names of the OpenSearch action to be evaluated. + * @return Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ - public PrivilegesEvaluatorResponse hasAnyClusterPrivilege(PrivilegesEvaluationContext context, Set actions) { - return cluster.providesAnyPrivilege(context, actions, context.getMappedRoles()); - } + PrivilegesEvaluatorResponse hasAnyClusterPrivilege(PrivilegesEvaluationContext context, Set actions); /** * Checks whether this instance provides explicit privileges for the combination of the provided action and the - * provided roles. + * provided context. *

* Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 - *

- * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. - * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + * + * @param context The context of the privilege evaluation. Depending on the ActionPrivileges implementation, + * the mapped role from the context might be used (RoleBasedActionPrivileges) or not. + * @param action The name of the OpenSearch action to be evaluated. + * @return Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. */ - public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluationContext context, String action) { - return cluster.providesExplicitPrivilege(context, action, context.getMappedRoles()); - } + PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluationContext context, String action); /** * Checks whether this instance provides privileges for the combination of the provided action, - * the provided indices and the provided roles. + * the provided indices and the provided context. *

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

@@ -172,50 +74,11 @@ public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluat * and the indices for which privileges are available are returned by getAvailableIndices(). This allows the * do_not_fail_on_forbidden behaviour. */ - public PrivilegesEvaluatorResponse hasIndexPrivilege( + PrivilegesEvaluatorResponse hasIndexPrivilege( PrivilegesEvaluationContext context, Set actions, IndexResolverReplacer.Resolved resolvedIndices - ) { - PrivilegesEvaluatorResponse response = this.index.providesWildcardPrivilege(context, actions); - if (response != null) { - return response; - } - - if (!resolvedIndices.isLocalAll() && resolvedIndices.getAllIndices().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"); - return PrivilegesEvaluatorResponse.ok(); - } - - // 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 - ); - - StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); - PrivilegesEvaluatorResponse resultFromStatefulIndex = null; - - Map indexMetadata = this.indexMetadataSupplier.get(); - - if (statefulIndex != null) { - resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, resolvedIndices, context, checkTable, indexMetadata); - - if (resultFromStatefulIndex != null) { - // If we get a result from statefulIndex, we are done. - return resultFromStatefulIndex; - } - - // Otherwise, we need to carry on checking privileges using the non-stateful object. - // Note: statefulIndex.hasPermission() modifies as a side effect the checkTable. - // We can carry on using this as an intermediate result and further complete checkTable below. - } - - return this.index.providesPrivilege(context, actions, resolvedIndices, checkTable, indexMetadata); - } + ); /** * Checks whether this instance provides explicit privileges for the combination of the provided action, @@ -224,986 +87,44 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 */ - public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( + PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( PrivilegesEvaluationContext context, Set actions, IndexResolverReplacer.Resolved resolvedIndices - ) { - CheckTable checkTable = CheckTable.create(resolvedIndices.getAllIndices(), actions); - return this.index.providesExplicitPrivilege(context, actions, resolvedIndices, checkTable, this.indexMetadataSupplier.get()); - } - - /** - * Updates the stateful index configuration with the given indices. Should be normally only called by - * updateStatefulIndexPrivilegesAsync(). Package visible for testing. - */ - void updateStatefulIndexPrivileges(Map indices, long metadataVersion) { - StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); - - indices = StatefulIndexPrivileges.relevantOnly(indices); - - if (statefulIndex == null || !statefulIndex.indices.equals(indices)) { - long start = System.currentTimeMillis(); - this.statefulIndex.set( - new StatefulIndexPrivileges(roles, actionGroups, wellKnownIndexActions, indices, metadataVersion, statefulIndexMaxHeapSize) - ); - long duration = System.currentTimeMillis() - start; - log.debug("Updating StatefulIndexPrivileges took {} ms", duration); - } else { - synchronized (this) { - // Even if the indices did not change, update the metadataVersion in statefulIndex to reflect - // that the instance is up-to-date. - if (statefulIndex.metadataVersion < metadataVersion) { - statefulIndex.metadataVersion = metadataVersion; - } - } - } - } - - @Override - protected void updateClusterStateMetadata(Metadata metadata) { - this.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); - } - - @Override - protected long getCurrentlyUsedMetadataVersion() { - StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); - return statefulIndex != null ? statefulIndex.metadataVersion : 0; - } - - int getEstimatedStatefulIndexByteSize() { - StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); - - if (statefulIndex != null) { - return statefulIndex.estimatedByteSize; - } else { - return 0; - } - } - - /** - * Pre-computed, optimized cluster privilege maps. Instances of this class are immutable. - *

- * The data structures in this class are optimized for answering the question - * "I have action A and roles [x,y,z]. Do I have authorization to execute the action?". - *

- * The check will be possible in time O(1) for "well-known" actions when the user actually has the privileges. - */ - static class ClusterPrivileges { - - /** - * Maps names of actions to the roles that provide a privilege for the respective action. - * Note that the mapping is not comprehensive, additionally the data structures rolesWithWildcardPermissions - * and rolesToActionMatcher need to be considered for a full view of the privileges. - *

- * This does not include privileges obtained via "*" action patterns. This is both meant as a - * optimization and to support explicit privileges. - */ - private final ImmutableMap> actionToRoles; - - /** - * This contains all role names that provide wildcard (*) privileges for cluster actions. - * This avoids a blow-up of the actionToRoles object by such roles. - */ - private final ImmutableSet rolesWithWildcardPermissions; - - /** - * This maps role names to a matcher which matches the action names this role provides privileges for. - * This is only used as a last resort if the test with actionToRole and rolesWithWildcardPermissions failed. - * This is only necessary for actions which are not contained in the list of "well-known" actions provided - * during construction. - * - * This does not include privileges obtained via "*" action patterns. This is both meant as a - * optimization and to support explicit privileges. - */ - private final ImmutableMap rolesToActionMatcher; - - private final ImmutableMap usersToActionMatcher; - - private final ImmutableSet wellKnownClusterActions; - - /** - * Creates pre-computed cluster privileges based on the given parameters. - *

- * This constructor will not throw an exception if it encounters any invalid configuration (that is, - * in particular, unparseable regular expressions). Rather, it will just log an error. This is okay, as it - * just results in fewer available privileges. However, having a proper error reporting mechanism would be - * kind of nice. - */ - ClusterPrivileges( - SecurityDynamicConfiguration roles, - FlattenedActionGroups actionGroups, - ImmutableSet wellKnownClusterActions, - Map> pluginToClusterActions - ) { - DeduplicatingCompactSubSetBuilder roleSetBuilder = new DeduplicatingCompactSubSetBuilder<>( - roles.getCEntries().keySet() - ); - Map> actionToRoles = new HashMap<>(); - ImmutableSet.Builder rolesWithWildcardPermissions = ImmutableSet.builder(); - ImmutableMap.Builder rolesToActionMatcher = ImmutableMap.builder(); - ImmutableMap.Builder usersToActionMatcher = ImmutableMap.builder(); - - for (Map.Entry entry : roles.getCEntries().entrySet()) { - try { - String roleName = entry.getKey(); - RoleV7 role = entry.getValue(); - - roleSetBuilder.next(roleName); - - ImmutableSet permissionPatterns = actionGroups.resolve(role.getCluster_permissions()); - - // This list collects all the matchers for action names that will be found for the current role - List wildcardMatchers = new ArrayList<>(); - - for (String permission : permissionPatterns) { - // If we have a permission which does not use any pattern, we just simply add it to the - // "actionToRoles" map. - // Otherwise, we match the pattern against the provided well-known cluster actions and add - // these to the "actionToRoles" map. Additionally, for the case that the well-known cluster - // actions are not complete, we also collect the matcher to be used as a last resort later. - - if (WildcardMatcher.isExact(permission)) { - actionToRoles.computeIfAbsent(permission, k -> roleSetBuilder.createSubSetBuilder()).add(roleName); - } else if (permission.equals("*")) { - // Special case: Roles with a wildcard "*" giving privileges for all actions. We will not resolve - // this stuff, but just note separately that this role just gets all the cluster privileges. - rolesWithWildcardPermissions.add(roleName); - } else { - WildcardMatcher wildcardMatcher = WildcardMatcher.from(permission); - Set matchedActions = wildcardMatcher.getMatchAny( - wellKnownClusterActions, - Collectors.toUnmodifiableSet() - ); - - for (String action : matchedActions) { - actionToRoles.computeIfAbsent(action, k -> roleSetBuilder.createSubSetBuilder()).add(roleName); - } - - wildcardMatchers.add(wildcardMatcher); - } - } - - if (!wildcardMatchers.isEmpty()) { - rolesToActionMatcher.put(roleName, WildcardMatcher.from(wildcardMatchers)); - } - } catch (Exception e) { - log.error("Unexpected exception while processing role: {}\nIgnoring role.", entry.getKey(), e); - } - } - - if (pluginToClusterActions != null) { - for (String pluginIdentifier : pluginToClusterActions.keySet()) { - Set clusterActions = pluginToClusterActions.get(pluginIdentifier); - WildcardMatcher matcher = WildcardMatcher.from(clusterActions); - usersToActionMatcher.put(pluginIdentifier, matcher); - } - } - - DeduplicatingCompactSubSetBuilder.Completed completedRoleSetBuilder = roleSetBuilder.build(); - - this.actionToRoles = actionToRoles.entrySet() - .stream() - .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry -> entry.getValue().build(completedRoleSetBuilder))); - this.rolesWithWildcardPermissions = rolesWithWildcardPermissions.build(); - this.rolesToActionMatcher = rolesToActionMatcher.build(); - this.usersToActionMatcher = usersToActionMatcher.build(); - this.wellKnownClusterActions = wellKnownClusterActions; - } - - /** - * Checks whether this instance provides privileges for the combination of the provided action and the - * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. - * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. - */ - PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext context, String action, Set roles) { - - // 1: Check roles with wildcards - if (CollectionUtils.containsAny(roles, this.rolesWithWildcardPermissions)) { - return PrivilegesEvaluatorResponse.ok(); - } - - // 2: Check well-known actions - this should cover most cases - ImmutableCompactSubSet rolesWithPrivileges = this.actionToRoles.get(action); - - if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(roles)) { - return PrivilegesEvaluatorResponse.ok(); - } - - // 3: Only if everything else fails: Check the matchers in case we have a non-well-known action - if (!this.wellKnownClusterActions.contains(action)) { - for (String role : roles) { - WildcardMatcher matcher = this.rolesToActionMatcher.get(role); - - if (matcher != null && matcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - } - - // 4: If plugin is performing the action, check if plugin has permission - if (context.getUser().isPluginUser() && this.usersToActionMatcher.containsKey(context.getUser().getName())) { - WildcardMatcher matcher = this.usersToActionMatcher.get(context.getUser().getName()); - if (matcher != null && matcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } + ); - return PrivilegesEvaluatorResponse.insufficient(action); + ActionPrivileges EMPTY = new ActionPrivileges() { + @Override + public PrivilegesEvaluatorResponse hasClusterPrivilege(PrivilegesEvaluationContext context, String action) { + return PrivilegesEvaluatorResponse.insufficient(action).reason("User has no privileges"); } - /** - * Checks whether this instance provides explicit privileges for the combination of the provided action and the - * provided roles. - *

- * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns - * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 - *

- * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. - * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. - */ - PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContext context, String action, Set roles) { - - // 1: Check well-known actions - this should cover most cases - ImmutableCompactSubSet rolesWithPrivileges = this.actionToRoles.get(action); - - if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(roles)) { - return PrivilegesEvaluatorResponse.ok(); - } - - // 2: Only if everything else fails: Check the matchers in case we have a non-well-known action - if (!this.wellKnownClusterActions.contains(action)) { - for (String role : roles) { - WildcardMatcher matcher = this.rolesToActionMatcher.get(role); - - if (matcher != null && matcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - } - - return PrivilegesEvaluatorResponse.insufficient(action); + @Override + public PrivilegesEvaluatorResponse hasAnyClusterPrivilege(PrivilegesEvaluationContext context, Set actions) { + return PrivilegesEvaluatorResponse.insufficient("any of " + actions).reason("User has no privileges"); } - /** - * Checks whether this instance provides privileges for the combination of any of the provided actions and the - * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. - * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. - */ - PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext context, Set actions, Set roles) { - // 1: Check roles with wildcards - if (CollectionUtils.containsAny(roles, this.rolesWithWildcardPermissions)) { - return PrivilegesEvaluatorResponse.ok(); - } - - // 2: Check well-known actions - this should cover most cases - for (String action : actions) { - ImmutableCompactSubSet rolesWithPrivileges = this.actionToRoles.get(action); - - if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(roles)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - - // 3: Only if everything else fails: Check the matchers in case we have a non-well-known action - for (String action : actions) { - if (!this.wellKnownClusterActions.contains(action)) { - for (String role : roles) { - WildcardMatcher matcher = this.rolesToActionMatcher.get(role); - - if (matcher != null && matcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - } - } - - // 4: If plugin is performing the action, check if plugin has permission - if (this.usersToActionMatcher.containsKey(context.getUser().getName())) { - WildcardMatcher matcher = this.usersToActionMatcher.get(context.getUser().getName()); - for (String action : actions) { - if (matcher != null && matcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - } - - if (actions.size() == 1) { - return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next()); - } else { - return PrivilegesEvaluatorResponse.insufficient("any of " + actions); - } + @Override + public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluationContext context, String action) { + return PrivilegesEvaluatorResponse.insufficient(action).reason("User has no privileges"); } - } - - /** - * Partially pre-computed, optimized index privilege maps. Instances of this class are immutable. - *

- * This class is independent of the actual indices present in the cluster. See StatefulIndexPermissions for a class - * that also takes actual indices into account and is thus fully pre-computed. - *

- * Purposes of this class: - *

- * 1. Answer the question "given an action and a set of roles, do I have wildcard index privileges" in O(1) - *

- * 2. Pre-compute the data structures as far as possible in cases that StatefulIndexPermissions cannot check the - * permissions. This is the case when: - *

- * a) StatefulIndexPermissions does not cover all indices - * b) The requested index does not exist (especially the case for create index actions) - * c) The index patterns use placeholders like "${user.name}" - these can be only resolved when the User object is present. - * d) The action is not among the "well known" actions. - */ - static class IndexPrivileges { - /** - * Maps role names to concrete action names to IndexPattern objects which define the indices the privileges apply to. - */ - private final ImmutableMap> rolesToActionToIndexPattern; - /** - * Maps role names to action names matchers to IndexPattern objects which define the indices the privileges apply to. - * This is especially for "non-well-known" actions. - */ - private final ImmutableMap> rolesToActionPatternToIndexPattern; - - /** - * Maps action names to the roles which provide wildcard ("*") index privileges for the respective action. - * This allows to answer the question "given an action and a set of roles, do I have wildcard index privileges" - * in O(1) - */ - private final ImmutableMap> actionToRolesWithWildcardIndexPrivileges; - - /** - * A pre-defined set of action names that is used to pre-compute the result of action patterns. - */ - private final ImmutableSet wellKnownIndexActions; - - /** - * A pre-defined set of action names that is included in the rolesToExplicitActionToIndexPattern data structure - */ - private final ImmutableSet explicitlyRequiredIndexActions; - - /** - * Maps role names to concrete action names to IndexPattern objects which define the indices the privileges apply to. - * The action names are only explicitly granted privileges which are listed in explicitlyRequiredIndexActions. - *

- * Compare https://github.com/opensearch-project/security/pull/2887 - */ - private final ImmutableMap> rolesToExplicitActionToIndexPattern; - - /** - * Creates pre-computed index privileges based on the given parameters. - *

- * This constructor will not throw an exception if it encounters any invalid configuration (that is, - * in particular, unparseable regular expressions). Rather, it will just log an error. This is okay, as it - * just results in fewer available privileges. However, having a proper error reporting mechanism would be - * kind of nice. - */ - IndexPrivileges( - SecurityDynamicConfiguration roles, - FlattenedActionGroups actionGroups, - ImmutableSet wellKnownIndexActions, - ImmutableSet explicitlyRequiredIndexActions - ) { - DeduplicatingCompactSubSetBuilder roleSetBuilder = new DeduplicatingCompactSubSetBuilder<>( - roles.getCEntries().keySet() - ); - - Map> rolesToActionToIndexPattern = new HashMap<>(); - Map> rolesToActionPatternToIndexPattern = new HashMap<>(); - Map> actionToRolesWithWildcardIndexPrivileges = new HashMap<>(); - Map> rolesToExplicitActionToIndexPattern = new HashMap<>(); - - for (Map.Entry entry : roles.getCEntries().entrySet()) { - try { - String roleName = entry.getKey(); - RoleV7 role = entry.getValue(); - - roleSetBuilder.next(roleName); - - for (RoleV7.Index indexPermissions : role.getIndex_permissions()) { - ImmutableSet permissions = actionGroups.resolve(indexPermissions.getAllowed_actions()); - - for (String permission : permissions) { - // If we have a permission which does not use any pattern, we just simply add it to the - // "rolesToActionToIndexPattern" map. - // Otherwise, we match the pattern against the provided well-known index actions and add - // these to the "rolesToActionToIndexPattern" map. Additionally, for the case that the - // well-known index actions are not complete, we also collect the actionMatcher to be used - // as a last resort later. - - if (WildcardMatcher.isExact(permission)) { - rolesToActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) - .computeIfAbsent(permission, k -> new IndexPattern.Builder()) - .add(indexPermissions.getIndex_patterns()); - - if (explicitlyRequiredIndexActions.contains(permission)) { - rolesToExplicitActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) - .computeIfAbsent(permission, k -> new IndexPattern.Builder()) - .add(indexPermissions.getIndex_patterns()); - } - - if (indexPermissions.getIndex_patterns().contains("*")) { - actionToRolesWithWildcardIndexPrivileges.computeIfAbsent( - permission, - k -> roleSetBuilder.createSubSetBuilder() - ).add(roleName); - } - } else { - WildcardMatcher actionMatcher = WildcardMatcher.from(permission); - - for (String action : actionMatcher.iterateMatching(wellKnownIndexActions)) { - rolesToActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) - .computeIfAbsent(action, k -> new IndexPattern.Builder()) - .add(indexPermissions.getIndex_patterns()); - - if (indexPermissions.getIndex_patterns().contains("*")) { - actionToRolesWithWildcardIndexPrivileges.computeIfAbsent( - permission, - k -> roleSetBuilder.createSubSetBuilder() - ).add(roleName); - } - } - - rolesToActionPatternToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) - .computeIfAbsent(actionMatcher, k -> new IndexPattern.Builder()) - .add(indexPermissions.getIndex_patterns()); - - if (actionMatcher != WildcardMatcher.ANY) { - for (String action : actionMatcher.iterateMatching(explicitlyRequiredIndexActions)) { - rolesToExplicitActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) - .computeIfAbsent(action, k -> new IndexPattern.Builder()) - .add(indexPermissions.getIndex_patterns()); - } - } - } - } - } - } catch (Exception e) { - log.error("Unexpected exception while processing role: {}\nIgnoring role.", entry.getKey(), e); - } - } - - DeduplicatingCompactSubSetBuilder.Completed completedRoleSetBuilder = roleSetBuilder.build(); - - this.rolesToActionToIndexPattern = rolesToActionToIndexPattern.entrySet() - .stream() - .collect( - ImmutableMap.toImmutableMap( - Map.Entry::getKey, - entry -> entry.getValue() - .entrySet() - .stream() - .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry2 -> entry2.getValue().build())) - ) - ); - - this.rolesToActionPatternToIndexPattern = rolesToActionPatternToIndexPattern.entrySet() - .stream() - .collect( - ImmutableMap.toImmutableMap( - Map.Entry::getKey, - entry -> entry.getValue() - .entrySet() - .stream() - .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry2 -> entry2.getValue().build())) - ) - ); - - this.actionToRolesWithWildcardIndexPrivileges = actionToRolesWithWildcardIndexPrivileges.entrySet() - .stream() - .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry -> entry.getValue().build(completedRoleSetBuilder))); - - this.rolesToExplicitActionToIndexPattern = rolesToExplicitActionToIndexPattern.entrySet() - .stream() - .collect( - ImmutableMap.toImmutableMap( - Map.Entry::getKey, - entry -> entry.getValue() - .entrySet() - .stream() - .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry2 -> entry2.getValue().build())) - ) - ); - - this.wellKnownIndexActions = wellKnownIndexActions; - this.explicitlyRequiredIndexActions = explicitlyRequiredIndexActions; - } - - /** - * Checks whether this instance provides privileges for the combination of the provided action, - * the provided indices and the provided roles. - *

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

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

- * This method will only verify privileges for the index/action combinations which are un-checked in - * the checkTable instance provided to this method. Checked index/action combinations are considered to be - * "already fulfilled by other means" - usually that comes from the stateful data structure. - * As a side-effect, this method will further mark the available index/action combinations in the provided - * checkTable instance as checked. - */ - PrivilegesEvaluatorResponse providesPrivilege( + @Override + public PrivilegesEvaluatorResponse hasIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices, - CheckTable checkTable, - Map indexMetadata + IndexResolverReplacer.Resolved resolvedIndices ) { - List exceptions = new ArrayList<>(); - - for (String role : context.getMappedRoles()) { - ImmutableMap actionToIndexPattern = this.rolesToActionToIndexPattern.get(role); - - if (actionToIndexPattern != null) { - for (String action : actions) { - IndexPattern indexPattern = actionToIndexPattern.get(action); - - if (indexPattern != null) { - for (String index : checkTable.iterateUncheckedRows(action)) { - try { - if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } 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)); - } - } - } - } - } - } - - // 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 - - boolean allActionsWellKnown = actions.stream().allMatch(a -> this.wellKnownIndexActions.contains(a)); - - if (!checkTable.isComplete() && !allActionsWellKnown) { - top: for (String role : context.getMappedRoles()) { - ImmutableMap actionPatternToIndexPattern = this.rolesToActionPatternToIndexPattern.get( - role - ); - - if (actionPatternToIndexPattern != null) { - for (String action : actions) { - if (this.wellKnownIndexActions.contains(action)) { - continue; - } - - for (Map.Entry entry : actionPatternToIndexPattern.entrySet()) { - WildcardMatcher actionMatcher = entry.getKey(); - IndexPattern indexPattern = entry.getValue(); - - if (actionMatcher.test(action)) { - for (String index : checkTable.iterateUncheckedRows(action)) { - try { - if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { - break top; - } - } 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)); - } - } - } - } - } - } - } - } - - if (checkTable.isComplete()) { - return PrivilegesEvaluatorResponse.ok(); - } - - Set availableIndices = checkTable.getCompleteRows(); - - if (!availableIndices.isEmpty()) { - return PrivilegesEvaluatorResponse.partiallyOk(availableIndices, checkTable).evaluationExceptions(exceptions); - } - - return PrivilegesEvaluatorResponse.insufficient(checkTable) - .reason( - resolvedIndices.getAllIndices().size() == 1 - ? "Insufficient permissions for the referenced index" - : "None of " + resolvedIndices.getAllIndices().size() + " referenced indices has sufficient permissions" - ) - .evaluationExceptions(exceptions); - } - - /** - * 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 - * the user's privileges. - */ - PrivilegesEvaluatorResponse providesWildcardPrivilege(PrivilegesEvaluationContext context, Set actions) { - ImmutableSet effectiveRoles = context.getMappedRoles(); - - for (String action : actions) { - ImmutableCompactSubSet rolesWithWildcardIndexPrivileges = this.actionToRolesWithWildcardIndexPrivileges.get(action); - - if (rolesWithWildcardIndexPrivileges == null || !rolesWithWildcardIndexPrivileges.containsAny(effectiveRoles)) { - return null; - } - } - - return PrivilegesEvaluatorResponse.ok(); + return PrivilegesEvaluatorResponse.insufficient("all of " + actions).reason("User has no privileges"); } - /** - * Checks whether this instance provides explicit privileges for the combination of the provided action, - * the provided indices and the provided roles. - *

- * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns - * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 - */ - PrivilegesEvaluatorResponse providesExplicitPrivilege( + @Override + public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices, - CheckTable checkTable, - Map indexMetadata + IndexResolverReplacer.Resolved resolvedIndices ) { - List exceptions = new ArrayList<>(); - - if (!CollectionUtils.containsAny(actions, this.explicitlyRequiredIndexActions)) { - return PrivilegesEvaluatorResponse.insufficient(CheckTable.create(ImmutableSet.of("_"), actions)); - } - - for (String role : context.getMappedRoles()) { - ImmutableMap actionToIndexPattern = this.rolesToExplicitActionToIndexPattern.get(role); - - if (actionToIndexPattern != null) { - for (String action : actions) { - IndexPattern indexPattern = actionToIndexPattern.get(action); - - if (indexPattern != null) { - for (String index : checkTable.iterateUncheckedRows(action)) { - try { - if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } 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 PrivilegesEvaluatorResponse.insufficient(checkTable) - .reason("No explicit privileges have been provided for the referenced indices.") - .evaluationExceptions(exceptions); + return PrivilegesEvaluatorResponse.insufficient("all of " + actions).reason("User has no privileges"); } - } - - /** - * Fully pre-computed, optimized index privilege maps. - *

- * The data structures in this class are optimized to answer the question "given an action, an index and a set of - * roles, do I have the respective privilege" in O(1). - *

- * There are cases where this class will not be able to answer this question. These cases are the following: - * - The requested index does not exist (especially the case for create index actions) - * - The action is not well-known. - * - The indices used for pre-computing the data structures are not complete (possibly due to race conditions) - * - The role definition uses placeholders (like "${user.name}") in index patterns. - * - The role definition grants privileges to all indices (via "*") (these are omitted here for efficiency reasons). - * In such cases, the question needs to be answered by IndexPermissions (see above). - *

- * This class also takes into account aliases and data streams. If a permission is granted on an alias, it will be - * automatically inherited by the indices it points to. The same holds for the backing indices of a data stream. - */ - static class StatefulIndexPrivileges { - - /** - * Maps concrete action names to concrete index names and then to the roles which provide privileges for the - * combination of action and index. This map can contain besides indices also names of data streams and aliases. - * For aliases and data streams, it will then contain both the actual alias/data stream and the backing indices. - */ - private final Map>> actionToIndexToRoles; - - /** - * The index information that was used to construct this instance. - */ - private final Map indices; - - /** - * The well known index actions that were used to construct this instance. - */ - private final ImmutableSet wellKnownIndexActions; - - private final int estimatedByteSize; - - private long metadataVersion; - - /** - * Creates pre-computed index privileges based on the given parameters. - *

- * This constructor will not throw an exception if it encounters any invalid configuration (that is, - * in particular, unparseable regular expressions). Rather, it will just log an error. This is okay, as it - * just results in fewer available privileges. - */ - StatefulIndexPrivileges( - SecurityDynamicConfiguration roles, - FlattenedActionGroups actionGroups, - ImmutableSet wellKnownIndexActions, - Map indices, - long metadataVersion, - ByteSizeValue statefulIndexMaxHeapSize - ) { - Map< - String, - CompactMapGroupBuilder.MapBuilder>> actionToIndexToRoles = - new HashMap<>(); - DeduplicatingCompactSubSetBuilder roleSetBuilder = new DeduplicatingCompactSubSetBuilder<>( - roles.getCEntries().keySet() - ); - CompactMapGroupBuilder> indexMapBuilder = - new CompactMapGroupBuilder<>(indices.keySet(), (k2) -> roleSetBuilder.createSubSetBuilder()); - - // We iterate here through the present RoleV7 instances and nested through their "index_permissions" sections. - // During the loop, the actionToIndexToRoles map is being built. - // For that, action patterns from the role will be matched against the "well-known actions" to build - // a concrete action map and index patterns from the role will be matched against the present indices - // to build a concrete index map. - // - // The complexity of this loop is O(n*m) where n is dependent on the structure of the roles configuration - // and m is the number of matched indices. This formula does not take the loop through matchedActions in - // account, as this is bound by a constant number and thus does not need to be considered in the O() notation. - - top: for (Map.Entry entry : roles.getCEntries().entrySet()) { - try { - String roleName = entry.getKey(); - RoleV7 role = entry.getValue(); - - roleSetBuilder.next(roleName); - - for (RoleV7.Index indexPermissions : role.getIndex_permissions()) { - ImmutableSet permissions = actionGroups.resolve(indexPermissions.getAllowed_actions()); - - if (indexPermissions.getIndex_patterns().contains("*")) { - // Wildcard index patterns are handled in the static IndexPermissions object. - // This avoids having to build huge data structures - when a very easy shortcut is available. - continue; - } - - WildcardMatcher indexMatcher = IndexPattern.from(indexPermissions.getIndex_patterns()).getStaticPattern(); - - if (indexMatcher == WildcardMatcher.NONE) { - // The pattern is likely blank because there are only templated patterns. - // Index patterns with templates are not handled here, but in the static IndexPermissions object - continue; - } - - for (String permission : permissions) { - WildcardMatcher actionMatcher = WildcardMatcher.from(permission); - Collection matchedActions = actionMatcher.getMatchAny(wellKnownIndexActions, Collectors.toList()); - - for (Map.Entry indicesEntry : indexMatcher.iterateMatching( - indices.entrySet(), - Map.Entry::getKey - )) { - for (String action : matchedActions) { - CompactMapGroupBuilder.MapBuilder< - String, - DeduplicatingCompactSubSetBuilder.SubSetBuilder> indexToRoles = actionToIndexToRoles - .computeIfAbsent(action, k -> indexMapBuilder.createMapBuilder()); - - indexToRoles.get(indicesEntry.getKey()).add(roleName); - - if (indicesEntry.getValue() instanceof IndexAbstraction.Alias) { - // For aliases we additionally add the sub-indices to the privilege map - for (IndexMetadata subIndex : indicesEntry.getValue().getIndices()) { - String subIndexName = subIndex.getIndex().getName(); - // We need to check whether the subIndex is part of the global indices - // metadata map because that map has been filtered by relevantOnly(). - // This method removes all closed indices and data stream backing indices - // because these indices get a separate treatment. However, these indices - // might still appear as member indices of aliases. Trying to add these - // to the SubSetBuilder indexToRoles would result in an IllegalArgumentException - // because the subIndex will not be part of the super set. - if (indices.containsKey(subIndexName)) { - indexToRoles.get(subIndexName).add(roleName); - } else { - log.debug( - "Ignoring member index {} of alias {}. This is usually the case because the index is closed or a data stream backing index.", - subIndexName, - indicesEntry.getKey() - ); - } - } - } - - if (roleSetBuilder.getEstimatedByteSize() + indexMapBuilder - .getEstimatedByteSize() > statefulIndexMaxHeapSize.getBytes()) { - log.info( - "Size of precomputed index privileges exceeds configured limit ({}). Using capped data structure." - + "This might lead to slightly lower performance during privilege evaluation. Consider raising {}.", - statefulIndexMaxHeapSize, - PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey() - ); - break top; - } - } - } - } - } - } catch (Exception e) { - log.error("Unexpected exception while processing role: {}\nIgnoring role.", entry.getKey(), e); - } - } - - DeduplicatingCompactSubSetBuilder.Completed completedRoleSetBuilder = roleSetBuilder.build(); - - this.estimatedByteSize = roleSetBuilder.getEstimatedByteSize() + indexMapBuilder.getEstimatedByteSize(); - log.debug("Estimated size of StatefulIndexPermissions data structure: {}", this.estimatedByteSize); - - this.actionToIndexToRoles = actionToIndexToRoles.entrySet() - .stream() - .collect( - ImmutableMap.toImmutableMap( - Map.Entry::getKey, - entry -> entry.getValue().build(subSetBuilder -> subSetBuilder.build(completedRoleSetBuilder)) - ) - ); - - this.indices = ImmutableMap.copyOf(indices); - this.metadataVersion = metadataVersion; - this.wellKnownIndexActions = wellKnownIndexActions; - } - - /** - * Checks whether the user has privileges based on the given parameters and information in this class. This method - * has two major channels for returning results: - *

- * 1. The return value is either PrivilegesEvaluatorResponse.ok() or null. If it is null, this method cannot - * completely tell whether the user has full privileges. A further check with IndexPermissions will be necessary. - * If PrivilegesEvaluatorResponse.ok() is returned, then full privileges could be already determined. - *

- * 2. As a side effect, this method will modify the supplied CheckTable object. This will be the case regardless - * of whether null or PrivilegesEvaluatorResponse.ok() is returned. The interesting case is actually when null - * 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. - */ - PrivilegesEvaluatorResponse providesPrivilege( - Set actions, - IndexResolverReplacer.Resolved resolvedIndices, - PrivilegesEvaluationContext context, - CheckTable checkTable, - Map indexMetadata - ) { - ImmutableSet effectiveRoles = context.getMappedRoles(); - - for (String action : actions) { - Map> indexToRoles = actionToIndexToRoles.get(action); - - if (indexToRoles != null) { - for (String index : resolvedIndices.getAllIndices()) { - String lookupIndex = index; - - if (index.startsWith(DataStream.BACKING_INDEX_PREFIX)) { - // If we have a backing index of a data stream, we will not try to test - // the backing index here, as we filter backing indices during initialization. - // Instead, we look up the containing data stream and check whether this has privileges. - lookupIndex = backingIndexToDataStream(index, indexMetadata); - } - - ImmutableCompactSubSet rolesWithPrivileges = indexToRoles.get(lookupIndex); - - if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(effectiveRoles)) { - if (checkTable.check(index, action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - } - } - } - - // If we reached this point, we cannot tell whether the user has privileges using this instance. - // Return null to indicate that there is no answer. - // The checkTable object might contain already a partial result. - return null; - } - - /** - * If the given index is the backing index of a data stream, the name of the data stream is returned. - * Otherwise, the name of the index itself is being returned. - */ - static String backingIndexToDataStream(String index, Map indexMetadata) { - IndexAbstraction indexAbstraction = indexMetadata.get(index); - - if (indexAbstraction instanceof IndexAbstraction.Index && indexAbstraction.getParentDataStream() != null) { - return indexAbstraction.getParentDataStream().getName(); - } else { - return index; - } - } - - /** - * Filters the given index abstraction map to only contain entries that are relevant the for stateful class. - * This has the goal to keep the heap footprint of instances of StatefulIndexPrivileges at a reasonable size. - *

- * This removes the following entries: - *

    - *
  • closed indices - closed indices do not need any fast privilege evaluation - *
  • backing indices of data streams - privileges should be only assigned directly to the data streams. - * the privilege evaluation code is able to recognize that an index is member of a data stream and test - * its privilege via that data stream. If a privilege is directly assigned to a backing index, we use - * the "slowish" code paths. - *
  • Indices which are not matched by includeIndices - *
- */ - static Map relevantOnly(Map indices) { - // First pass: Check if we need to filter at all - boolean doFilter = false; - - for (IndexAbstraction indexAbstraction : indices.values()) { - if (indexAbstraction instanceof IndexAbstraction.Index) { - if (indexAbstraction.getParentDataStream() != null - || indexAbstraction.getWriteIndex().getState() == IndexMetadata.State.CLOSE) { - doFilter = true; - break; - } - } - } - - 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) { - builder.put(indexAbstraction.getName(), indexAbstraction); - } - } else { - builder.put(indexAbstraction.getName(), indexAbstraction); - } - } - - return builder.build(); - } - } - + }; } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index f7e5d6de7d..b4cc2fe805 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -46,6 +46,12 @@ public class PrivilegesEvaluationContext { private final IndexNameExpressionResolver indexNameExpressionResolver; private final Supplier clusterStateSupplier; + /** + * Stores the ActionPrivileges instance to be used for this request. Plugin system users or users created from + * API tokens might use ActionPrivileges instances which do not correspond to the normal role configuration. + */ + private final ActionPrivileges actionPrivileges; + /** * This caches the ready to use WildcardMatcher instances for the current request. Many index patterns have * to be executed several times per request (for example first for action privileges, later for DLS). Thus, @@ -61,7 +67,8 @@ public PrivilegesEvaluationContext( Task task, IndexResolverReplacer indexResolverReplacer, IndexNameExpressionResolver indexNameExpressionResolver, - Supplier clusterStateSupplier + Supplier clusterStateSupplier, + ActionPrivileges actionPrivileges ) { this.user = user; this.mappedRoles = mappedRoles; @@ -71,6 +78,7 @@ public PrivilegesEvaluationContext( this.indexResolverReplacer = indexResolverReplacer; this.indexNameExpressionResolver = indexNameExpressionResolver; this.task = task; + this.actionPrivileges = actionPrivileges; } public User getUser() { @@ -156,6 +164,14 @@ public IndexNameExpressionResolver getIndexNameExpressionResolver() { return indexNameExpressionResolver; } + /** + * Returns the ActionPrivileges instance to be used for this request. Plugin system users or users created from + * API tokens might use ActionPrivileges instances which do not correspond to the normal role configuration. + */ + public ActionPrivileges getActionPrivileges() { + return actionPrivileges; + } + @Override public String toString() { return "PrivilegesEvaluationContext{" diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index d0d8408f28..14bbc88536 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -76,6 +76,7 @@ import org.opensearch.action.update.UpdateAction; import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.AliasMetadata; +import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.metadata.Metadata; @@ -93,6 +94,7 @@ import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.CType; @@ -155,9 +157,18 @@ public class PrivilegesEvaluator { private DynamicConfigModel dcm; private final NamedXContentRegistry namedXContentRegistry; private final Settings settings; - private final Map> pluginToClusterActions; - private final AtomicReference actionPrivileges = new AtomicReference<>(); + private final AtomicReference actionPrivileges = new AtomicReference<>(); private final AtomicReference tenantPrivileges = new AtomicReference<>(); + private final Map pluginIdToActionPrivileges = new HashMap<>(); + private final Supplier> indexMetadataSupplier; + + /** + * 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, @@ -180,7 +191,6 @@ public PrivilegesEvaluator( this.threadContext = threadContext; this.privilegesInterceptor = privilegesInterceptor; - this.pluginToClusterActions = new HashMap<>(); this.clusterStateSupplier = clusterStateSupplier; this.settings = settings; @@ -198,6 +208,10 @@ public PrivilegesEvaluator( pitPrivilegesEvaluator = new PitPrivilegesEvaluator(); this.namedXContentRegistry = namedXContentRegistry; this.configurationRepository = configurationRepository; + this.staticActionGroups = new FlattenedActionGroups( + DynamicConfigFactory.addStatics(SecurityDynamicConfiguration.empty(CType.ACTIONGROUPS)) + ); + this.indexMetadataSupplier = () -> clusterStateSupplier.get().metadata().getIndicesLookup(); if (configurationRepository != null) { configurationRepository.subscribeOnChange(configMap -> { @@ -213,7 +227,7 @@ public PrivilegesEvaluator( if (clusterService != null) { clusterService.addListener(event -> { - ActionPrivileges actionPrivileges = PrivilegesEvaluator.this.actionPrivileges.get(); + RoleBasedActionPrivileges actionPrivileges = PrivilegesEvaluator.this.actionPrivileges.get(); if (actionPrivileges != null) { actionPrivileges.updateClusterStateMetadataAsync(clusterService, threadPool); } @@ -231,16 +245,15 @@ void updateConfiguration( rolesConfiguration = rolesConfiguration.withStaticConfig(); tenantConfiguration = tenantConfiguration.withStaticConfig(); try { - ActionPrivileges actionPrivileges = new ActionPrivileges( + RoleBasedActionPrivileges actionPrivileges = new RoleBasedActionPrivileges( rolesConfiguration, flattenedActionGroups, - () -> clusterStateSupplier.get().metadata().getIndicesLookup(), - settings, - pluginToClusterActions + indexMetadataSupplier, + settings ); Metadata metadata = clusterStateSupplier.get().metadata(); actionPrivileges.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); - ActionPrivileges oldInstance = this.actionPrivileges.getAndSet(actionPrivileges); + RoleBasedActionPrivileges oldInstance = this.actionPrivileges.getAndSet(actionPrivileges); if (oldInstance != null) { oldInstance.shutdown(); @@ -266,13 +279,9 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { this.dcm = dcm; } - public ActionPrivileges getActionPrivileges() { - return this.actionPrivileges.get(); - } - public boolean hasRestAdminPermissions(final User user, final TransportAddress remoteAddress, final String permission) { PrivilegesEvaluationContext context = createContext(user, permission); - return this.actionPrivileges.get().hasExplicitClusterPrivilege(context, permission).isAllowed(); + return context.getActionPrivileges().hasExplicitClusterPrivilege(context, permission).isAllowed(); } public boolean isInitialized() { @@ -311,9 +320,32 @@ public PrivilegesEvaluationContext createContext( } TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - ImmutableSet mappedRoles = ImmutableSet.copyOf((injectedRoles == null) ? mapRoles(user, caller) : injectedRoles); - return new PrivilegesEvaluationContext(user, mappedRoles, action0, request, task, irr, resolver, clusterStateSupplier); + ActionPrivileges actionPrivileges; + ImmutableSet mappedRoles; + + if (user.isPluginUser()) { + mappedRoles = ImmutableSet.of(); + actionPrivileges = this.pluginIdToActionPrivileges.get(user.getName()); + if (actionPrivileges == null) { + actionPrivileges = ActionPrivileges.EMPTY; + } + } else { + mappedRoles = ImmutableSet.copyOf((injectedRoles == null) ? mapRoles(user, caller) : injectedRoles); + actionPrivileges = this.actionPrivileges.get(); + } + + return new PrivilegesEvaluationContext( + user, + mappedRoles, + action0, + request, + task, + irr, + resolver, + clusterStateSupplier, + actionPrivileges + ); } public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { @@ -366,7 +398,7 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) log.debug("Mapped roles: {}", mappedRoles.toString()); } - ActionPrivileges actionPrivileges = this.actionPrivileges.get(); + ActionPrivileges actionPrivileges = context.getActionPrivileges(); if (actionPrivileges == null) { throw new OpenSearchSecurityException("OpenSearch Security is not initialized: roles configuration is missing"); } @@ -851,5 +883,10 @@ private List toString(List aliases) { public void updatePluginToClusterActions(String pluginIdentifier, Set clusterActions) { pluginToClusterActions.put(pluginIdentifier, clusterActions); + public void updatePluginToPermissions(String pluginIdentifier, RoleV7 pluginPermissions) { + this.pluginIdToActionPrivileges.put( + pluginIdentifier, + new SubjectBasedActionPrivileges(pluginPermissions, this.staticActionGroups, this.indexMetadataSupplier) + ); } } diff --git a/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java index b1f994163c..5274ad3456 100644 --- a/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java @@ -36,7 +36,7 @@ public PrivilegesEvaluatorResponse evaluate(final User user, final String routeN log.debug("Mapped roles: {}", context.getMappedRoles().toString()); } - PrivilegesEvaluatorResponse result = privilegesEvaluator.getActionPrivileges().hasAnyClusterPrivilege(context, actions); + PrivilegesEvaluatorResponse result = context.getActionPrivileges().hasAnyClusterPrivilege(context, actions); if (!result.allowed) { log.info( diff --git a/src/main/java/org/opensearch/security/privileges/RoleBasedActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/RoleBasedActionPrivileges.java new file mode 100644 index 0000000000..0a8ef4c75b --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/RoleBasedActionPrivileges.java @@ -0,0 +1,1185 @@ +/* + * 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.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.cluster.metadata.DataStream; +import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.unit.ByteSizeUnit; +import org.opensearch.core.common.unit.ByteSizeValue; +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; +import org.opensearch.security.support.WildcardMatcher; + +import com.selectivem.collections.CheckTable; +import com.selectivem.collections.CompactMapGroupBuilder; +import com.selectivem.collections.DeduplicatingCompactSubSetBuilder; +import com.selectivem.collections.ImmutableCompactSubSet; + +/** + * This class converts role configuration into pre-computed, optimized data structures for checking privileges. + *

+ * With the exception of the statefulIndex property, instances of this class are immutable. The life-cycle of an + * instance of this class corresponds to the life-cycle of the role and action group configuration. If the role or + * action group configuration is changed, a new instance needs to be built. + */ +public class RoleBasedActionPrivileges extends ClusterStateMetadataDependentPrivileges implements ActionPrivileges { + + /** + * This setting controls the allowed heap size of the precomputed index privileges (in the inner class StatefulIndexPrivileges). + * If the size of the indices exceed the amount of bytes configured here, it will be truncated. Privileges evaluation will + * continue to work correctly, but it will be slower. + *

+ * This settings defaults to 10 MB. This is a generous limit. Experiments have shown that an example setup with + * 10,000 indices and 1,000 roles requires about 1 MB of heap. 100,000 indices and 100 roles require about 9 MB of heap. + * (Of course, these numbers can vary widely based on the actual role configuration). + */ + public static Setting PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE = Setting.memorySizeSetting( + "plugins.security.privileges_evaluation.precomputed_privileges.max_heap_size", + new ByteSizeValue(10, ByteSizeUnit.MB), + Setting.Property.NodeScope + ); + + private static final Logger log = LogManager.getLogger(RoleBasedActionPrivileges.class); + + private final ClusterPrivileges cluster; + private final IndexPrivileges index; + private final SecurityDynamicConfiguration roles; + private final FlattenedActionGroups actionGroups; + private final ImmutableSet wellKnownClusterActions; + private final ImmutableSet wellKnownIndexActions; + private final Supplier> indexMetadataSupplier; + private final ByteSizeValue statefulIndexMaxHeapSize; + + private final AtomicReference statefulIndex = new AtomicReference<>(); + + public RoleBasedActionPrivileges( + SecurityDynamicConfiguration roles, + FlattenedActionGroups actionGroups, + Supplier> indexMetadataSupplier, + Settings settings, + ImmutableSet wellKnownClusterActions, + ImmutableSet wellKnownIndexActions, + ImmutableSet explicitlyRequiredIndexActions + ) { + this.cluster = new ClusterPrivileges(roles, actionGroups, wellKnownClusterActions); + this.index = new IndexPrivileges(roles, actionGroups, wellKnownIndexActions, explicitlyRequiredIndexActions); + this.roles = roles; + this.actionGroups = actionGroups; + this.wellKnownClusterActions = wellKnownClusterActions; + this.wellKnownIndexActions = wellKnownIndexActions; + this.indexMetadataSupplier = indexMetadataSupplier; + this.statefulIndexMaxHeapSize = PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.get(settings); + } + + public RoleBasedActionPrivileges( + SecurityDynamicConfiguration roles, + FlattenedActionGroups actionGroups, + Supplier> indexMetadataSupplier, + Settings settings + ) { + this( + roles, + actionGroups, + indexMetadataSupplier, + settings, + WellKnownActions.CLUSTER_ACTIONS, + WellKnownActions.INDEX_ACTIONS, + WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS + ); + } + + @Override + public PrivilegesEvaluatorResponse hasClusterPrivilege(PrivilegesEvaluationContext context, String action) { + return cluster.providesPrivilege(context, action, context.getMappedRoles()); + } + + @Override + public PrivilegesEvaluatorResponse hasAnyClusterPrivilege(PrivilegesEvaluationContext context, Set actions) { + return cluster.providesAnyPrivilege(context, actions, context.getMappedRoles()); + } + + /** + * Checks whether this instance provides explicit privileges for the combination of the provided action and the + * provided roles. + *

+ * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns + * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 + *

+ * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ + @Override + public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluationContext context, String action) { + return cluster.providesExplicitPrivilege(context, action, context.getMappedRoles()); + } + + /** + * 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. + */ + @Override + public PrivilegesEvaluatorResponse hasIndexPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices + ) { + PrivilegesEvaluatorResponse response = this.index.providesWildcardPrivilege(context, actions); + if (response != null) { + return response; + } + + if (!resolvedIndices.isLocalAll() && resolvedIndices.getAllIndices().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"); + return PrivilegesEvaluatorResponse.ok(); + } + + // 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 + ); + + StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); + PrivilegesEvaluatorResponse resultFromStatefulIndex = null; + + Map indexMetadata = this.indexMetadataSupplier.get(); + + if (statefulIndex != null) { + resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, resolvedIndices, context, checkTable, indexMetadata); + + if (resultFromStatefulIndex != null) { + // If we get a result from statefulIndex, we are done. + return resultFromStatefulIndex; + } + + // Otherwise, we need to carry on checking privileges using the non-stateful object. + // Note: statefulIndex.hasPermission() modifies as a side effect the checkTable. + // We can carry on using this as an intermediate result and further complete checkTable below. + } + + return this.index.providesPrivilege(context, actions, resolvedIndices, checkTable, indexMetadata); + } + + /** + * Checks whether this instance provides explicit privileges for the combination of the provided action, + * the provided indices and the provided roles. + *

+ * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns + * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 + */ + @Override + public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices + ) { + CheckTable checkTable = CheckTable.create(resolvedIndices.getAllIndices(), actions); + return this.index.providesExplicitPrivilege(context, actions, resolvedIndices, checkTable, this.indexMetadataSupplier.get()); + } + + /** + * Updates the stateful index configuration with the given indices. Should be normally only called by + * updateStatefulIndexPrivilegesAsync(). Package visible for testing. + */ + void updateStatefulIndexPrivileges(Map indices, long metadataVersion) { + StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); + + indices = StatefulIndexPrivileges.relevantOnly(indices); + + if (statefulIndex == null || !statefulIndex.indices.equals(indices)) { + long start = System.currentTimeMillis(); + this.statefulIndex.set( + new StatefulIndexPrivileges(roles, actionGroups, wellKnownIndexActions, indices, metadataVersion, statefulIndexMaxHeapSize) + ); + long duration = System.currentTimeMillis() - start; + log.debug("Updating StatefulIndexPrivileges took {} ms", duration); + } else { + synchronized (this) { + // Even if the indices did not change, update the metadataVersion in statefulIndex to reflect + // that the instance is up-to-date. + if (statefulIndex.metadataVersion < metadataVersion) { + statefulIndex.metadataVersion = metadataVersion; + } + } + } + } + + @Override + protected void updateClusterStateMetadata(Metadata metadata) { + this.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); + } + + @Override + protected long getCurrentlyUsedMetadataVersion() { + StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); + return statefulIndex != null ? statefulIndex.metadataVersion : 0; + } + + int getEstimatedStatefulIndexByteSize() { + StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); + + if (statefulIndex != null) { + return statefulIndex.estimatedByteSize; + } else { + return 0; + } + } + + /** + * Pre-computed, optimized cluster privilege maps. Instances of this class are immutable. + *

+ * The data structures in this class are optimized for answering the question + * "I have action A and roles [x,y,z]. Do I have authorization to execute the action?". + *

+ * The check will be possible in time O(1) for "well-known" actions when the user actually has the privileges. + */ + static class ClusterPrivileges { + + /** + * Maps names of actions to the roles that provide a privilege for the respective action. + * Note that the mapping is not comprehensive, additionally the data structures rolesWithWildcardPermissions + * and rolesToActionMatcher need to be considered for a full view of the privileges. + *

+ * This does not include privileges obtained via "*" action patterns. This is both meant as a + * optimization and to support explicit privileges. + */ + private final ImmutableMap> actionToRoles; + + /** + * This contains all role names that provide wildcard (*) privileges for cluster actions. + * This avoids a blow-up of the actionToRoles object by such roles. + */ + private final ImmutableSet rolesWithWildcardPermissions; + + /** + * This maps role names to a matcher which matches the action names this role provides privileges for. + * This is only used as a last resort if the test with actionToRole and rolesWithWildcardPermissions failed. + * This is only necessary for actions which are not contained in the list of "well-known" actions provided + * during construction. + * + * This does not include privileges obtained via "*" action patterns. This is both meant as a + * optimization and to support explicit privileges. + */ + private final ImmutableMap rolesToActionMatcher; + + private final ImmutableMap usersToActionMatcher; + + private final ImmutableSet wellKnownClusterActions; + + /** + * Creates pre-computed cluster privileges based on the given parameters. + *

+ * This constructor will not throw an exception if it encounters any invalid configuration (that is, + * in particular, unparseable regular expressions). Rather, it will just log an error. This is okay, as it + * just results in fewer available privileges. However, having a proper error reporting mechanism would be + * kind of nice. + */ + ClusterPrivileges( + SecurityDynamicConfiguration roles, + FlattenedActionGroups actionGroups, + ImmutableSet wellKnownClusterActions + ) { + DeduplicatingCompactSubSetBuilder roleSetBuilder = new DeduplicatingCompactSubSetBuilder<>( + roles.getCEntries().keySet() + ); + Map> actionToRoles = new HashMap<>(); + ImmutableSet.Builder rolesWithWildcardPermissions = ImmutableSet.builder(); + ImmutableMap.Builder rolesToActionMatcher = ImmutableMap.builder(); + ImmutableMap.Builder usersToActionMatcher = ImmutableMap.builder(); + + for (Map.Entry entry : roles.getCEntries().entrySet()) { + try { + String roleName = entry.getKey(); + RoleV7 role = entry.getValue(); + + roleSetBuilder.next(roleName); + + ImmutableSet permissionPatterns = actionGroups.resolve(role.getCluster_permissions()); + + // This list collects all the matchers for action names that will be found for the current role + List wildcardMatchers = new ArrayList<>(); + + for (String permission : permissionPatterns) { + // If we have a permission which does not use any pattern, we just simply add it to the + // "actionToRoles" map. + // Otherwise, we match the pattern against the provided well-known cluster actions and add + // these to the "actionToRoles" map. Additionally, for the case that the well-known cluster + // actions are not complete, we also collect the matcher to be used as a last resort later. + + if (WildcardMatcher.isExact(permission)) { + actionToRoles.computeIfAbsent(permission, k -> roleSetBuilder.createSubSetBuilder()).add(roleName); + } else if (permission.equals("*")) { + // Special case: Roles with a wildcard "*" giving privileges for all actions. We will not resolve + // this stuff, but just note separately that this role just gets all the cluster privileges. + rolesWithWildcardPermissions.add(roleName); + } else { + WildcardMatcher wildcardMatcher = WildcardMatcher.from(permission); + Set matchedActions = wildcardMatcher.getMatchAny( + wellKnownClusterActions, + Collectors.toUnmodifiableSet() + ); + + for (String action : matchedActions) { + actionToRoles.computeIfAbsent(action, k -> roleSetBuilder.createSubSetBuilder()).add(roleName); + } + + wildcardMatchers.add(wildcardMatcher); + } + } + + if (!wildcardMatchers.isEmpty()) { + rolesToActionMatcher.put(roleName, WildcardMatcher.from(wildcardMatchers)); + } + } catch (Exception e) { + log.error("Unexpected exception while processing role: {}\nIgnoring role.", entry.getKey(), e); + } + } + + DeduplicatingCompactSubSetBuilder.Completed completedRoleSetBuilder = roleSetBuilder.build(); + + this.actionToRoles = actionToRoles.entrySet() + .stream() + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry -> entry.getValue().build(completedRoleSetBuilder))); + this.rolesWithWildcardPermissions = rolesWithWildcardPermissions.build(); + this.rolesToActionMatcher = rolesToActionMatcher.build(); + this.usersToActionMatcher = usersToActionMatcher.build(); + this.wellKnownClusterActions = wellKnownClusterActions; + } + + /** + * Checks whether this instance provides privileges for the combination of the provided action and the + * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ + PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext context, String action, Set roles) { + + // 1: Check roles with wildcards + if (CollectionUtils.containsAny(roles, this.rolesWithWildcardPermissions)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 2: Check well-known actions - this should cover most cases + ImmutableCompactSubSet rolesWithPrivileges = this.actionToRoles.get(action); + + if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(roles)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 3: Only if everything else fails: Check the matchers in case we have a non-well-known action + if (!this.wellKnownClusterActions.contains(action)) { + for (String role : roles) { + WildcardMatcher matcher = this.rolesToActionMatcher.get(role); + + if (matcher != null && matcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } + + // 4: If plugin is performing the action, check if plugin has permission + if (context.getUser().isPluginUser() && this.usersToActionMatcher.containsKey(context.getUser().getName())) { + WildcardMatcher matcher = this.usersToActionMatcher.get(context.getUser().getName()); + if (matcher != null && matcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + + return PrivilegesEvaluatorResponse.insufficient(action); + } + + /** + * Checks whether this instance provides explicit privileges for the combination of the provided action and the + * provided roles. + *

+ * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns + * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 + *

+ * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ + PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContext context, String action, Set roles) { + + // 1: Check well-known actions - this should cover most cases + ImmutableCompactSubSet rolesWithPrivileges = this.actionToRoles.get(action); + + if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(roles)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 2: Only if everything else fails: Check the matchers in case we have a non-well-known action + if (!this.wellKnownClusterActions.contains(action)) { + for (String role : roles) { + WildcardMatcher matcher = this.rolesToActionMatcher.get(role); + + if (matcher != null && matcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } + + return PrivilegesEvaluatorResponse.insufficient(action); + } + + /** + * Checks whether this instance provides privileges for the combination of any of the provided actions and the + * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ + PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext context, Set actions, Set roles) { + // 1: Check roles with wildcards + if (CollectionUtils.containsAny(roles, this.rolesWithWildcardPermissions)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 2: Check well-known actions - this should cover most cases + for (String action : actions) { + ImmutableCompactSubSet rolesWithPrivileges = this.actionToRoles.get(action); + + if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(roles)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + + // 3: Only if everything else fails: Check the matchers in case we have a non-well-known action + for (String action : actions) { + if (!this.wellKnownClusterActions.contains(action)) { + for (String role : roles) { + WildcardMatcher matcher = this.rolesToActionMatcher.get(role); + + if (matcher != null && matcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } + } + + // 4: If plugin is performing the action, check if plugin has permission + if (this.usersToActionMatcher.containsKey(context.getUser().getName())) { + WildcardMatcher matcher = this.usersToActionMatcher.get(context.getUser().getName()); + for (String action : actions) { + if (matcher != null && matcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } + + if (actions.size() == 1) { + return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next()); + } else { + return PrivilegesEvaluatorResponse.insufficient("any of " + actions); + } + } + } + + /** + * Partially pre-computed, optimized index privilege maps. Instances of this class are immutable. + *

+ * This class is independent of the actual indices present in the cluster. See StatefulIndexPermissions for a class + * that also takes actual indices into account and is thus fully pre-computed. + *

+ * Purposes of this class: + *

+ * 1. Answer the question "given an action and a set of roles, do I have wildcard index privileges" in O(1) + *

+ * 2. Pre-compute the data structures as far as possible in cases that StatefulIndexPermissions cannot check the + * permissions. This is the case when: + *

+ * a) StatefulIndexPermissions does not cover all indices + * b) The requested index does not exist (especially the case for create index actions) + * c) The index patterns use placeholders like "${user.name}" - these can be only resolved when the User object is present. + * d) The action is not among the "well known" actions. + */ + static class IndexPrivileges { + /** + * Maps role names to concrete action names to IndexPattern objects which define the indices the privileges apply to. + */ + private final ImmutableMap> rolesToActionToIndexPattern; + + /** + * Maps role names to action names matchers to IndexPattern objects which define the indices the privileges apply to. + * This is especially for "non-well-known" actions. + */ + private final ImmutableMap> rolesToActionPatternToIndexPattern; + + /** + * Maps action names to the roles which provide wildcard ("*") index privileges for the respective action. + * This allows to answer the question "given an action and a set of roles, do I have wildcard index privileges" + * in O(1) + */ + private final ImmutableMap> actionToRolesWithWildcardIndexPrivileges; + + /** + * A pre-defined set of action names that is used to pre-compute the result of action patterns. + */ + private final ImmutableSet wellKnownIndexActions; + + /** + * A pre-defined set of action names that is included in the rolesToExplicitActionToIndexPattern data structure + */ + private final ImmutableSet explicitlyRequiredIndexActions; + + /** + * Maps role names to concrete action names to IndexPattern objects which define the indices the privileges apply to. + * The action names are only explicitly granted privileges which are listed in explicitlyRequiredIndexActions. + *

+ * Compare https://github.com/opensearch-project/security/pull/2887 + */ + private final ImmutableMap> rolesToExplicitActionToIndexPattern; + + /** + * Creates pre-computed index privileges based on the given parameters. + *

+ * This constructor will not throw an exception if it encounters any invalid configuration (that is, + * in particular, unparseable regular expressions). Rather, it will just log an error. This is okay, as it + * just results in fewer available privileges. However, having a proper error reporting mechanism would be + * kind of nice. + */ + IndexPrivileges( + SecurityDynamicConfiguration roles, + FlattenedActionGroups actionGroups, + ImmutableSet wellKnownIndexActions, + ImmutableSet explicitlyRequiredIndexActions + ) { + + Map> rolesToActionToIndexPattern = new HashMap<>(); + Map> rolesToActionPatternToIndexPattern = new HashMap<>(); + Map> actionToRolesWithWildcardIndexPrivileges = new HashMap<>(); + Map> rolesToExplicitActionToIndexPattern = new HashMap<>(); + + Map permissionEntries = roles.getCEntries(); + + DeduplicatingCompactSubSetBuilder roleSetBuilder = new DeduplicatingCompactSubSetBuilder<>(permissionEntries.keySet()); + + for (Map.Entry entry : permissionEntries.entrySet()) { + try { + String roleName = entry.getKey(); + RoleV7 role = entry.getValue(); + + roleSetBuilder.next(roleName); + + for (RoleV7.Index indexPermissions : role.getIndex_permissions()) { + ImmutableSet permissions = actionGroups.resolve(indexPermissions.getAllowed_actions()); + + for (String permission : permissions) { + // If we have a permission which does not use any pattern, we just simply add it to the + // "rolesToActionToIndexPattern" map. + // Otherwise, we match the pattern against the provided well-known index actions and add + // these to the "rolesToActionToIndexPattern" map. Additionally, for the case that the + // well-known index actions are not complete, we also collect the actionMatcher to be used + // as a last resort later. + + if (WildcardMatcher.isExact(permission)) { + rolesToActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) + .computeIfAbsent(permission, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + + if (explicitlyRequiredIndexActions.contains(permission)) { + rolesToExplicitActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) + .computeIfAbsent(permission, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + } + + if (indexPermissions.getIndex_patterns().contains("*")) { + actionToRolesWithWildcardIndexPrivileges.computeIfAbsent( + permission, + k -> roleSetBuilder.createSubSetBuilder() + ).add(roleName); + } + } else { + WildcardMatcher actionMatcher = WildcardMatcher.from(permission); + + for (String action : actionMatcher.iterateMatching(wellKnownIndexActions)) { + rolesToActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) + .computeIfAbsent(action, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + + if (indexPermissions.getIndex_patterns().contains("*")) { + actionToRolesWithWildcardIndexPrivileges.computeIfAbsent( + permission, + k -> roleSetBuilder.createSubSetBuilder() + ).add(roleName); + } + } + + rolesToActionPatternToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) + .computeIfAbsent(actionMatcher, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + + if (actionMatcher != WildcardMatcher.ANY) { + for (String action : actionMatcher.iterateMatching(explicitlyRequiredIndexActions)) { + rolesToExplicitActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) + .computeIfAbsent(action, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + } + } + } + } + } + } catch (Exception e) { + log.error("Unexpected exception while processing role: {}\nIgnoring role.", entry.getKey(), e); + } + } + + DeduplicatingCompactSubSetBuilder.Completed completedRoleSetBuilder = roleSetBuilder.build(); + + this.rolesToActionToIndexPattern = rolesToActionToIndexPattern.entrySet() + .stream() + .collect( + ImmutableMap.toImmutableMap( + Map.Entry::getKey, + entry -> entry.getValue() + .entrySet() + .stream() + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry2 -> entry2.getValue().build())) + ) + ); + + this.rolesToActionPatternToIndexPattern = rolesToActionPatternToIndexPattern.entrySet() + .stream() + .collect( + ImmutableMap.toImmutableMap( + Map.Entry::getKey, + entry -> entry.getValue() + .entrySet() + .stream() + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry2 -> entry2.getValue().build())) + ) + ); + + this.actionToRolesWithWildcardIndexPrivileges = actionToRolesWithWildcardIndexPrivileges.entrySet() + .stream() + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry -> entry.getValue().build(completedRoleSetBuilder))); + + this.rolesToExplicitActionToIndexPattern = rolesToExplicitActionToIndexPattern.entrySet() + .stream() + .collect( + ImmutableMap.toImmutableMap( + Map.Entry::getKey, + entry -> entry.getValue() + .entrySet() + .stream() + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry2 -> entry2.getValue().build())) + ) + ); + + this.wellKnownIndexActions = wellKnownIndexActions; + this.explicitlyRequiredIndexActions = explicitlyRequiredIndexActions; + } + + /** + * Checks whether this instance provides privileges for the combination of the provided action, + * the provided indices and the provided roles. + *

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

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

+ * This method will only verify privileges for the index/action combinations which are un-checked in + * the checkTable instance provided to this method. Checked index/action combinations are considered to be + * "already fulfilled by other means" - usually that comes from the stateful data structure. + * As a side-effect, this method will further mark the available index/action combinations in the provided + * checkTable instance as checked. + */ + PrivilegesEvaluatorResponse providesPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices, + CheckTable checkTable, + Map indexMetadata + ) { + List exceptions = new ArrayList<>(); + + for (String role : context.getMappedRoles()) { + ImmutableMap actionToIndexPattern = this.rolesToActionToIndexPattern.get(role); + + if (actionToIndexPattern != null) { + for (String action : actions) { + IndexPattern indexPattern = actionToIndexPattern.get(action); + + if (indexPattern != null) { + for (String index : checkTable.iterateUncheckedRows(action)) { + try { + if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } 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)); + } + } + } + } + } + } + + // 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 + + boolean allActionsWellKnown = actions.stream().allMatch(a -> this.wellKnownIndexActions.contains(a)); + + if (!checkTable.isComplete() && !allActionsWellKnown) { + top: for (String role : context.getMappedRoles()) { + ImmutableMap actionPatternToIndexPattern = this.rolesToActionPatternToIndexPattern.get( + role + ); + + if (actionPatternToIndexPattern != null) { + for (String action : actions) { + if (this.wellKnownIndexActions.contains(action)) { + continue; + } + + for (Map.Entry entry : actionPatternToIndexPattern.entrySet()) { + WildcardMatcher actionMatcher = entry.getKey(); + IndexPattern indexPattern = entry.getValue(); + + if (actionMatcher.test(action)) { + for (String index : checkTable.iterateUncheckedRows(action)) { + try { + if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { + break top; + } + } 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)); + } + } + } + } + } + } + } + } + + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(); + } + + Set availableIndices = checkTable.getCompleteRows(); + + if (!availableIndices.isEmpty()) { + return PrivilegesEvaluatorResponse.partiallyOk(availableIndices, checkTable).evaluationExceptions(exceptions); + } + + return PrivilegesEvaluatorResponse.insufficient(checkTable) + .reason( + resolvedIndices.getAllIndices().size() == 1 + ? "Insufficient permissions for the referenced index" + : "None of " + resolvedIndices.getAllIndices().size() + " referenced indices has sufficient permissions" + ) + .evaluationExceptions(exceptions); + } + + /** + * 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 + * the user's privileges. + */ + PrivilegesEvaluatorResponse providesWildcardPrivilege(PrivilegesEvaluationContext context, Set actions) { + ImmutableSet effectiveRoles = context.getMappedRoles(); + + for (String action : actions) { + ImmutableCompactSubSet rolesWithWildcardIndexPrivileges = this.actionToRolesWithWildcardIndexPrivileges.get(action); + + if (rolesWithWildcardIndexPrivileges == null || !rolesWithWildcardIndexPrivileges.containsAny(effectiveRoles)) { + return null; + } + } + + return PrivilegesEvaluatorResponse.ok(); + } + + /** + * Checks whether this instance provides explicit privileges for the combination of the provided action, + * the provided indices and the provided roles. + *

+ * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns + * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 + */ + PrivilegesEvaluatorResponse providesExplicitPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices, + CheckTable checkTable, + Map indexMetadata + ) { + List exceptions = new ArrayList<>(); + + if (!CollectionUtils.containsAny(actions, this.explicitlyRequiredIndexActions)) { + return PrivilegesEvaluatorResponse.insufficient(CheckTable.create(ImmutableSet.of("_"), actions)); + } + + for (String role : context.getMappedRoles()) { + ImmutableMap actionToIndexPattern = this.rolesToExplicitActionToIndexPattern.get(role); + + if (actionToIndexPattern != null) { + for (String action : actions) { + IndexPattern indexPattern = actionToIndexPattern.get(action); + + if (indexPattern != null) { + for (String index : checkTable.iterateUncheckedRows(action)) { + try { + if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } 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 PrivilegesEvaluatorResponse.insufficient(checkTable) + .reason("No explicit privileges have been provided for the referenced indices.") + .evaluationExceptions(exceptions); + } + } + + /** + * Fully pre-computed, optimized index privilege maps. + *

+ * The data structures in this class are optimized to answer the question "given an action, an index and a set of + * roles, do I have the respective privilege" in O(1). + *

+ * There are cases where this class will not be able to answer this question. These cases are the following: + * - The requested index does not exist (especially the case for create index actions) + * - The action is not well-known. + * - The indices used for pre-computing the data structures are not complete (possibly due to race conditions) + * - The role definition uses placeholders (like "${user.name}") in index patterns. + * - The role definition grants privileges to all indices (via "*") (these are omitted here for efficiency reasons). + * In such cases, the question needs to be answered by IndexPermissions (see above). + *

+ * This class also takes into account aliases and data streams. If a permission is granted on an alias, it will be + * automatically inherited by the indices it points to. The same holds for the backing indices of a data stream. + */ + static class StatefulIndexPrivileges { + + /** + * Maps concrete action names to concrete index names and then to the roles which provide privileges for the + * combination of action and index. This map can contain besides indices also names of data streams and aliases. + * For aliases and data streams, it will then contain both the actual alias/data stream and the backing indices. + */ + private final Map>> actionToIndexToRoles; + + /** + * The index information that was used to construct this instance. + */ + private final Map indices; + + /** + * The well known index actions that were used to construct this instance. + */ + private final ImmutableSet wellKnownIndexActions; + + private final int estimatedByteSize; + + private long metadataVersion; + + /** + * Creates pre-computed index privileges based on the given parameters. + *

+ * This constructor will not throw an exception if it encounters any invalid configuration (that is, + * in particular, unparseable regular expressions). Rather, it will just log an error. This is okay, as it + * just results in fewer available privileges. + */ + StatefulIndexPrivileges( + SecurityDynamicConfiguration roles, + FlattenedActionGroups actionGroups, + ImmutableSet wellKnownIndexActions, + Map indices, + long metadataVersion, + ByteSizeValue statefulIndexMaxHeapSize + ) { + Map< + String, + CompactMapGroupBuilder.MapBuilder>> actionToIndexToRoles = + new HashMap<>(); + DeduplicatingCompactSubSetBuilder roleSetBuilder = new DeduplicatingCompactSubSetBuilder<>( + roles.getCEntries().keySet() + ); + CompactMapGroupBuilder> indexMapBuilder = + new CompactMapGroupBuilder<>(indices.keySet(), (k2) -> roleSetBuilder.createSubSetBuilder()); + + // We iterate here through the present RoleV7 instances and nested through their "index_permissions" sections. + // During the loop, the actionToIndexToRoles map is being built. + // For that, action patterns from the role will be matched against the "well-known actions" to build + // a concrete action map and index patterns from the role will be matched against the present indices + // to build a concrete index map. + // + // The complexity of this loop is O(n*m) where n is dependent on the structure of the roles configuration + // and m is the number of matched indices. This formula does not take the loop through matchedActions in + // account, as this is bound by a constant number and thus does not need to be considered in the O() notation. + + top: for (Map.Entry entry : roles.getCEntries().entrySet()) { + try { + String roleName = entry.getKey(); + RoleV7 role = entry.getValue(); + + roleSetBuilder.next(roleName); + + for (RoleV7.Index indexPermissions : role.getIndex_permissions()) { + ImmutableSet permissions = actionGroups.resolve(indexPermissions.getAllowed_actions()); + + if (indexPermissions.getIndex_patterns().contains("*")) { + // Wildcard index patterns are handled in the static IndexPermissions object. + // This avoids having to build huge data structures - when a very easy shortcut is available. + continue; + } + + WildcardMatcher indexMatcher = IndexPattern.from(indexPermissions.getIndex_patterns()).getStaticPattern(); + + if (indexMatcher == WildcardMatcher.NONE) { + // The pattern is likely blank because there are only templated patterns. + // Index patterns with templates are not handled here, but in the static IndexPermissions object + continue; + } + + for (String permission : permissions) { + WildcardMatcher actionMatcher = WildcardMatcher.from(permission); + Collection matchedActions = actionMatcher.getMatchAny(wellKnownIndexActions, Collectors.toList()); + + for (Map.Entry indicesEntry : indexMatcher.iterateMatching( + indices.entrySet(), + Map.Entry::getKey + )) { + for (String action : matchedActions) { + CompactMapGroupBuilder.MapBuilder< + String, + DeduplicatingCompactSubSetBuilder.SubSetBuilder> indexToRoles = actionToIndexToRoles + .computeIfAbsent(action, k -> indexMapBuilder.createMapBuilder()); + + indexToRoles.get(indicesEntry.getKey()).add(roleName); + + if (indicesEntry.getValue() instanceof IndexAbstraction.Alias) { + // For aliases we additionally add the sub-indices to the privilege map + for (IndexMetadata subIndex : indicesEntry.getValue().getIndices()) { + String subIndexName = subIndex.getIndex().getName(); + // We need to check whether the subIndex is part of the global indices + // metadata map because that map has been filtered by relevantOnly(). + // This method removes all closed indices and data stream backing indices + // because these indices get a separate treatment. However, these indices + // might still appear as member indices of aliases. Trying to add these + // to the SubSetBuilder indexToRoles would result in an IllegalArgumentException + // because the subIndex will not be part of the super set. + if (indices.containsKey(subIndexName)) { + indexToRoles.get(subIndexName).add(roleName); + } else { + log.debug( + "Ignoring member index {} of alias {}. This is usually the case because the index is closed or a data stream backing index.", + subIndexName, + indicesEntry.getKey() + ); + } + } + } + + if (roleSetBuilder.getEstimatedByteSize() + indexMapBuilder + .getEstimatedByteSize() > statefulIndexMaxHeapSize.getBytes()) { + log.info( + "Size of precomputed index privileges exceeds configured limit ({}). Using capped data structure." + + "This might lead to slightly lower performance during privilege evaluation. Consider raising {}.", + statefulIndexMaxHeapSize, + PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey() + ); + break top; + } + } + } + } + } + } catch (Exception e) { + log.error("Unexpected exception while processing role: {}\nIgnoring role.", entry.getKey(), e); + } + } + + DeduplicatingCompactSubSetBuilder.Completed completedRoleSetBuilder = roleSetBuilder.build(); + + this.estimatedByteSize = roleSetBuilder.getEstimatedByteSize() + indexMapBuilder.getEstimatedByteSize(); + log.debug("Estimated size of StatefulIndexPermissions data structure: {}", this.estimatedByteSize); + + this.actionToIndexToRoles = actionToIndexToRoles.entrySet() + .stream() + .collect( + ImmutableMap.toImmutableMap( + Map.Entry::getKey, + entry -> entry.getValue().build(subSetBuilder -> subSetBuilder.build(completedRoleSetBuilder)) + ) + ); + + this.indices = ImmutableMap.copyOf(indices); + this.metadataVersion = metadataVersion; + this.wellKnownIndexActions = wellKnownIndexActions; + } + + /** + * Checks whether the user has privileges based on the given parameters and information in this class. This method + * has two major channels for returning results: + *

+ * 1. The return value is either PrivilegesEvaluatorResponse.ok() or null. If it is null, this method cannot + * completely tell whether the user has full privileges. A further check with IndexPermissions will be necessary. + * If PrivilegesEvaluatorResponse.ok() is returned, then full privileges could be already determined. + *

+ * 2. As a side effect, this method will modify the supplied CheckTable object. This will be the case regardless + * of whether null or PrivilegesEvaluatorResponse.ok() is returned. The interesting case is actually when null + * 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. + */ + PrivilegesEvaluatorResponse providesPrivilege( + Set actions, + IndexResolverReplacer.Resolved resolvedIndices, + PrivilegesEvaluationContext context, + CheckTable checkTable, + Map indexMetadata + ) { + ImmutableSet effectiveRoles = context.getMappedRoles(); + + for (String action : actions) { + Map> indexToRoles = actionToIndexToRoles.get(action); + + if (indexToRoles != null) { + for (String index : resolvedIndices.getAllIndices()) { + String lookupIndex = index; + + if (index.startsWith(DataStream.BACKING_INDEX_PREFIX)) { + // If we have a backing index of a data stream, we will not try to test + // the backing index here, as we filter backing indices during initialization. + // Instead, we look up the containing data stream and check whether this has privileges. + lookupIndex = backingIndexToDataStream(index, indexMetadata); + } + + ImmutableCompactSubSet rolesWithPrivileges = indexToRoles.get(lookupIndex); + + if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(effectiveRoles)) { + if (checkTable.check(index, action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } + } + } + + // If we reached this point, we cannot tell whether the user has privileges using this instance. + // Return null to indicate that there is no answer. + // The checkTable object might contain already a partial result. + return null; + } + + /** + * If the given index is the backing index of a data stream, the name of the data stream is returned. + * Otherwise, the name of the index itself is being returned. + */ + static String backingIndexToDataStream(String index, Map indexMetadata) { + IndexAbstraction indexAbstraction = indexMetadata.get(index); + + if (indexAbstraction instanceof IndexAbstraction.Index && indexAbstraction.getParentDataStream() != null) { + return indexAbstraction.getParentDataStream().getName(); + } else { + return index; + } + } + + /** + * Filters the given index abstraction map to only contain entries that are relevant the for stateful class. + * This has the goal to keep the heap footprint of instances of StatefulIndexPrivileges at a reasonable size. + *

+ * This removes the following entries: + *

    + *
  • closed indices - closed indices do not need any fast privilege evaluation + *
  • backing indices of data streams - privileges should be only assigned directly to the data streams. + * the privilege evaluation code is able to recognize that an index is member of a data stream and test + * its privilege via that data stream. If a privilege is directly assigned to a backing index, we use + * the "slowish" code paths. + *
  • Indices which are not matched by includeIndices + *
+ */ + static Map relevantOnly(Map indices) { + // First pass: Check if we need to filter at all + boolean doFilter = false; + + for (IndexAbstraction indexAbstraction : indices.values()) { + if (indexAbstraction instanceof IndexAbstraction.Index) { + if (indexAbstraction.getParentDataStream() != null + || indexAbstraction.getWriteIndex().getState() == IndexMetadata.State.CLOSE) { + doFilter = true; + break; + } + } + } + + 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) { + builder.put(indexAbstraction.getName(), indexAbstraction); + } + } else { + builder.put(indexAbstraction.getName(), indexAbstraction); + } + } + + return builder.build(); + } + } + +} diff --git a/src/main/java/org/opensearch/security/privileges/SubjectBasedActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/SubjectBasedActionPrivileges.java new file mode 100644 index 0000000000..c78f55081a --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/SubjectBasedActionPrivileges.java @@ -0,0 +1,589 @@ +/* + * 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.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.cluster.metadata.IndexAbstraction; +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; + +import com.selectivem.collections.CheckTable; + +/** + * An ActionPrivileges implementation that is valid only for a single subject. + * This means that individual instances of this class must be created for individual subjects. The mapped roles + * from the context are not regarded by this method. + *

+ * The method PrivilegesEvaluator.createContext() is responsible for making sure that the correct class is used. + *

+ * This class is useful for plugin users and API tokens. + */ +public class SubjectBasedActionPrivileges implements ActionPrivileges { + private static final Logger log = LogManager.getLogger(SubjectBasedActionPrivileges.class); + + private final ClusterPrivileges cluster; + private final IndexPrivileges index; + private final Supplier> indexMetadataSupplier; + + public SubjectBasedActionPrivileges( + RoleV7 role, + FlattenedActionGroups actionGroups, + Supplier> indexMetadataSupplier + ) { + this.cluster = new ClusterPrivileges(role, actionGroups, WellKnownActions.CLUSTER_ACTIONS); + this.index = new IndexPrivileges( + role, + actionGroups, + WellKnownActions.INDEX_ACTIONS, + WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS + ); + this.indexMetadataSupplier = indexMetadataSupplier; + } + + @Override + public PrivilegesEvaluatorResponse hasClusterPrivilege(PrivilegesEvaluationContext context, String action) { + return cluster.providesPrivilege(context, action); + } + + @Override + public PrivilegesEvaluatorResponse hasAnyClusterPrivilege(PrivilegesEvaluationContext context, Set actions) { + return cluster.providesAnyPrivilege(context, actions); + } + + @Override + public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluationContext context, String action) { + return cluster.providesExplicitPrivilege(context, action); + } + + @Override + public PrivilegesEvaluatorResponse hasIndexPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices + ) { + PrivilegesEvaluatorResponse response = this.index.providesWildcardPrivilege(context, actions); + if (response != null) { + return response; + } + + if (!resolvedIndices.isLocalAll() && resolvedIndices.getAllIndices().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"); + return PrivilegesEvaluatorResponse.ok(); + } + + CheckTable checkTable = CheckTable.create( + resolvedIndices.getAllIndicesResolved(context.getClusterStateSupplier(), context.getIndexNameExpressionResolver()), + actions + ); + Map indexMetadata = this.indexMetadataSupplier.get(); + + return this.index.providesPrivilege(context, actions, resolvedIndices, checkTable, indexMetadata); + } + + @Override + public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices + ) { + CheckTable checkTable = CheckTable.create(resolvedIndices.getAllIndices(), actions); + return this.index.providesExplicitPrivilege(context, actions, resolvedIndices, checkTable, this.indexMetadataSupplier.get()); + } + + /** + * Pre-computed, optimized cluster privilege maps. Instances of this class are immutable. + *

+ * The data structures in this class are optimized for answering the question + * "I have action A and roles [x,y,z]. Do I have authorization to execute the action?". + *

+ * The check will be possible in time O(1) for "well-known" actions when the user actually has the privileges. + */ + static class ClusterPrivileges { + + /** + * Maps names of actions to the roles that provide a privilege for the respective action. + * Note that the mapping is not comprehensive, additionally the data structures rolesWithWildcardPermissions + * and rolesToActionMatcher need to be considered for a full view of the privileges. + *

+ * This does not include privileges obtained via "*" action patterns. This is both meant as a + * optimization and to support explicit privileges. + */ + private final ImmutableSet grantedActions; + + /** + * This contains all role names that provide wildcard (*) privileges for cluster actions. + * This avoids a blow-up of the actionToRoles object by such roles. + */ + private final boolean hasWildcardPermission; + + /** + * This maps role names to a matcher which matches the action names this role provides privileges for. + * This is only used as a last resort if the test with actionToRole and rolesWithWildcardPermissions failed. + * This is only necessary for actions which are not contained in the list of "well-known" actions provided + * during construction. + * + * This does not include privileges obtained via "*" action patterns. This is both meant as a + * optimization and to support explicit privileges. + */ + private final WildcardMatcher grantedActionMatcher; + + private final ImmutableSet wellKnownClusterActions; + + /** + * Creates pre-computed cluster privileges based on the given parameters. + *

+ * This constructor will not throw an exception if it encounters any invalid configuration (that is, + * in particular, unparseable regular expressions). Rather, it will just log an error. This is okay, as it + * just results in fewer available privileges. However, having a proper error reporting mechanism would be + * kind of nice. + */ + ClusterPrivileges(RoleV7 role, FlattenedActionGroups actionGroups, ImmutableSet wellKnownClusterActions) { + Set grantedActions = new HashSet<>(); + boolean hasWildcardPermission = false; + List wildcardMatchers = new ArrayList<>(); + + ImmutableSet permissionPatterns = actionGroups.resolve(role.getCluster_permissions()); + + for (String permission : permissionPatterns) { + // If we have a permission which does not use any pattern, we just simply add it to the + // "actionToRoles" map. + // Otherwise, we match the pattern against the provided well-known cluster actions and add + // these to the "actionToRoles" map. Additionally, for the case that the well-known cluster + // actions are not complete, we also collect the matcher to be used as a last resort later. + + if (WildcardMatcher.isExact(permission)) { + grantedActions.add(permission); + } else if (permission.equals("*")) { + // Special case: Roles with a wildcard "*" giving privileges for all actions. We will not resolve + // this stuff, but just note separately that this role just gets all the cluster privileges. + hasWildcardPermission = true; + } else { + WildcardMatcher wildcardMatcher = WildcardMatcher.from(permission); + Set matchedActions = wildcardMatcher.getMatchAny(wellKnownClusterActions, Collectors.toUnmodifiableSet()); + grantedActions.addAll(matchedActions); + wildcardMatchers.add(wildcardMatcher); + } + } + + this.grantedActions = ImmutableSet.copyOf(grantedActions); + this.hasWildcardPermission = hasWildcardPermission; + this.grantedActionMatcher = WildcardMatcher.from(wildcardMatchers); + this.wellKnownClusterActions = wellKnownClusterActions; + } + + /** + * Checks whether this instance provides privileges for the combination of the provided action and the + * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ + PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext context, String action) { + + // 1: Check roles with wildcards + if (this.hasWildcardPermission) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 2: Check well-known actions - this should cover most cases + if (this.grantedActions.contains(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 3: Only if everything else fails: Check the matchers in case we have a non-well-known action + if (!this.wellKnownClusterActions.contains(action)) { + if (this.grantedActionMatcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + + return PrivilegesEvaluatorResponse.insufficient(action); + } + + /** + * Checks whether this instance provides explicit privileges for the combination of the provided action and the + * provided roles. + *

+ * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns + * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 + *

+ * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ + PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContext context, String action) { + + // 1: Check well-known actions - this should cover most cases + if (this.grantedActions.contains(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 2: Only if everything else fails: Check the matchers in case we have a non-well-known action + if (!this.wellKnownClusterActions.contains(action)) { + if (this.grantedActionMatcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + + return PrivilegesEvaluatorResponse.insufficient(action); + } + + /** + * Checks whether this instance provides privileges for the combination of any of the provided actions and the + * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ + PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext context, Set actions) { + + // 1: Check roles with wildcards + if (this.hasWildcardPermission) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 2: Check well-known actions - this should cover most cases + for (String action : actions) { + if (this.grantedActions.contains(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + + // 3: Only if everything else fails: Check the matchers in case we have a non-well-known action + for (String action : actions) { + if (!this.wellKnownClusterActions.contains(action)) { + if (this.grantedActionMatcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } + + if (actions.size() == 1) { + return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next()); + } else { + return PrivilegesEvaluatorResponse.insufficient("any of " + actions); + } + } + } + + /** + * Partially pre-computed, optimized index privilege maps. Instances of this class are immutable. + *

+ * This class is independent of the actual indices present in the cluster. See StatefulIndexPermissions for a class + * that also takes actual indices into account and is thus fully pre-computed. + *

+ * Purposes of this class: + *

+ * 1. Answer the question "given an action and a set of roles, do I have wildcard index privileges" in O(1) + *

+ * 2. Pre-compute the data structures as far as possible in cases that StatefulIndexPermissions cannot check the + * permissions. This is the case when: + *

+ * a) StatefulIndexPermissions does not cover all indices + * b) The requested index does not exist (especially the case for create index actions) + * c) The index patterns use placeholders like "${user.name}" - these can be only resolved when the User object is present. + * d) The action is not among the "well known" actions. + */ + static class IndexPrivileges { + /** + * Maps role names to concrete action names to IndexPattern objects which define the indices the privileges apply to. + */ + private final ImmutableMap actionToIndexPattern; + + /** + * Maps role names to action names matchers to IndexPattern objects which define the indices the privileges apply to. + * This is especially for "non-well-known" actions. + */ + private final ImmutableMap actionPatternToIndexPattern; + + /** + * Maps action names to the roles which provide wildcard ("*") index privileges for the respective action. + * This allows to answer the question "given an action and a set of roles, do I have wildcard index privileges" + * in O(1) + */ + private final ImmutableSet actionsWithWildcardIndexPrivileges; + + /** + * A pre-defined set of action names that is used to pre-compute the result of action patterns. + */ + private final ImmutableSet wellKnownIndexActions; + + /** + * A pre-defined set of action names that is included in the rolesToExplicitActionToIndexPattern data structure + */ + private final ImmutableSet explicitlyRequiredIndexActions; + + /** + * Maps role names to concrete action names to IndexPattern objects which define the indices the privileges apply to. + * The action names are only explicitly granted privileges which are listed in explicitlyRequiredIndexActions. + *

+ * Compare https://github.com/opensearch-project/security/pull/2887 + */ + private final ImmutableMap explicitActionToIndexPattern; + + /** + * Creates pre-computed index privileges based on the given parameters. + *

+ * This constructor will not throw an exception if it encounters any invalid configuration (that is, + * in particular, unparseable regular expressions). Rather, it will just log an error. This is okay, as it + * just results in fewer available privileges. However, having a proper error reporting mechanism would be + * kind of nice. + */ + IndexPrivileges( + RoleV7 role, + FlattenedActionGroups actionGroups, + ImmutableSet wellKnownIndexActions, + ImmutableSet explicitlyRequiredIndexActions + ) { + + Map actionToIndexPattern = new HashMap<>(); + Map actionPatternToIndexPattern = new HashMap<>(); + Set actionWithWildcardIndexPrivileges = new HashSet<>(); + Map explicitActionToIndexPattern = new HashMap<>(); + + for (RoleV7.Index indexPermissions : role.getIndex_permissions()) { + ImmutableSet permissions = actionGroups.resolve(indexPermissions.getAllowed_actions()); + + for (String permission : permissions) { + // If we have a permission which does not use any pattern, we just simply add it to the + // "actionToIndexPattern" map. + // Otherwise, we match the pattern against the provided well-known index actions and add + // these to the "actionToIndexPattern" map. Additionally, for the case that the + // well-known index actions are not complete, we also collect the actionMatcher to be used + // as a last resort later. + + if (WildcardMatcher.isExact(permission)) { + actionToIndexPattern.computeIfAbsent(permission, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + + if (explicitlyRequiredIndexActions.contains(permission)) { + explicitActionToIndexPattern.computeIfAbsent(permission, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + } + + if (indexPermissions.getIndex_patterns().contains("*")) { + actionWithWildcardIndexPrivileges.add(permission); + } + } else { + WildcardMatcher actionMatcher = WildcardMatcher.from(permission); + + for (String action : actionMatcher.iterateMatching(wellKnownIndexActions)) { + actionToIndexPattern.computeIfAbsent(action, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + + if (indexPermissions.getIndex_patterns().contains("*")) { + actionWithWildcardIndexPrivileges.add(permission); + } + } + + actionPatternToIndexPattern.computeIfAbsent(actionMatcher, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + + if (actionMatcher != WildcardMatcher.ANY) { + for (String action : actionMatcher.iterateMatching(explicitlyRequiredIndexActions)) { + explicitActionToIndexPattern.computeIfAbsent(action, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + } + } + } + } + } + + this.actionToIndexPattern = actionToIndexPattern.entrySet() + .stream() + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry -> entry.getValue().build())); + + this.actionPatternToIndexPattern = actionPatternToIndexPattern.entrySet() + .stream() + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry -> entry.getValue().build())); + + this.actionsWithWildcardIndexPrivileges = ImmutableSet.copyOf(actionWithWildcardIndexPrivileges); + + this.explicitActionToIndexPattern = explicitActionToIndexPattern.entrySet() + .stream() + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry -> entry.getValue().build())); + + this.wellKnownIndexActions = wellKnownIndexActions; + this.explicitlyRequiredIndexActions = explicitlyRequiredIndexActions; + } + + /** + * Checks whether this instance provides privileges for the combination of the provided action, + * the provided indices and the provided roles. + *

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

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

+ * This method will only verify privileges for the index/action combinations which are un-checked in + * the checkTable instance provided to this method. Checked index/action combinations are considered to be + * "already fulfilled by other means" - usually that comes from the stateful data structure. + * As a side-effect, this method will further mark the available index/action combinations in the provided + * checkTable instance as checked. + */ + PrivilegesEvaluatorResponse providesPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices, + CheckTable checkTable, + Map indexMetadata + ) { + List exceptions = new ArrayList<>(); + + for (String action : actions) { + IndexPattern indexPattern = actionToIndexPattern.get(action); + + if (indexPattern != null) { + for (String index : checkTable.iterateUncheckedRows(action)) { + try { + if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } 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)); + } + } + } + } + + // 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 + + boolean allActionsWellKnown = this.wellKnownIndexActions.containsAll(actions); + + if (!checkTable.isComplete() && !allActionsWellKnown) { + + for (String action : actions) { + if (this.wellKnownIndexActions.contains(action)) { + continue; + } + + for (Map.Entry entry : actionPatternToIndexPattern.entrySet()) { + WildcardMatcher actionMatcher = entry.getKey(); + IndexPattern indexPattern = entry.getValue(); + + if (actionMatcher.test(action)) { + for (String index : checkTable.iterateUncheckedRows(action)) { + try { + if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { + break; + } + } 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", this, e); + exceptions.add(new PrivilegesEvaluationException("Error while evaluating " + this, e)); + } + } + } + } + } + + } + + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(); + } + + Set availableIndices = checkTable.getCompleteRows(); + + if (!availableIndices.isEmpty()) { + return PrivilegesEvaluatorResponse.partiallyOk(availableIndices, checkTable).evaluationExceptions(exceptions); + } + + return PrivilegesEvaluatorResponse.insufficient(checkTable) + .reason( + resolvedIndices.getAllIndices().size() == 1 + ? "Insufficient permissions for the referenced index" + : "None of " + resolvedIndices.getAllIndices().size() + " referenced indices has sufficient permissions" + ) + .evaluationExceptions(exceptions); + } + + /** + * 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 + * the user's privileges. + */ + PrivilegesEvaluatorResponse providesWildcardPrivilege(PrivilegesEvaluationContext context, Set actions) { + + for (String action : actions) { + if (!this.actionsWithWildcardIndexPrivileges.contains(action)) { + return null; + } + } + + return PrivilegesEvaluatorResponse.ok(); + } + + /** + * Checks whether this instance provides explicit privileges for the combination of the provided action, + * the provided indices and the provided roles. + *

+ * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns + * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 + */ + PrivilegesEvaluatorResponse providesExplicitPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices, + CheckTable checkTable, + Map indexMetadata + ) { + List exceptions = new ArrayList<>(); + + if (!CollectionUtils.containsAny(actions, this.explicitlyRequiredIndexActions)) { + return PrivilegesEvaluatorResponse.insufficient(CheckTable.create(ImmutableSet.of("_"), actions)); + } + + for (String action : actions) { + IndexPattern indexPattern = this.explicitActionToIndexPattern.get(action); + + if (indexPattern != null) { + for (String index : checkTable.iterateUncheckedRows(action)) { + try { + if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } 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)); + } + } + } + } + + return PrivilegesEvaluatorResponse.insufficient(checkTable) + .reason("No explicit privileges have been provided for the referenced indices.") + .evaluationExceptions(exceptions); + } + } + +} diff --git a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java index aaeeb59ce9..934053b929 100644 --- a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java @@ -88,7 +88,7 @@ public class SystemIndexAccessEvaluatorTest { User user; IndexNameExpressionResolver indexNameExpressionResolver; - ActionPrivileges actionPrivileges; + RoleBasedActionPrivileges actionPrivileges; private ThreadContext createThreadContext() { return new ThreadContext(Settings.EMPTY); @@ -128,7 +128,12 @@ public void setup( CType.ROLES ); - this.actionPrivileges = new ActionPrivileges(rolesConfig, FlattenedActionGroups.EMPTY, () -> indexMetadata, Settings.EMPTY); + this.actionPrivileges = new RoleBasedActionPrivileges( + rolesConfig, + FlattenedActionGroups.EMPTY, + () -> indexMetadata, + Settings.EMPTY + ); } catch (JsonProcessingException e) { throw new RuntimeException(e); } @@ -162,7 +167,8 @@ PrivilegesEvaluationContext ctx(String action) { null, null, indexNameExpressionResolver, - null + null, + actionPrivileges ); } From 1cbc5190c5d76360fca37f0a7e6feb1268f93cdf Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 3 Jun 2025 11:38:53 +0200 Subject: [PATCH 02/10] Extracted ActionPrivileges interface and created RoleBasedActionPrivileges implementation and SubjectBasedActionPrivileges implementation Signed-off-by: Nils Bandener --- .../RestEndpointPermissionTests.java | 1 + .../RoleBasedActionPrivilegesTest.java | 20 +- .../SubjectBasedActionPrivilegesTest.java | 1096 +++++++++++++++++ .../security/OpenSearchSecurityPlugin.java | 4 +- .../security/filter/SecurityFilter.java | 4 - .../security/privileges/IndexPattern.java | 6 +- .../privileges/PrivilegesEvaluator.java | 10 +- .../RoleBasedActionPrivileges.java | 436 ++----- .../RuntimeOptimizedActionPrivileges.java | 448 +++++++ .../SubjectBasedActionPrivileges.java | 303 ++--- .../{ => actionlevel}/WellKnownActions.java | 18 +- .../SystemIndexAccessEvaluatorTest.java | 1 + 12 files changed, 1727 insertions(+), 620 deletions(-) rename src/integrationTest/java/org/opensearch/security/privileges/{ => actionlevel}/RoleBasedActionPrivilegesTest.java (98%) create mode 100644 src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java rename src/main/java/org/opensearch/security/privileges/{ => actionlevel}/RoleBasedActionPrivileges.java (70%) create mode 100644 src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java rename src/main/java/org/opensearch/security/privileges/{ => actionlevel}/SubjectBasedActionPrivileges.java (61%) rename src/main/java/org/opensearch/security/privileges/{ => actionlevel}/WellKnownActions.java (88%) diff --git a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java index 33841b9fd8..684f1d152b 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java @@ -46,6 +46,7 @@ import org.opensearch.security.DefaultObjectMapper; 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.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; diff --git a/src/integrationTest/java/org/opensearch/security/privileges/RoleBasedActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java similarity index 98% rename from src/integrationTest/java/org/opensearch/security/privileges/RoleBasedActionPrivilegesTest.java rename to src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java index 17b11606ed..7773cd9043 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/RoleBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java @@ -8,7 +8,7 @@ * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ -package org.opensearch.security.privileges; +package org.opensearch.security.privileges.actionlevel; import java.util.ArrayList; import java.util.Arrays; @@ -38,6 +38,8 @@ import org.opensearch.common.util.concurrent.ThreadContext; 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; @@ -72,17 +74,6 @@ RoleBasedActionPrivilegesTest.Misc.class, RoleBasedActionPrivilegesTest.StatefulIndexPrivilegesHeapSize.class }) public class RoleBasedActionPrivilegesTest { - // TODO Create unlimited role statically here - private static final RoleV7 UNLIMITED_ROLE = RoleV7.fromYamlStringUnchecked(""" - cluster_permissions: - - "*" - index_permissions: - - index_patterns: - - "*" - allowed_actions: - - "*" - """); - public static class ClusterPrivileges { @Test public void wellKnown() throws Exception { @@ -466,10 +457,7 @@ public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec, Statefulnes roles, FlattenedActionGroups.EMPTY, () -> INDEX_METADATA, - settings, - WellKnownActions.CLUSTER_ACTIONS, - WellKnownActions.INDEX_ACTIONS, - WellKnownActions.INDEX_ACTIONS + settings ); 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 new file mode 100644 index 0000000000..ff8515701d --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java @@ -0,0 +1,1096 @@ +/* + * 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; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Suite; +import org.opensearch.action.support.IndicesOptions; +import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +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; +import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.user.User; +import org.opensearch.security.util.MockIndexMetadataBuilder; + +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; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isAllowed; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isForbidden; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isPartiallyOk; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.missingPrivileges; +import static org.opensearch.security.util.MockIndexMetadataBuilder.dataStreams; +import static org.opensearch.security.util.MockIndexMetadataBuilder.indices; + +/** + * Unit tests for SubjectBasedActionPrivileges. As the SubjectBasedActionPrivileges provides quite a few different code paths for checking + * privileges with different performance characteristics, this test suite defines different test cases for making sure + * all these code paths are tested. So, all functionality must be tested for "well-known" actions and non-well-known + * actions. For index privileges, there are a couple of more tests dimensions. See below. + */ +@RunWith(Suite.class) +@Suite.SuiteClasses({ + SubjectBasedActionPrivilegesTest.ClusterPrivileges.class, + SubjectBasedActionPrivilegesTest.IndexPrivileges.IndicesAndAliases.class, + SubjectBasedActionPrivilegesTest.IndexPrivileges.DataStreams.class, + SubjectBasedActionPrivilegesTest.Misc.class, + SubjectBasedActionPrivilegesTest.StatefulIndexPrivilegesHeapSize.class }) +public class SubjectBasedActionPrivilegesTest { + public static class ClusterPrivileges { + @Test + public void wellKnown() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/stats*", CType.ROLES); + + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/stats"), isAllowed()); + assertThat( + subject.hasClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/stats"), + isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) + ); + assertThat( + subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/other"), + isForbidden(missingPrivileges("cluster:monitor/nodes/other")) + ); + } + + @Test + public void notWellKnown() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/stats*", CType.ROLES); + + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/stats/somethingnotwellknown"), isAllowed()); + assertThat( + subject.hasClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/stats/somethingnotwellknown"), + isForbidden(missingPrivileges("cluster:monitor/nodes/stats/somethingnotwellknown")) + ); + assertThat( + subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/something/else"), + isForbidden(missingPrivileges("cluster:monitor/nodes/something/else")) + ); + } + + @Test + public void wildcard() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - '*'", CType.ROLES); + + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:whatever"), isAllowed()); + assertThat( + subject.hasClusterPrivilege(ctx("other_role"), "cluster:whatever"), + isForbidden(missingPrivileges("cluster:whatever")) + ); + } + + @Test + public void explicit_wellKnown() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("non_explicit_role:\n" + // + " cluster_permissions:\n" + // + " - '*'\n" + // + "explicit_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/stats\n" + // + "semi_explicit_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/stats*\n", // + CType.ROLES + ); + + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat(subject.hasExplicitClusterPrivilege(ctx("explicit_role"), "cluster:monitor/nodes/stats"), isAllowed()); + assertThat(subject.hasExplicitClusterPrivilege(ctx("semi_explicit_role"), "cluster:monitor/nodes/stats"), isAllowed()); + assertThat( + subject.hasExplicitClusterPrivilege(ctx("non_explicit_role"), "cluster:monitor/nodes/stats"), + isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) + ); + assertThat( + subject.hasExplicitClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/stats"), + isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) + ); + } + + @Test + public void explicit_notWellKnown() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("non_explicit_role:\n" + // + " cluster_permissions:\n" + // + " - '*'\n" + // + "explicit_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/notwellknown\n" + // + "semi_explicit_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/*\n", // + CType.ROLES + ); + + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat(subject.hasExplicitClusterPrivilege(ctx("explicit_role"), "cluster:monitor/nodes/notwellknown"), isAllowed()); + assertThat(subject.hasExplicitClusterPrivilege(ctx("semi_explicit_role"), "cluster:monitor/nodes/notwellknown"), isAllowed()); + assertThat( + subject.hasExplicitClusterPrivilege(ctx("non_explicit_role"), "cluster:monitor/nodes/notwellknown"), + isForbidden(missingPrivileges("cluster:monitor/nodes/notwellknown")) + ); + assertThat( + subject.hasExplicitClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/notwellknown"), + isForbidden(missingPrivileges("cluster:monitor/nodes/notwellknown")) + ); + } + + @Test + public void hasAny_wellKnown() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/stats*", CType.ROLES); + + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat(subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/nodes/stats")), isAllowed()); + assertThat( + subject.hasAnyClusterPrivilege( + ctx("test_role"), + ImmutableSet.of("cluster:monitor/nodes/foo", "cluster:monitor/nodes/stats") + ), + isAllowed() + ); + + assertThat( + subject.hasAnyClusterPrivilege(ctx("other_role"), ImmutableSet.of("cluster:monitor/nodes/stats")), + isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) + ); + assertThat( + subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/nodes/other")), + isForbidden(missingPrivileges("cluster:monitor/nodes/other")) + ); + } + + @Test + public void hasAny_notWellKnown() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/*", CType.ROLES); + + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat( + subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/nodes/notwellknown")), + isAllowed() + ); + assertThat( + subject.hasAnyClusterPrivilege( + ctx("test_role"), + ImmutableSet.of("cluster:monitor/other", "cluster:monitor/nodes/notwellknown") + ), + isAllowed() + ); + + assertThat( + subject.hasAnyClusterPrivilege(ctx("other_role"), ImmutableSet.of("cluster:monitor/nodes/notwellknown")), + isForbidden(missingPrivileges("cluster:monitor/nodes/notwellknown")) + ); + assertThat( + subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/other")), + isForbidden(missingPrivileges("cluster:monitor/other")) + ); + assertThat( + subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/other", "cluster:monitor/yetanother")), + isForbidden() + ); + } + + @Test + public void hasAny_wildcard() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - '*'", CType.ROLES); + + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat(subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:whatever")), isAllowed()); + + assertThat( + subject.hasAnyClusterPrivilege(ctx("other_role"), ImmutableSet.of("cluster:whatever")), + isForbidden(missingPrivileges("cluster:whatever")) + ); + } + } + + /** + * Tests for index privileges. This class contains two parameterized test suites, first for indices and aliases, + * second for data streams. + *

+ * Both test suites use parameters to create a 3-dimensional test case space to make sure all code paths are covered. + *

+ * The dimensions are (see also the params() methods): + *

    + *
  1. 1. roles.yml; index patterns: Different usages of patterns, wildcards and constant names. + *
  2. 2. roles.yml; action patterns: Well known actions vs non-well known actions combined with use of patterns vs use of constant action names + *
  3. 3. Statefulness: Shall the data structures from ActionPrivileges.StatefulIndexPrivileges be used or not + *
+ * As so many different situations need to be tested, the test oracle method covers() is used to verify the results. + */ + public static class IndexPrivileges { + + @RunWith(Parameterized.class) + public static class IndicesAndAliases { + final ActionSpec actionSpec; + final IndexSpec indexSpec; + final SecurityDynamicConfiguration roles; + final String primaryAction; + final ImmutableSet requiredActions; + final ImmutableSet otherActions; + final RoleBasedActionPrivileges subject; + + @Test + public void positive_full() throws Exception { + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx("test_role"), requiredActions, resolved("index_a11")); + assertThat(result, isAllowed()); + } + + @Test + public void positive_partial() throws Exception { + PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("index_a11", "index_a12")); + + if (covers(ctx, "index_a11", "index_a12")) { + assertThat(result, isAllowed()); + } else if (covers(ctx, "index_a11")) { + assertThat(result, isPartiallyOk("index_a11")); + } else { + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } + } + + @Test + public void positive_partial2() throws Exception { + PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( + ctx, + requiredActions, + resolved("index_a11", "index_a12", "index_b1") + ); + + if (covers(ctx, "index_a11", "index_a12", "index_b1")) { + assertThat(result, isAllowed()); + } else if (covers(ctx, "index_a11", "index_a12")) { + assertThat(result, isPartiallyOk("index_a11", "index_a12")); + } else if (covers(ctx, "index_a11")) { + assertThat(result, isPartiallyOk("index_a11")); + } else { + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } + } + + @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 + ); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx("test_role"), requiredActions, resolved); + assertThat(result, isAllowed()); + } + + @Test + public void negative_wrongRole() throws Exception { + PrivilegesEvaluationContext ctx = ctx("other_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("index_a11")); + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } + + @Test + public void negative_wrongAction() throws Exception { + PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, otherActions, resolved("index_a11")); + + if (actionSpec.givenPrivs.contains("*")) { + assertThat(result, isAllowed()); + } else { + assertThat(result, isForbidden(missingPrivileges(otherActions))); + } + } + + @Test + public void positive_hasExplicit_full() { + PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege(ctx, requiredActions, resolved("index_a11")); + + if (actionSpec.givenPrivs.contains("*")) { + // The * is forbidden for explicit privileges + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } else if (!requiredActions.contains("indices:data/read/search")) { + // For test purposes, we have designated "indices:data/read/search" as an action requiring explicit privileges + // Other actions are not covered here + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } else { + assertThat(result, isAllowed()); + } + } + + private boolean covers(PrivilegesEvaluationContext ctx, String... indices) { + for (String index : indices) { + if (!indexSpec.covers(ctx.getUser(), index)) { + return false; + } + } + return true; + } + + @Parameterized.Parameters(name = "{0}; actions: {1}; {2}") + public static Collection params() { + List result = new ArrayList<>(); + + for (IndexSpec indexSpec : Arrays.asList( + new IndexSpec().givenIndexPrivs("*"), // + new IndexSpec().givenIndexPrivs("index_*"), // + new IndexSpec().givenIndexPrivs("index_a11"), // + new IndexSpec().givenIndexPrivs("index_a1*"), // + new IndexSpec().givenIndexPrivs("index_${attrs.dept_no}"), // + new IndexSpec().givenIndexPrivs("alias_a1*") // + )) { + for (ActionSpec actionSpec : Arrays.asList( + new ActionSpec("wildcard")// + .givenPrivs("*") + .requiredPrivs("indices:data/read/search"), // + new ActionSpec("constant, well known")// + .givenPrivs("indices:data/read/search") + .requiredPrivs("indices:data/read/search"), // + new ActionSpec("pattern, well known")// + .givenPrivs("indices:data/read/*") + .requiredPrivs("indices:data/read/search"), // + new ActionSpec("pattern, well known, two required privs")// + .givenPrivs("indices:data/read/*") + .requiredPrivs("indices:data/read/search", "indices:data/read/get"), // + new ActionSpec("constant, non well known")// + .givenPrivs("indices:unknown/unwell") + .requiredPrivs("indices:unknown/unwell"), // + new ActionSpec("pattern, non well known")// + .givenPrivs("indices:unknown/*") + .requiredPrivs("indices:unknown/unwell"), // + new ActionSpec("pattern, non well known, two required privs")// + .givenPrivs("indices:unknown/*") + .requiredPrivs("indices:unknown/unwell", "indices:unknown/notatall")// + + )) { + for (Statefulness statefulness : Statefulness.values()) { + result.add(new Object[] { indexSpec, actionSpec, statefulness }); + } + } + } + return result; + } + + public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness statefulness) throws Exception { + this.indexSpec = indexSpec; + this.actionSpec = actionSpec; + this.roles = indexSpec.toRolesConfig(actionSpec); + + this.primaryAction = actionSpec.primaryAction; + this.requiredActions = actionSpec.requiredPrivs; + + this.otherActions = actionSpec.wellKnownActions + ? ImmutableSet.of("indices:data/write/update") + : ImmutableSet.of("indices:foobar/unknown"); + this.indexSpec.indexMetadata = INDEX_METADATA; + + 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(); + } + + this.subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + () -> INDEX_METADATA, + settings + ); + + if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { + this.subject.updateStatefulIndexPrivileges(INDEX_METADATA, 1); + } + } + + final static Map INDEX_METADATA = // + indices("index_a11", "index_a12", "index_a21", "index_a22", "index_b1", "index_b2")// + .alias("alias_a") + .of("index_a11", "index_a12", "index_a21", "index_a22")// + .alias("alias_a1") + .of("index_a11", "index_a12")// + .alias("alias_a2") + .of("index_a21", "index_a22")// + .alias("alias_b") + .of("index_b1", "index_b2")// + .build() + .getIndicesLookup(); + + static IndexResolverReplacer.Resolved resolved(String... indices) { + return new IndexResolverReplacer.Resolved( + ImmutableSet.of(), + ImmutableSet.copyOf(indices), + ImmutableSet.copyOf(indices), + ImmutableSet.of(), + IndicesOptions.LENIENT_EXPAND_OPEN + ); + } + } + + @RunWith(Parameterized.class) + public static class DataStreams { + final ActionSpec actionSpec; + final IndexSpec indexSpec; + final SecurityDynamicConfiguration roles; + final String primaryAction; + final ImmutableSet requiredActions; + final ImmutableSet otherActions; + final RoleBasedActionPrivileges subject; + + @Test + public void positive_full() throws Exception { + PrivilegesEvaluationContext ctx = ctx("test_role"); + 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))); + } + } + + @Test + public void positive_partial() throws Exception { + PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( + ctx, + requiredActions, + resolved("data_stream_a11", "data_stream_a12") + ); + + if (covers(ctx, "data_stream_a11", "data_stream_a12")) { + assertThat(result, isAllowed()); + } else if (covers(ctx, "data_stream_a11")) { + assertThat( + result, + isPartiallyOk( + "data_stream_a11", + ".ds-data_stream_a11-000001", + ".ds-data_stream_a11-000002", + ".ds-data_stream_a11-000003" + ) + ); + } else if (covers(ctx, ".ds-data_stream_a11-000001")) { + assertThat( + result, + isPartiallyOk(".ds-data_stream_a11-000001", ".ds-data_stream_a11-000002", ".ds-data_stream_a11-000003") + ); + } else { + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } + } + + @Test + public void negative_wrongRole() throws Exception { + PrivilegesEvaluationContext ctx = ctx("other_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } + + @Test + public void negative_wrongAction() throws Exception { + PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, otherActions, resolved("data_stream_a11")); + assertThat(result, isForbidden(missingPrivileges(otherActions))); + } + + private boolean covers(PrivilegesEvaluationContext ctx, String... indices) { + for (String index : indices) { + if (!indexSpec.covers(ctx.getUser(), index)) { + return false; + } + } + return true; + } + + @Parameterized.Parameters(name = "{0}; actions: {1}; {2}") + public static Collection params() { + List result = new ArrayList<>(); + + for (IndexSpec indexSpec : Arrays.asList( + new IndexSpec().givenIndexPrivs("*"), // + new IndexSpec().givenIndexPrivs("data_stream_*"), // + new IndexSpec().givenIndexPrivs("data_stream_a11"), // + new IndexSpec().givenIndexPrivs("data_stream_a1*"), // + new IndexSpec().givenIndexPrivs("data_stream_${attrs.dept_no}"), // + new IndexSpec().givenIndexPrivs(".ds-data_stream_a11*") // + )) { + for (ActionSpec actionSpec : Arrays.asList( + new ActionSpec("constant, well known")// + .givenPrivs("indices:data/read/search") + .requiredPrivs("indices:data/read/search"), // + new ActionSpec("pattern, well known")// + .givenPrivs("indices:data/read/*") + .requiredPrivs("indices:data/read/search"), // + new ActionSpec("pattern, well known, two required privs")// + .givenPrivs("indices:data/read/*") + .requiredPrivs("indices:data/read/search", "indices:data/read/get"), // + new ActionSpec("constant, non well known")// + .givenPrivs("indices:unknown/unwell") + .requiredPrivs("indices:unknown/unwell"), // + new ActionSpec("pattern, non well known")// + .givenPrivs("indices:unknown/*") + .requiredPrivs("indices:unknown/unwell"), // + new ActionSpec("pattern, non well known, two required privs")// + .givenPrivs("indices:unknown/*") + .requiredPrivs("indices:unknown/unwell", "indices:unknown/notatall")// + + )) { + for (Statefulness statefulness : Statefulness.values()) { + result.add(new Object[] { indexSpec, actionSpec, statefulness }); + } + } + } + return result; + } + + public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness statefulness) throws Exception { + this.indexSpec = indexSpec; + this.actionSpec = actionSpec; + this.roles = indexSpec.toRolesConfig(actionSpec); + + this.primaryAction = actionSpec.primaryAction; + this.requiredActions = actionSpec.requiredPrivs; + + this.otherActions = actionSpec.wellKnownActions + ? ImmutableSet.of("indices:data/write/update") + : ImmutableSet.of("indices:foobar/unknown"); + this.indexSpec.indexMetadata = INDEX_METADATA; + + 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(); + } + + this.subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, () -> INDEX_METADATA, settings); + + if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { + this.subject.updateStatefulIndexPrivileges(INDEX_METADATA, 1); + } + } + + final static Map INDEX_METADATA = // + dataStreams("data_stream_a11", "data_stream_a12", "data_stream_a21", "data_stream_a22", "data_stream_b1", "data_stream_b2") + .build() + .getIndicesLookup(); + + static IndexResolverReplacer.Resolved resolved(String... indices) { + ImmutableSet.Builder allIndices = ImmutableSet.builder(); + + for (String index : indices) { + IndexAbstraction indexAbstraction = INDEX_METADATA.get(index); + + if (indexAbstraction instanceof IndexAbstraction.DataStream) { + allIndices.addAll( + indexAbstraction.getIndices().stream().map(i -> i.getIndex().getName()).collect(Collectors.toList()) + ); + } + + allIndices.add(index); + } + + return new IndexResolverReplacer.Resolved( + ImmutableSet.of(), + allIndices.build(), + ImmutableSet.copyOf(indices), + ImmutableSet.of(), + IndicesOptions.LENIENT_EXPAND_OPEN + ); + } + } + + static class IndexSpec { + ImmutableList givenIndexPrivs = ImmutableList.of(); + boolean wildcardPrivs; + Map indexMetadata; + + IndexSpec() {} + + IndexSpec givenIndexPrivs(String... indexPatterns) { + this.givenIndexPrivs = ImmutableList.copyOf(indexPatterns); + this.wildcardPrivs = this.givenIndexPrivs.contains("*"); + return this; + } + + boolean covers(User user, String index) { + if (this.wildcardPrivs) { + return true; + } + + for (String givenIndexPriv : this.givenIndexPrivs) { + if (givenIndexPriv.contains("${")) { + for (Map.Entry entry : user.getCustomAttributesMap().entrySet()) { + givenIndexPriv = givenIndexPriv.replace("${" + entry.getKey() + "}", entry.getValue()); + } + } + + if (givenIndexPriv.endsWith("*")) { + if (index.startsWith(givenIndexPriv.substring(0, givenIndexPriv.length() - 1))) { + return true; + } + + for (IndexAbstraction indexAbstraction : indexMetadata.values()) { + if ((indexAbstraction instanceof IndexAbstraction.Alias + || indexAbstraction instanceof IndexAbstraction.DataStream) + && indexAbstraction.getName().startsWith(givenIndexPriv.substring(0, givenIndexPriv.length() - 1))) { + if (indexAbstraction.getIndices().stream().anyMatch(i -> i.getIndex().getName().equals(index))) { + return true; + } + } + } + } else if (givenIndexPrivs.contains("*")) { + // For simplicity, we only allow a sub-set of patterns. We assume here that the WildcardMatcher + // class fulfills all other cases correctly as per its contract + throw new RuntimeException("The tests only support index patterns with * at the end"); + } else { + if (index.equals(givenIndexPriv)) { + return true; + } + + IndexAbstraction indexAbstraction = indexMetadata.get(index); + + if (indexAbstraction instanceof IndexAbstraction.Alias || indexAbstraction instanceof IndexAbstraction.DataStream) { + if (indexAbstraction.getIndices().stream().anyMatch(i -> i.getIndex().getName().equals(index))) { + return true; + } + } + } + } + + return false; + } + + SecurityDynamicConfiguration toRolesConfig(ActionSpec actionSpec) { + try { + return SecurityDynamicConfiguration.fromMap( + ImmutableMap.of( + "test_role", + ImmutableMap.of( + "index_permissions", + Arrays.asList( + ImmutableMap.of("index_patterns", this.givenIndexPrivs, "allowed_actions", actionSpec.givenPrivs) + ) + ) + ), + CType.ROLES + ); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + return this.givenIndexPrivs.stream().collect(Collectors.joining(",")); + } + } + + static class ActionSpec { + String name; + ImmutableList givenPrivs; + ImmutableSet requiredPrivs; + String primaryAction; + boolean wellKnownActions; + + ActionSpec(String name) { + super(); + this.name = name; + } + + ActionSpec givenPrivs(String... actions) { + this.givenPrivs = ImmutableList.copyOf(actions); + return this; + } + + ActionSpec requiredPrivs(String... requiredPrivs) { + this.requiredPrivs = ImmutableSet.copyOf(requiredPrivs); + this.primaryAction = requiredPrivs[0]; + this.wellKnownActions = this.requiredPrivs.stream().anyMatch(a -> WellKnownActions.INDEX_ACTIONS.contains(a)); + return this; + } + + @Override + public String toString() { + return name; + } + } + + enum Statefulness { + STATEFUL, + STATEFUL_LIMITED, + NON_STATEFUL + } + } + + public static class Misc { + @Test + public void relevantOnly_identity() throws Exception { + Map metadata = // + indices("index_a11", "index_a12", "index_b")// + .alias("alias_a") + .of("index_a11", "index_a12")// + .build() + .getIndicesLookup(); + + assertTrue( + "relevantOnly() returned identical object", + RoleBasedActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata) == metadata + ); + } + + @Test + public void relevantOnly_closed() throws Exception { + Map metadata = indices("index_open_1", "index_open_2")// + .index("index_closed", IndexMetadata.State.CLOSE) + .build() + .getIndicesLookup(); + + 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); + + assertNotNull("Filtered metadata contains index_open_1", filteredMetadata.get("index_open_1")); + assertNull("Filtered metadata does not contain index_closed", filteredMetadata.get("index_closed")); + } + + @Test + public void relevantOnly_dataStreamBackingIndices() throws Exception { + Map metadata = dataStreams("data_stream_1").build().getIndicesLookup(); + + 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); + + 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")); + } + + @Test + public void backingIndexToDataStream() { + Map metadata = indices("index").dataStream("data_stream").build().getIndicesLookup(); + + assertEquals("index", RoleBasedActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream("index", metadata)); + assertEquals( + "data_stream", + RoleBasedActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream(".ds-data_stream-000001", metadata) + ); + assertEquals( + "non_existing", + RoleBasedActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream("non_existing", metadata) + ); + } + + @Test + public void hasIndexPrivilege_errors() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( + "role_with_errors:\n" + + " index_permissions:\n" + + " - index_patterns: ['/invalid_regex_with_attr${user.name}\\/']\n" + + " allowed_actions: ['indices:some_action*', 'indices:data/write/index']", + CType.ROLES + ); + + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + () -> Collections.emptyMap(), + Settings.EMPTY + ); + + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( + ctx("role_with_errors"), + Set.of("indices:some_action", "indices:data/write/index"), + IndexResolverReplacer.Resolved.ofIndex("any_index") + ); + assertThat(result, isForbidden()); + assertTrue(result.hasEvaluationExceptions()); + assertTrue( + "Result mentions role_with_errors: " + result.getEvaluationExceptionInfo(), + result.getEvaluationExceptionInfo() + .startsWith("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating role role_with_errors") + ); + } + + @Test + public void hasExplicitIndexPrivilege_errors() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( + "role_with_errors:\n" + + " index_permissions:\n" + + " - index_patterns: ['/invalid_regex_with_attr${user.name}\\/']\n" + + " allowed_actions: ['system:admin/system_index*']", + CType.ROLES + ); + + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + () -> Collections.emptyMap(), + Settings.EMPTY + ); + + PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( + ctx("role_with_errors"), + Set.of("system:admin/system_index"), + IndexResolverReplacer.Resolved.ofIndex("any_index") + ); + assertThat(result, isForbidden()); + assertTrue(result.hasEvaluationExceptions()); + assertTrue( + "Result mentions role_with_errors: " + result.getEvaluationExceptionInfo(), + result.getEvaluationExceptionInfo() + .startsWith("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating role role_with_errors") + ); + } + + @Test + public void aliasesOnDataStreamBackingIndices() throws Exception { + // We create a meta data object with a data stream ds_a. Implicitly, the utility method will create + // the backing indices ".ds-ds_a-000001", ".ds-ds_a-000002" and ".ds-ds_a-000003". + // Additionally, we create an alias which only contains ".ds-ds_a-000001", but not the other backing indices. + Map metadata = dataStreams("ds_a").alias("alias_a").of(".ds-ds_a-000001").build().getIndicesLookup(); + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( + "role:\n" + + " index_permissions:\n" + + " - index_patterns: ['alias_a']\n" + + " allowed_actions: ['indices:data/write/index']", + CType.ROLES + ); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + () -> metadata, + Settings.EMPTY + ); + subject.updateStatefulIndexPrivileges(metadata, 2); + + PrivilegesEvaluatorResponse resultForIndexCoveredByAlias = subject.hasIndexPrivilege( + ctx("role"), + Set.of("indices:data/write/index"), + IndexResolverReplacer.Resolved.ofIndex(".ds-ds_a-000001") + ); + assertThat(resultForIndexCoveredByAlias, isAllowed()); + + PrivilegesEvaluatorResponse resultForIndexNotCoveredByAlias = subject.hasIndexPrivilege( + ctx("role"), + Set.of("indices:data/write/index"), + IndexResolverReplacer.Resolved.ofIndex(".ds-ds_a-000002") + ); + assertThat(resultForIndexNotCoveredByAlias, isForbidden()); + } + } + + /** + * Verifies that the heap size used by StatefulIndexPrivileges stays within expected bounds. + */ + @RunWith(Parameterized.class) + public static class StatefulIndexPrivilegesHeapSize { + + final Map indices; + final SecurityDynamicConfiguration roles; + final int expectedEstimatedNumberOfBytes; + + @Test + public void estimatedSize() throws Exception { + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + () -> indices, + Settings.EMPTY + ); + + subject.updateStatefulIndexPrivileges(indices, 1); + + int lowerBound = (int) (expectedEstimatedNumberOfBytes * 0.9); + int upperBound = (int) (expectedEstimatedNumberOfBytes * 1.1); + + int actualEstimatedNumberOfBytes = subject.getEstimatedStatefulIndexByteSize(); + + assertTrue( + "estimatedNumberOfBytes: " + lowerBound + " < " + actualEstimatedNumberOfBytes + " < " + upperBound, + lowerBound < actualEstimatedNumberOfBytes && actualEstimatedNumberOfBytes < upperBound + ); + } + + public StatefulIndexPrivilegesHeapSize(int numberOfIndices, int numberOfRoles, int expectedEstimatedNumberOfBytes) { + this.indices = createIndices(numberOfIndices); + this.roles = createRoles(numberOfRoles, numberOfIndices); + this.expectedEstimatedNumberOfBytes = expectedEstimatedNumberOfBytes; + } + + @Parameterized.Parameters(name = "{0} indices; {1} roles; estimated number of bytes: {2}") + public static Collection params() { + List result = new ArrayList<>(); + + // indices; roles; expected number of bytes + result.add(new Object[] { 100, 10, 10_000 }); + result.add(new Object[] { 100, 100, 13_000 }); + result.add(new Object[] { 100, 1000, 26_000 }); + + result.add(new Object[] { 1000, 10, 92_000 }); + result.add(new Object[] { 1000, 100, 94_000 }); + result.add(new Object[] { 1000, 1000, 112_000 }); + + result.add(new Object[] { 10_000, 10, 890_000 }); + result.add(new Object[] { 10_000, 100, 930_000 }); + + return result; + } + + static Map createIndices(int numberOfIndices) { + String[] names = new String[numberOfIndices]; + + for (int i = 0; i < numberOfIndices; i++) { + names[i] = "index_" + i; + } + + return MockIndexMetadataBuilder.indices(names).build().getIndicesLookup(); + } + + static SecurityDynamicConfiguration createRoles(int numberOfRoles, int numberOfIndices) { + try { + Random random = new Random(1); + Map rolesDocument = new HashMap<>(); + List allowedActions = Arrays.asList( + "indices:data/read*", + "indices:admin/mappings/fields/get*", + "indices:admin/resolve/index", + "indices:data/write*", + "indices:admin/mapping/put" + ); + + for (int i = 0; i < numberOfRoles; i++) { + List indexPatterns = new ArrayList<>(); + int numberOfIndexPatterns = Math.min( + (int) ((Math.abs(random.nextGaussian() + 0.3)) * 0.5 * numberOfIndices), + numberOfIndices + ); + + int numberOfIndexPatterns10th = numberOfIndexPatterns / 10; + + if (numberOfIndexPatterns10th > 0) { + for (int k = 0; k < numberOfIndexPatterns10th; k++) { + indexPatterns.add("index_" + random.nextInt(numberOfIndices / 10) + "*"); + } + } else { + for (int k = 0; k < numberOfIndexPatterns; k++) { + indexPatterns.add("index_" + random.nextInt(numberOfIndices)); + } + } + + Map roleDocument = ImmutableMap.of( + "index_permissions", + Arrays.asList(ImmutableMap.of("index_patterns", indexPatterns, "allowed_actions", allowedActions)) + ); + + rolesDocument.put("role_" + i, roleDocument); + } + + return SecurityDynamicConfiguration.fromMap(rolesDocument, CType.ROLES); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + + static PrivilegesEvaluationContext ctx(String... roles) { + User user = new User("test_user").withAttributes(ImmutableMap.of("attrs.dept_no", "a11")); + return new PrivilegesEvaluationContext( + user, + ImmutableSet.copyOf(roles), + null, + null, + null, + null, + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), + null, + null + ); + } + + static PrivilegesEvaluationContext ctxByUsername(String username) { + User user = new User(username).withAttributes(ImmutableMap.of("attrs.dept_no", "a11")); + return new PrivilegesEvaluationContext( + user, + ImmutableSet.of(), + null, + null, + null, + null, + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), + null, + null + ); + } +} diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index f6701d6dc6..b3dbfd15dd 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -172,7 +172,7 @@ import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; -import org.opensearch.security.privileges.RoleBasedActionPrivileges; +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; @@ -2265,7 +2265,7 @@ public PluginSubject getPluginSubject(Plugin plugin) { Set clusterActions = new HashSet<>(); clusterActions.add(BulkAction.NAME); PluginSubject subject = new ContextProvidingPluginSubject(threadPool, settings, plugin); - sf.updatePluginToClusterActions(subject.getPrincipal().getName(), clusterActions); + evaluator.updatePluginToClusterActions(subject.getPrincipal().getName(), clusterActions); return subject; } diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index 642679a8fd..096e56d5a9 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -522,10 +522,6 @@ private boolean checkImmutableIndices(Object request, ActionListener listener) { return false; } - public void updatePluginToClusterActions(String pluginIdentifier, Set clusterActions) { - evalp.updatePluginToClusterActions(pluginIdentifier, clusterActions); - } - private boolean isRequestIndexImmutable(Object request) { final IndexResolverReplacer.Resolved resolved = indexResolverReplacer.resolveRequest(request); if (resolved.isLocalAll()) { diff --git a/src/main/java/org/opensearch/security/privileges/IndexPattern.java b/src/main/java/org/opensearch/security/privileges/IndexPattern.java index d5d419f72b..9ce65dea40 100644 --- a/src/main/java/org/opensearch/security/privileges/IndexPattern.java +++ b/src/main/java/org/opensearch/security/privileges/IndexPattern.java @@ -208,12 +208,12 @@ public int hashCode() { return hashCode; } - static class Builder { + public static class Builder { private List constantPatterns = new ArrayList<>(); private List patternTemplates = new ArrayList<>(); private List dateMathExpressions = new ArrayList<>(); - void add(List source) { + public void add(List source) { for (int i = 0; i < source.size(); i++) { try { String indexPattern = source.get(i); @@ -232,7 +232,7 @@ void add(List source) { } } - IndexPattern build() { + public IndexPattern build() { return new IndexPattern( constantPatterns.size() != 0 ? WildcardMatcher.from(constantPatterns) : WildcardMatcher.NONE, ImmutableList.copyOf(patternTemplates), diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 14bbc88536..4307ae8f9c 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -91,6 +91,8 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.ClusterInfoHolder; import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; +import org.opensearch.security.privileges.actionlevel.SubjectBasedActionPrivileges; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.securityconf.ConfigModel; @@ -229,7 +231,7 @@ public PrivilegesEvaluator( clusterService.addListener(event -> { RoleBasedActionPrivileges actionPrivileges = PrivilegesEvaluator.this.actionPrivileges.get(); if (actionPrivileges != null) { - actionPrivileges.updateClusterStateMetadataAsync(clusterService, threadPool); + actionPrivileges.clusterStateMetadataDependentPrivileges().updateClusterStateMetadataAsync(clusterService, threadPool); } }); } @@ -256,7 +258,7 @@ void updateConfiguration( RoleBasedActionPrivileges oldInstance = this.actionPrivileges.getAndSet(actionPrivileges); if (oldInstance != null) { - oldInstance.shutdown(); + oldInstance.clusterStateMetadataDependentPrivileges().shutdown(); } } catch (Exception e) { log.error("Error while updating ActionPrivileges", e); @@ -882,8 +884,8 @@ private List toString(List aliases) { } public void updatePluginToClusterActions(String pluginIdentifier, Set clusterActions) { - pluginToClusterActions.put(pluginIdentifier, clusterActions); - public void updatePluginToPermissions(String pluginIdentifier, RoleV7 pluginPermissions) { + RoleV7 pluginPermissions = new RoleV7(); + pluginPermissions.setCluster_permissions(ImmutableList.copyOf(clusterActions)); this.pluginIdToActionPrivileges.put( pluginIdentifier, new SubjectBasedActionPrivileges(pluginPermissions, this.staticActionGroups, this.indexMetadataSupplier) diff --git a/src/main/java/org/opensearch/security/privileges/RoleBasedActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java similarity index 70% rename from src/main/java/org/opensearch/security/privileges/RoleBasedActionPrivileges.java rename to src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java index 0a8ef4c75b..90375081f1 100644 --- a/src/main/java/org/opensearch/security/privileges/RoleBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java @@ -9,7 +9,7 @@ * GitHub history for details. */ -package org.opensearch.security.privileges; +package org.opensearch.security.privileges.actionlevel; import java.util.ArrayList; import java.util.Collection; @@ -35,6 +35,12 @@ 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.ActionPrivileges; +import org.opensearch.security.privileges.ClusterStateMetadataDependentPrivileges; +import org.opensearch.security.privileges.IndexPattern; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.PrivilegesEvaluationException; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -46,6 +52,7 @@ import com.selectivem.collections.DeduplicatingCompactSubSetBuilder; import com.selectivem.collections.ImmutableCompactSubSet; +import static org.opensearch.security.privileges.actionlevel.WellKnownActions.*; /** * This class converts role configuration into pre-computed, optimized data structures for checking privileges. *

@@ -53,7 +60,7 @@ * instance of this class corresponds to the life-cycle of the role and action group configuration. If the role or * action group configuration is changed, a new instance needs to be built. */ -public class RoleBasedActionPrivileges extends ClusterStateMetadataDependentPrivileges implements ActionPrivileges { +public class RoleBasedActionPrivileges extends RuntimeOptimizedActionPrivileges implements ActionPrivileges { /** * This setting controls the allowed heap size of the precomputed index privileges (in the inner class StatefulIndexPrivileges). @@ -72,13 +79,8 @@ public class RoleBasedActionPrivileges extends ClusterStateMetadataDependentPriv private static final Logger log = LogManager.getLogger(RoleBasedActionPrivileges.class); - private final ClusterPrivileges cluster; - private final IndexPrivileges index; private final SecurityDynamicConfiguration roles; private final FlattenedActionGroups actionGroups; - private final ImmutableSet wellKnownClusterActions; - private final ImmutableSet wellKnownIndexActions; - private final Supplier> indexMetadataSupplier; private final ByteSizeValue statefulIndexMaxHeapSize; private final AtomicReference statefulIndex = new AtomicReference<>(); @@ -87,141 +89,19 @@ public RoleBasedActionPrivileges( SecurityDynamicConfiguration roles, FlattenedActionGroups actionGroups, Supplier> indexMetadataSupplier, - Settings settings, - ImmutableSet wellKnownClusterActions, - ImmutableSet wellKnownIndexActions, - ImmutableSet explicitlyRequiredIndexActions + Settings settings ) { - this.cluster = new ClusterPrivileges(roles, actionGroups, wellKnownClusterActions); - this.index = new IndexPrivileges(roles, actionGroups, wellKnownIndexActions, explicitlyRequiredIndexActions); + super(new ClusterPrivileges(roles, actionGroups), new IndexPrivileges(roles, actionGroups), indexMetadataSupplier); this.roles = roles; this.actionGroups = actionGroups; - this.wellKnownClusterActions = wellKnownClusterActions; - this.wellKnownIndexActions = wellKnownIndexActions; - this.indexMetadataSupplier = indexMetadataSupplier; this.statefulIndexMaxHeapSize = PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.get(settings); } - public RoleBasedActionPrivileges( - SecurityDynamicConfiguration roles, - FlattenedActionGroups actionGroups, - Supplier> indexMetadataSupplier, - Settings settings - ) { - this( - roles, - actionGroups, - indexMetadataSupplier, - settings, - WellKnownActions.CLUSTER_ACTIONS, - WellKnownActions.INDEX_ACTIONS, - WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS - ); - } - - @Override - public PrivilegesEvaluatorResponse hasClusterPrivilege(PrivilegesEvaluationContext context, String action) { - return cluster.providesPrivilege(context, action, context.getMappedRoles()); - } - - @Override - public PrivilegesEvaluatorResponse hasAnyClusterPrivilege(PrivilegesEvaluationContext context, Set actions) { - return cluster.providesAnyPrivilege(context, actions, context.getMappedRoles()); - } - - /** - * Checks whether this instance provides explicit privileges for the combination of the provided action and the - * provided roles. - *

- * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns - * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 - *

- * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. - * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. - */ - @Override - public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluationContext context, String action) { - return cluster.providesExplicitPrivilege(context, action, context.getMappedRoles()); - } - - /** - * 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. - */ - @Override - public PrivilegesEvaluatorResponse hasIndexPrivilege( - PrivilegesEvaluationContext context, - Set actions, - IndexResolverReplacer.Resolved resolvedIndices - ) { - PrivilegesEvaluatorResponse response = this.index.providesWildcardPrivilege(context, actions); - if (response != null) { - return response; - } - - if (!resolvedIndices.isLocalAll() && resolvedIndices.getAllIndices().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"); - return PrivilegesEvaluatorResponse.ok(); - } - - // 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 - ); - - StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); - PrivilegesEvaluatorResponse resultFromStatefulIndex = null; - - Map indexMetadata = this.indexMetadataSupplier.get(); - - if (statefulIndex != null) { - resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, resolvedIndices, context, checkTable, indexMetadata); - - if (resultFromStatefulIndex != null) { - // If we get a result from statefulIndex, we are done. - return resultFromStatefulIndex; - } - - // Otherwise, we need to carry on checking privileges using the non-stateful object. - // Note: statefulIndex.hasPermission() modifies as a side effect the checkTable. - // We can carry on using this as an intermediate result and further complete checkTable below. - } - - return this.index.providesPrivilege(context, actions, resolvedIndices, checkTable, indexMetadata); - } - - /** - * Checks whether this instance provides explicit privileges for the combination of the provided action, - * the provided indices and the provided roles. - *

- * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns - * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 - */ - @Override - public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( - PrivilegesEvaluationContext context, - Set actions, - IndexResolverReplacer.Resolved resolvedIndices - ) { - CheckTable checkTable = CheckTable.create(resolvedIndices.getAllIndices(), actions); - return this.index.providesExplicitPrivilege(context, actions, resolvedIndices, checkTable, this.indexMetadataSupplier.get()); - } - /** * Updates the stateful index configuration with the given indices. Should be normally only called by * updateStatefulIndexPrivilegesAsync(). Package visible for testing. */ - void updateStatefulIndexPrivileges(Map indices, long metadataVersion) { + public void updateStatefulIndexPrivileges(Map indices, long metadataVersion) { StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); indices = StatefulIndexPrivileges.relevantOnly(indices); @@ -229,7 +109,7 @@ void updateStatefulIndexPrivileges(Map indices, long m if (statefulIndex == null || !statefulIndex.indices.equals(indices)) { long start = System.currentTimeMillis(); this.statefulIndex.set( - new StatefulIndexPrivileges(roles, actionGroups, wellKnownIndexActions, indices, metadataVersion, statefulIndexMaxHeapSize) + new StatefulIndexPrivileges(roles, actionGroups, indices, metadataVersion, statefulIndexMaxHeapSize) ); long duration = System.currentTimeMillis() - start; log.debug("Updating StatefulIndexPrivileges took {} ms", duration); @@ -244,17 +124,6 @@ void updateStatefulIndexPrivileges(Map indices, long m } } - @Override - protected void updateClusterStateMetadata(Metadata metadata) { - this.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); - } - - @Override - protected long getCurrentlyUsedMetadataVersion() { - StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); - return statefulIndex != null ? statefulIndex.metadataVersion : 0; - } - int getEstimatedStatefulIndexByteSize() { StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); @@ -265,6 +134,15 @@ int getEstimatedStatefulIndexByteSize() { } } + @Override + protected RuntimeOptimizedActionPrivileges.StatefulIndexPrivileges currentStatefulIndexPrivileges() { + return this.statefulIndex.get(); + } + + public ClusterStateMetadataDependentPrivileges clusterStateMetadataDependentPrivileges() { + return this.clusterStateMetadataDependentPrivileges; + } + /** * Pre-computed, optimized cluster privilege maps. Instances of this class are immutable. *

@@ -273,7 +151,7 @@ int getEstimatedStatefulIndexByteSize() { *

* The check will be possible in time O(1) for "well-known" actions when the user actually has the privileges. */ - static class ClusterPrivileges { + static class ClusterPrivileges extends RuntimeOptimizedActionPrivileges.ClusterPrivileges { /** * Maps names of actions to the roles that provide a privilege for the respective action. @@ -304,8 +182,6 @@ static class ClusterPrivileges { private final ImmutableMap usersToActionMatcher; - private final ImmutableSet wellKnownClusterActions; - /** * Creates pre-computed cluster privileges based on the given parameters. *

@@ -316,8 +192,7 @@ static class ClusterPrivileges { */ ClusterPrivileges( SecurityDynamicConfiguration roles, - FlattenedActionGroups actionGroups, - ImmutableSet wellKnownClusterActions + FlattenedActionGroups actionGroups ) { DeduplicatingCompactSubSetBuilder roleSetBuilder = new DeduplicatingCompactSubSetBuilder<>( roles.getCEntries().keySet() @@ -355,7 +230,7 @@ static class ClusterPrivileges { } else { WildcardMatcher wildcardMatcher = WildcardMatcher.from(permission); Set matchedActions = wildcardMatcher.getMatchAny( - wellKnownClusterActions, + WellKnownActions.CLUSTER_ACTIONS, Collectors.toUnmodifiableSet() ); @@ -383,131 +258,32 @@ static class ClusterPrivileges { this.rolesWithWildcardPermissions = rolesWithWildcardPermissions.build(); this.rolesToActionMatcher = rolesToActionMatcher.build(); this.usersToActionMatcher = usersToActionMatcher.build(); - this.wellKnownClusterActions = wellKnownClusterActions; } - /** - * Checks whether this instance provides privileges for the combination of the provided action and the - * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. - * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. - */ - PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext context, String action, Set roles) { - - // 1: Check roles with wildcards - if (CollectionUtils.containsAny(roles, this.rolesWithWildcardPermissions)) { - return PrivilegesEvaluatorResponse.ok(); - } - - // 2: Check well-known actions - this should cover most cases - ImmutableCompactSubSet rolesWithPrivileges = this.actionToRoles.get(action); - - if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(roles)) { - return PrivilegesEvaluatorResponse.ok(); - } - - // 3: Only if everything else fails: Check the matchers in case we have a non-well-known action - if (!this.wellKnownClusterActions.contains(action)) { - for (String role : roles) { - WildcardMatcher matcher = this.rolesToActionMatcher.get(role); - - if (matcher != null && matcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - } - - // 4: If plugin is performing the action, check if plugin has permission - if (context.getUser().isPluginUser() && this.usersToActionMatcher.containsKey(context.getUser().getName())) { - WildcardMatcher matcher = this.usersToActionMatcher.get(context.getUser().getName()); - if (matcher != null && matcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - - return PrivilegesEvaluatorResponse.insufficient(action); + @Override + protected boolean checkWildcardPrivilege(PrivilegesEvaluationContext context) { + return CollectionUtils.containsAny(context.getMappedRoles(), this.rolesWithWildcardPermissions); } - /** - * Checks whether this instance provides explicit privileges for the combination of the provided action and the - * provided roles. - *

- * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns - * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 - *

- * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. - * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. - */ - PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContext context, String action, Set roles) { - - // 1: Check well-known actions - this should cover most cases + @Override + protected boolean checkPrivilegeForWellKnownAction(PrivilegesEvaluationContext context, String action) { ImmutableCompactSubSet rolesWithPrivileges = this.actionToRoles.get(action); - - if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(roles)) { - return PrivilegesEvaluatorResponse.ok(); - } - - // 2: Only if everything else fails: Check the matchers in case we have a non-well-known action - if (!this.wellKnownClusterActions.contains(action)) { - for (String role : roles) { - WildcardMatcher matcher = this.rolesToActionMatcher.get(role); - - if (matcher != null && matcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - } - - return PrivilegesEvaluatorResponse.insufficient(action); + return rolesWithPrivileges != null && rolesWithPrivileges.containsAny(context.getMappedRoles()); } - /** - * Checks whether this instance provides privileges for the combination of any of the provided actions and the - * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. - * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. - */ - PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext context, Set actions, Set roles) { - // 1: Check roles with wildcards - if (CollectionUtils.containsAny(roles, this.rolesWithWildcardPermissions)) { - return PrivilegesEvaluatorResponse.ok(); - } - - // 2: Check well-known actions - this should cover most cases - for (String action : actions) { - ImmutableCompactSubSet rolesWithPrivileges = this.actionToRoles.get(action); - - if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(roles)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - - // 3: Only if everything else fails: Check the matchers in case we have a non-well-known action - for (String action : actions) { - if (!this.wellKnownClusterActions.contains(action)) { - for (String role : roles) { - WildcardMatcher matcher = this.rolesToActionMatcher.get(role); - - if (matcher != null && matcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - } - } + @Override + protected boolean checkPrivilegeViaActionMatcher(PrivilegesEvaluationContext context, String action) { + if (!WellKnownActions.CLUSTER_ACTIONS.contains(action)) { + for (String role : context.getMappedRoles()) { + WildcardMatcher matcher = this.rolesToActionMatcher.get(role); - // 4: If plugin is performing the action, check if plugin has permission - if (this.usersToActionMatcher.containsKey(context.getUser().getName())) { - WildcardMatcher matcher = this.usersToActionMatcher.get(context.getUser().getName()); - for (String action : actions) { if (matcher != null && matcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); + return true; } } } - if (actions.size() == 1) { - return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next()); - } else { - return PrivilegesEvaluatorResponse.insufficient("any of " + actions); - } + return false; } } @@ -529,7 +305,7 @@ PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext con * c) The index patterns use placeholders like "${user.name}" - these can be only resolved when the User object is present. * d) The action is not among the "well known" actions. */ - static class IndexPrivileges { + static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticIndexPrivileges { /** * Maps role names to concrete action names to IndexPattern objects which define the indices the privileges apply to. */ @@ -548,16 +324,6 @@ static class IndexPrivileges { */ private final ImmutableMap> actionToRolesWithWildcardIndexPrivileges; - /** - * A pre-defined set of action names that is used to pre-compute the result of action patterns. - */ - private final ImmutableSet wellKnownIndexActions; - - /** - * A pre-defined set of action names that is included in the rolesToExplicitActionToIndexPattern data structure - */ - private final ImmutableSet explicitlyRequiredIndexActions; - /** * Maps role names to concrete action names to IndexPattern objects which define the indices the privileges apply to. * The action names are only explicitly granted privileges which are listed in explicitlyRequiredIndexActions. @@ -576,9 +342,7 @@ static class IndexPrivileges { */ IndexPrivileges( SecurityDynamicConfiguration roles, - FlattenedActionGroups actionGroups, - ImmutableSet wellKnownIndexActions, - ImmutableSet explicitlyRequiredIndexActions + FlattenedActionGroups actionGroups ) { Map> rolesToActionToIndexPattern = new HashMap<>(); @@ -613,7 +377,7 @@ static class IndexPrivileges { .computeIfAbsent(permission, k -> new IndexPattern.Builder()) .add(indexPermissions.getIndex_patterns()); - if (explicitlyRequiredIndexActions.contains(permission)) { + if (WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS.contains(permission)) { rolesToExplicitActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) .computeIfAbsent(permission, k -> new IndexPattern.Builder()) .add(indexPermissions.getIndex_patterns()); @@ -628,7 +392,7 @@ static class IndexPrivileges { } else { WildcardMatcher actionMatcher = WildcardMatcher.from(permission); - for (String action : actionMatcher.iterateMatching(wellKnownIndexActions)) { + for (String action : actionMatcher.iterateMatching(WellKnownActions.INDEX_ACTIONS)) { rolesToActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) .computeIfAbsent(action, k -> new IndexPattern.Builder()) .add(indexPermissions.getIndex_patterns()); @@ -646,7 +410,7 @@ static class IndexPrivileges { .add(indexPermissions.getIndex_patterns()); if (actionMatcher != WildcardMatcher.ANY) { - for (String action : actionMatcher.iterateMatching(explicitlyRequiredIndexActions)) { + for (String action : actionMatcher.iterateMatching(WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS)) { rolesToExplicitActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) .computeIfAbsent(action, k -> new IndexPattern.Builder()) .add(indexPermissions.getIndex_patterns()); @@ -702,10 +466,10 @@ static class IndexPrivileges { ) ); - this.wellKnownIndexActions = wellKnownIndexActions; - this.explicitlyRequiredIndexActions = explicitlyRequiredIndexActions; } + + /** * Checks whether this instance provides privileges for the combination of the provided action, * the provided indices and the provided roles. @@ -722,7 +486,8 @@ static class IndexPrivileges { * As a side-effect, this method will further mark the available index/action combinations in the provided * checkTable instance as checked. */ - PrivilegesEvaluatorResponse providesPrivilege( + @Override + protected PrivilegesEvaluatorResponse providesPrivilege( PrivilegesEvaluationContext context, Set actions, IndexResolverReplacer.Resolved resolvedIndices, @@ -733,24 +498,10 @@ PrivilegesEvaluatorResponse providesPrivilege( for (String role : context.getMappedRoles()) { ImmutableMap actionToIndexPattern = this.rolesToActionToIndexPattern.get(role); - if (actionToIndexPattern != null) { - for (String action : actions) { - IndexPattern indexPattern = actionToIndexPattern.get(action); - - if (indexPattern != null) { - for (String index : checkTable.iterateUncheckedRows(action)) { - try { - if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } 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)); - } - } - } + checkPrivilegeWithIndexPatternOnWellKnownActions(context, actions, checkTable, indexMetadata, actionToIndexPattern, exceptions); + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(); } } } @@ -759,60 +510,22 @@ PrivilegesEvaluatorResponse providesPrivilege( // 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 - boolean allActionsWellKnown = actions.stream().allMatch(a -> this.wellKnownIndexActions.contains(a)); - - if (!checkTable.isComplete() && !allActionsWellKnown) { - top: for (String role : context.getMappedRoles()) { + if (!checkTable.isComplete() && !allWellKnownIndexActions(actions)) { + for (String role : context.getMappedRoles()) { ImmutableMap actionPatternToIndexPattern = this.rolesToActionPatternToIndexPattern.get( role ); if (actionPatternToIndexPattern != null) { - for (String action : actions) { - if (this.wellKnownIndexActions.contains(action)) { - continue; - } - - for (Map.Entry entry : actionPatternToIndexPattern.entrySet()) { - WildcardMatcher actionMatcher = entry.getKey(); - IndexPattern indexPattern = entry.getValue(); - - if (actionMatcher.test(action)) { - for (String index : checkTable.iterateUncheckedRows(action)) { - try { - if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { - break top; - } - } 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)); - } - } - } - } + checkPrivilegesForNonWellKnownActions(context, actions, checkTable, indexMetadata, actionPatternToIndexPattern, exceptions); + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(); } } } } - if (checkTable.isComplete()) { - return PrivilegesEvaluatorResponse.ok(); - } - - Set availableIndices = checkTable.getCompleteRows(); - - if (!availableIndices.isEmpty()) { - return PrivilegesEvaluatorResponse.partiallyOk(availableIndices, checkTable).evaluationExceptions(exceptions); - } - - return PrivilegesEvaluatorResponse.insufficient(checkTable) - .reason( - resolvedIndices.getAllIndices().size() == 1 - ? "Insufficient permissions for the referenced index" - : "None of " + resolvedIndices.getAllIndices().size() + " referenced indices has sufficient permissions" - ) - .evaluationExceptions(exceptions); + return responseForIncompletePrivileges(context, checkTable, exceptions); } /** @@ -820,7 +533,8 @@ PrivilegesEvaluatorResponse providesPrivilege( * indices (using *) for the given actions. Returns null otherwise. Then, further checks must be done to check * the user's privileges. */ - PrivilegesEvaluatorResponse providesWildcardPrivilege(PrivilegesEvaluationContext context, Set actions) { + @Override + protected PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownActions(PrivilegesEvaluationContext context, Set actions) { ImmutableSet effectiveRoles = context.getMappedRoles(); for (String action : actions) { @@ -841,19 +555,15 @@ PrivilegesEvaluatorResponse providesWildcardPrivilege(PrivilegesEvaluationContex * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 */ - PrivilegesEvaluatorResponse providesExplicitPrivilege( + @Override + protected PrivilegesEvaluatorResponse providesExplicitPrivilege( PrivilegesEvaluationContext context, Set actions, - IndexResolverReplacer.Resolved resolvedIndices, CheckTable checkTable, Map indexMetadata ) { List exceptions = new ArrayList<>(); - if (!CollectionUtils.containsAny(actions, this.explicitlyRequiredIndexActions)) { - return PrivilegesEvaluatorResponse.insufficient(CheckTable.create(ImmutableSet.of("_"), actions)); - } - for (String role : context.getMappedRoles()) { ImmutableMap actionToIndexPattern = this.rolesToExplicitActionToIndexPattern.get(role); @@ -901,7 +611,7 @@ PrivilegesEvaluatorResponse providesExplicitPrivilege( * This class also takes into account aliases and data streams. If a permission is granted on an alias, it will be * automatically inherited by the indices it points to. The same holds for the backing indices of a data stream. */ - static class StatefulIndexPrivileges { + static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.StatefulIndexPrivileges { /** * Maps concrete action names to concrete index names and then to the roles which provide privileges for the @@ -915,11 +625,6 @@ static class StatefulIndexPrivileges { */ private final Map indices; - /** - * The well known index actions that were used to construct this instance. - */ - private final ImmutableSet wellKnownIndexActions; - private final int estimatedByteSize; private long metadataVersion; @@ -934,7 +639,6 @@ static class StatefulIndexPrivileges { StatefulIndexPrivileges( SecurityDynamicConfiguration roles, FlattenedActionGroups actionGroups, - ImmutableSet wellKnownIndexActions, Map indices, long metadataVersion, ByteSizeValue statefulIndexMaxHeapSize @@ -985,7 +689,7 @@ static class StatefulIndexPrivileges { for (String permission : permissions) { WildcardMatcher actionMatcher = WildcardMatcher.from(permission); - Collection matchedActions = actionMatcher.getMatchAny(wellKnownIndexActions, Collectors.toList()); + Collection matchedActions = actionMatcher.getMatchAny(WellKnownActions.INDEX_ACTIONS, Collectors.toList()); for (Map.Entry indicesEntry : indexMatcher.iterateMatching( indices.entrySet(), @@ -1057,7 +761,6 @@ static class StatefulIndexPrivileges { this.indices = ImmutableMap.copyOf(indices); this.metadataVersion = metadataVersion; - this.wellKnownIndexActions = wellKnownIndexActions; } /** @@ -1078,7 +781,8 @@ 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. */ - PrivilegesEvaluatorResponse providesPrivilege( + @Override + protected PrivilegesEvaluatorResponse providesPrivilege( Set actions, IndexResolverReplacer.Resolved resolvedIndices, PrivilegesEvaluationContext context, @@ -1182,4 +886,16 @@ static Map relevantOnly(Map } } + final ClusterStateMetadataDependentPrivileges clusterStateMetadataDependentPrivileges = new ClusterStateMetadataDependentPrivileges() { + @Override + protected void updateClusterStateMetadata(Metadata metadata) { + RoleBasedActionPrivileges.this.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); + } + + @Override + protected long getCurrentlyUsedMetadataVersion() { + StatefulIndexPrivileges statefulIndex = RoleBasedActionPrivileges.this.statefulIndex.get(); + return statefulIndex != null ? statefulIndex.metadataVersion : 0; + } + }; } diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java new file mode 100644 index 0000000000..04b995bce0 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java @@ -0,0 +1,448 @@ +/* + * 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; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.selectivem.collections.CheckTable; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.security.privileges.ActionPrivileges; +import org.opensearch.security.privileges.IndexPattern; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.PrivilegesEvaluationException; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.support.WildcardMatcher; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import static org.opensearch.security.privileges.actionlevel.WellKnownActions.*; + +/** + * This is a common base class for ActionPrivileges implementations that implement a certain + * runtime optimization pattern: + *

    + *
  • First check for universal wildcard privileges (very fast)
  • + *
  • Then check for well known actions (very fast)
  • + *
  • Then do pattern matching (not so fast)
  • + *
+ */ +public abstract class RuntimeOptimizedActionPrivileges implements ActionPrivileges { + private static final Logger log = LogManager.getLogger(RuntimeOptimizedActionPrivileges.class); + + protected final ClusterPrivileges cluster; + protected final StaticIndexPrivileges index; + protected final Supplier> indexMetadataSupplier; + + RuntimeOptimizedActionPrivileges(ClusterPrivileges cluster, StaticIndexPrivileges index, Supplier> indexMetadataSupplier) { + this.cluster = cluster; + this.index = index; + this.indexMetadataSupplier = indexMetadataSupplier; + } + + @Override + public PrivilegesEvaluatorResponse hasClusterPrivilege(PrivilegesEvaluationContext context, String action) { + return cluster.providesPrivilege(context, action); + } + + @Override + public PrivilegesEvaluatorResponse hasAnyClusterPrivilege(PrivilegesEvaluationContext context, Set actions) { + return cluster.providesAnyPrivilege(context, actions); + } + + @Override + public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluationContext context, String action) { + return cluster.providesExplicitPrivilege(context, action); + } + + /** + * 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. + */ + @Override + public PrivilegesEvaluatorResponse hasIndexPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices + ) { + PrivilegesEvaluatorResponse response = this.index.checkWildcardIndexPrivilegesOnWellKnownActions(context, actions); + if (response != null) { + return response; + } + + if (!resolvedIndices.isLocalAll() && resolvedIndices.getAllIndices().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"); + return PrivilegesEvaluatorResponse.ok(); + } + + // 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 + ); + + Map indexMetadata = this.indexMetadataSupplier.get(); + + StatefulIndexPrivileges statefulIndex = this.currentStatefulIndexPrivileges(); + PrivilegesEvaluatorResponse resultFromStatefulIndex = null; + + if (statefulIndex != null) { + resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, resolvedIndices, context, checkTable, indexMetadata); + + if (resultFromStatefulIndex != null) { + // If we get a result from statefulIndex, we are done. + return resultFromStatefulIndex; + } + + // Otherwise, we need to carry on checking privileges using the non-stateful object. + // Note: statefulIndex.hasPermission() modifies as a side effect the checkTable. + // We can carry on using this as an intermediate result and further complete checkTable below. + } + + return this.index.providesPrivilege(context, actions, resolvedIndices, checkTable, indexMetadata); + } + + /** + * Checks whether this instance provides explicit privileges for the combination of the provided action, + * the provided indices and the provided roles. + *

+ * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns + * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 + */ + @Override + public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved 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); + return this.index.providesExplicitPrivilege(context, actions, checkTable, this.indexMetadataSupplier.get()); + } + + /** + * Returns the current stateful index privileges that can be used for privilege evaluation. Implementations + * can choose to return null here; then, a slower evaluation path will be used. + */ + protected abstract StatefulIndexPrivileges currentStatefulIndexPrivileges(); + + /** + * Base class for evaluating cluster privileges. + */ + protected abstract static class ClusterPrivileges { + /** + * Checks whether this instance provides privileges for the combination of the provided action and the + * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ + PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext context, String action) { + // 1: Check roles with wildcards + if (checkWildcardPrivilege(context)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 2: Check well-known actions - this should cover most cases + if (checkPrivilegeForWellKnownAction(context, action)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 3: Only if everything else fails: Check the matchers in case we have a non-well-known action + if (!isWellKnownClusterAction(action) && checkPrivilegeViaActionMatcher(context, action)) { + return PrivilegesEvaluatorResponse.ok(); + } + + return PrivilegesEvaluatorResponse.insufficient(action); + } + + /** + * Checks whether this instance provides explicit privileges for the combination of the provided action and the + * provided roles. + *

+ * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns + * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 + *

+ * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ + PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContext context, String action) { + + // 1: Check well-known actions - this should cover most cases + if (checkPrivilegeForWellKnownAction(context, action)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 2: Only if everything else fails: Check the matchers in case we have a non-well-known action + if (!isWellKnownClusterAction(action) && checkPrivilegeViaActionMatcher(context, action)) { + return PrivilegesEvaluatorResponse.ok(); + } + + return PrivilegesEvaluatorResponse.insufficient(action); + } + + /** + * Checks whether this instance provides privileges for the combination of any of the provided actions and the + * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ + PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext context, Set actions) { + // 1: Check roles with wildcards + if (checkWildcardPrivilege(context)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 2: Check well-known actions - this should cover most cases + for (String action : actions) { + if (checkPrivilegeForWellKnownAction(context, action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + + // 3: Only if everything else fails: Check the matchers in case we have a non-well-known action + for (String action : actions) { + if (!isWellKnownClusterAction(action) && checkPrivilegeViaActionMatcher(context, action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + + if (actions.size() == 1) { + return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next()); + } else { + return PrivilegesEvaluatorResponse.insufficient("any of " + actions); + } + } + + /** + * Tests whether the current user (according to the context data) has wildcard cluster privileges. + *

+ * Implementations of this class may interpret the context data differently; they can check the mapped roles + * or just the subject. + */ + protected abstract boolean checkWildcardPrivilege(PrivilegesEvaluationContext context); + + /** + * Tests whether the current user (according to the context data) has privileges for the given well known cluster action. + * Returns false if no privileges are given or if the given action is not a well known action. + *

+ * Implementations of this class may interpret the context data differently; they can check the mapped roles + * or just the subject. + */ + protected abstract boolean checkPrivilegeForWellKnownAction(PrivilegesEvaluationContext context, String action); + + /** + * Tests whether a privilege is provided via a pattern on an action (like "indices:data/read/*"). + * This does NOT include the full wildcard pattern "*". + *

+ * This is the slowest way to check for a privilege. + */ + protected abstract boolean checkPrivilegeViaActionMatcher(PrivilegesEvaluationContext context, String action); + + + } + + /** + * Base class for evaluating index permissions which evaluates index patterns at privilege evaluation time. + */ + protected abstract static class StaticIndexPrivileges { + + + /** + * Checks whether this instance provides privileges for the combination of the provided action, + * the provided indices and the provided roles. + *

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

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

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

+ * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns + * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 + */ + protected abstract PrivilegesEvaluatorResponse providesExplicitPrivilege( + PrivilegesEvaluationContext context, + Set actions, + CheckTable checkTable, + Map indexMetadata + ); + + /** + * 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. + *

+ * Implementations of this class may interpret the context data differently; they can check the mapped roles + * or just the subject. + */ + protected abstract PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownActions(PrivilegesEvaluationContext context, Set actions); + + + /** + * Tests whether the current user (according to the context data) has index privileges for the given well known + * index actions via index patterns. + *

+ * This method has two side-effects which transport the result of this check: + *

    + *
  • The action/index combinations for which privileges are found are checked in the given check table. + *
  • In case of any PrivilegeEvaluationException, it is added to the given list + *
+ */ + protected void checkPrivilegeWithIndexPatternOnWellKnownActions(PrivilegesEvaluationContext context, Set actions, CheckTable checkTable, Map indexMetadata, ImmutableMap actionToIndexPattern, List exceptions) { + for (String action : actions) { + IndexPattern indexPattern = actionToIndexPattern.get(action); + + if (indexPattern != null) { + for (String index : checkTable.iterateUncheckedRows(action)) { + try { + if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { + return; + } + } 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)); + } + } + } + } + } + + /** + * Does a privilege check for non-well known actions. This is the slowest method and should be used last. + *

+ * This method has two side-effects which transport the result of this check: + *

    + *
  • The action/index combinations for which privileges are found are checked in the given check table. + *
  • In case of any PrivilegeEvaluationException, it is added to the given list + *
+ */ + protected void checkPrivilegesForNonWellKnownActions(PrivilegesEvaluationContext context, Set actions, CheckTable checkTable, Map indexMetadata, ImmutableMap actionPatternToIndexPattern, List exceptions) { + for (String action : actions) { + if (isWellKnownIndexAction(action)) { + continue; + } + + for (Map.Entry entry : actionPatternToIndexPattern.entrySet()) { + WildcardMatcher actionMatcher = entry.getKey(); + IndexPattern indexPattern = entry.getValue(); + + if (actionMatcher.test(action)) { + for (String index : checkTable.iterateUncheckedRows(action)) { + try { + if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { + return; + } + } catch (PrivilegesEvaluationException e) { + // We can ignore these errors, as this max leads to fewer privileges than available + log.error("Error while evaluating index pattern {}. Ignoring entry", indexPattern, e); + exceptions.add(new PrivilegesEvaluationException("Error while evaluating index pattern " + indexPattern, e)); + } + } + } + } + } + } + + /** + * 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) { + Set availableIndices = checkTable.getCompleteRows(); + + if (!availableIndices.isEmpty()) { + return PrivilegesEvaluatorResponse.partiallyOk(availableIndices, checkTable).evaluationExceptions(exceptions); + } + + return PrivilegesEvaluatorResponse.insufficient(checkTable) + .reason( + context.getResolvedRequest().getAllIndices().size() == 1 + ? "Insufficient permissions for the referenced index" + : "None of " + context.getResolvedRequest().getAllIndices().size() + " referenced indices has sufficient permissions" + ) + .evaluationExceptions(exceptions); + } + } + + /** + * Base class for evaluating index permissions which evaluates index patterns ahead of the time using the current + * cluster state. + */ + protected abstract static class StatefulIndexPrivileges { + + /** + * Checks whether the user has privileges based on the given parameters and information in this class. This method + * has two major channels for returning results: + *

+ * 1. The return value is either PrivilegesEvaluatorResponse.ok() or null. If it is null, this method cannot + * completely tell whether the user has full privileges. A further check with IndexPermissions will be necessary. + * If PrivilegesEvaluatorResponse.ok() is returned, then full privileges could be already determined. + *

+ * 2. As a side effect, this method will modify the supplied CheckTable object. This will be the case regardless + * of whether null or PrivilegesEvaluatorResponse.ok() is returned. The interesting case is actually when null + * 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, + Map indexMetadata + ); + } + +} diff --git a/src/main/java/org/opensearch/security/privileges/SubjectBasedActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java similarity index 61% rename from src/main/java/org/opensearch/security/privileges/SubjectBasedActionPrivileges.java rename to src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java index c78f55081a..25f06a06da 100644 --- a/src/main/java/org/opensearch/security/privileges/SubjectBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java @@ -9,7 +9,7 @@ * GitHub history for details. */ -package org.opensearch.security.privileges; +package org.opensearch.security.privileges.actionlevel; import java.util.ArrayList; import java.util.HashMap; @@ -27,6 +27,11 @@ import org.apache.logging.log4j.Logger; import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.security.privileges.ActionPrivileges; +import org.opensearch.security.privileges.IndexPattern; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.PrivilegesEvaluationException; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.v7.RoleV7; @@ -34,87 +39,54 @@ import com.selectivem.collections.CheckTable; +import static org.opensearch.security.privileges.actionlevel.WellKnownActions.allWellKnownIndexActions; + /** - * An ActionPrivileges implementation that is valid only for a single subject. - * This means that individual instances of this class must be created for individual subjects. The mapped roles + * An ActionPrivileges implementation that is valid only for a single entity. + * This means that individual instances of this class must be created for individual entities. The mapped roles * from the context are not regarded by this method. *

* The method PrivilegesEvaluator.createContext() is responsible for making sure that the correct class is used. *

* This class is useful for plugin users and API tokens. + *

+ * */ -public class SubjectBasedActionPrivileges implements ActionPrivileges { +public class SubjectBasedActionPrivileges extends RuntimeOptimizedActionPrivileges implements ActionPrivileges { private static final Logger log = LogManager.getLogger(SubjectBasedActionPrivileges.class); - private final ClusterPrivileges cluster; - private final IndexPrivileges index; - private final Supplier> indexMetadataSupplier; - + /** + * Creates a new immutable instance from the given parameters. + * + * @param role defines the privileges configuration. This is not a role per se, but the existing class has a + * suitable structure to carry the information. At one point, it might make sense to define an + * abstract interface. + * @param actionGroups The FlattenedActionGroups instance that shall be used to resolve the action groups + * specified in the roles configuration. + * @param indexMetadataSupplier A supplier for the cluster's current index meta data. This is used during privileges + * evaluation to gain information about alias and data stream members. + */ public SubjectBasedActionPrivileges( RoleV7 role, FlattenedActionGroups actionGroups, Supplier> indexMetadataSupplier ) { - this.cluster = new ClusterPrivileges(role, actionGroups, WellKnownActions.CLUSTER_ACTIONS); - this.index = new IndexPrivileges( - role, - actionGroups, - WellKnownActions.INDEX_ACTIONS, - WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS - ); - this.indexMetadataSupplier = indexMetadataSupplier; - } - - @Override - public PrivilegesEvaluatorResponse hasClusterPrivilege(PrivilegesEvaluationContext context, String action) { - return cluster.providesPrivilege(context, action); - } - - @Override - public PrivilegesEvaluatorResponse hasAnyClusterPrivilege(PrivilegesEvaluationContext context, Set actions) { - return cluster.providesAnyPrivilege(context, actions); - } - - @Override - public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluationContext context, String action) { - return cluster.providesExplicitPrivilege(context, action); - } - - @Override - public PrivilegesEvaluatorResponse hasIndexPrivilege( - PrivilegesEvaluationContext context, - Set actions, - IndexResolverReplacer.Resolved resolvedIndices - ) { - PrivilegesEvaluatorResponse response = this.index.providesWildcardPrivilege(context, actions); - if (response != null) { - return response; - } - - if (!resolvedIndices.isLocalAll() && resolvedIndices.getAllIndices().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"); - return PrivilegesEvaluatorResponse.ok(); - } - - CheckTable checkTable = CheckTable.create( - resolvedIndices.getAllIndicesResolved(context.getClusterStateSupplier(), context.getIndexNameExpressionResolver()), - actions - ); - Map indexMetadata = this.indexMetadataSupplier.get(); - - return this.index.providesPrivilege(context, actions, resolvedIndices, checkTable, indexMetadata); + super(new ClusterPrivileges(actionGroups.resolve(role.getCluster_permissions())), new IndexPrivileges( + role, + actionGroups, + WellKnownActions.INDEX_ACTIONS, + WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS + ), indexMetadataSupplier); } + /** + * At the moment, this class does not provide StatefulIndexPrivileges. + * Thus, always the slightly slower index matching code path will be used. For plugins, however, + * that should be okay, as they likely request specific indices without patterns. + */ @Override - public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( - PrivilegesEvaluationContext context, - Set actions, - IndexResolverReplacer.Resolved resolvedIndices - ) { - CheckTable checkTable = CheckTable.create(resolvedIndices.getAllIndices(), actions); - return this.index.providesExplicitPrivilege(context, actions, resolvedIndices, checkTable, this.indexMetadataSupplier.get()); + protected StatefulIndexPrivileges currentStatefulIndexPrivileges() { + return null; } /** @@ -125,7 +97,7 @@ public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( *

* The check will be possible in time O(1) for "well-known" actions when the user actually has the privileges. */ - static class ClusterPrivileges { + static class ClusterPrivileges extends RuntimeOptimizedActionPrivileges.ClusterPrivileges { /** * Maps names of actions to the roles that provide a privilege for the respective action. @@ -141,7 +113,7 @@ static class ClusterPrivileges { * This contains all role names that provide wildcard (*) privileges for cluster actions. * This avoids a blow-up of the actionToRoles object by such roles. */ - private final boolean hasWildcardPermission; + private final boolean providesWildcardPrivilege; /** * This maps role names to a matcher which matches the action names this role provides privileges for. @@ -154,23 +126,23 @@ static class ClusterPrivileges { */ private final WildcardMatcher grantedActionMatcher; - private final ImmutableSet wellKnownClusterActions; - /** - * Creates pre-computed cluster privileges based on the given parameters. + * Creates pre-computed cluster privileges based on the given permission patterns. *

* This constructor will not throw an exception if it encounters any invalid configuration (that is, * in particular, unparseable regular expressions). Rather, it will just log an error. This is okay, as it * just results in fewer available privileges. However, having a proper error reporting mechanism would be * kind of nice. + * + * @param permissionPatterns a collection of strings representing WildcardMatcher patterns that can match + * on action names. Any action groups must have been already resolved before these + * are passed here. */ - ClusterPrivileges(RoleV7 role, FlattenedActionGroups actionGroups, ImmutableSet wellKnownClusterActions) { + ClusterPrivileges(ImmutableSet permissionPatterns) { Set grantedActions = new HashSet<>(); boolean hasWildcardPermission = false; List wildcardMatchers = new ArrayList<>(); - ImmutableSet permissionPatterns = actionGroups.resolve(role.getCluster_permissions()); - for (String permission : permissionPatterns) { // If we have a permission which does not use any pattern, we just simply add it to the // "actionToRoles" map. @@ -186,105 +158,30 @@ static class ClusterPrivileges { hasWildcardPermission = true; } else { WildcardMatcher wildcardMatcher = WildcardMatcher.from(permission); - Set matchedActions = wildcardMatcher.getMatchAny(wellKnownClusterActions, Collectors.toUnmodifiableSet()); + Set matchedActions = wildcardMatcher.getMatchAny(WellKnownActions.CLUSTER_ACTIONS, Collectors.toUnmodifiableSet()); grantedActions.addAll(matchedActions); wildcardMatchers.add(wildcardMatcher); } } this.grantedActions = ImmutableSet.copyOf(grantedActions); - this.hasWildcardPermission = hasWildcardPermission; + this.providesWildcardPrivilege = hasWildcardPermission; this.grantedActionMatcher = WildcardMatcher.from(wildcardMatchers); - this.wellKnownClusterActions = wellKnownClusterActions; } - /** - * Checks whether this instance provides privileges for the combination of the provided action and the - * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. - * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. - */ - PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext context, String action) { - - // 1: Check roles with wildcards - if (this.hasWildcardPermission) { - return PrivilegesEvaluatorResponse.ok(); - } - - // 2: Check well-known actions - this should cover most cases - if (this.grantedActions.contains(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - - // 3: Only if everything else fails: Check the matchers in case we have a non-well-known action - if (!this.wellKnownClusterActions.contains(action)) { - if (this.grantedActionMatcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - - return PrivilegesEvaluatorResponse.insufficient(action); + @Override + protected boolean checkWildcardPrivilege(PrivilegesEvaluationContext context) { + return this.providesWildcardPrivilege; } - /** - * Checks whether this instance provides explicit privileges for the combination of the provided action and the - * provided roles. - *

- * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns - * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 - *

- * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. - * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. - */ - PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContext context, String action) { - - // 1: Check well-known actions - this should cover most cases - if (this.grantedActions.contains(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - - // 2: Only if everything else fails: Check the matchers in case we have a non-well-known action - if (!this.wellKnownClusterActions.contains(action)) { - if (this.grantedActionMatcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - - return PrivilegesEvaluatorResponse.insufficient(action); + @Override + protected boolean checkPrivilegeForWellKnownAction(PrivilegesEvaluationContext context, String action) { + return this.grantedActions.contains(action); } - /** - * Checks whether this instance provides privileges for the combination of any of the provided actions and the - * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. - * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. - */ - PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext context, Set actions) { - - // 1: Check roles with wildcards - if (this.hasWildcardPermission) { - return PrivilegesEvaluatorResponse.ok(); - } - - // 2: Check well-known actions - this should cover most cases - for (String action : actions) { - if (this.grantedActions.contains(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - - // 3: Only if everything else fails: Check the matchers in case we have a non-well-known action - for (String action : actions) { - if (!this.wellKnownClusterActions.contains(action)) { - if (this.grantedActionMatcher.test(action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } - } - - if (actions.size() == 1) { - return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next()); - } else { - return PrivilegesEvaluatorResponse.insufficient("any of " + actions); - } + @Override + protected boolean checkPrivilegeViaActionMatcher(PrivilegesEvaluationContext context, String action) { + return this.grantedActionMatcher.test(action); } } @@ -306,7 +203,7 @@ PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext con * c) The index patterns use placeholders like "${user.name}" - these can be only resolved when the User object is present. * d) The action is not among the "well known" actions. */ - static class IndexPrivileges { + static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticIndexPrivileges { /** * Maps role names to concrete action names to IndexPattern objects which define the indices the privileges apply to. */ @@ -429,6 +326,7 @@ static class IndexPrivileges { this.explicitlyRequiredIndexActions = explicitlyRequiredIndexActions; } + /** * Checks whether this instance provides privileges for the combination of the provided action, * the provided indices and the provided roles. @@ -445,7 +343,8 @@ static class IndexPrivileges { * As a side-effect, this method will further mark the available index/action combinations in the provided * checkTable instance as checked. */ - PrivilegesEvaluatorResponse providesPrivilege( + @Override + protected PrivilegesEvaluatorResponse providesPrivilege( PrivilegesEvaluationContext context, Set actions, IndexResolverReplacer.Resolved resolvedIndices, @@ -454,85 +353,34 @@ PrivilegesEvaluatorResponse providesPrivilege( ) { List exceptions = new ArrayList<>(); - for (String action : actions) { - IndexPattern indexPattern = actionToIndexPattern.get(action); - - if (indexPattern != null) { - for (String index : checkTable.iterateUncheckedRows(action)) { - try { - if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { - return PrivilegesEvaluatorResponse.ok(); - } - } 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)); - } - } - } + checkPrivilegeWithIndexPatternOnWellKnownActions(context, actions, checkTable, indexMetadata, actionToIndexPattern, exceptions); + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(); } // 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 - boolean allActionsWellKnown = this.wellKnownIndexActions.containsAll(actions); - - if (!checkTable.isComplete() && !allActionsWellKnown) { - - for (String action : actions) { - if (this.wellKnownIndexActions.contains(action)) { - continue; - } - - for (Map.Entry entry : actionPatternToIndexPattern.entrySet()) { - WildcardMatcher actionMatcher = entry.getKey(); - IndexPattern indexPattern = entry.getValue(); - - if (actionMatcher.test(action)) { - for (String index : checkTable.iterateUncheckedRows(action)) { - try { - if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { - break; - } - } 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", this, e); - exceptions.add(new PrivilegesEvaluationException("Error while evaluating " + this, e)); - } - } - } - } + if (!checkTable.isComplete() && !allWellKnownIndexActions(actions)) { + checkPrivilegesForNonWellKnownActions(context, actions, checkTable, indexMetadata, this.actionPatternToIndexPattern, exceptions); + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(); } - } - if (checkTable.isComplete()) { - return PrivilegesEvaluatorResponse.ok(); - } - - Set availableIndices = checkTable.getCompleteRows(); + return responseForIncompletePrivileges(context, checkTable, exceptions); + } - if (!availableIndices.isEmpty()) { - return PrivilegesEvaluatorResponse.partiallyOk(availableIndices, checkTable).evaluationExceptions(exceptions); - } - return PrivilegesEvaluatorResponse.insufficient(checkTable) - .reason( - resolvedIndices.getAllIndices().size() == 1 - ? "Insufficient permissions for the referenced index" - : "None of " + resolvedIndices.getAllIndices().size() + " referenced indices has sufficient permissions" - ) - .evaluationExceptions(exceptions); - } /** * 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 * the user's privileges. */ - PrivilegesEvaluatorResponse providesWildcardPrivilege(PrivilegesEvaluationContext context, Set actions) { - + @Override + protected PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownActions(PrivilegesEvaluationContext context, Set actions) { for (String action : actions) { if (!this.actionsWithWildcardIndexPrivileges.contains(action)) { return null; @@ -549,13 +397,8 @@ PrivilegesEvaluatorResponse providesWildcardPrivilege(PrivilegesEvaluationContex * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 */ - PrivilegesEvaluatorResponse providesExplicitPrivilege( - PrivilegesEvaluationContext context, - Set actions, - IndexResolverReplacer.Resolved resolvedIndices, - CheckTable checkTable, - Map indexMetadata - ) { + @Override + protected PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContext context, Set actions, CheckTable checkTable, Map indexMetadata) { List exceptions = new ArrayList<>(); if (!CollectionUtils.containsAny(actions, this.explicitlyRequiredIndexActions)) { diff --git a/src/main/java/org/opensearch/security/privileges/WellKnownActions.java b/src/main/java/org/opensearch/security/privileges/actionlevel/WellKnownActions.java similarity index 88% rename from src/main/java/org/opensearch/security/privileges/WellKnownActions.java rename to src/main/java/org/opensearch/security/privileges/actionlevel/WellKnownActions.java index af4f0bb025..777cabed69 100644 --- a/src/main/java/org/opensearch/security/privileges/WellKnownActions.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/WellKnownActions.java @@ -8,7 +8,7 @@ * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ -package org.opensearch.security.privileges; +package org.opensearch.security.privileges.actionlevel; import com.google.common.collect.ImmutableSet; @@ -41,6 +41,8 @@ import org.opensearch.index.reindex.UpdateByQueryAction; import org.opensearch.security.support.ConfigConstants; +import java.util.Collection; + /** * This class lists so-called "well-known actions". These are taken into account when creating the pre-computed * data structures of the ActionPrivileges class. Thus, a very fast performance evaluation will be possible for @@ -85,4 +87,18 @@ public class WellKnownActions { * Compare https://github.com/opensearch-project/security/pull/2887 */ public static final ImmutableSet EXPLICITLY_REQUIRED_INDEX_ACTIONS = ImmutableSet.of(ConfigConstants.SYSTEM_INDEX_PERMISSION); + + public static boolean isWellKnownClusterAction(String action) { + return CLUSTER_ACTIONS.contains(action); + } + + public static boolean isWellKnownIndexAction(String action) { + return INDEX_ACTIONS.contains(action); + } + + public static boolean allWellKnownIndexActions(Collection actions) { + return actions.stream().allMatch(WellKnownActions::isWellKnownIndexAction); + } + + } diff --git a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java index 934053b929..01ee885fcc 100644 --- a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java @@ -33,6 +33,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.securityconf.FlattenedActionGroups; From 7f0a05c5d82ec54d67fd97e24099198ebe01fc24 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 3 Jun 2025 13:31:52 +0200 Subject: [PATCH 03/10] Extracted ActionPrivileges interface and created RoleBasedActionPrivileges implementation and SubjectBasedActionPrivileges implementation Signed-off-by: Nils Bandener --- .../RestEndpointPermissionTests.java | 2 +- .../RoleBasedActionPrivilegesTest.java | 325 ++++---- .../SubjectBasedActionPrivilegesTest.java | 725 ++++-------------- ...MockPrivilegeEvaluationContextBuilder.java | 84 ++ .../privileges/PrivilegesEvaluator.java | 12 +- .../RoleBasedActionPrivileges.java | 62 +- .../RuntimeOptimizedActionPrivileges.java | 115 +-- .../SubjectBasedActionPrivileges.java | 50 +- .../actionlevel/WellKnownActions.java | 7 +- .../security/securityconf/impl/v7/RoleV7.java | 26 + 10 files changed, 572 insertions(+), 836 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java diff --git a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java index 684f1d152b..33ec6a918c 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java @@ -117,7 +117,7 @@ static String[] allRestApiPermissions() { final RoleBasedActionPrivileges actionPrivileges; public RestEndpointPermissionTests() throws IOException { - this.actionPrivileges = new RoleBasedActionPrivileges(createRolesConfig(), FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + this.actionPrivileges = new RoleBasedActionPrivileges(createRolesConfig(), FlattenedActionGroups.EMPTY, 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 7773cd9043..2f17f0f283 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java @@ -13,7 +13,6 @@ 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; @@ -31,11 +30,11 @@ import org.junit.runners.Suite; import org.opensearch.action.support.IndicesOptions; +import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexMetadata; -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.core.common.unit.ByteSizeUnit; import org.opensearch.core.common.unit.ByteSizeValue; import org.opensearch.security.privileges.PrivilegesEvaluationContext; @@ -55,6 +54,7 @@ import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.missingPrivileges; import static org.opensearch.security.util.MockIndexMetadataBuilder.dataStreams; import static org.opensearch.security.util.MockIndexMetadataBuilder.indices; +import static org.opensearch.security.util.MockPrivilegeEvaluationContextBuilder.ctx; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -81,15 +81,15 @@ public void wellKnown() throws Exception { " cluster_permissions:\n" + // " - cluster:monitor/nodes/stats*", CType.ROLES); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); - assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/stats"), isAllowed()); + assertThat(subject.hasClusterPrivilege(ctx().roles("test_role").get(), "cluster:monitor/nodes/stats"), isAllowed()); assertThat( - subject.hasClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/stats"), + subject.hasClusterPrivilege(ctx().roles("other_role").get(), "cluster:monitor/nodes/stats"), isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) ); assertThat( - subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/other"), + subject.hasClusterPrivilege(ctx().roles("test_role").get(), "cluster:monitor/nodes/other"), isForbidden(missingPrivileges("cluster:monitor/nodes/other")) ); } @@ -100,15 +100,18 @@ public void notWellKnown() throws Exception { " cluster_permissions:\n" + // " - cluster:monitor/nodes/stats*", CType.ROLES); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); - assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/stats/somethingnotwellknown"), isAllowed()); assertThat( - subject.hasClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/stats/somethingnotwellknown"), + subject.hasClusterPrivilege(ctx().roles("test_role").get(), "cluster:monitor/nodes/stats/somethingnotwellknown"), + isAllowed() + ); + assertThat( + subject.hasClusterPrivilege(ctx().roles("other_role").get(), "cluster:monitor/nodes/stats/somethingnotwellknown"), isForbidden(missingPrivileges("cluster:monitor/nodes/stats/somethingnotwellknown")) ); assertThat( - subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/something/else"), + subject.hasClusterPrivilege(ctx().roles("test_role").get(), "cluster:monitor/nodes/something/else"), isForbidden(missingPrivileges("cluster:monitor/nodes/something/else")) ); } @@ -119,11 +122,11 @@ public void wildcard() throws Exception { " cluster_permissions:\n" + // " - '*'", CType.ROLES); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); - assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:whatever"), isAllowed()); + assertThat(subject.hasClusterPrivilege(ctx().roles("test_role").get(), "cluster:whatever"), isAllowed()); assertThat( - subject.hasClusterPrivilege(ctx("other_role"), "cluster:whatever"), + subject.hasClusterPrivilege(ctx().roles("other_role").get(), "cluster:whatever"), isForbidden(missingPrivileges("cluster:whatever")) ); } @@ -142,16 +145,19 @@ public void explicit_wellKnown() throws Exception { CType.ROLES ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); - assertThat(subject.hasExplicitClusterPrivilege(ctx("explicit_role"), "cluster:monitor/nodes/stats"), isAllowed()); - assertThat(subject.hasExplicitClusterPrivilege(ctx("semi_explicit_role"), "cluster:monitor/nodes/stats"), isAllowed()); + assertThat(subject.hasExplicitClusterPrivilege(ctx().roles("explicit_role").get(), "cluster:monitor/nodes/stats"), isAllowed()); assertThat( - subject.hasExplicitClusterPrivilege(ctx("non_explicit_role"), "cluster:monitor/nodes/stats"), + subject.hasExplicitClusterPrivilege(ctx().roles("semi_explicit_role").get(), "cluster:monitor/nodes/stats"), + isAllowed() + ); + assertThat( + subject.hasExplicitClusterPrivilege(ctx().roles("non_explicit_role").get(), "cluster:monitor/nodes/stats"), isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) ); assertThat( - subject.hasExplicitClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/stats"), + subject.hasExplicitClusterPrivilege(ctx().roles("other_role").get(), "cluster:monitor/nodes/stats"), isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) ); } @@ -170,16 +176,22 @@ public void explicit_notWellKnown() throws Exception { CType.ROLES ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); - assertThat(subject.hasExplicitClusterPrivilege(ctx("explicit_role"), "cluster:monitor/nodes/notwellknown"), isAllowed()); - assertThat(subject.hasExplicitClusterPrivilege(ctx("semi_explicit_role"), "cluster:monitor/nodes/notwellknown"), isAllowed()); assertThat( - subject.hasExplicitClusterPrivilege(ctx("non_explicit_role"), "cluster:monitor/nodes/notwellknown"), + subject.hasExplicitClusterPrivilege(ctx().roles("explicit_role").get(), "cluster:monitor/nodes/notwellknown"), + isAllowed() + ); + assertThat( + subject.hasExplicitClusterPrivilege(ctx().roles("semi_explicit_role").get(), "cluster:monitor/nodes/notwellknown"), + isAllowed() + ); + assertThat( + subject.hasExplicitClusterPrivilege(ctx().roles("non_explicit_role").get(), "cluster:monitor/nodes/notwellknown"), isForbidden(missingPrivileges("cluster:monitor/nodes/notwellknown")) ); assertThat( - subject.hasExplicitClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/notwellknown"), + subject.hasExplicitClusterPrivilege(ctx().roles("other_role").get(), "cluster:monitor/nodes/notwellknown"), isForbidden(missingPrivileges("cluster:monitor/nodes/notwellknown")) ); } @@ -190,23 +202,26 @@ public void hasAny_wellKnown() throws Exception { " cluster_permissions:\n" + // " - cluster:monitor/nodes/stats*", CType.ROLES); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); - assertThat(subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/nodes/stats")), isAllowed()); + assertThat( + subject.hasAnyClusterPrivilege(ctx().roles("test_role").get(), ImmutableSet.of("cluster:monitor/nodes/stats")), + isAllowed() + ); assertThat( subject.hasAnyClusterPrivilege( - ctx("test_role"), + ctx().roles("test_role").get(), ImmutableSet.of("cluster:monitor/nodes/foo", "cluster:monitor/nodes/stats") ), isAllowed() ); assertThat( - subject.hasAnyClusterPrivilege(ctx("other_role"), ImmutableSet.of("cluster:monitor/nodes/stats")), + subject.hasAnyClusterPrivilege(ctx().roles("other_role").get(), ImmutableSet.of("cluster:monitor/nodes/stats")), isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) ); assertThat( - subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/nodes/other")), + subject.hasAnyClusterPrivilege(ctx().roles("test_role").get(), ImmutableSet.of("cluster:monitor/nodes/other")), isForbidden(missingPrivileges("cluster:monitor/nodes/other")) ); } @@ -217,30 +232,33 @@ public void hasAny_notWellKnown() throws Exception { " cluster_permissions:\n" + // " - cluster:monitor/nodes/*", CType.ROLES); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); assertThat( - subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/nodes/notwellknown")), + subject.hasAnyClusterPrivilege(ctx().roles("test_role").get(), ImmutableSet.of("cluster:monitor/nodes/notwellknown")), isAllowed() ); assertThat( subject.hasAnyClusterPrivilege( - ctx("test_role"), + ctx().roles("test_role").get(), ImmutableSet.of("cluster:monitor/other", "cluster:monitor/nodes/notwellknown") ), isAllowed() ); assertThat( - subject.hasAnyClusterPrivilege(ctx("other_role"), ImmutableSet.of("cluster:monitor/nodes/notwellknown")), + subject.hasAnyClusterPrivilege(ctx().roles("other_role").get(), ImmutableSet.of("cluster:monitor/nodes/notwellknown")), isForbidden(missingPrivileges("cluster:monitor/nodes/notwellknown")) ); assertThat( - subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/other")), + subject.hasAnyClusterPrivilege(ctx().roles("test_role").get(), ImmutableSet.of("cluster:monitor/other")), isForbidden(missingPrivileges("cluster:monitor/other")) ); assertThat( - subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/other", "cluster:monitor/yetanother")), + subject.hasAnyClusterPrivilege( + ctx().roles("test_role").get(), + ImmutableSet.of("cluster:monitor/other", "cluster:monitor/yetanother") + ), isForbidden() ); } @@ -251,15 +269,16 @@ public void hasAny_wildcard() throws Exception { " cluster_permissions:\n" + // " - '*'", CType.ROLES); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); - assertThat(subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:whatever")), isAllowed()); + assertThat(subject.hasAnyClusterPrivilege(ctx().roles("test_role").get(), ImmutableSet.of("cluster:whatever")), isAllowed()); assertThat( - subject.hasAnyClusterPrivilege(ctx("other_role"), ImmutableSet.of("cluster:whatever")), + subject.hasAnyClusterPrivilege(ctx().roles("other_role").get(), ImmutableSet.of("cluster:whatever")), isForbidden(missingPrivileges("cluster:whatever")) ); } + } /** @@ -290,13 +309,17 @@ public static class IndicesAndAliases { @Test public void positive_full() throws Exception { - PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx("test_role"), requiredActions, resolved("index_a11")); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( + ctx().roles("test_role").attr("attrs.dept_no", "a11").indexMetadata(INDEX_METADATA).get(), + requiredActions, + resolved("index_a11") + ); assertThat(result, isAllowed()); } @Test public void positive_partial() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluationContext ctx = ctx().roles("test_role").indexMetadata(INDEX_METADATA).get(); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("index_a11", "index_a12")); if (covers(ctx, "index_a11", "index_a12")) { @@ -310,7 +333,7 @@ public void positive_partial() throws Exception { @Test public void positive_partial2() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluationContext ctx = ctx().roles("test_role").indexMetadata(INDEX_METADATA).get(); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( ctx, requiredActions, @@ -337,20 +360,24 @@ public void positive_noLocal() throws Exception { ImmutableSet.of("remote:a"), IndicesOptions.LENIENT_EXPAND_OPEN ); - PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx("test_role"), requiredActions, resolved); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( + ctx().roles("test_role").indexMetadata(INDEX_METADATA).get(), + requiredActions, + resolved + ); assertThat(result, isAllowed()); } @Test public void negative_wrongRole() throws Exception { - PrivilegesEvaluationContext ctx = ctx("other_role"); + PrivilegesEvaluationContext ctx = ctx().roles("other_role").indexMetadata(INDEX_METADATA).get(); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("index_a11")); assertThat(result, isForbidden(missingPrivileges(requiredActions))); } @Test public void negative_wrongAction() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluationContext ctx = ctx().roles("test_role").attr("attrs.dept_no", "a11").indexMetadata(INDEX_METADATA).get(); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, otherActions, resolved("index_a11")); if (actionSpec.givenPrivs.contains("*")) { @@ -360,23 +387,6 @@ public void negative_wrongAction() throws Exception { } } - @Test - public void positive_hasExplicit_full() { - PrivilegesEvaluationContext ctx = ctx("test_role"); - PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege(ctx, requiredActions, resolved("index_a11")); - - if (actionSpec.givenPrivs.contains("*")) { - // The * is forbidden for explicit privileges - assertThat(result, isForbidden(missingPrivileges(requiredActions))); - } else if (!requiredActions.contains("indices:data/read/search")) { - // For test purposes, we have designated "indices:data/read/search" as an action requiring explicit privileges - // Other actions are not covered here - assertThat(result, isForbidden(missingPrivileges(requiredActions))); - } else { - assertThat(result, isAllowed()); - } - } - private boolean covers(PrivilegesEvaluationContext ctx, String... indices) { for (String index : indices) { if (!indexSpec.covers(ctx.getUser(), index)) { @@ -441,7 +451,7 @@ public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec, Statefulnes this.otherActions = actionSpec.wellKnownActions ? ImmutableSet.of("indices:data/write/update") : ImmutableSet.of("indices:foobar/unknown"); - this.indexSpec.indexMetadata = INDEX_METADATA; + this.indexSpec.indexMetadata = INDEX_METADATA.getIndicesLookup(); Settings settings = Settings.EMPTY; if (statefulness == Statefulness.STATEFUL_LIMITED) { @@ -453,19 +463,14 @@ public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec, Statefulnes .build(); } - this.subject = new RoleBasedActionPrivileges( - roles, - FlattenedActionGroups.EMPTY, - () -> INDEX_METADATA, - settings - ); + this.subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, settings); if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { - this.subject.updateStatefulIndexPrivileges(INDEX_METADATA, 1); + this.subject.updateStatefulIndexPrivileges(INDEX_METADATA.getIndicesLookup(), 1); } } - final static Map INDEX_METADATA = // + final static Metadata INDEX_METADATA = // indices("index_a11", "index_a12", "index_a21", "index_a22", "index_b1", "index_b2")// .alias("alias_a") .of("index_a11", "index_a12", "index_a21", "index_a22")// @@ -475,8 +480,7 @@ public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec, Statefulnes .of("index_a21", "index_a22")// .alias("alias_b") .of("index_b1", "index_b2")// - .build() - .getIndicesLookup(); + .build(); static IndexResolverReplacer.Resolved resolved(String... indices) { return new IndexResolverReplacer.Resolved( @@ -487,6 +491,7 @@ static IndexResolverReplacer.Resolved resolved(String... indices) { IndicesOptions.LENIENT_EXPAND_OPEN ); } + } @RunWith(Parameterized.class) @@ -501,7 +506,7 @@ public static class DataStreams { @Test public void positive_full() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluationContext ctx = ctx().roles("test_role").indexMetadata(INDEX_METADATA).get(); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); if (covers(ctx, "data_stream_a11")) { assertThat(result, isAllowed()); @@ -517,7 +522,7 @@ public void positive_full() throws Exception { @Test public void positive_partial() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluationContext ctx = ctx().roles("test_role").indexMetadata(INDEX_METADATA).get(); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( ctx, requiredActions, @@ -548,14 +553,14 @@ public void positive_partial() throws Exception { @Test public void negative_wrongRole() throws Exception { - PrivilegesEvaluationContext ctx = ctx("other_role"); + PrivilegesEvaluationContext ctx = ctx().roles("other_role").indexMetadata(INDEX_METADATA).get(); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); assertThat(result, isForbidden(missingPrivileges(requiredActions))); } @Test public void negative_wrongAction() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluationContext ctx = ctx().roles("test_role").indexMetadata(INDEX_METADATA).get(); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, otherActions, resolved("data_stream_a11")); assertThat(result, isForbidden(missingPrivileges(otherActions))); } @@ -621,7 +626,7 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness stat this.otherActions = actionSpec.wellKnownActions ? ImmutableSet.of("indices:data/write/update") : ImmutableSet.of("indices:foobar/unknown"); - this.indexSpec.indexMetadata = INDEX_METADATA; + this.indexSpec.indexMetadata = INDEX_METADATA.getIndicesLookup(); Settings settings = Settings.EMPTY; if (statefulness == Statefulness.STATEFUL_LIMITED) { @@ -633,23 +638,28 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness stat .build(); } - this.subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, () -> INDEX_METADATA, settings); + this.subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, settings); if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { - this.subject.updateStatefulIndexPrivileges(INDEX_METADATA, 1); + this.subject.updateStatefulIndexPrivileges(INDEX_METADATA.getIndicesLookup(), 1); } } - final static Map INDEX_METADATA = // + final static Metadata INDEX_METADATA = // dataStreams("data_stream_a11", "data_stream_a12", "data_stream_a21", "data_stream_a22", "data_stream_b1", "data_stream_b2") - .build() - .getIndicesLookup(); + .build(); + + /** + * A mock cluster state; this transports the INDEX_METADATA via PrivilegeEvaluationContext to the + * actual privileges evaluation implementation. + */ + final static ClusterState CLUSTER_STATE = ClusterState.builder(ClusterState.EMPTY_STATE).metadata(INDEX_METADATA).build(); static IndexResolverReplacer.Resolved resolved(String... indices) { ImmutableSet.Builder allIndices = ImmutableSet.builder(); for (String index : indices) { - IndexAbstraction indexAbstraction = INDEX_METADATA.get(index); + IndexAbstraction indexAbstraction = INDEX_METADATA.getIndicesLookup().get(index); if (indexAbstraction instanceof IndexAbstraction.DataStream) { allIndices.addAll( @@ -863,15 +873,10 @@ public void hasIndexPrivilege_errors() throws Exception { CType.ROLES ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( - roles, - FlattenedActionGroups.EMPTY, - () -> Collections.emptyMap(), - Settings.EMPTY - ); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( - ctx("role_with_errors"), + ctx().roles("role_with_errors").get(), Set.of("indices:some_action", "indices:data/write/index"), IndexResolverReplacer.Resolved.ofIndex("any_index") ); @@ -880,10 +885,87 @@ 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 role role_with_errors") + .startsWith("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating") ); } + @Test + public void hasExplicitIndexPrivilege_positive() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( + "test_role:\n" + + " index_permissions:\n" + + " - index_patterns: ['test_index']\n" + + " allowed_actions: ['system:admin/system_index']", + CType.ROLES + ); + + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + + PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( + ctx().roles("test_role").get(), + Set.of("system:admin/system_index"), + IndexResolverReplacer.Resolved.ofIndex("test_index") + ); + assertThat(result, isAllowed()); + } + + @Test + public void hasExplicitIndexPrivilege_positive_wildcard() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( + "test_role:\n" + + " index_permissions:\n" + + " - index_patterns: ['test_index']\n" + + " allowed_actions: ['system:admin/system_index*']", + CType.ROLES + ); + + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + + PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( + ctx().roles("test_role").get(), + Set.of("system:admin/system_index"), + IndexResolverReplacer.Resolved.ofIndex("test_index") + ); + assertThat(result, isAllowed()); + } + + @Test + public void hasExplicitIndexPrivilege_noWildcard() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( + "test_role:\n" + " index_permissions:\n" + " - index_patterns: ['test_index']\n" + " allowed_actions: ['*']", + CType.ROLES + ); + + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + + PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( + ctx().roles("test_role").get(), + Set.of("system:admin/system_index"), + IndexResolverReplacer.Resolved.ofIndex("test_index") + ); + assertThat(result, isForbidden()); + } + + @Test + public void hasExplicitIndexPrivilege_negative_wrongAction() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( + "test_role:\n" + + " index_permissions:\n" + + " - index_patterns: ['test_index']\n" + + " allowed_actions: ['system:admin/system*']", + CType.ROLES + ); + + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + + PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( + ctx().roles("test_role").get(), + Set.of("system:admin/system_foo"), + IndexResolverReplacer.Resolved.ofIndex("test_index") + ); + assertThat(result, isForbidden()); + } + @Test public void hasExplicitIndexPrivilege_errors() throws Exception { SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( @@ -894,15 +976,10 @@ public void hasExplicitIndexPrivilege_errors() throws Exception { CType.ROLES ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( - roles, - FlattenedActionGroups.EMPTY, - () -> Collections.emptyMap(), - Settings.EMPTY - ); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( - ctx("role_with_errors"), + ctx().roles("role_with_errors").get(), Set.of("system:admin/system_index"), IndexResolverReplacer.Resolved.ofIndex("any_index") ); @@ -920,7 +997,7 @@ public void aliasesOnDataStreamBackingIndices() throws Exception { // We create a meta data object with a data stream ds_a. Implicitly, the utility method will create // the backing indices ".ds-ds_a-000001", ".ds-ds_a-000002" and ".ds-ds_a-000003". // Additionally, we create an alias which only contains ".ds-ds_a-000001", but not the other backing indices. - Map metadata = dataStreams("ds_a").alias("alias_a").of(".ds-ds_a-000001").build().getIndicesLookup(); + Metadata metadata = dataStreams("ds_a").alias("alias_a").of(".ds-ds_a-000001").build(); SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( "role:\n" + " index_permissions:\n" @@ -928,23 +1005,18 @@ public void aliasesOnDataStreamBackingIndices() throws Exception { + " allowed_actions: ['indices:data/write/index']", CType.ROLES ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( - roles, - FlattenedActionGroups.EMPTY, - () -> metadata, - Settings.EMPTY - ); - subject.updateStatefulIndexPrivileges(metadata, 2); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + subject.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), 2); PrivilegesEvaluatorResponse resultForIndexCoveredByAlias = subject.hasIndexPrivilege( - ctx("role"), + ctx().roles("role").indexMetadata(metadata).get(), Set.of("indices:data/write/index"), IndexResolverReplacer.Resolved.ofIndex(".ds-ds_a-000001") ); assertThat(resultForIndexCoveredByAlias, isAllowed()); PrivilegesEvaluatorResponse resultForIndexNotCoveredByAlias = subject.hasIndexPrivilege( - ctx("role"), + ctx().roles("role").indexMetadata(metadata).get(), Set.of("indices:data/write/index"), IndexResolverReplacer.Resolved.ofIndex(".ds-ds_a-000002") ); @@ -964,12 +1036,7 @@ public static class StatefulIndexPrivilegesHeapSize { @Test public void estimatedSize() throws Exception { - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( - roles, - FlattenedActionGroups.EMPTY, - () -> indices, - Settings.EMPTY - ); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); subject.updateStatefulIndexPrivileges(indices, 1); @@ -1064,34 +1131,4 @@ static SecurityDynamicConfiguration createRoles(int numberOfRoles, int n } } } - - static PrivilegesEvaluationContext ctx(String... roles) { - User user = new User("test_user").withAttributes(ImmutableMap.of("attrs.dept_no", "a11")); - return new PrivilegesEvaluationContext( - user, - ImmutableSet.copyOf(roles), - null, - null, - null, - null, - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - null, - null - ); - } - - static PrivilegesEvaluationContext ctxByUsername(String username) { - User user = new User(username).withAttributes(ImmutableMap.of("attrs.dept_no", "a11")); - return new PrivilegesEvaluationContext( - user, - ImmutableSet.of(), - null, - null, - null, - null, - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - null, - null - ); - } } 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 ff8515701d..30ed2dadfe 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java @@ -10,22 +10,26 @@ */ package org.opensearch.security.privileges.actionlevel; -import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Suite; + import org.opensearch.action.support.IndicesOptions; import org.opensearch.cluster.metadata.IndexAbstraction; -import org.opensearch.cluster.metadata.IndexMetadata; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.core.common.unit.ByteSizeUnit; -import org.opensearch.core.common.unit.ByteSizeValue; +import org.opensearch.cluster.metadata.Metadata; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.resolver.IndexResolverReplacer; @@ -34,33 +38,18 @@ import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.user.User; -import org.opensearch.security.util.MockIndexMetadataBuilder; - -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; -import java.util.Random; -import java.util.Set; -import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isAllowed; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isForbidden; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isPartiallyOk; import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.missingPrivileges; import static org.opensearch.security.util.MockIndexMetadataBuilder.dataStreams; import static org.opensearch.security.util.MockIndexMetadataBuilder.indices; +import static org.opensearch.security.util.MockPrivilegeEvaluationContextBuilder.ctx; /** - * Unit tests for SubjectBasedActionPrivileges. As the SubjectBasedActionPrivileges provides quite a few different code paths for checking + * Unit tests for SubjectBasedActionPrivilegesTest. As the ActionPrivileges provides quite a few different code paths for checking * privileges with different performance characteristics, this test suite defines different test cases for making sure * all these code paths are tested. So, all functionality must be tested for "well-known" actions and non-well-known * actions. For index privileges, there are a couple of more tests dimensions. See below. @@ -70,232 +59,137 @@ SubjectBasedActionPrivilegesTest.ClusterPrivileges.class, SubjectBasedActionPrivilegesTest.IndexPrivileges.IndicesAndAliases.class, SubjectBasedActionPrivilegesTest.IndexPrivileges.DataStreams.class, - SubjectBasedActionPrivilegesTest.Misc.class, - SubjectBasedActionPrivilegesTest.StatefulIndexPrivilegesHeapSize.class }) + SubjectBasedActionPrivilegesTest.Misc.class }) public class SubjectBasedActionPrivilegesTest { public static class ClusterPrivileges { @Test public void wellKnown() throws Exception { - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // - " cluster_permissions:\n" + // - " - cluster:monitor/nodes/stats*", CType.ROLES); - - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleV7 config = config(""" + cluster_permissions: + - cluster:monitor/nodes/stats* + """); - assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/stats"), isAllowed()); - assertThat( - subject.hasClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/stats"), - isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) - ); - assertThat( - subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/other"), - isForbidden(missingPrivileges("cluster:monitor/nodes/other")) - ); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), isAllowed()); } @Test public void notWellKnown() throws Exception { - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // - " cluster_permissions:\n" + // - " - cluster:monitor/nodes/stats*", CType.ROLES); + RoleV7 config = config(""" + cluster_permissions: + - cluster:monitor/nodes/stats* + """); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats/somethingnotwellknown"), isAllowed()); + } - assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/stats/somethingnotwellknown"), isAllowed()); - assertThat( - subject.hasClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/stats/somethingnotwellknown"), - isForbidden(missingPrivileges("cluster:monitor/nodes/stats/somethingnotwellknown")) - ); - assertThat( - subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/something/else"), - isForbidden(missingPrivileges("cluster:monitor/nodes/something/else")) - ); + @Test + public void negative() throws Exception { + RoleV7 config = config(""" + cluster_permissions: + - cluster:monitor/nodes/stats* + """); + + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:monitor/nodes/foo"), isForbidden()); } @Test public void wildcard() throws Exception { - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // - " cluster_permissions:\n" + // - " - '*'", CType.ROLES); - - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleV7 config = config(""" + cluster_permissions: + - '*' + """); - assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:whatever"), isAllowed()); - assertThat( - subject.hasClusterPrivilege(ctx("other_role"), "cluster:whatever"), - isForbidden(missingPrivileges("cluster:whatever")) - ); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:whatever"), isAllowed()); } @Test public void explicit_wellKnown() throws Exception { - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("non_explicit_role:\n" + // - " cluster_permissions:\n" + // - " - '*'\n" + // - "explicit_role:\n" + // - " cluster_permissions:\n" + // - " - cluster:monitor/nodes/stats\n" + // - "semi_explicit_role:\n" + // - " cluster_permissions:\n" + // - " - cluster:monitor/nodes/stats*\n", // - CType.ROLES - ); - - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleV7 config = config(""" + cluster_permissions: + - cluster:monitor/nodes/stats + """); - assertThat(subject.hasExplicitClusterPrivilege(ctx("explicit_role"), "cluster:monitor/nodes/stats"), isAllowed()); - assertThat(subject.hasExplicitClusterPrivilege(ctx("semi_explicit_role"), "cluster:monitor/nodes/stats"), isAllowed()); - assertThat( - subject.hasExplicitClusterPrivilege(ctx("non_explicit_role"), "cluster:monitor/nodes/stats"), - isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) - ); - assertThat( - subject.hasExplicitClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/stats"), - isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) - ); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + assertThat(subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), isAllowed()); } @Test public void explicit_notWellKnown() throws Exception { - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("non_explicit_role:\n" + // - " cluster_permissions:\n" + // - " - '*'\n" + // - "explicit_role:\n" + // - " cluster_permissions:\n" + // - " - cluster:monitor/nodes/notwellknown\n" + // - "semi_explicit_role:\n" + // - " cluster_permissions:\n" + // - " - cluster:monitor/nodes/*\n", // - CType.ROLES - ); - - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleV7 config = config(""" + cluster_permissions: + - cluster:monitor/nodes/* + """); - assertThat(subject.hasExplicitClusterPrivilege(ctx("explicit_role"), "cluster:monitor/nodes/notwellknown"), isAllowed()); - assertThat(subject.hasExplicitClusterPrivilege(ctx("semi_explicit_role"), "cluster:monitor/nodes/notwellknown"), isAllowed()); - assertThat( - subject.hasExplicitClusterPrivilege(ctx("non_explicit_role"), "cluster:monitor/nodes/notwellknown"), - isForbidden(missingPrivileges("cluster:monitor/nodes/notwellknown")) - ); - assertThat( - subject.hasExplicitClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/notwellknown"), - isForbidden(missingPrivileges("cluster:monitor/nodes/notwellknown")) - ); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + assertThat(subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/notwellknown"), isAllowed()); } @Test - public void hasAny_wellKnown() throws Exception { - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // - " cluster_permissions:\n" + // - " - cluster:monitor/nodes/stats*", CType.ROLES); - - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); - - assertThat(subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/nodes/stats")), isAllowed()); - assertThat( - subject.hasAnyClusterPrivilege( - ctx("test_role"), - ImmutableSet.of("cluster:monitor/nodes/foo", "cluster:monitor/nodes/stats") - ), - isAllowed() - ); + public void explicit_notExplicit() throws Exception { + RoleV7 config = config(""" + cluster_permissions: + - '*' + """); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); assertThat( - subject.hasAnyClusterPrivilege(ctx("other_role"), ImmutableSet.of("cluster:monitor/nodes/stats")), + subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) ); - assertThat( - subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/nodes/other")), - isForbidden(missingPrivileges("cluster:monitor/nodes/other")) - ); } @Test - public void hasAny_notWellKnown() throws Exception { - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // - " cluster_permissions:\n" + // - " - cluster:monitor/nodes/*", CType.ROLES); - - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); - - assertThat( - subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/nodes/notwellknown")), - isAllowed() - ); - assertThat( - subject.hasAnyClusterPrivilege( - ctx("test_role"), - ImmutableSet.of("cluster:monitor/other", "cluster:monitor/nodes/notwellknown") - ), - isAllowed() - ); + public void hasAny_wellKnown() throws Exception { + RoleV7 config = config(""" + cluster_permissions: + - cluster:monitor/nodes/stats* + """); - assertThat( - subject.hasAnyClusterPrivilege(ctx("other_role"), ImmutableSet.of("cluster:monitor/nodes/notwellknown")), - isForbidden(missingPrivileges("cluster:monitor/nodes/notwellknown")) - ); - assertThat( - subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/other")), - isForbidden(missingPrivileges("cluster:monitor/other")) - ); - assertThat( - subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/other", "cluster:monitor/yetanother")), - isForbidden() - ); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + assertThat(subject.hasAnyClusterPrivilege(ctx().get(), ImmutableSet.of("cluster:monitor/nodes/stats")), isAllowed()); } @Test public void hasAny_wildcard() throws Exception { - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // - " cluster_permissions:\n" + // - " - '*'", CType.ROLES); - - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + RoleV7 config = config(""" + cluster_permissions: + - '*' + """); - assertThat(subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:whatever")), isAllowed()); - - assertThat( - subject.hasAnyClusterPrivilege(ctx("other_role"), ImmutableSet.of("cluster:whatever")), - isForbidden(missingPrivileges("cluster:whatever")) - ); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + assertThat(subject.hasAnyClusterPrivilege(ctx().get(), ImmutableSet.of("cluster:monitor/nodes/stats")), isAllowed()); } } - /** - * Tests for index privileges. This class contains two parameterized test suites, first for indices and aliases, - * second for data streams. - *

- * Both test suites use parameters to create a 3-dimensional test case space to make sure all code paths are covered. - *

- * The dimensions are (see also the params() methods): - *

    - *
  1. 1. roles.yml; index patterns: Different usages of patterns, wildcards and constant names. - *
  2. 2. roles.yml; action patterns: Well known actions vs non-well known actions combined with use of patterns vs use of constant action names - *
  3. 3. Statefulness: Shall the data structures from ActionPrivileges.StatefulIndexPrivileges be used or not - *
- * As so many different situations need to be tested, the test oracle method covers() is used to verify the results. - */ public static class IndexPrivileges { @RunWith(Parameterized.class) public static class IndicesAndAliases { final ActionSpec actionSpec; final IndexSpec indexSpec; - final SecurityDynamicConfiguration roles; + final RoleV7 config; final String primaryAction; final ImmutableSet requiredActions; final ImmutableSet otherActions; - final RoleBasedActionPrivileges subject; + final SubjectBasedActionPrivileges subject; @Test public void positive_full() throws Exception { - PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx("test_role"), requiredActions, resolved("index_a11")); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( + ctx().attr("attrs.dept_no", "a11").indexMetadata(INDEX_METADATA).get(), + requiredActions, + resolved("index_a11") + ); assertThat(result, isAllowed()); } @Test public void positive_partial() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluationContext ctx = ctx().indexMetadata(INDEX_METADATA).get(); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("index_a11", "index_a12")); if (covers(ctx, "index_a11", "index_a12")) { @@ -309,7 +203,7 @@ public void positive_partial() throws Exception { @Test public void positive_partial2() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluationContext ctx = ctx().indexMetadata(INDEX_METADATA).get(); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( ctx, requiredActions, @@ -336,20 +230,17 @@ public void positive_noLocal() throws Exception { ImmutableSet.of("remote:a"), IndicesOptions.LENIENT_EXPAND_OPEN ); - PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx("test_role"), requiredActions, resolved); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( + ctx().indexMetadata(INDEX_METADATA).get(), + requiredActions, + resolved + ); assertThat(result, isAllowed()); } - @Test - public void negative_wrongRole() throws Exception { - PrivilegesEvaluationContext ctx = ctx("other_role"); - PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("index_a11")); - assertThat(result, isForbidden(missingPrivileges(requiredActions))); - } - @Test public void negative_wrongAction() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluationContext ctx = ctx().attr("attrs.dept_no", "a11").indexMetadata(INDEX_METADATA).get(); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, otherActions, resolved("index_a11")); if (actionSpec.givenPrivs.contains("*")) { @@ -359,23 +250,6 @@ public void negative_wrongAction() throws Exception { } } - @Test - public void positive_hasExplicit_full() { - PrivilegesEvaluationContext ctx = ctx("test_role"); - PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege(ctx, requiredActions, resolved("index_a11")); - - if (actionSpec.givenPrivs.contains("*")) { - // The * is forbidden for explicit privileges - assertThat(result, isForbidden(missingPrivileges(requiredActions))); - } else if (!requiredActions.contains("indices:data/read/search")) { - // For test purposes, we have designated "indices:data/read/search" as an action requiring explicit privileges - // Other actions are not covered here - assertThat(result, isForbidden(missingPrivileges(requiredActions))); - } else { - assertThat(result, isAllowed()); - } - } - private boolean covers(PrivilegesEvaluationContext ctx, String... indices) { for (String index : indices) { if (!indexSpec.covers(ctx.getUser(), index)) { @@ -385,7 +259,7 @@ private boolean covers(PrivilegesEvaluationContext ctx, String... indices) { return true; } - @Parameterized.Parameters(name = "{0}; actions: {1}; {2}") + @Parameterized.Parameters(name = "{0}; actions: {1}") public static Collection params() { List result = new ArrayList<>(); @@ -421,18 +295,16 @@ public static Collection params() { .requiredPrivs("indices:unknown/unwell", "indices:unknown/notatall")// )) { - for (Statefulness statefulness : Statefulness.values()) { - result.add(new Object[] { indexSpec, actionSpec, statefulness }); - } + result.add(new Object[] { indexSpec, actionSpec }); } } return result; } - public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness statefulness) throws Exception { + public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec) throws Exception { this.indexSpec = indexSpec; this.actionSpec = actionSpec; - this.roles = indexSpec.toRolesConfig(actionSpec); + this.config = indexSpec.toConfig(actionSpec); this.primaryAction = actionSpec.primaryAction; this.requiredActions = actionSpec.requiredPrivs; @@ -440,31 +312,12 @@ public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec, Statefulnes this.otherActions = actionSpec.wellKnownActions ? ImmutableSet.of("indices:data/write/update") : ImmutableSet.of("indices:foobar/unknown"); - this.indexSpec.indexMetadata = INDEX_METADATA; - - 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(); - } - - this.subject = new RoleBasedActionPrivileges( - roles, - FlattenedActionGroups.EMPTY, - () -> INDEX_METADATA, - settings - ); + this.indexSpec.indexMetadata = INDEX_METADATA.getIndicesLookup(); - if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { - this.subject.updateStatefulIndexPrivileges(INDEX_METADATA, 1); - } + this.subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); } - final static Map INDEX_METADATA = // + final static Metadata INDEX_METADATA = // indices("index_a11", "index_a12", "index_a21", "index_a22", "index_b1", "index_b2")// .alias("alias_a") .of("index_a11", "index_a12", "index_a21", "index_a22")// @@ -474,8 +327,7 @@ public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec, Statefulnes .of("index_a21", "index_a22")// .alias("alias_b") .of("index_b1", "index_b2")// - .build() - .getIndicesLookup(); + .build(); static IndexResolverReplacer.Resolved resolved(String... indices) { return new IndexResolverReplacer.Resolved( @@ -486,21 +338,22 @@ static IndexResolverReplacer.Resolved resolved(String... indices) { IndicesOptions.LENIENT_EXPAND_OPEN ); } + } @RunWith(Parameterized.class) public static class DataStreams { final ActionSpec actionSpec; final IndexSpec indexSpec; - final SecurityDynamicConfiguration roles; + final RoleV7 config; final String primaryAction; final ImmutableSet requiredActions; final ImmutableSet otherActions; - final RoleBasedActionPrivileges subject; + final SubjectBasedActionPrivileges subject; @Test public void positive_full() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluationContext ctx = ctx().indexMetadata(INDEX_METADATA).get(); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); if (covers(ctx, "data_stream_a11")) { assertThat(result, isAllowed()); @@ -516,7 +369,7 @@ public void positive_full() throws Exception { @Test public void positive_partial() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluationContext ctx = ctx().indexMetadata(INDEX_METADATA).get(); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( ctx, requiredActions, @@ -545,16 +398,9 @@ public void positive_partial() throws Exception { } } - @Test - public void negative_wrongRole() throws Exception { - PrivilegesEvaluationContext ctx = ctx("other_role"); - PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); - assertThat(result, isForbidden(missingPrivileges(requiredActions))); - } - @Test public void negative_wrongAction() throws Exception { - PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluationContext ctx = ctx().indexMetadata(INDEX_METADATA).get(); PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, otherActions, resolved("data_stream_a11")); assertThat(result, isForbidden(missingPrivileges(otherActions))); } @@ -568,7 +414,7 @@ private boolean covers(PrivilegesEvaluationContext ctx, String... indices) { return true; } - @Parameterized.Parameters(name = "{0}; actions: {1}; {2}") + @Parameterized.Parameters(name = "{0}; actions: {1}") public static Collection params() { List result = new ArrayList<>(); @@ -601,18 +447,16 @@ public static Collection params() { .requiredPrivs("indices:unknown/unwell", "indices:unknown/notatall")// )) { - for (Statefulness statefulness : Statefulness.values()) { - result.add(new Object[] { indexSpec, actionSpec, statefulness }); - } + result.add(new Object[] { indexSpec, actionSpec }); } } return result; } - public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness statefulness) throws Exception { + public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec) throws Exception { this.indexSpec = indexSpec; this.actionSpec = actionSpec; - this.roles = indexSpec.toRolesConfig(actionSpec); + this.config = indexSpec.toConfig(actionSpec); this.primaryAction = actionSpec.primaryAction; this.requiredActions = actionSpec.requiredPrivs; @@ -620,35 +464,19 @@ public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness stat this.otherActions = actionSpec.wellKnownActions ? ImmutableSet.of("indices:data/write/update") : ImmutableSet.of("indices:foobar/unknown"); - this.indexSpec.indexMetadata = INDEX_METADATA; - - 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(); - } - - this.subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, () -> INDEX_METADATA, settings); - - if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { - this.subject.updateStatefulIndexPrivileges(INDEX_METADATA, 1); - } + this.indexSpec.indexMetadata = INDEX_METADATA.getIndicesLookup(); + this.subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); } - final static Map INDEX_METADATA = // + final static Metadata INDEX_METADATA = // dataStreams("data_stream_a11", "data_stream_a12", "data_stream_a21", "data_stream_a22", "data_stream_b1", "data_stream_b2") - .build() - .getIndicesLookup(); + .build(); static IndexResolverReplacer.Resolved resolved(String... indices) { ImmutableSet.Builder allIndices = ImmutableSet.builder(); for (String index : indices) { - IndexAbstraction indexAbstraction = INDEX_METADATA.get(index); + IndexAbstraction indexAbstraction = INDEX_METADATA.getIndicesLookup().get(index); if (indexAbstraction instanceof IndexAbstraction.DataStream) { allIndices.addAll( @@ -730,7 +558,7 @@ boolean covers(User user, String index) { return false; } - SecurityDynamicConfiguration toRolesConfig(ActionSpec actionSpec) { + RoleV7 toConfig(ActionSpec actionSpec) { try { return SecurityDynamicConfiguration.fromMap( ImmutableMap.of( @@ -743,7 +571,7 @@ SecurityDynamicConfiguration toRolesConfig(ActionSpec actionSpec) { ) ), CType.ROLES - ); + ).getCEntry("test_role"); } catch (JsonProcessingException e) { throw new RuntimeException(e); } @@ -784,313 +612,82 @@ public String toString() { return name; } } - - enum Statefulness { - STATEFUL, - STATEFUL_LIMITED, - NON_STATEFUL - } } public static class Misc { - @Test - public void relevantOnly_identity() throws Exception { - Map metadata = // - indices("index_a11", "index_a12", "index_b")// - .alias("alias_a") - .of("index_a11", "index_a12")// - .build() - .getIndicesLookup(); - - assertTrue( - "relevantOnly() returned identical object", - RoleBasedActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata) == metadata - ); - } - - @Test - public void relevantOnly_closed() throws Exception { - Map metadata = indices("index_open_1", "index_open_2")// - .index("index_closed", IndexMetadata.State.CLOSE) - .build() - .getIndicesLookup(); - - 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); - - assertNotNull("Filtered metadata contains index_open_1", filteredMetadata.get("index_open_1")); - assertNull("Filtered metadata does not contain index_closed", filteredMetadata.get("index_closed")); - } @Test - public void relevantOnly_dataStreamBackingIndices() throws Exception { - Map metadata = dataStreams("data_stream_1").build().getIndicesLookup(); - - 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); - - 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")); - } + public void hasExplicitIndexPrivilege_positive() throws Exception { + RoleV7 config = config(""" + index_permissions: + - index_patterns: ['test_index'] + allowed_actions: ['system:admin/system_index'] + """); - @Test - public void backingIndexToDataStream() { - Map metadata = indices("index").dataStream("data_stream").build().getIndicesLookup(); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); - assertEquals("index", RoleBasedActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream("index", metadata)); - assertEquals( - "data_stream", - RoleBasedActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream(".ds-data_stream-000001", metadata) - ); - assertEquals( - "non_existing", - RoleBasedActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream("non_existing", metadata) + PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( + ctx().get(), + Set.of("system:admin/system_index"), + IndexResolverReplacer.Resolved.ofIndex("test_index") ); + assertThat(result, isAllowed()); } @Test - public void hasIndexPrivilege_errors() throws Exception { - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( - "role_with_errors:\n" - + " index_permissions:\n" - + " - index_patterns: ['/invalid_regex_with_attr${user.name}\\/']\n" - + " allowed_actions: ['indices:some_action*', 'indices:data/write/index']", - CType.ROLES - ); + public void hasExplicitIndexPrivilege_positive_pattern() throws Exception { + RoleV7 config = config(""" + index_permissions: + - index_patterns: ['test_index'] + allowed_actions: ['system:admin/system_index*'] + """); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( - roles, - FlattenedActionGroups.EMPTY, - () -> Collections.emptyMap(), - Settings.EMPTY - ); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); - PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( - ctx("role_with_errors"), - Set.of("indices:some_action", "indices:data/write/index"), - IndexResolverReplacer.Resolved.ofIndex("any_index") - ); - assertThat(result, isForbidden()); - assertTrue(result.hasEvaluationExceptions()); - assertTrue( - "Result mentions role_with_errors: " + result.getEvaluationExceptionInfo(), - result.getEvaluationExceptionInfo() - .startsWith("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating role role_with_errors") + PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( + ctx().get(), + Set.of("system:admin/system_index"), + IndexResolverReplacer.Resolved.ofIndex("test_index") ); + assertThat(result, isAllowed()); } @Test - public void hasExplicitIndexPrivilege_errors() throws Exception { - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( - "role_with_errors:\n" - + " index_permissions:\n" - + " - index_patterns: ['/invalid_regex_with_attr${user.name}\\/']\n" - + " allowed_actions: ['system:admin/system_index*']", - CType.ROLES - ); - - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( - roles, - FlattenedActionGroups.EMPTY, - () -> Collections.emptyMap(), - Settings.EMPTY - ); + public void hasExplicitIndexPrivilege_noWildcard() throws Exception { + RoleV7 config = config(""" + index_permissions: + - index_patterns: ['test_index'] + allowed_actions: ['*'] + """); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( - ctx("role_with_errors"), + ctx().get(), Set.of("system:admin/system_index"), - IndexResolverReplacer.Resolved.ofIndex("any_index") + IndexResolverReplacer.Resolved.ofIndex("test_index") ); assertThat(result, isForbidden()); - assertTrue(result.hasEvaluationExceptions()); - assertTrue( - "Result mentions role_with_errors: " + result.getEvaluationExceptionInfo(), - result.getEvaluationExceptionInfo() - .startsWith("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating role role_with_errors") - ); } @Test - public void aliasesOnDataStreamBackingIndices() throws Exception { - // We create a meta data object with a data stream ds_a. Implicitly, the utility method will create - // the backing indices ".ds-ds_a-000001", ".ds-ds_a-000002" and ".ds-ds_a-000003". - // Additionally, we create an alias which only contains ".ds-ds_a-000001", but not the other backing indices. - Map metadata = dataStreams("ds_a").alias("alias_a").of(".ds-ds_a-000001").build().getIndicesLookup(); - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( - "role:\n" - + " index_permissions:\n" - + " - index_patterns: ['alias_a']\n" - + " allowed_actions: ['indices:data/write/index']", - CType.ROLES - ); - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( - roles, - FlattenedActionGroups.EMPTY, - () -> metadata, - Settings.EMPTY - ); - subject.updateStatefulIndexPrivileges(metadata, 2); + public void hasExplicitIndexPrivilege_negative_wrongAction() throws Exception { + RoleV7 config = config(""" + index_permissions: + - index_patterns: ['test_index'] + allowed_actions: ['system:admin/system*'] + """); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); - PrivilegesEvaluatorResponse resultForIndexCoveredByAlias = subject.hasIndexPrivilege( - ctx("role"), - Set.of("indices:data/write/index"), - IndexResolverReplacer.Resolved.ofIndex(".ds-ds_a-000001") - ); - assertThat(resultForIndexCoveredByAlias, isAllowed()); - - PrivilegesEvaluatorResponse resultForIndexNotCoveredByAlias = subject.hasIndexPrivilege( - ctx("role"), - Set.of("indices:data/write/index"), - IndexResolverReplacer.Resolved.ofIndex(".ds-ds_a-000002") - ); - assertThat(resultForIndexNotCoveredByAlias, isForbidden()); - } - } - - /** - * Verifies that the heap size used by StatefulIndexPrivileges stays within expected bounds. - */ - @RunWith(Parameterized.class) - public static class StatefulIndexPrivilegesHeapSize { - - final Map indices; - final SecurityDynamicConfiguration roles; - final int expectedEstimatedNumberOfBytes; - - @Test - public void estimatedSize() throws Exception { - RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges( - roles, - FlattenedActionGroups.EMPTY, - () -> indices, - Settings.EMPTY - ); - - subject.updateStatefulIndexPrivileges(indices, 1); - - int lowerBound = (int) (expectedEstimatedNumberOfBytes * 0.9); - int upperBound = (int) (expectedEstimatedNumberOfBytes * 1.1); - - int actualEstimatedNumberOfBytes = subject.getEstimatedStatefulIndexByteSize(); - - assertTrue( - "estimatedNumberOfBytes: " + lowerBound + " < " + actualEstimatedNumberOfBytes + " < " + upperBound, - lowerBound < actualEstimatedNumberOfBytes && actualEstimatedNumberOfBytes < upperBound + PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( + ctx().get(), + Set.of("system:admin/system_foo"), + IndexResolverReplacer.Resolved.ofIndex("test_index") ); + assertThat(result, isForbidden()); } - - public StatefulIndexPrivilegesHeapSize(int numberOfIndices, int numberOfRoles, int expectedEstimatedNumberOfBytes) { - this.indices = createIndices(numberOfIndices); - this.roles = createRoles(numberOfRoles, numberOfIndices); - this.expectedEstimatedNumberOfBytes = expectedEstimatedNumberOfBytes; - } - - @Parameterized.Parameters(name = "{0} indices; {1} roles; estimated number of bytes: {2}") - public static Collection params() { - List result = new ArrayList<>(); - - // indices; roles; expected number of bytes - result.add(new Object[] { 100, 10, 10_000 }); - result.add(new Object[] { 100, 100, 13_000 }); - result.add(new Object[] { 100, 1000, 26_000 }); - - result.add(new Object[] { 1000, 10, 92_000 }); - result.add(new Object[] { 1000, 100, 94_000 }); - result.add(new Object[] { 1000, 1000, 112_000 }); - - result.add(new Object[] { 10_000, 10, 890_000 }); - result.add(new Object[] { 10_000, 100, 930_000 }); - - return result; - } - - static Map createIndices(int numberOfIndices) { - String[] names = new String[numberOfIndices]; - - for (int i = 0; i < numberOfIndices; i++) { - names[i] = "index_" + i; - } - - return MockIndexMetadataBuilder.indices(names).build().getIndicesLookup(); - } - - static SecurityDynamicConfiguration createRoles(int numberOfRoles, int numberOfIndices) { - try { - Random random = new Random(1); - Map rolesDocument = new HashMap<>(); - List allowedActions = Arrays.asList( - "indices:data/read*", - "indices:admin/mappings/fields/get*", - "indices:admin/resolve/index", - "indices:data/write*", - "indices:admin/mapping/put" - ); - - for (int i = 0; i < numberOfRoles; i++) { - List indexPatterns = new ArrayList<>(); - int numberOfIndexPatterns = Math.min( - (int) ((Math.abs(random.nextGaussian() + 0.3)) * 0.5 * numberOfIndices), - numberOfIndices - ); - - int numberOfIndexPatterns10th = numberOfIndexPatterns / 10; - - if (numberOfIndexPatterns10th > 0) { - for (int k = 0; k < numberOfIndexPatterns10th; k++) { - indexPatterns.add("index_" + random.nextInt(numberOfIndices / 10) + "*"); - } - } else { - for (int k = 0; k < numberOfIndexPatterns; k++) { - indexPatterns.add("index_" + random.nextInt(numberOfIndices)); - } - } - - Map roleDocument = ImmutableMap.of( - "index_permissions", - Arrays.asList(ImmutableMap.of("index_patterns", indexPatterns, "allowed_actions", allowedActions)) - ); - - rolesDocument.put("role_" + i, roleDocument); - } - - return SecurityDynamicConfiguration.fromMap(rolesDocument, CType.ROLES); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - } - - static PrivilegesEvaluationContext ctx(String... roles) { - User user = new User("test_user").withAttributes(ImmutableMap.of("attrs.dept_no", "a11")); - return new PrivilegesEvaluationContext( - user, - ImmutableSet.copyOf(roles), - null, - null, - null, - null, - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - null, - null - ); } - static PrivilegesEvaluationContext ctxByUsername(String username) { - User user = new User(username).withAttributes(ImmutableMap.of("attrs.dept_no", "a11")); - return new PrivilegesEvaluationContext( - user, - ImmutableSet.of(), - null, - null, - null, - null, - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - null, - null - ); + static RoleV7 config(String config) { + return RoleV7.fromYamlStringUnchecked(config); } } diff --git a/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java b/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java new file mode 100644 index 0000000000..4dac4727e9 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java @@ -0,0 +1,84 @@ +/* + * 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.util; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +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.PrivilegesEvaluationContext; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.user.User; + +/** + * A utility test class that helps building PrivilegesEvaluationContext objects for testing. + */ +public class MockPrivilegeEvaluationContextBuilder { + public static MockPrivilegeEvaluationContextBuilder ctx() { + return new MockPrivilegeEvaluationContextBuilder(); + } + + private static final ClusterState EMPTY_CLUSTER_STATE = ClusterState.builder(ClusterState.EMPTY_STATE) + .metadata(MockIndexMetadataBuilder.indices().build()) + .build(); + + private String username = "test_user"; + private Map attributes = new HashMap<>(); + private Set roles = new HashSet<>(); + private ClusterState clusterState = EMPTY_CLUSTER_STATE; + + public MockPrivilegeEvaluationContextBuilder attr(String key, String value) { + this.attributes.put(key, value); + return this; + } + + public MockPrivilegeEvaluationContextBuilder clusterState(ClusterState clusterState) { + this.clusterState = clusterState; + return this; + } + + public MockPrivilegeEvaluationContextBuilder indexMetadata(Metadata metadata) { + return this.clusterState(ClusterState.builder(ClusterState.EMPTY_STATE).metadata(metadata).build()); + } + + public MockPrivilegeEvaluationContextBuilder roles(String... roles) { + this.roles.addAll(Arrays.asList(roles)); + return this; + } + + public PrivilegesEvaluationContext get() { + IndexNameExpressionResolver indexNameExpressionResolver = new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)); + + User user = new User(this.username).withAttributes(ImmutableMap.copyOf(this.attributes)); + return new PrivilegesEvaluationContext( + user, + ImmutableSet.copyOf(roles), + null, + null, + null, + new IndexResolverReplacer(indexNameExpressionResolver, () -> clusterState, null), + indexNameExpressionResolver, + () -> clusterState, + null + ); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 4307ae8f9c..14de3f1dab 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -247,12 +247,7 @@ void updateConfiguration( rolesConfiguration = rolesConfiguration.withStaticConfig(); tenantConfiguration = tenantConfiguration.withStaticConfig(); try { - RoleBasedActionPrivileges actionPrivileges = new RoleBasedActionPrivileges( - rolesConfiguration, - flattenedActionGroups, - indexMetadataSupplier, - settings - ); + RoleBasedActionPrivileges actionPrivileges = new RoleBasedActionPrivileges(rolesConfiguration, flattenedActionGroups, settings); Metadata metadata = clusterStateSupplier.get().metadata(); actionPrivileges.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); RoleBasedActionPrivileges oldInstance = this.actionPrivileges.getAndSet(actionPrivileges); @@ -886,9 +881,6 @@ private List toString(List aliases) { public void updatePluginToClusterActions(String pluginIdentifier, Set clusterActions) { RoleV7 pluginPermissions = new RoleV7(); pluginPermissions.setCluster_permissions(ImmutableList.copyOf(clusterActions)); - this.pluginIdToActionPrivileges.put( - pluginIdentifier, - new SubjectBasedActionPrivileges(pluginPermissions, this.staticActionGroups, this.indexMetadataSupplier) - ); + this.pluginIdToActionPrivileges.put(pluginIdentifier, new SubjectBasedActionPrivileges(pluginPermissions, this.staticActionGroups)); } } 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 90375081f1..59dd4c789b 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java @@ -18,7 +18,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; import java.util.stream.Collectors; import com.google.common.collect.ImmutableMap; @@ -53,6 +52,7 @@ import com.selectivem.collections.ImmutableCompactSubSet; import static org.opensearch.security.privileges.actionlevel.WellKnownActions.*; + /** * This class converts role configuration into pre-computed, optimized data structures for checking privileges. *

@@ -85,13 +85,8 @@ public class RoleBasedActionPrivileges extends RuntimeOptimizedActionPrivileges private final AtomicReference statefulIndex = new AtomicReference<>(); - public RoleBasedActionPrivileges( - SecurityDynamicConfiguration roles, - FlattenedActionGroups actionGroups, - Supplier> indexMetadataSupplier, - Settings settings - ) { - super(new ClusterPrivileges(roles, actionGroups), new IndexPrivileges(roles, actionGroups), indexMetadataSupplier); + public RoleBasedActionPrivileges(SecurityDynamicConfiguration roles, FlattenedActionGroups actionGroups, Settings settings) { + super(new ClusterPrivileges(roles, actionGroups), new IndexPrivileges(roles, actionGroups)); this.roles = roles; this.actionGroups = actionGroups; this.statefulIndexMaxHeapSize = PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.get(settings); @@ -108,9 +103,7 @@ public void updateStatefulIndexPrivileges(Map indices, if (statefulIndex == null || !statefulIndex.indices.equals(indices)) { long start = System.currentTimeMillis(); - this.statefulIndex.set( - new StatefulIndexPrivileges(roles, actionGroups, indices, metadataVersion, statefulIndexMaxHeapSize) - ); + this.statefulIndex.set(new StatefulIndexPrivileges(roles, actionGroups, indices, metadataVersion, statefulIndexMaxHeapSize)); long duration = System.currentTimeMillis() - start; log.debug("Updating StatefulIndexPrivileges took {} ms", duration); } else { @@ -180,8 +173,6 @@ static class ClusterPrivileges extends RuntimeOptimizedActionPrivileges.ClusterP */ private final ImmutableMap rolesToActionMatcher; - private final ImmutableMap usersToActionMatcher; - /** * Creates pre-computed cluster privileges based on the given parameters. *

@@ -190,17 +181,13 @@ static class ClusterPrivileges extends RuntimeOptimizedActionPrivileges.ClusterP * just results in fewer available privileges. However, having a proper error reporting mechanism would be * kind of nice. */ - ClusterPrivileges( - SecurityDynamicConfiguration roles, - FlattenedActionGroups actionGroups - ) { + ClusterPrivileges(SecurityDynamicConfiguration roles, FlattenedActionGroups actionGroups) { DeduplicatingCompactSubSetBuilder roleSetBuilder = new DeduplicatingCompactSubSetBuilder<>( roles.getCEntries().keySet() ); Map> actionToRoles = new HashMap<>(); ImmutableSet.Builder rolesWithWildcardPermissions = ImmutableSet.builder(); ImmutableMap.Builder rolesToActionMatcher = ImmutableMap.builder(); - ImmutableMap.Builder usersToActionMatcher = ImmutableMap.builder(); for (Map.Entry entry : roles.getCEntries().entrySet()) { try { @@ -257,7 +244,6 @@ static class ClusterPrivileges extends RuntimeOptimizedActionPrivileges.ClusterP .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry -> entry.getValue().build(completedRoleSetBuilder))); this.rolesWithWildcardPermissions = rolesWithWildcardPermissions.build(); this.rolesToActionMatcher = rolesToActionMatcher.build(); - this.usersToActionMatcher = usersToActionMatcher.build(); } @Override @@ -340,10 +326,7 @@ 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) { Map> rolesToActionToIndexPattern = new HashMap<>(); Map> rolesToActionPatternToIndexPattern = new HashMap<>(); @@ -410,7 +393,9 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde .add(indexPermissions.getIndex_patterns()); if (actionMatcher != WildcardMatcher.ANY) { - for (String action : actionMatcher.iterateMatching(WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS)) { + for (String action : actionMatcher.iterateMatching( + WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS + )) { rolesToExplicitActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) .computeIfAbsent(action, k -> new IndexPattern.Builder()) .add(indexPermissions.getIndex_patterns()); @@ -468,8 +453,6 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde } - - /** * Checks whether this instance provides privileges for the combination of the provided action, * the provided indices and the provided roles. @@ -491,15 +474,14 @@ protected PrivilegesEvaluatorResponse providesPrivilege( PrivilegesEvaluationContext context, Set actions, IndexResolverReplacer.Resolved resolvedIndices, - CheckTable checkTable, - Map indexMetadata + CheckTable checkTable ) { List exceptions = new ArrayList<>(); for (String role : context.getMappedRoles()) { ImmutableMap actionToIndexPattern = this.rolesToActionToIndexPattern.get(role); if (actionToIndexPattern != null) { - checkPrivilegeWithIndexPatternOnWellKnownActions(context, actions, checkTable, indexMetadata, actionToIndexPattern, exceptions); + checkPrivilegeWithIndexPatternOnWellKnownActions(context, actions, checkTable, actionToIndexPattern, exceptions); if (checkTable.isComplete()) { return PrivilegesEvaluatorResponse.ok(); } @@ -517,7 +499,7 @@ protected PrivilegesEvaluatorResponse providesPrivilege( ); if (actionPatternToIndexPattern != null) { - checkPrivilegesForNonWellKnownActions(context, actions, checkTable, indexMetadata, actionPatternToIndexPattern, exceptions); + checkPrivilegesForNonWellKnownActions(context, actions, checkTable, actionPatternToIndexPattern, exceptions); if (checkTable.isComplete()) { return PrivilegesEvaluatorResponse.ok(); } @@ -525,7 +507,7 @@ protected PrivilegesEvaluatorResponse providesPrivilege( } } - return responseForIncompletePrivileges(context, checkTable, exceptions); + return responseForIncompletePrivileges(context, resolvedIndices, checkTable, exceptions); } /** @@ -534,7 +516,10 @@ protected PrivilegesEvaluatorResponse providesPrivilege( * the user's privileges. */ @Override - protected PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownActions(PrivilegesEvaluationContext context, Set actions) { + protected PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownActions( + PrivilegesEvaluationContext context, + Set actions + ) { ImmutableSet effectiveRoles = context.getMappedRoles(); for (String action : actions) { @@ -559,9 +544,9 @@ protected PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownAct protected PrivilegesEvaluatorResponse providesExplicitPrivilege( PrivilegesEvaluationContext context, Set actions, - CheckTable checkTable, - Map indexMetadata + CheckTable checkTable ) { + Map indexMetadata = context.getIndicesLookup(); List exceptions = new ArrayList<>(); for (String role : context.getMappedRoles()) { @@ -689,7 +674,10 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St for (String permission : permissions) { WildcardMatcher actionMatcher = WildcardMatcher.from(permission); - Collection matchedActions = actionMatcher.getMatchAny(WellKnownActions.INDEX_ACTIONS, Collectors.toList()); + Collection matchedActions = actionMatcher.getMatchAny( + WellKnownActions.INDEX_ACTIONS, + Collectors.toList() + ); for (Map.Entry indicesEntry : indexMatcher.iterateMatching( indices.entrySet(), @@ -786,9 +774,9 @@ protected PrivilegesEvaluatorResponse providesPrivilege( Set actions, IndexResolverReplacer.Resolved resolvedIndices, PrivilegesEvaluationContext context, - CheckTable checkTable, - Map indexMetadata + CheckTable checkTable ) { + Map indexMetadata = context.getIndicesLookup(); ImmutableSet effectiveRoles = context.getMappedRoles(); for (String action : actions) { 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 04b995bce0..df96c08c24 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java @@ -11,12 +11,16 @@ package org.opensearch.security.privileges.actionlevel; +import java.util.List; +import java.util.Map; +import java.util.Set; + import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.selectivem.collections.CheckTable; import org.apache.commons.collections4.CollectionUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; + import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.security.privileges.ActionPrivileges; import org.opensearch.security.privileges.IndexPattern; @@ -26,10 +30,7 @@ import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.support.WildcardMatcher; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Supplier; +import com.selectivem.collections.CheckTable; import static org.opensearch.security.privileges.actionlevel.WellKnownActions.*; @@ -47,12 +48,10 @@ public abstract class RuntimeOptimizedActionPrivileges implements ActionPrivileg protected final ClusterPrivileges cluster; protected final StaticIndexPrivileges index; - protected final Supplier> indexMetadataSupplier; - RuntimeOptimizedActionPrivileges(ClusterPrivileges cluster, StaticIndexPrivileges index, Supplier> indexMetadataSupplier) { + RuntimeOptimizedActionPrivileges(ClusterPrivileges cluster, StaticIndexPrivileges index) { this.cluster = cluster; this.index = index; - this.indexMetadataSupplier = indexMetadataSupplier; } @Override @@ -82,9 +81,9 @@ public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluat */ @Override public PrivilegesEvaluatorResponse hasIndexPrivilege( - PrivilegesEvaluationContext context, - Set actions, - IndexResolverReplacer.Resolved resolvedIndices + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices ) { PrivilegesEvaluatorResponse response = this.index.checkWildcardIndexPrivilegesOnWellKnownActions(context, actions); if (response != null) { @@ -101,17 +100,15 @@ 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 + resolvedIndices.getAllIndicesResolved(context.getClusterStateSupplier(), context.getIndexNameExpressionResolver()), + actions ); - Map indexMetadata = this.indexMetadataSupplier.get(); - StatefulIndexPrivileges statefulIndex = this.currentStatefulIndexPrivileges(); PrivilegesEvaluatorResponse resultFromStatefulIndex = null; if (statefulIndex != null) { - resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, resolvedIndices, context, checkTable, indexMetadata); + resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, resolvedIndices, context, checkTable); if (resultFromStatefulIndex != null) { // If we get a result from statefulIndex, we are done. @@ -123,7 +120,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, indexMetadata); + return this.index.providesPrivilege(context, actions, resolvedIndices, checkTable); } /** @@ -135,16 +132,16 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege( */ @Override public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( - PrivilegesEvaluationContext context, - Set actions, - IndexResolverReplacer.Resolved resolvedIndices + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved 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); - return this.index.providesExplicitPrivilege(context, actions, checkTable, this.indexMetadataSupplier.get()); + return this.index.providesExplicitPrivilege(context, actions, checkTable); } /** @@ -263,7 +260,6 @@ PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext con */ protected abstract boolean checkPrivilegeViaActionMatcher(PrivilegesEvaluationContext context, String action); - } /** @@ -271,7 +267,6 @@ PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext con */ protected abstract static class StaticIndexPrivileges { - /** * Checks whether this instance provides privileges for the combination of the provided action, * the provided indices and the provided roles. @@ -289,11 +284,10 @@ protected abstract static class StaticIndexPrivileges { * checkTable instance as checked. */ protected abstract PrivilegesEvaluatorResponse providesPrivilege( - PrivilegesEvaluationContext context, - Set actions, - IndexResolverReplacer.Resolved resolvedIndices, - CheckTable checkTable, - Map indexMetadata + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices, + CheckTable checkTable ); /** @@ -304,10 +298,9 @@ protected abstract PrivilegesEvaluatorResponse providesPrivilege( * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 */ protected abstract PrivilegesEvaluatorResponse providesExplicitPrivilege( - PrivilegesEvaluationContext context, - Set actions, - CheckTable checkTable, - Map indexMetadata + PrivilegesEvaluationContext context, + Set actions, + CheckTable checkTable ); /** @@ -317,8 +310,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(PrivilegesEvaluationContext context, Set actions); - + protected abstract PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownActions( + PrivilegesEvaluationContext context, + Set actions + ); /** * Tests whether the current user (according to the context data) has index privileges for the given well known @@ -330,7 +325,15 @@ protected abstract PrivilegesEvaluatorResponse providesExplicitPrivilege( *

  • In case of any PrivilegeEvaluationException, it is added to the given list * */ - protected void checkPrivilegeWithIndexPatternOnWellKnownActions(PrivilegesEvaluationContext context, Set actions, CheckTable checkTable, Map indexMetadata, ImmutableMap actionToIndexPattern, List exceptions) { + protected void checkPrivilegeWithIndexPatternOnWellKnownActions( + PrivilegesEvaluationContext context, + Set actions, + CheckTable checkTable, + ImmutableMap actionToIndexPattern, + List exceptions + ) { + Map indexMetadata = context.getIndicesLookup(); + for (String action : actions) { IndexPattern indexPattern = actionToIndexPattern.get(action); @@ -359,7 +362,15 @@ protected void checkPrivilegeWithIndexPatternOnWellKnownActions(PrivilegesEvalua *
  • In case of any PrivilegeEvaluationException, it is added to the given list * */ - protected void checkPrivilegesForNonWellKnownActions(PrivilegesEvaluationContext context, Set actions, CheckTable checkTable, Map indexMetadata, ImmutableMap actionPatternToIndexPattern, List exceptions) { + protected void checkPrivilegesForNonWellKnownActions( + PrivilegesEvaluationContext context, + Set actions, + CheckTable checkTable, + ImmutableMap actionPatternToIndexPattern, + List exceptions + ) { + Map indexMetadata = context.getIndicesLookup(); + for (String action : actions) { if (isWellKnownIndexAction(action)) { continue; @@ -378,7 +389,9 @@ protected void checkPrivilegesForNonWellKnownActions(PrivilegesEvaluationContext } catch (PrivilegesEvaluationException e) { // We can ignore these errors, as this max leads to fewer privileges than available log.error("Error while evaluating index pattern {}. Ignoring entry", indexPattern, e); - exceptions.add(new PrivilegesEvaluationException("Error while evaluating index pattern " + indexPattern, e)); + exceptions.add( + new PrivilegesEvaluationException("Error while evaluating index pattern " + indexPattern, e) + ); } } } @@ -395,7 +408,12 @@ protected void checkPrivilegesForNonWellKnownActions(PrivilegesEvaluationContext * 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 responseForIncompletePrivileges( + PrivilegesEvaluationContext context, + IndexResolverReplacer.Resolved resolvedIndices, + CheckTable checkTable, + List exceptions + ) { Set availableIndices = checkTable.getCompleteRows(); if (!availableIndices.isEmpty()) { @@ -403,12 +421,12 @@ protected PrivilegesEvaluatorResponse responseForIncompletePrivileges(Privileges } return PrivilegesEvaluatorResponse.insufficient(checkTable) - .reason( - context.getResolvedRequest().getAllIndices().size() == 1 - ? "Insufficient permissions for the referenced index" - : "None of " + context.getResolvedRequest().getAllIndices().size() + " referenced indices has sufficient permissions" - ) - .evaluationExceptions(exceptions); + .reason( + resolvedIndices.getAllIndices().size() == 1 + ? "Insufficient permissions for the referenced index" + : "None of " + resolvedIndices.getAllIndices().size() + " referenced indices has sufficient permissions" + ) + .evaluationExceptions(exceptions); } } @@ -437,11 +455,10 @@ protected abstract static class StatefulIndexPrivileges { * @return PrivilegesEvaluatorResponse.ok() or null. */ protected abstract PrivilegesEvaluatorResponse providesPrivilege( - Set actions, - IndexResolverReplacer.Resolved resolvedIndices, - PrivilegesEvaluationContext context, - CheckTable checkTable, - Map indexMetadata + Set actions, + IndexResolverReplacer.Resolved resolvedIndices, + PrivilegesEvaluationContext context, + CheckTable checkTable ); } 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 25f06a06da..bbfa3e4464 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java @@ -17,7 +17,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Supplier; import java.util.stream.Collectors; import com.google.common.collect.ImmutableMap; @@ -49,8 +48,6 @@ * The method PrivilegesEvaluator.createContext() is responsible for making sure that the correct class is used. *

    * This class is useful for plugin users and API tokens. - *

    - * */ public class SubjectBasedActionPrivileges extends RuntimeOptimizedActionPrivileges implements ActionPrivileges { private static final Logger log = LogManager.getLogger(SubjectBasedActionPrivileges.class); @@ -63,20 +60,12 @@ public class SubjectBasedActionPrivileges extends RuntimeOptimizedActionPrivileg * abstract interface. * @param actionGroups The FlattenedActionGroups instance that shall be used to resolve the action groups * specified in the roles configuration. - * @param indexMetadataSupplier A supplier for the cluster's current index meta data. This is used during privileges - * evaluation to gain information about alias and data stream members. */ - public SubjectBasedActionPrivileges( - RoleV7 role, - FlattenedActionGroups actionGroups, - Supplier> indexMetadataSupplier - ) { - super(new ClusterPrivileges(actionGroups.resolve(role.getCluster_permissions())), new IndexPrivileges( - role, - actionGroups, - WellKnownActions.INDEX_ACTIONS, - WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS - ), indexMetadataSupplier); + public SubjectBasedActionPrivileges(RoleV7 role, FlattenedActionGroups actionGroups) { + super( + new ClusterPrivileges(actionGroups.resolve(role.getCluster_permissions())), + new IndexPrivileges(role, actionGroups, WellKnownActions.INDEX_ACTIONS, WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS) + ); } /** @@ -158,7 +147,10 @@ static class ClusterPrivileges extends RuntimeOptimizedActionPrivileges.ClusterP hasWildcardPermission = true; } else { WildcardMatcher wildcardMatcher = WildcardMatcher.from(permission); - Set matchedActions = wildcardMatcher.getMatchAny(WellKnownActions.CLUSTER_ACTIONS, Collectors.toUnmodifiableSet()); + Set matchedActions = wildcardMatcher.getMatchAny( + WellKnownActions.CLUSTER_ACTIONS, + Collectors.toUnmodifiableSet() + ); grantedActions.addAll(matchedActions); wildcardMatchers.add(wildcardMatcher); } @@ -326,7 +318,6 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde this.explicitlyRequiredIndexActions = explicitlyRequiredIndexActions; } - /** * Checks whether this instance provides privileges for the combination of the provided action, * the provided indices and the provided roles. @@ -348,12 +339,11 @@ protected PrivilegesEvaluatorResponse providesPrivilege( PrivilegesEvaluationContext context, Set actions, IndexResolverReplacer.Resolved resolvedIndices, - CheckTable checkTable, - Map indexMetadata + CheckTable checkTable ) { List exceptions = new ArrayList<>(); - checkPrivilegeWithIndexPatternOnWellKnownActions(context, actions, checkTable, indexMetadata, actionToIndexPattern, exceptions); + checkPrivilegeWithIndexPatternOnWellKnownActions(context, actions, checkTable, actionToIndexPattern, exceptions); if (checkTable.isComplete()) { return PrivilegesEvaluatorResponse.ok(); } @@ -363,24 +353,25 @@ protected PrivilegesEvaluatorResponse providesPrivilege( // actions, we also have to evaluate action patterns to check the authorization if (!checkTable.isComplete() && !allWellKnownIndexActions(actions)) { - checkPrivilegesForNonWellKnownActions(context, actions, checkTable, indexMetadata, this.actionPatternToIndexPattern, exceptions); + checkPrivilegesForNonWellKnownActions(context, actions, checkTable, this.actionPatternToIndexPattern, exceptions); if (checkTable.isComplete()) { return PrivilegesEvaluatorResponse.ok(); } } - return responseForIncompletePrivileges(context, checkTable, exceptions); + return responseForIncompletePrivileges(context, resolvedIndices, checkTable, exceptions); } - - /** * 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 * the user's privileges. */ @Override - protected PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownActions(PrivilegesEvaluationContext context, Set actions) { + protected PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownActions( + PrivilegesEvaluationContext context, + Set actions + ) { for (String action : actions) { if (!this.actionsWithWildcardIndexPrivileges.contains(action)) { return null; @@ -398,7 +389,12 @@ protected PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownAct * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 */ @Override - protected PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContext context, Set actions, CheckTable checkTable, Map indexMetadata) { + protected PrivilegesEvaluatorResponse providesExplicitPrivilege( + PrivilegesEvaluationContext context, + Set actions, + CheckTable checkTable + ) { + Map indexMetadata = context.getIndicesLookup(); List exceptions = new ArrayList<>(); if (!CollectionUtils.containsAny(actions, this.explicitlyRequiredIndexActions)) { diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/WellKnownActions.java b/src/main/java/org/opensearch/security/privileges/actionlevel/WellKnownActions.java index 777cabed69..8588f4dee0 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/WellKnownActions.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/WellKnownActions.java @@ -10,6 +10,8 @@ */ package org.opensearch.security.privileges.actionlevel; +import java.util.Collection; + import com.google.common.collect.ImmutableSet; import org.opensearch.action.admin.cluster.health.ClusterHealthAction; @@ -41,8 +43,6 @@ import org.opensearch.index.reindex.UpdateByQueryAction; import org.opensearch.security.support.ConfigConstants; -import java.util.Collection; - /** * This class lists so-called "well-known actions". These are taken into account when creating the pre-computed * data structures of the ActionPrivileges class. Thus, a very fast performance evaluation will be possible for @@ -97,8 +97,7 @@ public static boolean isWellKnownIndexAction(String action) { } public static boolean allWellKnownIndexActions(Collection actions) { - return actions.stream().allMatch(WellKnownActions::isWellKnownIndexAction); + return actions.stream().allMatch(WellKnownActions::isWellKnownIndexAction); } - } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java index 2b2da40927..53d6222c63 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java @@ -27,16 +27,42 @@ package org.opensearch.security.securityconf.impl.v7; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; import java.util.Collections; import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; +import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.securityconf.Hideable; import org.opensearch.security.securityconf.StaticDefinable; public class RoleV7 implements Hideable, StaticDefinable { + public static RoleV7 fromYamlString(String yamlString) throws IOException { + try (Reader yamlReader = new StringReader(yamlString)) { + return fromYaml(yamlReader); + } + } + + /** + * Converts any validation error exceptions into runtime exceptions. Only use when you are sure that is safe; + * useful for tests. + */ + public static RoleV7 fromYamlStringUnchecked(String yamlString) { + try (Reader yamlReader = new StringReader(yamlString)) { + return fromYaml(yamlReader); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static RoleV7 fromYaml(Reader yamlReader) throws IOException { + return DefaultObjectMapper.YAML_MAPPER.readValue(yamlReader, RoleV7.class); + } + private boolean reserved; private boolean hidden; @JsonProperty(value = "static") From a787e547a89a1f0d42b7899ab5493f69bc3d7798 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 3 Jun 2025 13:49:42 +0200 Subject: [PATCH 04/10] Fix Signed-off-by: Nils Bandener --- .../SystemIndexAccessEvaluatorTest.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java index 01ee885fcc..17a78501c9 100644 --- a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java @@ -14,6 +14,8 @@ import java.util.Arrays; import java.util.List; import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -27,8 +29,10 @@ import org.opensearch.action.get.MultiGetRequest; import org.opensearch.action.search.SearchRequest; import org.opensearch.action.support.IndicesOptions; +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.service.ClusterService; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; @@ -75,6 +79,10 @@ public class SystemIndexAccessEvaluatorTest { private Logger log; @Mock ClusterService cs; + @Mock + Metadata metadata; + @Mock + ClusterState clusterState; private SystemIndexAccessEvaluator evaluator; private static final String UNPROTECTED_ACTION = "indices:data/read"; @@ -84,8 +92,9 @@ public class SystemIndexAccessEvaluatorTest { private static final String TEST_INDEX = ".test"; private static final String SECURITY_INDEX = ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX; - ImmutableMap indexMetadata = MockIndexMetadataBuilder.indices(TEST_INDEX, TEST_SYSTEM_INDEX, SECURITY_INDEX) - .build(); + static final SortedMap indexMetadata = new TreeMap<>( + MockIndexMetadataBuilder.indices(TEST_INDEX, TEST_SYSTEM_INDEX, SECURITY_INDEX).build() + ); User user; IndexNameExpressionResolver indexNameExpressionResolver; @@ -129,12 +138,7 @@ public void setup( CType.ROLES ); - this.actionPrivileges = new RoleBasedActionPrivileges( - rolesConfig, - FlattenedActionGroups.EMPTY, - () -> indexMetadata, - Settings.EMPTY - ); + this.actionPrivileges = new RoleBasedActionPrivileges(rolesConfig, FlattenedActionGroups.EMPTY, Settings.EMPTY); } catch (JsonProcessingException e) { throw new RuntimeException(e); } @@ -157,6 +161,8 @@ public void setup( when(log.isDebugEnabled()).thenReturn(true); when(log.isInfoEnabled()).thenReturn(true); + when(clusterState.metadata()).thenReturn(metadata); + when(metadata.getIndicesLookup()).thenReturn(indexMetadata); } PrivilegesEvaluationContext ctx(String action) { @@ -168,7 +174,7 @@ PrivilegesEvaluationContext ctx(String action) { null, null, indexNameExpressionResolver, - null, + () -> clusterState, actionPrivileges ); } From a7588f65df5e648f5136c54e9cc7aac610f0ebc0 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 3 Jun 2025 14:29:14 +0200 Subject: [PATCH 05/10] Fix Signed-off-by: Nils Bandener --- .../RoleBasedActionPrivileges.java | 28 +++++++++---------- .../RuntimeOptimizedActionPrivileges.java | 13 ++++----- 2 files changed, 19 insertions(+), 22 deletions(-) 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 59dd4c789b..64f718fb50 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java @@ -11,21 +11,15 @@ package org.opensearch.security.privileges.actionlevel; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.selectivem.collections.CheckTable; +import com.selectivem.collections.CompactMapGroupBuilder; +import com.selectivem.collections.DeduplicatingCompactSubSetBuilder; +import com.selectivem.collections.ImmutableCompactSubSet; import org.apache.commons.collections4.CollectionUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - import org.opensearch.cluster.metadata.DataStream; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexMetadata; @@ -46,12 +40,16 @@ import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.WildcardMatcher; -import com.selectivem.collections.CheckTable; -import com.selectivem.collections.CompactMapGroupBuilder; -import com.selectivem.collections.DeduplicatingCompactSubSetBuilder; -import com.selectivem.collections.ImmutableCompactSubSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; -import static org.opensearch.security.privileges.actionlevel.WellKnownActions.*; +import static org.opensearch.security.privileges.actionlevel.WellKnownActions.allWellKnownIndexActions; /** * This class converts role configuration into pre-computed, optimized data structures for checking privileges. 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 df96c08c24..3a3f5d4482 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java @@ -11,16 +11,12 @@ package org.opensearch.security.privileges.actionlevel; -import java.util.List; -import java.util.Map; -import java.util.Set; - import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.selectivem.collections.CheckTable; import org.apache.commons.collections4.CollectionUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.security.privileges.ActionPrivileges; import org.opensearch.security.privileges.IndexPattern; @@ -30,9 +26,12 @@ import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.support.WildcardMatcher; -import com.selectivem.collections.CheckTable; +import java.util.List; +import java.util.Map; +import java.util.Set; -import static org.opensearch.security.privileges.actionlevel.WellKnownActions.*; +import static org.opensearch.security.privileges.actionlevel.WellKnownActions.isWellKnownClusterAction; +import static org.opensearch.security.privileges.actionlevel.WellKnownActions.isWellKnownIndexAction; /** * This is a common base class for ActionPrivileges implementations that implement a certain From 15a35970bab13df6a2764a6cda7bab62f759ea8d Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 3 Jun 2025 14:45:45 +0200 Subject: [PATCH 06/10] Fix Signed-off-by: Nils Bandener --- .../RoleBasedActionPrivileges.java | 26 ++++++++++--------- .../RuntimeOptimizedActionPrivileges.java | 10 ++++--- 2 files changed, 20 insertions(+), 16 deletions(-) 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 64f718fb50..49eb30d6ca 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java @@ -11,15 +11,21 @@ package org.opensearch.security.privileges.actionlevel; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.selectivem.collections.CheckTable; -import com.selectivem.collections.CompactMapGroupBuilder; -import com.selectivem.collections.DeduplicatingCompactSubSetBuilder; -import com.selectivem.collections.ImmutableCompactSubSet; import org.apache.commons.collections4.CollectionUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; + import org.opensearch.cluster.metadata.DataStream; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexMetadata; @@ -40,14 +46,10 @@ import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.WildcardMatcher; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; +import com.selectivem.collections.CheckTable; +import com.selectivem.collections.CompactMapGroupBuilder; +import com.selectivem.collections.DeduplicatingCompactSubSetBuilder; +import com.selectivem.collections.ImmutableCompactSubSet; import static org.opensearch.security.privileges.actionlevel.WellKnownActions.allWellKnownIndexActions; 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 3a3f5d4482..1ab6a11fbb 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java @@ -11,12 +11,16 @@ package org.opensearch.security.privileges.actionlevel; +import java.util.List; +import java.util.Map; +import java.util.Set; + import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.selectivem.collections.CheckTable; import org.apache.commons.collections4.CollectionUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; + import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.security.privileges.ActionPrivileges; import org.opensearch.security.privileges.IndexPattern; @@ -26,9 +30,7 @@ import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.support.WildcardMatcher; -import java.util.List; -import java.util.Map; -import java.util.Set; +import com.selectivem.collections.CheckTable; import static org.opensearch.security.privileges.actionlevel.WellKnownActions.isWellKnownClusterAction; import static org.opensearch.security.privileges.actionlevel.WellKnownActions.isWellKnownIndexAction; From b4b8f3c1caac63e610931728a1b4776ae8b91572 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Wed, 4 Jun 2025 08:42:34 +0200 Subject: [PATCH 07/10] CLeanup Signed-off-by: Nils Bandener --- .../RestEndpointPermissionTests.java | 14 +--- .../privileges/TenantPrivilegesTest.java | 32 +-------- .../EmptyActionPrivilegesTest.java | 67 +++++++++++++++++++ .../RoleBasedActionPrivilegesTest.java | 7 -- .../SubjectBasedActionPrivilegesTest.java | 24 +++++++ .../dlsfls/DocumentPrivilegesTest.java | 15 +---- ...MockPrivilegeEvaluationContextBuilder.java | 9 ++- .../security/OpenSearchSecurityPlugin.java | 3 +- .../privileges/PrivilegesEvaluator.java | 9 +-- .../SubjectBasedActionPrivileges.java | 42 +++--------- .../security/securityconf/impl/v7/RoleV7.java | 4 +- .../RestLayerPrivilegesEvaluatorTest.java | 1 - 12 files changed, 118 insertions(+), 109 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/privileges/actionlevel/EmptyActionPrivilegesTest.java diff --git a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java index 33ec6a918c..09c122cc2b 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java @@ -51,7 +51,7 @@ import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; -import org.opensearch.security.user.User; +import org.opensearch.security.util.MockPrivilegeEvaluationContextBuilder; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; @@ -252,17 +252,7 @@ static SecurityDynamicConfiguration createRolesConfig() throws IOExcepti } PrivilegesEvaluationContext ctx(String... roles) { - return new PrivilegesEvaluationContext( - new User("test_user"), - ImmutableSet.copyOf(roles), - null, - null, - null, - null, - null, - null, - actionPrivileges - ); + return MockPrivilegeEvaluationContextBuilder.ctx().roles(roles).actionPrivileges(actionPrivileges).get(); } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/TenantPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/TenantPrivilegesTest.java index 315e738bc1..e0ca9c07ca 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/TenantPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/TenantPrivilegesTest.java @@ -22,7 +22,6 @@ import java.util.Set; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.fasterxml.jackson.core.JsonProcessingException; import org.apache.commons.io.IOUtils; import org.junit.Test; @@ -30,9 +29,6 @@ import org.junit.runners.Parameterized; import org.junit.runners.Suite; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.CType; @@ -40,7 +36,7 @@ import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.securityconf.impl.v7.TenantV7; -import org.opensearch.security.user.User; +import org.opensearch.security.util.MockPrivilegeEvaluationContextBuilder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -377,33 +373,11 @@ public void implicitGlobalTenantAccessGrantedByKibanaUserRole_notGranted() throw } static PrivilegesEvaluationContext ctx(String... roles) { - User user = new User("test_user").withAttributes(ImmutableMap.of("attrs.dept_no", "a1")); - return new PrivilegesEvaluationContext( - user, - ImmutableSet.copyOf(roles), - null, - null, - null, - null, - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - null, - ActionPrivileges.EMPTY - ); + return MockPrivilegeEvaluationContextBuilder.ctx().roles(roles).attr("attrs.dept_no", "a1").get(); } static PrivilegesEvaluationContext ctxWithDifferentUserAttr(String... roles) { - User user = new User("test_user").withAttributes(ImmutableMap.of("attrs.dept_no", "a10")); - return new PrivilegesEvaluationContext( - user, - ImmutableSet.copyOf(roles), - null, - null, - null, - null, - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - null, - ActionPrivileges.EMPTY - ); + return MockPrivilegeEvaluationContextBuilder.ctx().roles(roles).attr("attrs.dept_no", "a10").get(); } static String testResource(String fileName) throws IOException { diff --git a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/EmptyActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/EmptyActionPrivilegesTest.java new file mode 100644 index 0000000000..f7278325f2 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/EmptyActionPrivilegesTest.java @@ -0,0 +1,67 @@ +/* + * 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; + +import java.util.Set; + +import org.junit.Test; + +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; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.missingPrivileges; +import static org.opensearch.security.util.MockPrivilegeEvaluationContextBuilder.ctx; + +public class EmptyActionPrivilegesTest { + final ActionPrivileges subject = ActionPrivileges.EMPTY; + + @Test + public void hasClusterPrivilege() { + assertThat( + subject.hasClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), + isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) + ); + } + + @Test + public void hasAnyClusterPrivilege() { + assertThat(subject.hasAnyClusterPrivilege(ctx().get(), Set.of("cluster:monitor/nodes/stats")), isForbidden()); + } + + @Test + public void hasExplicitClusterPrivilege() { + assertThat(subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), isForbidden()); + } + + @Test + public void hasIndexPrivilege() { + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( + ctx().get(), + Set.of("indices:data/write/index"), + IndexResolverReplacer.Resolved.ofIndex("any_index") + ); + assertThat(result, isForbidden()); + } + + @Test + public void hasExplicitIndexPrivilege() { + PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( + ctx().get(), + Set.of("indices:data/write/index"), + IndexResolverReplacer.Resolved.ofIndex("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 2f17f0f283..475cc8fc22 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java @@ -30,7 +30,6 @@ import org.junit.runners.Suite; import org.opensearch.action.support.IndicesOptions; -import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.Metadata; @@ -649,12 +648,6 @@ 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(); - /** - * A mock cluster state; this transports the INDEX_METADATA via PrivilegeEvaluationContext to the - * actual privileges evaluation implementation. - */ - final static ClusterState CLUSTER_STATE = ClusterState.builder(ClusterState.EMPTY_STATE).metadata(INDEX_METADATA).build(); - static IndexResolverReplacer.Resolved resolved(String... indices) { ImmutableSet.Builder allIndices = ImmutableSet.builder(); 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 30ed2dadfe..63d630b289 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java @@ -47,6 +47,7 @@ import static org.opensearch.security.util.MockIndexMetadataBuilder.dataStreams; import static org.opensearch.security.util.MockIndexMetadataBuilder.indices; import static org.opensearch.security.util.MockPrivilegeEvaluationContextBuilder.ctx; +import static org.junit.Assert.assertTrue; /** * Unit tests for SubjectBasedActionPrivilegesTest. As the ActionPrivileges provides quite a few different code paths for checking @@ -685,6 +686,29 @@ public void hasExplicitIndexPrivilege_negative_wrongAction() throws Exception { ); assertThat(result, isForbidden()); } + + @Test + public void hasExplicitIndexPrivilege_errors() throws Exception { + RoleV7 config = config(""" + index_permissions: + - index_patterns: ['/invalid_regex${user.name}\\/'] + allowed_actions: ['system:admin/system*'] + """); + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + + PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( + ctx().get(), + Set.of("system:admin/system_index"), + IndexResolverReplacer.Resolved.ofIndex("test_index") + ); + assertThat(result, isForbidden()); + assertTrue(result.hasEvaluationExceptions()); + assertTrue( + "Result contains exception info: " + result.getEvaluationExceptionInfo(), + result.getEvaluationExceptionInfo().startsWith("Exceptions encountered during privilege evaluation:") + ); + } + } static RoleV7 config(String config) { 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 8814bc10e1..4d93f05ece 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java @@ -59,6 +59,7 @@ import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.user.User; +import org.opensearch.security.util.MockPrivilegeEvaluationContextBuilder; import org.opensearch.test.framework.TestSecurityConfig; import static org.hamcrest.MatcherAssert.assertThat; @@ -1150,19 +1151,7 @@ public void invalidQuery() throws Exception { @Test(expected = PrivilegesEvaluationException.class) public void invalidTemplatedQuery() throws Exception { DocumentPrivileges.DlsQuery.create("{\"invalid\": \"totally ${attr.foo}\"}", xContentRegistry) - .evaluate( - new PrivilegesEvaluationContext( - new User("test_user"), - ImmutableSet.of(), - null, - null, - null, - null, - null, - null, - ActionPrivileges.EMPTY - ) - ); + .evaluate(MockPrivilegeEvaluationContextBuilder.ctx().get()); } @Test diff --git a/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java b/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java index 4dac4727e9..a2312f0f7f 100644 --- a/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java +++ b/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java @@ -25,6 +25,7 @@ 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.PrivilegesEvaluationContext; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.user.User; @@ -45,6 +46,7 @@ public static MockPrivilegeEvaluationContextBuilder ctx() { private Map attributes = new HashMap<>(); private Set roles = new HashSet<>(); private ClusterState clusterState = EMPTY_CLUSTER_STATE; + private ActionPrivileges actionPrivileges = ActionPrivileges.EMPTY; public MockPrivilegeEvaluationContextBuilder attr(String key, String value) { this.attributes.put(key, value); @@ -65,6 +67,11 @@ public MockPrivilegeEvaluationContextBuilder roles(String... roles) { return this; } + public MockPrivilegeEvaluationContextBuilder actionPrivileges(ActionPrivileges actionPrivileges) { + this.actionPrivileges = actionPrivileges; + return this; + } + public PrivilegesEvaluationContext get() { IndexNameExpressionResolver indexNameExpressionResolver = new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)); @@ -78,7 +85,7 @@ public PrivilegesEvaluationContext get() { new IndexResolverReplacer(indexNameExpressionResolver, () -> clusterState, null), indexNameExpressionResolver, () -> clusterState, - null + this.actionPrivileges ); } } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index b3dbfd15dd..bf229ed995 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -1160,8 +1160,7 @@ public Collection createComponents( settings, privilegesInterceptor, cih, - irr, - namedXContentRegistry.get() + irr ); dlsFlsBaseContext = new DlsFlsBaseContext(evaluator, threadPool.getThreadContext(), adminDns); diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 14de3f1dab..211098ae0c 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -76,7 +76,6 @@ import org.opensearch.action.update.UpdateAction; import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.metadata.AliasMetadata; -import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.metadata.Metadata; @@ -85,7 +84,6 @@ import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.Strings; import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.index.reindex.ReindexAction; import org.opensearch.script.mustache.RenderSearchTemplateAction; import org.opensearch.security.auditlog.AuditLog; @@ -157,12 +155,10 @@ public class PrivilegesEvaluator { private final TermsAggregationEvaluator termsAggregationEvaluator; private final PitPrivilegesEvaluator pitPrivilegesEvaluator; private DynamicConfigModel dcm; - private final NamedXContentRegistry namedXContentRegistry; private final Settings settings; private final AtomicReference actionPrivileges = new AtomicReference<>(); private final AtomicReference tenantPrivileges = new AtomicReference<>(); private final Map pluginIdToActionPrivileges = new HashMap<>(); - private final Supplier> indexMetadataSupplier; /** * The pure static action groups should be ONLY used by action privileges for plugins; only those cannot and should @@ -183,8 +179,7 @@ public PrivilegesEvaluator( final Settings settings, final PrivilegesInterceptor privilegesInterceptor, final ClusterInfoHolder clusterInfoHolder, - final IndexResolverReplacer irr, - NamedXContentRegistry namedXContentRegistry + final IndexResolverReplacer irr ) { super(); @@ -208,12 +203,10 @@ public PrivilegesEvaluator( protectedIndexAccessEvaluator = new ProtectedIndexAccessEvaluator(settings, auditLog); termsAggregationEvaluator = new TermsAggregationEvaluator(); pitPrivilegesEvaluator = new PitPrivilegesEvaluator(); - this.namedXContentRegistry = namedXContentRegistry; this.configurationRepository = configurationRepository; this.staticActionGroups = new FlattenedActionGroups( DynamicConfigFactory.addStatics(SecurityDynamicConfiguration.empty(CType.ACTIONGROUPS)) ); - this.indexMetadataSupplier = () -> clusterStateSupplier.get().metadata().getIndicesLookup(); if (configurationRepository != null) { configurationRepository.subscribeOnChange(configMap -> { 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 bbfa3e4464..27ddaff3df 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java @@ -21,7 +21,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import org.apache.commons.collections4.CollectionUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -62,10 +61,7 @@ public class SubjectBasedActionPrivileges extends RuntimeOptimizedActionPrivileg * specified in the roles configuration. */ public SubjectBasedActionPrivileges(RoleV7 role, FlattenedActionGroups actionGroups) { - super( - new ClusterPrivileges(actionGroups.resolve(role.getCluster_permissions())), - new IndexPrivileges(role, actionGroups, WellKnownActions.INDEX_ACTIONS, WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS) - ); + super(new ClusterPrivileges(actionGroups.resolve(role.getCluster_permissions())), new IndexPrivileges(role, actionGroups)); } /** @@ -214,16 +210,6 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde */ private final ImmutableSet actionsWithWildcardIndexPrivileges; - /** - * A pre-defined set of action names that is used to pre-compute the result of action patterns. - */ - private final ImmutableSet wellKnownIndexActions; - - /** - * A pre-defined set of action names that is included in the rolesToExplicitActionToIndexPattern data structure - */ - private final ImmutableSet explicitlyRequiredIndexActions; - /** * Maps role names to concrete action names to IndexPattern objects which define the indices the privileges apply to. * The action names are only explicitly granted privileges which are listed in explicitlyRequiredIndexActions. @@ -240,12 +226,7 @@ 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( - RoleV7 role, - FlattenedActionGroups actionGroups, - ImmutableSet wellKnownIndexActions, - ImmutableSet explicitlyRequiredIndexActions - ) { + IndexPrivileges(RoleV7 role, FlattenedActionGroups actionGroups) { Map actionToIndexPattern = new HashMap<>(); Map actionPatternToIndexPattern = new HashMap<>(); @@ -267,7 +248,7 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde actionToIndexPattern.computeIfAbsent(permission, k -> new IndexPattern.Builder()) .add(indexPermissions.getIndex_patterns()); - if (explicitlyRequiredIndexActions.contains(permission)) { + if (WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS.contains(permission)) { explicitActionToIndexPattern.computeIfAbsent(permission, k -> new IndexPattern.Builder()) .add(indexPermissions.getIndex_patterns()); } @@ -278,7 +259,7 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde } else { WildcardMatcher actionMatcher = WildcardMatcher.from(permission); - for (String action : actionMatcher.iterateMatching(wellKnownIndexActions)) { + for (String action : actionMatcher.iterateMatching(WellKnownActions.INDEX_ACTIONS)) { actionToIndexPattern.computeIfAbsent(action, k -> new IndexPattern.Builder()) .add(indexPermissions.getIndex_patterns()); @@ -291,7 +272,7 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde .add(indexPermissions.getIndex_patterns()); if (actionMatcher != WildcardMatcher.ANY) { - for (String action : actionMatcher.iterateMatching(explicitlyRequiredIndexActions)) { + for (String action : actionMatcher.iterateMatching(WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS)) { explicitActionToIndexPattern.computeIfAbsent(action, k -> new IndexPattern.Builder()) .add(indexPermissions.getIndex_patterns()); } @@ -313,9 +294,6 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde this.explicitActionToIndexPattern = explicitActionToIndexPattern.entrySet() .stream() .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry -> entry.getValue().build())); - - this.wellKnownIndexActions = wellKnownIndexActions; - this.explicitlyRequiredIndexActions = explicitlyRequiredIndexActions; } /** @@ -352,7 +330,7 @@ protected PrivilegesEvaluatorResponse providesPrivilege( // 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)) { + if (!allWellKnownIndexActions(actions)) { checkPrivilegesForNonWellKnownActions(context, actions, checkTable, this.actionPatternToIndexPattern, exceptions); if (checkTable.isComplete()) { return PrivilegesEvaluatorResponse.ok(); @@ -397,10 +375,6 @@ protected PrivilegesEvaluatorResponse providesExplicitPrivilege( Map indexMetadata = context.getIndicesLookup(); List exceptions = new ArrayList<>(); - if (!CollectionUtils.containsAny(actions, this.explicitlyRequiredIndexActions)) { - return PrivilegesEvaluatorResponse.insufficient(CheckTable.create(ImmutableSet.of("_"), actions)); - } - for (String action : actions) { IndexPattern indexPattern = this.explicitActionToIndexPattern.get(action); @@ -412,8 +386,8 @@ protected PrivilegesEvaluatorResponse providesExplicitPrivilege( } } 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)); + log.error("Error while evaluating {}. Ignoring entry", indexPattern, e); + exceptions.add(new PrivilegesEvaluationException("Error while evaluating " + indexPattern, e)); } } } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java index 53d6222c63..064207c5f8 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java @@ -52,8 +52,8 @@ public static RoleV7 fromYamlString(String yamlString) throws IOException { * useful for tests. */ public static RoleV7 fromYamlStringUnchecked(String yamlString) { - try (Reader yamlReader = new StringReader(yamlString)) { - return fromYaml(yamlReader); + try { + return fromYamlString(yamlString); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index cfda9bc386..c37f191778 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -159,7 +159,6 @@ PrivilegesEvaluator createPrivilegesEvaluator(SecurityDynamicConfiguration Date: Sat, 7 Jun 2025 07:48:25 +0200 Subject: [PATCH 08/10] Changes after review remarks Signed-off-by: Nils Bandener --- .../actionlevel/RoleBasedActionPrivileges.java | 12 ++++++++---- .../actionlevel/SubjectBasedActionPrivileges.java | 5 ++--- 2 files changed, 10 insertions(+), 7 deletions(-) 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 49eb30d6ca..5fc9a4c578 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java @@ -34,7 +34,6 @@ 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.ActionPrivileges; import org.opensearch.security.privileges.ClusterStateMetadataDependentPrivileges; import org.opensearch.security.privileges.IndexPattern; import org.opensearch.security.privileges.PrivilegesEvaluationContext; @@ -60,7 +59,7 @@ * instance of this class corresponds to the life-cycle of the role and action group configuration. If the role or * action group configuration is changed, a new instance needs to be built. */ -public class RoleBasedActionPrivileges extends RuntimeOptimizedActionPrivileges implements ActionPrivileges { +public class RoleBasedActionPrivileges extends RuntimeOptimizedActionPrivileges { /** * This setting controls the allowed heap size of the precomputed index privileges (in the inner class StatefulIndexPrivileges). @@ -93,8 +92,13 @@ public RoleBasedActionPrivileges(SecurityDynamicConfiguration roles, Fla } /** - * Updates the stateful index configuration with the given indices. Should be normally only called by - * updateStatefulIndexPrivilegesAsync(). Package visible for testing. + * Updates the stateful index configuration with the given indices. This should be only used in two situations: + *
      + *
    • A new instance of RoleBasedActionPrivileges is created
    • + *
    • The cluster state changes
    • + *
    + * On large clusters this update can take a time in the magnitude of 1000 ms to complete. Thus, calling + * the async method updateStatefulIndexPrivilegesAsync(). Should be preferred. */ public void updateStatefulIndexPrivileges(Map indices, long metadataVersion) { StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); 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 27ddaff3df..3b9de0c85f 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java @@ -25,7 +25,6 @@ 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; @@ -42,13 +41,13 @@ /** * An ActionPrivileges implementation that is valid only for a single entity. * This means that individual instances of this class must be created for individual entities. The mapped roles - * from the context are not regarded by this method. + * from the context are not regarded by this class. *

    * The method PrivilegesEvaluator.createContext() is responsible for making sure that the correct class is used. *

    * This class is useful for plugin users and API tokens. */ -public class SubjectBasedActionPrivileges extends RuntimeOptimizedActionPrivileges implements ActionPrivileges { +public class SubjectBasedActionPrivileges extends RuntimeOptimizedActionPrivileges { private static final Logger log = LogManager.getLogger(SubjectBasedActionPrivileges.class); /** From 7b6bb65ab02bac8d96d23b10381cffcb4607a4ba Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Sat, 7 Jun 2025 07:59:25 +0200 Subject: [PATCH 09/10] Corrected comments Signed-off-by: Nils Bandener --- .../SubjectBasedActionPrivileges.java | 56 ++++++++----------- 1 file changed, 23 insertions(+), 33 deletions(-) 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 3b9de0c85f..ee04f61105 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java @@ -77,16 +77,16 @@ protected StatefulIndexPrivileges currentStatefulIndexPrivileges() { * Pre-computed, optimized cluster privilege maps. Instances of this class are immutable. *

    * The data structures in this class are optimized for answering the question - * "I have action A and roles [x,y,z]. Do I have authorization to execute the action?". + * "I have action A. Do I have authorization to execute the action?". *

    * The check will be possible in time O(1) for "well-known" actions when the user actually has the privileges. */ static class ClusterPrivileges extends RuntimeOptimizedActionPrivileges.ClusterPrivileges { /** - * Maps names of actions to the roles that provide a privilege for the respective action. - * Note that the mapping is not comprehensive, additionally the data structures rolesWithWildcardPermissions - * and rolesToActionMatcher need to be considered for a full view of the privileges. + * A set of action names for which the subject has been granted a privilege. + * Note that the mapping is not comprehensive, additionally the attribute providesWildcardPrivilege + * and grantedActionMatcher need to be considered for a full view of the privileges. *

    * This does not include privileges obtained via "*" action patterns. This is both meant as a * optimization and to support explicit privileges. @@ -94,14 +94,14 @@ static class ClusterPrivileges extends RuntimeOptimizedActionPrivileges.ClusterP private final ImmutableSet grantedActions; /** - * This contains all role names that provide wildcard (*) privileges for cluster actions. - * This avoids a blow-up of the actionToRoles object by such roles. + * This is true if the current subject was granted wildcard (*) privileges for cluster actions. + * This avoids a blow-up of the grantedActions object by such configurations. */ private final boolean providesWildcardPrivilege; /** - * This maps role names to a matcher which matches the action names this role provides privileges for. - * This is only used as a last resort if the test with actionToRole and rolesWithWildcardPermissions failed. + * This WildcardMatcher matches the privileges of the current subject against action names this. + * This is only used as a last resort if the test with grantedActions and providesWildcardPrivilege failed. * This is only necessary for actions which are not contained in the list of "well-known" actions provided * during construction. * @@ -112,11 +112,6 @@ static class ClusterPrivileges extends RuntimeOptimizedActionPrivileges.ClusterP /** * Creates pre-computed cluster privileges based on the given permission patterns. - *

    - * This constructor will not throw an exception if it encounters any invalid configuration (that is, - * in particular, unparseable regular expressions). Rather, it will just log an error. This is okay, as it - * just results in fewer available privileges. However, having a proper error reporting mechanism would be - * kind of nice. * * @param permissionPatterns a collection of strings representing WildcardMatcher patterns that can match * on action names. Any action groups must have been already resolved before these @@ -129,16 +124,16 @@ static class ClusterPrivileges extends RuntimeOptimizedActionPrivileges.ClusterP for (String permission : permissionPatterns) { // If we have a permission which does not use any pattern, we just simply add it to the - // "actionToRoles" map. + // "grantedActions" set. // Otherwise, we match the pattern against the provided well-known cluster actions and add - // these to the "actionToRoles" map. Additionally, for the case that the well-known cluster + // these to the "grantedActions" set. Additionally, for the case that the well-known cluster // actions are not complete, we also collect the matcher to be used as a last resort later. if (WildcardMatcher.isExact(permission)) { grantedActions.add(permission); } else if (permission.equals("*")) { - // Special case: Roles with a wildcard "*" giving privileges for all actions. We will not resolve - // this stuff, but just note separately that this role just gets all the cluster privileges. + // Special case: Configurations with a wildcard "*" giving privileges for all actions. We will not resolve + // this stuff, but just note separately that this subject just gets all the cluster privileges. hasWildcardPermission = true; } else { WildcardMatcher wildcardMatcher = WildcardMatcher.from(permission); @@ -180,7 +175,7 @@ protected boolean checkPrivilegeViaActionMatcher(PrivilegesEvaluationContext con *

    * Purposes of this class: *

    - * 1. Answer the question "given an action and a set of roles, do I have wildcard index privileges" in O(1) + * 1. Answer the question "given an action, do I have wildcard index privileges" in O(1) *

    * 2. Pre-compute the data structures as far as possible in cases that StatefulIndexPermissions cannot check the * permissions. This is the case when: @@ -192,25 +187,25 @@ protected boolean checkPrivilegeViaActionMatcher(PrivilegesEvaluationContext con */ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticIndexPrivileges { /** - * Maps role names to concrete action names to IndexPattern objects which define the indices the privileges apply to. + * Maps concrete action names to IndexPattern objects which define the indices the privileges apply to. */ private final ImmutableMap actionToIndexPattern; /** - * Maps role names to action names matchers to IndexPattern objects which define the indices the privileges apply to. + * Maps action names matchers to IndexPattern objects which define the indices the privileges apply to. * This is especially for "non-well-known" actions. */ private final ImmutableMap actionPatternToIndexPattern; /** - * Maps action names to the roles which provide wildcard ("*") index privileges for the respective action. - * This allows to answer the question "given an action and a set of roles, do I have wildcard index privileges" + * A set of action names for which the subject has wildcard ("*") index privileges. + * This allows to answer the question "given an action, do I have wildcard index privileges" * in O(1) */ private final ImmutableSet actionsWithWildcardIndexPrivileges; /** - * Maps role names to concrete action names to IndexPattern objects which define the indices the privileges apply to. + * Maps concrete action names to IndexPattern objects which define the indices the privileges apply to. * The action names are only explicitly granted privileges which are listed in explicitlyRequiredIndexActions. *

    * Compare https://github.com/opensearch-project/security/pull/2887 @@ -219,11 +214,6 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde /** * Creates pre-computed index privileges based on the given parameters. - *

    - * This constructor will not throw an exception if it encounters any invalid configuration (that is, - * in particular, unparseable regular expressions). Rather, it will just log an error. This is okay, as it - * just results in fewer available privileges. However, having a proper error reporting mechanism would be - * kind of nice. */ IndexPrivileges(RoleV7 role, FlattenedActionGroups actionGroups) { @@ -296,8 +286,8 @@ static class IndexPrivileges extends RuntimeOptimizedActionPrivileges.StaticInde } /** - * Checks whether this instance provides privileges for the combination of the provided action, - * the provided indices and the provided roles. + * Checks whether this instance provides privileges for the combination of the provided action and + * the provided indices. *

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

    @@ -325,7 +315,7 @@ protected PrivilegesEvaluatorResponse providesPrivilege( return PrivilegesEvaluatorResponse.ok(); } - // If all actions are well-known, the index.rolesToActionToIndexPattern data structure that was evaluated above, + // If all actions are well-known, the index.actionToIndexPattern 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 @@ -359,8 +349,8 @@ protected PrivilegesEvaluatorResponse checkWildcardIndexPrivilegesOnWellKnownAct } /** - * Checks whether this instance provides explicit privileges for the combination of the provided action, - * the provided indices and the provided roles. + * Checks whether this instance provides explicit privileges for the combination of the provided action and + * the provided indices. *

    * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 From 7931ab2b7abbc25f7417c2bcf1bf55a2a7597822 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Wed, 11 Jun 2025 08:17:49 +0200 Subject: [PATCH 10/10] Fix Signed-off-by: Nils Bandener --- .../security/privileges/PrivilegesEvaluatorUnitTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java b/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java index f412b6f501..787d35e285 100644 --- a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java +++ b/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java @@ -171,8 +171,7 @@ public void setUp() { settings, privilegesInterceptor, clusterInfoHolder, - irr, - namedXContentRegistry + irr ); }