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..09c122cc2b 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java @@ -46,11 +46,12 @@ 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; 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; @@ -113,10 +114,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, Settings.EMPTY); } @Test @@ -250,8 +251,8 @@ 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 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 6c549a911d..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,31 +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 - ); + 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 - ); + 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/ActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java similarity index 75% rename from src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java rename to src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java index e2830246da..475cc8fc22 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java @@ -8,12 +8,11 @@ * 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; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,11 +32,12 @@ 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.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; +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; @@ -53,6 +53,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; @@ -66,12 +67,12 @@ */ @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 { public static class ClusterPrivileges { @Test public void wellKnown() throws Exception { @@ -79,15 +80,15 @@ 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, 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")) ); } @@ -98,15 +99,18 @@ 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, 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")) ); } @@ -117,33 +121,11 @@ public void wildcard() throws Exception { " cluster_permissions:\n" + // " - '*'", CType.ROLES); - ActionPrivileges subject = new ActionPrivileges(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 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("*")) - ); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + assertThat(subject.hasClusterPrivilege(ctx().roles("test_role").get(), "cluster:whatever"), isAllowed()); assertThat( - subject.hasClusterPrivilege(ctxByUsername("plugin:org.opensearch.sample.SamplePlugin"), "cluster:whatever"), - isAllowed() - ); - assertThat( - subject.hasClusterPrivilege(ctx("plugin:org.opensearch.other.OtherPlugin"), "cluster:whatever"), + subject.hasClusterPrivilege(ctx().roles("other_role").get(), "cluster:whatever"), isForbidden(missingPrivileges("cluster:whatever")) ); } @@ -162,16 +144,19 @@ 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, 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")) ); } @@ -190,16 +175,22 @@ 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, 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")) ); } @@ -210,23 +201,26 @@ 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, 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")) ); } @@ -237,30 +231,33 @@ 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, 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() ); } @@ -271,15 +268,16 @@ 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, 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")) ); } + } /** @@ -306,17 +304,21 @@ 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 { - 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")) { @@ -330,7 +332,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, @@ -357,20 +359,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("*")) { @@ -380,23 +386,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)) { @@ -461,32 +450,26 @@ 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) { 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, - WellKnownActions.CLUSTER_ACTIONS, - WellKnownActions.INDEX_ACTIONS, - WellKnownActions.INDEX_ACTIONS, - Map.of() - ); + 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")// @@ -496,8 +479,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( @@ -508,6 +490,7 @@ static IndexResolverReplacer.Resolved resolved(String... indices) { IndicesOptions.LENIENT_EXPAND_OPEN ); } + } @RunWith(Parameterized.class) @@ -518,11 +501,11 @@ 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 { - 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()); @@ -538,7 +521,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, @@ -569,14 +552,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))); } @@ -642,32 +625,34 @@ 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) { 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, 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(); 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( @@ -823,7 +808,7 @@ public void relevantOnly_identity() throws Exception { assertTrue( "relevantOnly() returned identical object", - ActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata) == metadata + RoleBasedActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata) == metadata ); } @@ -837,7 +822,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 +835,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 +845,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,15 +866,10 @@ public void hasIndexPrivilege_errors() throws Exception { CType.ROLES ); - ActionPrivileges subject = new ActionPrivileges( - 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") ); @@ -895,8 +878,85 @@ 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 @@ -909,15 +969,10 @@ public void hasExplicitIndexPrivilege_errors() throws Exception { CType.ROLES ); - ActionPrivileges subject = new ActionPrivileges( - 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") ); @@ -935,7 +990,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" @@ -943,18 +998,18 @@ public void aliasesOnDataStreamBackingIndices() throws Exception { + " allowed_actions: ['indices:data/write/index']", CType.ROLES ); - ActionPrivileges subject = new ActionPrivileges(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") ); @@ -974,7 +1029,7 @@ 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, Settings.EMPTY); subject.updateStatefulIndexPrivileges(indices, 1); @@ -1069,32 +1124,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 - ); - } - - 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 - ); - } } 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..63d630b289 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivilegesTest.java @@ -0,0 +1,717 @@ +/* + * 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.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.Metadata; +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 static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isAllowed; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isForbidden; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.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; +import static org.junit.Assert.assertTrue; + +/** + * 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. + */ +@RunWith(Suite.class) +@Suite.SuiteClasses({ + SubjectBasedActionPrivilegesTest.ClusterPrivileges.class, + SubjectBasedActionPrivilegesTest.IndexPrivileges.IndicesAndAliases.class, + SubjectBasedActionPrivilegesTest.IndexPrivileges.DataStreams.class, + SubjectBasedActionPrivilegesTest.Misc.class }) +public class SubjectBasedActionPrivilegesTest { + public static class ClusterPrivileges { + @Test + public void wellKnown() 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/stats"), isAllowed()); + } + + @Test + public void notWellKnown() 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/stats/somethingnotwellknown"), isAllowed()); + } + + @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 { + RoleV7 config = config(""" + cluster_permissions: + - '*' + """); + + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + assertThat(subject.hasClusterPrivilege(ctx().get(), "cluster:whatever"), isAllowed()); + } + + @Test + public void explicit_wellKnown() throws Exception { + RoleV7 config = config(""" + cluster_permissions: + - 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 { + RoleV7 config = config(""" + cluster_permissions: + - cluster:monitor/nodes/* + """); + + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + assertThat(subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/notwellknown"), isAllowed()); + } + + @Test + public void explicit_notExplicit() throws Exception { + RoleV7 config = config(""" + cluster_permissions: + - '*' + """); + + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + assertThat( + subject.hasExplicitClusterPrivilege(ctx().get(), "cluster:monitor/nodes/stats"), + isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) + ); + } + + @Test + public void hasAny_wellKnown() throws Exception { + RoleV7 config = config(""" + cluster_permissions: + - cluster:monitor/nodes/stats* + """); + + 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 { + RoleV7 config = config(""" + cluster_permissions: + - '*' + """); + + SubjectBasedActionPrivileges subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + assertThat(subject.hasAnyClusterPrivilege(ctx().get(), ImmutableSet.of("cluster:monitor/nodes/stats")), isAllowed()); + } + } + + public static class IndexPrivileges { + + @RunWith(Parameterized.class) + public static class IndicesAndAliases { + final ActionSpec actionSpec; + final IndexSpec indexSpec; + final RoleV7 config; + final String primaryAction; + final ImmutableSet requiredActions; + final ImmutableSet otherActions; + final SubjectBasedActionPrivileges subject; + + @Test + public void positive_full() throws Exception { + 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().indexMetadata(INDEX_METADATA).get(); + 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().indexMetadata(INDEX_METADATA).get(); + 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().indexMetadata(INDEX_METADATA).get(), + requiredActions, + resolved + ); + assertThat(result, isAllowed()); + } + + @Test + public void negative_wrongAction() throws Exception { + 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("*")) { + assertThat(result, isAllowed()); + } else { + 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}") + 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")// + + )) { + result.add(new Object[] { indexSpec, actionSpec }); + } + } + return result; + } + + public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec) throws Exception { + this.indexSpec = indexSpec; + this.actionSpec = actionSpec; + this.config = indexSpec.toConfig(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.getIndicesLookup(); + + this.subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + } + + 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")// + .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(); + + 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 RoleV7 config; + final String primaryAction; + final ImmutableSet requiredActions; + final ImmutableSet otherActions; + final SubjectBasedActionPrivileges subject; + + @Test + public void positive_full() throws Exception { + 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()); + } 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().indexMetadata(INDEX_METADATA).get(); + 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_wrongAction() throws Exception { + PrivilegesEvaluationContext ctx = ctx().indexMetadata(INDEX_METADATA).get(); + 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}") + 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")// + + )) { + result.add(new Object[] { indexSpec, actionSpec }); + } + } + return result; + } + + public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec) throws Exception { + this.indexSpec = indexSpec; + this.actionSpec = actionSpec; + this.config = indexSpec.toConfig(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.getIndicesLookup(); + this.subject = new SubjectBasedActionPrivileges(config, FlattenedActionGroups.EMPTY); + } + + final static Metadata INDEX_METADATA = // + dataStreams("data_stream_a11", "data_stream_a12", "data_stream_a21", "data_stream_a22", "data_stream_b1", "data_stream_b2") + .build(); + + static IndexResolverReplacer.Resolved resolved(String... indices) { + ImmutableSet.Builder allIndices = ImmutableSet.builder(); + + for (String index : indices) { + IndexAbstraction indexAbstraction = INDEX_METADATA.getIndicesLookup().get(index); + + if (indexAbstraction instanceof IndexAbstraction.DataStream) { + allIndices.addAll( + indexAbstraction.getIndices().stream().map(i -> i.getIndex().getName()).collect(Collectors.toList()) + ); + } + + allIndices.add(index); + } + + return new IndexResolverReplacer.Resolved( + ImmutableSet.of(), + allIndices.build(), + ImmutableSet.copyOf(indices), + ImmutableSet.of(), + IndicesOptions.LENIENT_EXPAND_OPEN + ); + } + } + + static 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; + } + + RoleV7 toConfig(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 + ).getCEntry("test_role"); + } 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; + } + } + } + + public static class Misc { + + @Test + public void hasExplicitIndexPrivilege_positive() throws Exception { + RoleV7 config = config(""" + index_permissions: + - index_patterns: ['test_index'] + allowed_actions: ['system:admin/system_index'] + """); + + 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, isAllowed()); + } + + @Test + public void hasExplicitIndexPrivilege_positive_pattern() throws Exception { + RoleV7 config = config(""" + index_permissions: + - index_patterns: ['test_index'] + allowed_actions: ['system:admin/system_index*'] + """); + + 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, isAllowed()); + } + + @Test + 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().get(), + Set.of("system:admin/system_index"), + IndexResolverReplacer.Resolved.ofIndex("test_index") + ); + assertThat(result, isForbidden()); + } + + @Test + 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 result = subject.hasExplicitIndexPrivilege( + ctx().get(), + Set.of("system:admin/system_foo"), + IndexResolverReplacer.Resolved.ofIndex("test_index") + ); + 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) { + return RoleV7.fromYamlStringUnchecked(config); + } +} 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..4d93f05ece 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; @@ -58,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; @@ -526,7 +528,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 +844,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 +1130,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 +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)); + .evaluate(MockPrivilegeEvaluationContextBuilder.ctx().get()); } @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/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java b/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java new file mode 100644 index 0000000000..a2312f0f7f --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java @@ -0,0 +1,91 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * 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.ActionPrivileges; +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; + private ActionPrivileges actionPrivileges = ActionPrivileges.EMPTY; + + 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 MockPrivilegeEvaluationContextBuilder actionPrivileges(ActionPrivileges actionPrivileges) { + this.actionPrivileges = actionPrivileges; + 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, + this.actionPrivileges + ); + } +} diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 27bc923e5d..70255f5c6f 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.actionlevel.RoleBasedActionPrivileges; import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resources.ResourceAccessControlClient; @@ -1156,8 +1156,7 @@ public Collection createComponents( settings, privilegesInterceptor, cih, - irr, - namedXContentRegistry.get() + irr ); dlsFlsBaseContext = new DlsFlsBaseContext(evaluator, threadPool.getThreadContext(), adminDns); @@ -2130,7 +2129,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( @@ -2266,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/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/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index 59c715e5a2..8ecb975238 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -530,10 +530,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/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/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/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 35cd53499c..133cce391f 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -84,15 +84,17 @@ 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; import org.opensearch.security.configuration.ClusterInfoHolder; import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; +import org.opensearch.security.privileges.actionlevel.SubjectBasedActionPrivileges; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.CType; @@ -153,11 +155,18 @@ public class PrivilegesEvaluator { private final TermsAggregationEvaluator termsAggregationEvaluator; private final PitPrivilegesEvaluator pitPrivilegesEvaluator; 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<>(); + + /** + * 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, @@ -170,8 +179,7 @@ public PrivilegesEvaluator( final Settings settings, final PrivilegesInterceptor privilegesInterceptor, final ClusterInfoHolder clusterInfoHolder, - final IndexResolverReplacer irr, - NamedXContentRegistry namedXContentRegistry + final IndexResolverReplacer irr ) { super(); @@ -180,7 +188,6 @@ public PrivilegesEvaluator( this.threadContext = threadContext; this.privilegesInterceptor = privilegesInterceptor; - this.pluginToClusterActions = new HashMap<>(); this.clusterStateSupplier = clusterStateSupplier; this.settings = settings; @@ -196,8 +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)) + ); if (configurationRepository != null) { configurationRepository.subscribeOnChange(configMap -> { @@ -213,9 +222,9 @@ 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); + actionPrivileges.clusterStateMetadataDependentPrivileges().updateClusterStateMetadataAsync(clusterService, threadPool); } }); } @@ -231,19 +240,13 @@ void updateConfiguration( rolesConfiguration = rolesConfiguration.withStaticConfig(); tenantConfiguration = tenantConfiguration.withStaticConfig(); try { - ActionPrivileges actionPrivileges = new ActionPrivileges( - rolesConfiguration, - flattenedActionGroups, - () -> clusterStateSupplier.get().metadata().getIndicesLookup(), - settings, - pluginToClusterActions - ); + RoleBasedActionPrivileges actionPrivileges = new RoleBasedActionPrivileges(rolesConfiguration, flattenedActionGroups, 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(); + oldInstance.clusterStateMetadataDependentPrivileges().shutdown(); } } catch (Exception e) { log.error("Error while updating ActionPrivileges", e); @@ -266,13 +269,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() { @@ -315,9 +314,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) { @@ -374,7 +396,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"); } @@ -858,6 +880,8 @@ private List toString(List aliases) { } public void updatePluginToClusterActions(String pluginIdentifier, Set clusterActions) { - pluginToClusterActions.put(pluginIdentifier, clusterActions); + RoleV7 pluginPermissions = new RoleV7(); + pluginPermissions.setCluster_permissions(ImmutableList.copyOf(clusterActions)); + this.pluginIdToActionPrivileges.put(pluginIdentifier, new SubjectBasedActionPrivileges(pluginPermissions, this.staticActionGroups)); } } 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/actionlevel/RoleBasedActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java new file mode 100644 index 0000000000..5fc9a4c578 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java @@ -0,0 +1,893 @@ +/* + * 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.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 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.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; +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 static org.opensearch.security.privileges.actionlevel.WellKnownActions.allWellKnownIndexActions; + +/** + * 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 RuntimeOptimizedActionPrivileges { + + /** + * 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 SecurityDynamicConfiguration roles; + private final FlattenedActionGroups actionGroups; + private final ByteSizeValue statefulIndexMaxHeapSize; + + private final AtomicReference statefulIndex = new AtomicReference<>(); + + 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); + } + + /** + * 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(); + + indices = StatefulIndexPrivileges.relevantOnly(indices); + + if (statefulIndex == null || !statefulIndex.indices.equals(indices)) { + long start = System.currentTimeMillis(); + this.statefulIndex.set(new StatefulIndexPrivileges(roles, actionGroups, 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; + } + } + } + } + + int getEstimatedStatefulIndexByteSize() { + StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); + + if (statefulIndex != null) { + return statefulIndex.estimatedByteSize; + } else { + return 0; + } + } + + @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. + *

+ * 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 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. + *

+ * 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; + + /** + * 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) { + DeduplicatingCompactSubSetBuilder roleSetBuilder = new DeduplicatingCompactSubSetBuilder<>( + roles.getCEntries().keySet() + ); + Map> actionToRoles = new HashMap<>(); + ImmutableSet.Builder rolesWithWildcardPermissions = ImmutableSet.builder(); + ImmutableMap.Builder rolesToActionMatcher = 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( + WellKnownActions.CLUSTER_ACTIONS, + 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(); + } + + @Override + protected boolean checkWildcardPrivilege(PrivilegesEvaluationContext context) { + return CollectionUtils.containsAny(context.getMappedRoles(), this.rolesWithWildcardPermissions); + } + + @Override + protected boolean checkPrivilegeForWellKnownAction(PrivilegesEvaluationContext context, String action) { + ImmutableCompactSubSet rolesWithPrivileges = this.actionToRoles.get(action); + return rolesWithPrivileges != null && rolesWithPrivileges.containsAny(context.getMappedRoles()); + } + + @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); + + if (matcher != null && matcher.test(action)) { + return true; + } + } + } + + return false; + } + } + + /** + * 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 extends RuntimeOptimizedActionPrivileges.StaticIndexPrivileges { + /** + * 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; + + /** + * 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) { + + 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 (WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS.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(WellKnownActions.INDEX_ACTIONS)) { + 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( + WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS + )) { + 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())) + ) + ); + + } + + /** + * 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. + */ + @Override + protected PrivilegesEvaluatorResponse providesPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices, + CheckTable checkTable + ) { + List exceptions = new ArrayList<>(); + + for (String role : context.getMappedRoles()) { + ImmutableMap actionToIndexPattern = this.rolesToActionToIndexPattern.get(role); + if (actionToIndexPattern != null) { + checkPrivilegeWithIndexPatternOnWellKnownActions(context, actions, checkTable, 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 + + if (!checkTable.isComplete() && !allWellKnownIndexActions(actions)) { + for (String role : context.getMappedRoles()) { + ImmutableMap actionPatternToIndexPattern = this.rolesToActionPatternToIndexPattern.get( + role + ); + + if (actionPatternToIndexPattern != null) { + checkPrivilegesForNonWellKnownActions(context, actions, checkTable, actionPatternToIndexPattern, exceptions); + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } + } + + 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 + ) { + 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 + */ + @Override + protected PrivilegesEvaluatorResponse providesExplicitPrivilege( + PrivilegesEvaluationContext context, + Set actions, + CheckTable checkTable + ) { + Map indexMetadata = context.getIndicesLookup(); + List exceptions = new ArrayList<>(); + + 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 extends RuntimeOptimizedActionPrivileges.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; + + 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, + 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( + WellKnownActions.INDEX_ACTIONS, + 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; + } + + /** + * 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. + */ + @Override + protected PrivilegesEvaluatorResponse providesPrivilege( + Set actions, + IndexResolverReplacer.Resolved resolvedIndices, + PrivilegesEvaluationContext context, + CheckTable checkTable + ) { + Map indexMetadata = context.getIndicesLookup(); + 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(); + } + } + + 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..1ab6a11fbb --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RuntimeOptimizedActionPrivileges.java @@ -0,0 +1,466 @@ +/* + * 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.List; +import java.util.Map; +import java.util.Set; + +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.privileges.ActionPrivileges; +import org.opensearch.security.privileges.IndexPattern; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.PrivilegesEvaluationException; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.support.WildcardMatcher; + +import com.selectivem.collections.CheckTable; + +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 + * 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; + + RuntimeOptimizedActionPrivileges(ClusterPrivileges cluster, StaticIndexPrivileges index) { + this.cluster = cluster; + this.index = index; + } + + @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 + ); + + StatefulIndexPrivileges statefulIndex = this.currentStatefulIndexPrivileges(); + PrivilegesEvaluatorResponse resultFromStatefulIndex = null; + + if (statefulIndex != null) { + resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, resolvedIndices, context, checkTable); + + 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); + } + + /** + * 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); + } + + /** + * 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 + ); + + /** + * 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 + ); + + /** + * 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, + ImmutableMap actionToIndexPattern, + List exceptions + ) { + Map indexMetadata = context.getIndicesLookup(); + + 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, + ImmutableMap actionPatternToIndexPattern, + List exceptions + ) { + Map indexMetadata = context.getIndicesLookup(); + + 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, + IndexResolverReplacer.Resolved resolvedIndices, + CheckTable checkTable, + List exceptions + ) { + 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); + } + } + + /** + * 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 + ); + } + +} diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java new file mode 100644 index 0000000000..ee04f61105 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/SubjectBasedActionPrivileges.java @@ -0,0 +1,391 @@ +/* + * 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.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.security.privileges.IndexPattern; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.PrivilegesEvaluationException; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.securityconf.FlattenedActionGroups; +import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.support.WildcardMatcher; + +import com.selectivem.collections.CheckTable; + +import static org.opensearch.security.privileges.actionlevel.WellKnownActions.allWellKnownIndexActions; + +/** + * 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 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 { + private static final Logger log = LogManager.getLogger(SubjectBasedActionPrivileges.class); + + /** + * 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. + */ + public SubjectBasedActionPrivileges(RoleV7 role, FlattenedActionGroups actionGroups) { + super(new ClusterPrivileges(actionGroups.resolve(role.getCluster_permissions())), new IndexPrivileges(role, actionGroups)); + } + + /** + * 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 + protected StatefulIndexPrivileges currentStatefulIndexPrivileges() { + return null; + } + + /** + * 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. 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 { + + /** + * 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. + */ + private final ImmutableSet grantedActions; + + /** + * 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 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. + * + * 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; + + /** + * Creates pre-computed cluster privileges based on the given permission patterns. + * + * @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(ImmutableSet permissionPatterns) { + Set grantedActions = new HashSet<>(); + boolean hasWildcardPermission = false; + 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 + // "grantedActions" set. + // Otherwise, we match the pattern against the provided well-known cluster actions and add + // 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: 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); + Set matchedActions = wildcardMatcher.getMatchAny( + WellKnownActions.CLUSTER_ACTIONS, + Collectors.toUnmodifiableSet() + ); + grantedActions.addAll(matchedActions); + wildcardMatchers.add(wildcardMatcher); + } + } + + this.grantedActions = ImmutableSet.copyOf(grantedActions); + this.providesWildcardPrivilege = hasWildcardPermission; + this.grantedActionMatcher = WildcardMatcher.from(wildcardMatchers); + } + + @Override + protected boolean checkWildcardPrivilege(PrivilegesEvaluationContext context) { + return this.providesWildcardPrivilege; + } + + @Override + protected boolean checkPrivilegeForWellKnownAction(PrivilegesEvaluationContext context, String action) { + return this.grantedActions.contains(action); + } + + @Override + protected boolean checkPrivilegeViaActionMatcher(PrivilegesEvaluationContext context, String action) { + return this.grantedActionMatcher.test(action); + } + } + + /** + * 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, 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 extends RuntimeOptimizedActionPrivileges.StaticIndexPrivileges { + /** + * Maps concrete action names to IndexPattern objects which define the indices the privileges apply to. + */ + private final ImmutableMap actionToIndexPattern; + + /** + * 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; + + /** + * 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 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. + */ + IndexPrivileges(RoleV7 role, FlattenedActionGroups actionGroups) { + + 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 (WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS.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(WellKnownActions.INDEX_ACTIONS)) { + 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(WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS)) { + 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())); + } + + /** + * 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. + *

+ * 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. + */ + @Override + protected PrivilegesEvaluatorResponse providesPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices, + CheckTable checkTable + ) { + List exceptions = new ArrayList<>(); + + checkPrivilegeWithIndexPatternOnWellKnownActions(context, actions, checkTable, actionToIndexPattern, exceptions); + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 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 + + if (!allWellKnownIndexActions(actions)) { + checkPrivilegesForNonWellKnownActions(context, actions, checkTable, this.actionPatternToIndexPattern, exceptions); + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(); + } + } + + 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 + ) { + 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 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 + */ + @Override + protected PrivilegesEvaluatorResponse providesExplicitPrivilege( + PrivilegesEvaluationContext context, + Set actions, + CheckTable checkTable + ) { + Map indexMetadata = context.getIndicesLookup(); + List exceptions = new ArrayList<>(); + + 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 {}. Ignoring entry", indexPattern, e); + exceptions.add(new PrivilegesEvaluationException("Error while evaluating " + indexPattern, e)); + } + } + } + } + + return PrivilegesEvaluatorResponse.insufficient(checkTable) + .reason("No explicit privileges have been provided for the referenced indices.") + .evaluationExceptions(exceptions); + } + } + +} 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..8588f4dee0 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,9 @@ * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ -package org.opensearch.security.privileges; +package org.opensearch.security.privileges.actionlevel; + +import java.util.Collection; import com.google.common.collect.ImmutableSet; @@ -85,4 +87,17 @@ 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/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/RoleV7.java index 2b2da40927..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 @@ -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 { + return fromYamlString(yamlString); + } 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") 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 ); } diff --git a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index 26be32f1e5..150b15bac1 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -167,7 +167,6 @@ PrivilegesEvaluator createPrivilegesEvaluator(SecurityDynamicConfiguration 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; - ActionPrivileges actionPrivileges; + RoleBasedActionPrivileges actionPrivileges; private ThreadContext createThreadContext() { return new ThreadContext(Settings.EMPTY); @@ -128,7 +138,7 @@ public void setup( CType.ROLES ); - this.actionPrivileges = new ActionPrivileges(rolesConfig, FlattenedActionGroups.EMPTY, () -> indexMetadata, Settings.EMPTY); + this.actionPrivileges = new RoleBasedActionPrivileges(rolesConfig, FlattenedActionGroups.EMPTY, Settings.EMPTY); } catch (JsonProcessingException e) { throw new RuntimeException(e); } @@ -151,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) { @@ -162,7 +174,8 @@ PrivilegesEvaluationContext ctx(String action) { null, null, indexNameExpressionResolver, - null + () -> clusterState, + actionPrivileges ); }