From c58ac456b836748be26c74af0e62c2956b687724 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 20 Mar 2025 10:10:16 +0100 Subject: [PATCH] Failure Store Access Authorization (#123986) This PR implements authorization logic for failure store access. It builds on https://github.com/elastic/elasticsearch/pull/122715. Access to the failure store is granted by two privileges: `read_failure_store` and `manage_failure_store`. Either of these privileges lets a user access a failure store via the `::failures` selector, as well as access its backing failure indices. `read_failure_store` grants read access (for example to search documents in a failure store), `manage_failure_store` grants access to write operations, such as rollover. Users with only `read` or `manage` on a data stream do not get failure store access. Vice versa, users with `read_failure_store` and `manage_failure_store` do not get access to regular data in a data stream. The PR implements this by making authorization logic selector-aware. It involves two main changes: 1. Index permission groups now compare the selector under which an index resource is accessed to the selector associated with the group. 2. The `AuthorizedIndices` interface likewise uses selectors to decide which indices to treat as authorized. This part of the change requires a sizable refactor and changes to the interface. The high-level behavior for selector-aware search is as follows: For a user with `read_failure_store` over data stream `logs`: - `POST /logs::failures/_search` returns the documents in the failure store. - `POST /logs/_search` returns a 403. - `POST /logs/_search?ignore_unavailable=true` and `POST /*/_search` return an empty result. Similarly, for a user with `read` over data stream `logs`: - `POST /logs::failures/_search` returns a 403. - `POST /logs/_search` returns documents in the data stream. - `POST /logs::failures/_search?ignore_unavailable=true` and `POST /*::failures/_search` return an empty result. A user with both `read` and `read_failure_store` over data stream `logs` gets access to both `POST /logs::failures/_search` and `POST /logs/_search`. The index privilege `all` automatically grants access to both data and the failures store, as well as all hypothetical future selectors. Resolves: ES-10873 --- .../example/CustomAuthorizationEngine.java | 10 +- .../support/IndexComponentSelector.java | 18 + .../cluster/metadata/IndexAbstraction.java | 12 + .../metadata/IndexAbstractionResolver.java | 13 +- .../metadata/IndexNameExpressionResolver.java | 17 + .../IndexAbstractionResolverTests.java | 52 +- .../security/authz/AuthorizationEngine.java | 38 +- .../accesscontrol/IndicesAccessControl.java | 10 +- .../authz/permission/IndicesPermission.java | 155 +- .../authz/privilege/IndexPrivilege.java | 94 +- .../core/security/user/InternalUsers.java | 26 +- .../ProfileHasPrivilegesRequestTests.java | 11 +- .../authz/permission/LimitedRoleTests.java | 133 +- .../security/user/InternalUsersTests.java | 2 +- .../downsample/TransportDownsampleAction.java | 4 +- .../FailureStoreSecurityRestIT.java | 1724 ++++++++++++++++- .../xpack/security/Security.java | 1 + .../authz/IndicesAndAliasesResolver.java | 35 +- .../xpack/security/authz/RBACEngine.java | 91 +- .../RestGetBuiltinPrivilegesAction.java | 2 +- .../authz/AuthorizedIndicesTests.java | 455 ++++- .../authz/IndicesAndAliasesResolverTests.java | 125 +- .../xpack/security/authz/RBACEngineTests.java | 14 +- .../accesscontrol/IndicesPermissionTests.java | 340 +++- 24 files changed, 3050 insertions(+), 332 deletions(-) diff --git a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java index ea99880117f1..a59db7a8ea56 100644 --- a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java +++ b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java @@ -10,6 +10,7 @@ package org.elasticsearch.example; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.action.support.SubscribableListener; import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.cluster.metadata.ProjectMetadata; @@ -35,7 +36,6 @@ import org.elasticsearch.xpack.core.security.user.User; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.function.Supplier; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -119,19 +119,19 @@ public class CustomAuthorizationEngine implements AuthorizationEngine { ) { if (isSuperuser(requestInfo.getAuthentication().getEffectiveSubject().getUser())) { listener.onResponse(new AuthorizedIndices() { - public Supplier> all() { + public Set all(IndexComponentSelector selector) { return () -> indicesLookup.keySet(); } - public boolean check(String name) { + public boolean check(String name, IndexComponentSelector selector) { return indicesLookup.containsKey(name); } }); } else { listener.onResponse(new AuthorizedIndices() { - public Supplier> all() { + public Set all(IndexComponentSelector selector) { return () -> Set.of(); } - public boolean check(String name) { + public boolean check(String name, IndexComponentSelector selector) { return false; } }); diff --git a/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java b/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java index 5a0d2e3141ae..0f8533b5c5d5 100644 --- a/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java +++ b/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java @@ -72,6 +72,24 @@ public enum IndexComponentSelector implements Writeable { return KEY_REGISTRY.get(key); } + /** + * Like {@link #getByKey(String)} but throws an exception if the key is not recognised. + * @return the selector if recognized. `null` input will return `DATA`. + * @throws IllegalArgumentException if the key was not recognised. + */ + public static IndexComponentSelector getByKeyOrThrow(@Nullable String key) { + if (key == null) { + return DATA; + } + IndexComponentSelector selector = getByKey(key); + if (selector == null) { + throw new IllegalArgumentException( + "Unknown key of index component selector [" + key + "], available options are: " + KEY_REGISTRY.keySet() + ); + } + return selector; + } + public static IndexComponentSelector read(StreamInput in) throws IOException { byte id = in.readByte(); if (in.getTransportVersion().onOrAfter(TransportVersions.REMOVE_ALL_APPLICABLE_SELECTOR) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstraction.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstraction.java index cf6431266f16..87ff1b199d88 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstraction.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstraction.java @@ -100,6 +100,13 @@ public interface IndexAbstraction { return false; } + /** + * @return whether this index abstraction is a failure index of a data stream + */ + default boolean isFailureIndexOfDataStream() { + return false; + } + /** * An index abstraction type. */ @@ -183,6 +190,11 @@ public interface IndexAbstraction { return dataStream; } + @Override + public boolean isFailureIndexOfDataStream() { + return getParentDataStream() != null && getParentDataStream().isFailureStoreIndex(getName()); + } + @Override public boolean isHidden() { return isHidden; diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java index 69fb0146b2e8..6b8619cec34f 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java @@ -22,8 +22,8 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.function.Predicate; -import java.util.function.Supplier; +import java.util.function.BiPredicate; +import java.util.function.Function; public class IndexAbstractionResolver { @@ -37,8 +37,8 @@ public class IndexAbstractionResolver { Iterable indices, IndicesOptions indicesOptions, ProjectMetadata projectMetadata, - Supplier> allAuthorizedAndAvailable, - Predicate isAuthorized, + Function> allAuthorizedAndAvailableBySelector, + BiPredicate isAuthorized, boolean includeDataStreams ) { List finalIndices = new ArrayList<>(); @@ -64,6 +64,7 @@ public class IndexAbstractionResolver { ); } indexAbstraction = expressionAndSelector.v1(); + IndexComponentSelector selector = IndexComponentSelector.getByKeyOrThrow(selectorString); // we always need to check for date math expressions indexAbstraction = IndexNameExpressionResolver.resolveDateMathExpression(indexAbstraction); @@ -71,7 +72,7 @@ public class IndexAbstractionResolver { if (indicesOptions.expandWildcardExpressions() && Regex.isSimpleMatchPattern(indexAbstraction)) { wildcardSeen = true; Set resolvedIndices = new HashSet<>(); - for (String authorizedIndex : allAuthorizedAndAvailable.get()) { + for (String authorizedIndex : allAuthorizedAndAvailableBySelector.apply(selector)) { if (Regex.simpleMatch(indexAbstraction, authorizedIndex) && isIndexVisible( indexAbstraction, @@ -102,7 +103,7 @@ public class IndexAbstractionResolver { resolveSelectorsAndCollect(indexAbstraction, selectorString, indicesOptions, resolvedIndices, projectMetadata); if (minus) { finalIndices.removeAll(resolvedIndices); - } else if (indicesOptions.ignoreUnavailable() == false || isAuthorized.test(indexAbstraction)) { + } else if (indicesOptions.ignoreUnavailable() == false || isAuthorized.test(indexAbstraction, selector)) { // Unauthorized names are considered unavailable, so if `ignoreUnavailable` is `true` they should be silently // discarded from the `finalIndices` list. Other "ways of unavailable" must be handled by the action // handler, see: https://github.com/elastic/elasticsearch/issues/90215 diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java index e221cb5c08e5..fa359794166f 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -28,6 +28,7 @@ import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.core.Assertions; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Predicates; import org.elasticsearch.core.Tuple; @@ -1001,6 +1002,14 @@ public class IndexNameExpressionResolver { return expression.contains(SelectorResolver.SELECTOR_SEPARATOR); } + public static boolean hasSelector(@Nullable String expression, IndexComponentSelector selector) { + Objects.requireNonNull(selector, "null selectors not supported"); + if (expression == null) { + return false; + } + return expression.endsWith(SelectorResolver.SELECTOR_SEPARATOR + selector.getKey()); + } + /** * @return If the specified string is a selector expression then this method returns the base expression and its selector part. */ @@ -1022,6 +1031,14 @@ public class IndexNameExpressionResolver { : (baseExpression + SelectorResolver.SELECTOR_SEPARATOR + selectorExpression); } + public static void assertExpressionHasNullOrDataSelector(String expression) { + if (Assertions.ENABLED) { + var tuple = splitSelectorExpression(expression); + assert tuple.v2() == null || IndexComponentSelector.DATA.getKey().equals(tuple.v2()) + : "Expected expression [" + expression + "] to have a data selector but found [" + tuple.v2() + "]"; + } + } + /** * Resolve an array of expressions to the set of indices and aliases that these expressions match. */ diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolverTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolverTests.java index 96170a3c4d5f..ee2119e2c3b5 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolverTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolverTests.java @@ -27,7 +27,6 @@ import java.io.UncheckedIOException; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; import static org.elasticsearch.index.mapper.MapperService.SINGLE_MAPPING_NAME; import static org.elasticsearch.indices.SystemIndices.EXTERNAL_SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY; @@ -48,7 +47,7 @@ public class IndexAbstractionResolverTests extends ESTestCase { private String dateTimeIndexTomorrow; // Only used when resolving wildcard expressions - private final Supplier> defaultMask = () -> Set.of("index1", "index2", "data-stream1"); + private final Set defaultMask = Set.of("index1", "index2", "data-stream1"); @Override public void setUp() throws Exception { @@ -215,13 +214,11 @@ public class IndexAbstractionResolverTests extends ESTestCase { public void testIsIndexVisible() { assertThat(isIndexVisible("index1", null), is(true)); - assertThat(isIndexVisible("index1", "*"), is(true)); assertThat(isIndexVisible("index1", "data"), is(true)); assertThat(isIndexVisible("index1", "failures"), is(false)); // * // * Indices don't have failure components so the failure component is not visible assertThat(isIndexVisible("data-stream1", null), is(true)); - assertThat(isIndexVisible("data-stream1", "*"), is(true)); assertThat(isIndexVisible("data-stream1", "data"), is(true)); assertThat(isIndexVisible("data-stream1", "failures"), is(true)); } @@ -290,14 +287,14 @@ public class IndexAbstractionResolverTests extends ESTestCase { indexAbstractionResolver = new IndexAbstractionResolver(indexNameExpressionResolver); // this covers the GET * case -- with system access, you can see everything - assertThat(isIndexVisible("other", "*"), is(true)); - assertThat(isIndexVisible(".foo", "*"), is(true)); - assertThat(isIndexVisible(".bar", "*"), is(true)); + assertThat(isIndexVisible("other", null), is(true)); + assertThat(isIndexVisible(".foo", null), is(true)); + assertThat(isIndexVisible(".bar", null), is(true)); // but if you don't ask for hidden and aliases, you won't see hidden indices or aliases, naturally - assertThat(isIndexVisible("other", "*", noHiddenNoAliases), is(true)); - assertThat(isIndexVisible(".foo", "*", noHiddenNoAliases), is(false)); - assertThat(isIndexVisible(".bar", "*", noHiddenNoAliases), is(false)); + assertThat(isIndexVisible("other", null, noHiddenNoAliases), is(true)); + assertThat(isIndexVisible(".foo", null, noHiddenNoAliases), is(false)); + assertThat(isIndexVisible(".bar", null, noHiddenNoAliases), is(false)); } { @@ -311,14 +308,14 @@ public class IndexAbstractionResolverTests extends ESTestCase { indexAbstractionResolver = new IndexAbstractionResolver(indexNameExpressionResolver); // this covers the GET * case -- without system access, you can't see everything - assertThat(isIndexVisible("other", "*"), is(true)); - assertThat(isIndexVisible(".foo", "*"), is(false)); - assertThat(isIndexVisible(".bar", "*"), is(false)); + assertThat(isIndexVisible("other", null), is(true)); + assertThat(isIndexVisible(".foo", null), is(false)); + assertThat(isIndexVisible(".bar", null), is(false)); // no difference here in the datastream case, you can't see these then, either - assertThat(isIndexVisible("other", "*", noHiddenNoAliases), is(true)); - assertThat(isIndexVisible(".foo", "*", noHiddenNoAliases), is(false)); - assertThat(isIndexVisible(".bar", "*", noHiddenNoAliases), is(false)); + assertThat(isIndexVisible("other", null, noHiddenNoAliases), is(true)); + assertThat(isIndexVisible(".foo", null, noHiddenNoAliases), is(false)); + assertThat(isIndexVisible(".bar", null, noHiddenNoAliases), is(false)); } { @@ -333,14 +330,14 @@ public class IndexAbstractionResolverTests extends ESTestCase { indexAbstractionResolver = new IndexAbstractionResolver(indexNameExpressionResolver); // this covers the GET * case -- with product (only) access, you can't see everything - assertThat(isIndexVisible("other", "*"), is(true)); - assertThat(isIndexVisible(".foo", "*"), is(false)); - assertThat(isIndexVisible(".bar", "*"), is(false)); + assertThat(isIndexVisible("other", null), is(true)); + assertThat(isIndexVisible(".foo", null), is(false)); + assertThat(isIndexVisible(".bar", null), is(false)); // no difference here in the datastream case, you can't see these then, either - assertThat(isIndexVisible("other", "*", noHiddenNoAliases), is(true)); - assertThat(isIndexVisible(".foo", "*", noHiddenNoAliases), is(false)); - assertThat(isIndexVisible(".bar", "*", noHiddenNoAliases), is(false)); + assertThat(isIndexVisible("other", null, noHiddenNoAliases), is(true)); + assertThat(isIndexVisible(".foo", null, noHiddenNoAliases), is(false)); + assertThat(isIndexVisible(".bar", null, noHiddenNoAliases), is(false)); } } @@ -366,8 +363,15 @@ public class IndexAbstractionResolverTests extends ESTestCase { return resolveAbstractions(expressions, IndicesOptions.strictExpandOpen(), defaultMask); } - private List resolveAbstractions(List expressions, IndicesOptions indicesOptions, Supplier> mask) { - return indexAbstractionResolver.resolveIndexAbstractions(expressions, indicesOptions, projectMetadata, mask, (idx) -> true, true); + private List resolveAbstractions(List expressions, IndicesOptions indicesOptions, Set mask) { + return indexAbstractionResolver.resolveIndexAbstractions( + expressions, + indicesOptions, + projectMetadata, + (ignored) -> mask, + (ignored, nothing) -> true, + true + ); } private boolean isIndexVisible(String index, String selector) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 9f18e7915a72..a718b1dee04c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -11,8 +11,11 @@ import org.elasticsearch.TransportVersion; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.action.support.SubscribableListener; +import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.cluster.metadata.IndexAbstraction; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.ProjectMetadata; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; @@ -39,7 +42,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Supplier; import java.util.stream.Collectors; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -281,22 +283,23 @@ public interface AuthorizationEngine { } /** - * Used to retrieve index-like resources that the user has access to, for a specific access action type, + * Used to retrieve index-like resources that the user has access to, for a specific access action type and selector, * at a specific point in time (for a fixed cluster state view). * It can also be used to check if a specific resource name is authorized (access to the resource name * can be authorized even if it doesn't exist). */ interface AuthorizedIndices { /** - * Returns all the index-like resource names that are available and accessible for an action type by a user, + * Returns all the index-like resource names that are available and accessible for an action type and selector by a user, * at a fixed point in time (for a single cluster state view). + * The result is cached and subsequent calls to this method are idempotent. */ - Supplier> all(); + Set all(IndexComponentSelector selector); /** * Checks if an index-like resource name is authorized, for an action by a user. The resource might or might not exist. */ - boolean check(String name); + boolean check(String name, IndexComponentSelector selector); } /** @@ -366,6 +369,31 @@ public interface AuthorizationEngine { && application.length == 0) { validationException = addValidationError("must specify at least one privilege", validationException); } + if (index != null) { + // no need to validate failure-store related constraints if it's not enabled + if (DataStream.isFailureStoreFeatureFlagEnabled()) { + for (RoleDescriptor.IndicesPrivileges indexPrivilege : index) { + if (indexPrivilege.getIndices() != null + && Arrays.stream(indexPrivilege.getIndices()) + // best effort prevent users from attempting to check failure selectors + .anyMatch(idx -> IndexNameExpressionResolver.hasSelector(idx, IndexComponentSelector.FAILURES))) { + validationException = addValidationError( + // TODO adjust message once HasPrivileges check supports checking failure store privileges + "failures selector is not supported in index patterns", + validationException + ); + } + if (indexPrivilege.getPrivileges() != null + && Arrays.stream(indexPrivilege.getPrivileges()) + .anyMatch(p -> "read_failure_store".equals(p) || "manage_failure_store".equals(p))) { + validationException = addValidationError( + "checking failure store privileges is not supported", + validationException + ); + } + } + } + } return validationException; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java index 3f0fb18e825c..fe7cbc11df5a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java @@ -6,13 +6,13 @@ */ package org.elasticsearch.xpack.core.security.authz.accesscontrol; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.util.CachedSupplier; import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.core.Nullable; -import org.elasticsearch.core.Tuple; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; import org.elasticsearch.xpack.core.security.authz.permission.DocumentPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; @@ -56,13 +56,15 @@ public class IndicesAccessControl { } /** - * @return The document and field permissions for an index if exist, otherwise null is returned. + * @return The document and field permissions for an index if they exist, otherwise null is returned. * If null is being returned this means that there are no field or document level restrictions. */ @Nullable public IndexAccessControl getIndexPermissions(String index) { - Tuple indexAndSelector = IndexNameExpressionResolver.splitSelectorExpression(index); - return this.getAllIndexPermissions().get(indexAndSelector.v1()); + assert false == IndexNameExpressionResolver.hasSelectorSuffix(index) + || IndexNameExpressionResolver.hasSelector(index, IndexComponentSelector.FAILURES) + : "index name [" + index + "] cannot have explicit selector other than ::failures"; + return getAllIndexPermissions().get(index); } public boolean hasIndexPermissions(String index) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 7d159a8f0b15..3ad3f6fa4f7a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -142,17 +142,33 @@ public final class IndicesPermission { } private IsResourceAuthorizedPredicate buildIndexMatcherPredicateForAction(String action) { - final Set ordinaryIndices = new HashSet<>(); - final Set restrictedIndices = new HashSet<>(); + final Set dataAccessOrdinaryIndices = new HashSet<>(); + final Set failuresAccessOrdinaryIndices = new HashSet<>(); + final Set dataAccessRestrictedIndices = new HashSet<>(); + final Set failuresAccessRestrictedIndices = new HashSet<>(); final Set grantMappingUpdatesOnIndices = new HashSet<>(); final Set grantMappingUpdatesOnRestrictedIndices = new HashSet<>(); final boolean isMappingUpdateAction = isMappingUpdateAction(action); for (final Group group : groups) { if (group.actionMatcher.test(action)) { + final List indexList = Arrays.asList(group.indices()); + final boolean dataAccess = group.checkSelector(IndexComponentSelector.DATA); + final boolean failuresAccess = group.checkSelector(IndexComponentSelector.FAILURES); + assert dataAccess || failuresAccess : "group must grant access at least one of [DATA, FAILURES] selectors"; if (group.allowRestrictedIndices) { - restrictedIndices.addAll(Arrays.asList(group.indices())); + if (dataAccess) { + dataAccessRestrictedIndices.addAll(indexList); + } + if (failuresAccess) { + failuresAccessRestrictedIndices.addAll(indexList); + } } else { - ordinaryIndices.addAll(Arrays.asList(group.indices())); + if (dataAccess) { + dataAccessOrdinaryIndices.addAll(indexList); + } + if (failuresAccess) { + failuresAccessOrdinaryIndices.addAll(indexList); + } } } else if (isMappingUpdateAction && containsPrivilegeThatGrantsMappingUpdatesForBwc(group)) { // special BWC case for certain privileges: allow put mapping on indices and aliases (but not on data streams), even if @@ -164,30 +180,44 @@ public final class IndicesPermission { } } } - final StringMatcher nameMatcher = indexMatcher(ordinaryIndices, restrictedIndices); + final StringMatcher dataAccessNameMatcher = indexMatcher(dataAccessOrdinaryIndices, dataAccessRestrictedIndices); + final StringMatcher failuresAccessNameMatcher = indexMatcher(failuresAccessOrdinaryIndices, failuresAccessRestrictedIndices); final StringMatcher bwcSpecialCaseMatcher = indexMatcher(grantMappingUpdatesOnIndices, grantMappingUpdatesOnRestrictedIndices); - return new IsResourceAuthorizedPredicate(nameMatcher, bwcSpecialCaseMatcher); + return new IsResourceAuthorizedPredicate(dataAccessNameMatcher, failuresAccessNameMatcher, bwcSpecialCaseMatcher); } /** * This encapsulates the authorization test for resources. * There is an additional test for resources that are missing or that are not a datastream or a backing index. */ - public static class IsResourceAuthorizedPredicate implements BiPredicate { + public static class IsResourceAuthorizedPredicate { - private final BiPredicate biPredicate; + private final BiPredicate isAuthorizedForDataAccess; + private final BiPredicate isAuthorizedForFailuresAccess; // public for tests - public IsResourceAuthorizedPredicate(StringMatcher resourceNameMatcher, StringMatcher additionalNonDatastreamNameMatcher) { + public IsResourceAuthorizedPredicate( + StringMatcher dataResourceNameMatcher, + StringMatcher failuresResourceNameMatcher, + StringMatcher additionalNonDatastreamNameMatcher + ) { this((String name, @Nullable IndexAbstraction indexAbstraction) -> { assert indexAbstraction == null || name.equals(indexAbstraction.getName()); - return resourceNameMatcher.test(name) + return dataResourceNameMatcher.test(name) || (isPartOfDatastream(indexAbstraction) == false && additionalNonDatastreamNameMatcher.test(name)); + }, (String name, @Nullable IndexAbstraction indexAbstraction) -> { + assert indexAbstraction == null || name.equals(indexAbstraction.getName()); + // we can't enforce that the abstraction is part of a data stream since we need to account for non-existent resources + return failuresResourceNameMatcher.test(name); }); } - private IsResourceAuthorizedPredicate(BiPredicate biPredicate) { - this.biPredicate = biPredicate; + private IsResourceAuthorizedPredicate( + BiPredicate isAuthorizedForDataAccess, + BiPredicate isAuthorizedForFailuresAccess + ) { + this.isAuthorizedForDataAccess = isAuthorizedForDataAccess; + this.isAuthorizedForFailuresAccess = isAuthorizedForFailuresAccess; } /** @@ -195,18 +225,25 @@ public final class IndicesPermission { * return a new {@link IsResourceAuthorizedPredicate} instance that is equivalent to the conjunction of * authorization tests of that other instance and this one. */ - @Override - public final IsResourceAuthorizedPredicate and(BiPredicate other) { - return new IsResourceAuthorizedPredicate(this.biPredicate.and(other)); + public final IsResourceAuthorizedPredicate and(IsResourceAuthorizedPredicate other) { + return new IsResourceAuthorizedPredicate( + this.isAuthorizedForDataAccess.and(other.isAuthorizedForDataAccess), + this.isAuthorizedForFailuresAccess.and(other.isAuthorizedForFailuresAccess) + ); + } + + // TODO remove me (this has >700 usages in tests which would make for a horrible diff; will remove this once the main PR is merged) + public boolean test(IndexAbstraction indexAbstraction) { + return test(indexAbstraction.getName(), indexAbstraction, IndexComponentSelector.DATA); } /** * Verifies if access is authorized to the given {@param indexAbstraction} resource. - * The resource must exist. Otherwise, use the {@link #test(String, IndexAbstraction)} method. + * The resource must exist. Otherwise, use the {@link #test(String, IndexAbstraction, IndexComponentSelector)} method. * Returns {@code true} if access to the given resource is authorized or {@code false} otherwise. */ - public final boolean test(IndexAbstraction indexAbstraction) { - return test(indexAbstraction.getName(), indexAbstraction); + public boolean test(IndexAbstraction indexAbstraction, IndexComponentSelector selector) { + return test(indexAbstraction.getName(), indexAbstraction, selector); } /** @@ -215,9 +252,10 @@ public final class IndicesPermission { * if it doesn't. * Returns {@code true} if access to the given resource is authorized or {@code false} otherwise. */ - @Override - public boolean test(String name, @Nullable IndexAbstraction indexAbstraction) { - return biPredicate.test(name, indexAbstraction); + public boolean test(String name, @Nullable IndexAbstraction indexAbstraction, IndexComponentSelector selector) { + return IndexComponentSelector.FAILURES.equals(selector) + ? isAuthorizedForFailuresAccess.test(name, indexAbstraction) + : isAuthorizedForDataAccess.test(name, indexAbstraction); } private static boolean isPartOfDatastream(IndexAbstraction indexAbstraction) { @@ -283,6 +321,7 @@ public final class IndicesPermission { combineIndexGroups && checkForIndexPatterns.stream().anyMatch(Automatons::isLuceneRegex) ); for (String forIndexPattern : checkForIndexPatterns) { + IndexNameExpressionResolver.assertExpressionHasNullOrDataSelector(forIndexPattern); Automaton checkIndexAutomaton = Automatons.patterns(forIndexPattern); if (false == allowRestrictedIndices && false == isConcreteRestrictedIndex(forIndexPattern)) { checkIndexAutomaton = Automatons.minusAndMinimize(checkIndexAutomaton, restrictedIndices.getAutomaton()); @@ -338,9 +377,12 @@ public final class IndicesPermission { } public Automaton allowedActionsMatcher(String index) { + Tuple tuple = IndexNameExpressionResolver.splitSelectorExpression(index); + String indexName = tuple.v1(); + IndexComponentSelector selector = IndexComponentSelector.getByKey(tuple.v2()); List automatonList = new ArrayList<>(); for (Group group : groups) { - if (group.indexNameMatcher.test(index)) { + if (group.checkSelector(selector) && group.indexNameMatcher.test(indexName)) { automatonList.add(group.privilege.getAutomaton()); } } @@ -373,15 +415,6 @@ public final class IndicesPermission { assert name != null : "Resource name cannot be null"; assert abstraction == null || abstraction.getName().equals(name) : "Index abstraction has unexpected name [" + abstraction.getName() + "] vs [" + name + "]"; - assert abstraction == null - || selector == null - || IndexComponentSelector.FAILURES.equals(selector) == false - || abstraction.isDataStreamRelated() - : "Invalid index component selector [" - + selector.getKey() - + "] applied to abstraction of type [" - + abstraction.getType() - + "]"; this.name = name; this.indexAbstraction = abstraction; this.selector = selector; @@ -411,11 +444,19 @@ public final class IndicesPermission { public boolean checkIndex(Group group) { final DataStream ds = indexAbstraction == null ? null : indexAbstraction.getParentDataStream(); if (ds != null) { - if (group.checkIndex(ds.getName())) { - return true; - } + if (indexAbstraction.isFailureIndexOfDataStream()) { + // failure indices are special: when accessed directly (not through ::failures on parent data stream) they are accessed + // implicitly as data. However, authz to the parent data stream happens via the failures selector + if (group.checkSelector(IndexComponentSelector.FAILURES) && group.checkIndex(ds.getName())) { + return true; + } + } else if (IndexComponentSelector.DATA.equals(selector) || selector == null) { + if (group.checkSelector(IndexComponentSelector.DATA) && group.checkIndex(ds.getName())) { + return true; + } + } // we don't support granting access to a backing index with a failure selector via the parent data stream } - return group.checkIndex(name); + return group.checkSelector(selector) && group.checkIndex(name); } /** @@ -478,6 +519,13 @@ public final class IndicesPermission { public boolean canHaveBackingIndices() { return indexAbstraction != null && indexAbstraction.getType() != IndexAbstraction.Type.CONCRETE_INDEX; } + + public String nameWithSelector() { + String combined = IndexNameExpressionResolver.combineSelector(name, selector); + assert false != IndexComponentSelector.FAILURES.equals(selector) || name.equals(combined) + : "Only failures selectors should result in explicit selectors suffix"; + return combined; + } } /** @@ -500,18 +548,20 @@ public final class IndicesPermission { int totalResourceCount = 0; Map lookup = metadata.getIndicesLookup(); for (String indexOrAlias : requestedIndicesOrAliases) { - // Remove any selectors from abstraction name. Discard them for this check as we do not have access control for them (yet) + // Remove any selectors from abstraction name. Access control is based on the `selector` field of the IndexResource Tuple expressionAndSelector = IndexNameExpressionResolver.splitSelectorExpression(indexOrAlias); indexOrAlias = expressionAndSelector.v1(); IndexComponentSelector selector = expressionAndSelector.v2() == null ? null : IndexComponentSelector.getByKey(expressionAndSelector.v2()); final IndexResource resource = new IndexResource(indexOrAlias, lookup.get(indexOrAlias), selector); - resources.put(resource.name, resource); + // We can't use resource.name here because we may be accessing a data stream _and_ its failure store, + // where the selector-free name is the same for both and thus ambiguous. + resources.put(resource.nameWithSelector(), resource); totalResourceCount += resource.size(lookup); } - final boolean overallGranted = isActionGranted(action, resources); + final boolean overallGranted = isActionGranted(action, resources.values()); final int finalTotalResourceCount = totalResourceCount; final Supplier> indexPermissions = () -> buildIndicesAccessControl( action, @@ -540,10 +590,11 @@ public final class IndicesPermission { final boolean isMappingUpdateAction = isMappingUpdateAction(action); - for (IndexResource resource : requestedResources.values()) { + for (Map.Entry resourceEntry : requestedResources.entrySet()) { // true if ANY group covers the given index AND the given action boolean granted = false; - + final String resourceName = resourceEntry.getKey(); + final IndexResource resource = resourceEntry.getValue(); final Collection concreteIndices = resource.resolveConcreteIndices(metadata); for (Group group : groups) { // the group covers the given index OR the given index is a backing index and the group covers the parent data stream @@ -590,9 +641,9 @@ public final class IndicesPermission { roleQueriesByIndex.put(index, docPermissions); } - if (index.equals(resource.name) == false) { - fieldPermissionsByIndex.put(resource.name, fieldPermissions); - roleQueriesByIndex.put(resource.name, docPermissions); + if (index.equals(resourceName) == false) { + fieldPermissionsByIndex.put(resourceName, fieldPermissions); + roleQueriesByIndex.put(resourceName, docPermissions); } } } @@ -600,10 +651,11 @@ public final class IndicesPermission { } if (granted) { - grantedResources.add(resource.name); + grantedResources.add(resourceName); + if (resource.canHaveBackingIndices()) { for (String concreteIndex : concreteIndices) { - // If the name appear directly as part of the requested indices, it takes precedence over implicit access + // If the name appears directly as part of the requested indices, it takes precedence over implicit access if (false == requestedResources.containsKey(concreteIndex)) { grantedResources.add(concreteIndex); } @@ -639,11 +691,11 @@ public final class IndicesPermission { * Returns {@code true} if action is granted for all {@code requestedResources}. * If action is not granted for at least one resource, this method will return {@code false}. */ - private boolean isActionGranted(final String action, final Map requestedResources) { + private boolean isActionGranted(final String action, final Collection requestedResources) { final boolean isMappingUpdateAction = isMappingUpdateAction(action); - for (IndexResource resource : requestedResources.values()) { + for (IndexResource resource : requestedResources) { // true if ANY group covers the given index AND the given action boolean granted = false; // true if ANY group, which contains certain ingest privileges, covers the given index AND the action is a mapping update for @@ -758,6 +810,11 @@ public final class IndicesPermission { // Map of privilege automaton object references (cached by IndexPrivilege::CACHE) Map allAutomatons = new HashMap<>(); for (Group group : groups) { + // TODO support failure store privileges + // we also check that the group does not support data access to avoid erroneously filtering out `all` privilege groups + if (group.checkSelector(IndexComponentSelector.FAILURES) && false == group.checkSelector(IndexComponentSelector.DATA)) { + continue; + } Automaton indexAutomaton = group.getIndexMatcherAutomaton(); allAutomatons.compute( group.privilege().getAutomaton(), @@ -864,8 +921,8 @@ public final class IndicesPermission { return query != null; } - public boolean checkSelector(IndexComponentSelector selector) { - return selectorPredicate.test(selector); + public boolean checkSelector(@Nullable IndexComponentSelector selector) { + return selectorPredicate.test(selector == null ? IndexComponentSelector.DATA : selector); } public boolean allowRestrictedIndices() { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 0af2c276beff..f93af05cfd7b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -48,11 +48,13 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.SortedMap; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -86,6 +88,7 @@ public final class IndexPrivilege extends Privilege { ResolveIndexAction.NAME, TransportResolveClusterAction.NAME ); + private static final Automaton READ_FAILURE_STORE_AUTOMATON = patterns("indices:data/read/*", ResolveIndexAction.NAME); private static final Automaton READ_CROSS_CLUSTER_AUTOMATON = patterns( "internal:transport/proxy/indices:data/read/*", TransportClusterSearchShardsAction.TYPE.name(), @@ -183,12 +186,6 @@ public final class IndexPrivilege extends Privilege { public static final IndexPrivilege NONE = new IndexPrivilege("none", Automatons.EMPTY); public static final IndexPrivilege ALL = new IndexPrivilege("all", ALL_AUTOMATON, IndexComponentSelectorPredicate.ALL); - public static final IndexPrivilege READ_FAILURE_STORE = new IndexPrivilege( - "read_failure_store", - // TODO use READ_AUTOMATON here in authorization follow-up - Automatons.EMPTY, - IndexComponentSelectorPredicate.FAILURES - ); public static final IndexPrivilege READ = new IndexPrivilege("read", READ_AUTOMATON); public static final IndexPrivilege READ_CROSS_CLUSTER = new IndexPrivilege("read_cross_cluster", READ_CROSS_CLUSTER_AUTOMATON); public static final IndexPrivilege CREATE = new IndexPrivilege("create", CREATE_AUTOMATON); @@ -219,38 +216,71 @@ public final class IndexPrivilege extends Privilege { CROSS_CLUSTER_REPLICATION_INTERNAL_AUTOMATON ); + public static final IndexPrivilege READ_FAILURE_STORE = new IndexPrivilege( + "read_failure_store", + READ_FAILURE_STORE_AUTOMATON, + IndexComponentSelectorPredicate.FAILURES + ); + public static final IndexPrivilege MANAGE_FAILURE_STORE = new IndexPrivilege( + "manage_failure_store", + MANAGE_AUTOMATON, + IndexComponentSelectorPredicate.FAILURES + ); + /** * If you are adding a new named index privilege, also add it to the * docs. */ - private static final Map VALUES = sortByAccessLevel( - Stream.of( - entry("none", NONE), - entry("all", ALL), - entry("manage", MANAGE), - entry("create_index", CREATE_INDEX), - entry("monitor", MONITOR), - entry("read", READ), - entry("index", INDEX), - entry("delete", DELETE), - entry("write", WRITE), - entry("create", CREATE), - entry("create_doc", CREATE_DOC), - entry("delete_index", DELETE_INDEX), - entry("view_index_metadata", VIEW_METADATA), - entry("read_cross_cluster", READ_CROSS_CLUSTER), - entry("manage_follow_index", MANAGE_FOLLOW_INDEX), - entry("manage_leader_index", MANAGE_LEADER_INDEX), - entry("manage_ilm", MANAGE_ILM), - entry("manage_data_stream_lifecycle", MANAGE_DATA_STREAM_LIFECYCLE), - entry("maintenance", MAINTENANCE), - entry("auto_configure", AUTO_CONFIGURE), - entry("cross_cluster_replication", CROSS_CLUSTER_REPLICATION), - entry("cross_cluster_replication_internal", CROSS_CLUSTER_REPLICATION_INTERNAL), - DataStream.isFailureStoreFeatureFlagEnabled() ? entry("read_failure_store", READ_FAILURE_STORE) : null - ).filter(Objects::nonNull).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)) + private static final Map VALUES = combineSortedInOrder( + sortByAccessLevel( + Stream.of( + DataStream.isFailureStoreFeatureFlagEnabled() ? entry("read_failure_store", READ_FAILURE_STORE) : null, + DataStream.isFailureStoreFeatureFlagEnabled() ? entry("manage_failure_store", MANAGE_FAILURE_STORE) : null + ).filter(Objects::nonNull).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)) + ), + sortByAccessLevel( + Stream.of( + entry("none", NONE), + entry("all", ALL), + entry("manage", MANAGE), + entry("create_index", CREATE_INDEX), + entry("monitor", MONITOR), + entry("read", READ), + entry("index", INDEX), + entry("delete", DELETE), + entry("write", WRITE), + entry("create", CREATE), + entry("create_doc", CREATE_DOC), + entry("delete_index", DELETE_INDEX), + entry("view_index_metadata", VIEW_METADATA), + entry("read_cross_cluster", READ_CROSS_CLUSTER), + entry("manage_follow_index", MANAGE_FOLLOW_INDEX), + entry("manage_leader_index", MANAGE_LEADER_INDEX), + entry("manage_ilm", MANAGE_ILM), + entry("manage_data_stream_lifecycle", MANAGE_DATA_STREAM_LIFECYCLE), + entry("maintenance", MAINTENANCE), + entry("auto_configure", AUTO_CONFIGURE), + entry("cross_cluster_replication", CROSS_CLUSTER_REPLICATION), + entry("cross_cluster_replication_internal", CROSS_CLUSTER_REPLICATION_INTERNAL) + ).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)) + ) ); + private static Map combineSortedInOrder( + SortedMap first, + SortedMap second + ) { + if (first.isEmpty()) { + return second; + } + if (second.isEmpty()) { + return first; + } + final Map combined = new LinkedHashMap<>(first); + combined.putAll(second); + return Collections.unmodifiableMap(combined); + } + public static final Predicate ACTION_MATCHER = ALL.predicate(); public static final Predicate CREATE_INDEX_MATCHER = CREATE_INDEX.predicate(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java index 244d393858cb..eaba08c0aad8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java @@ -28,6 +28,7 @@ import org.elasticsearch.action.downsample.DownsampleAction; import org.elasticsearch.action.index.TransportIndexAction; import org.elasticsearch.action.search.TransportSearchAction; import org.elasticsearch.action.search.TransportSearchScrollAction; +import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.index.reindex.ReindexAction; import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.ilm.action.ILMActions; @@ -245,12 +246,25 @@ public class InternalUsers { new RoleDescriptor( UsernamesField.LAZY_ROLLOVER_ROLE, new String[] {}, - new RoleDescriptor.IndicesPrivileges[] { - RoleDescriptor.IndicesPrivileges.builder() - .indices("*") - .privileges(LazyRolloverAction.NAME) - .allowRestrictedIndices(true) - .build() }, + DataStream.isFailureStoreFeatureFlagEnabled() + ? new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("*") + .privileges(LazyRolloverAction.NAME) + .allowRestrictedIndices(true) + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("*") + // needed to rollover failure store + .privileges("manage_failure_store") + .allowRestrictedIndices(true) + .build() } + : new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("*") + .privileges(LazyRolloverAction.NAME) + .allowRestrictedIndices(true) + .build(), }, null, null, new String[] {}, diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/profile/ProfileHasPrivilegesRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/profile/ProfileHasPrivilegesRequestTests.java index 0d84eb041ee0..e14539bbc9c1 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/profile/ProfileHasPrivilegesRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/profile/ProfileHasPrivilegesRequestTests.java @@ -18,6 +18,8 @@ import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import java.util.List; import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.hasItem; @@ -134,13 +136,20 @@ public class ProfileHasPrivilegesRequestTests extends AbstractWireSerializingTes )]; for (int i = 0; i < indicesPrivileges.length; i++) { indicesPrivileges[i] = RoleDescriptor.IndicesPrivileges.builder() - .privileges(randomSubsetOf(randomIntBetween(1, 5), IndexPrivilege.names())) + .privileges(randomSubsetOf(randomIntBetween(1, 5), validPrivilegeNames())) .indices(randomList(1, 3, () -> randomAlphaOfLengthBetween(2, 8) + (randomBoolean() ? "*" : ""))) .build(); } return indicesPrivileges; } + private static Set validPrivilegeNames() { + return IndexPrivilege.names() + .stream() + .filter(name -> false == name.equals("read_failure_store") && false == name.equals("manage_failure_store")) + .collect(Collectors.toSet()); + } + private static RoleDescriptor.ApplicationResourcePrivileges[] randomApplicationResourcePrivileges(boolean allowEmpty) { RoleDescriptor.ApplicationResourcePrivileges[] appPrivileges = new RoleDescriptor.ApplicationResourcePrivileges[randomIntBetween( allowEmpty ? 0 : 1, diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java index dcc0bdec7911..9472f0da9e79 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java @@ -529,22 +529,22 @@ public class LimitedRoleTests extends ESTestCase { public void testAllowedIndicesMatcher() { Role fromRole = Role.builder(EMPTY_RESTRICTED_INDICES, "a-role").add(IndexPrivilege.READ, "ind-1*").build(); - assertThat(fromRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1")), is(true)); - assertThat(fromRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11")), is(true)); - assertThat(fromRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2")), is(false)); + assertThat(fromRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1"), null), is(true)); + assertThat(fromRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11"), null), is(true)); + assertThat(fromRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2"), null), is(false)); { Role limitedByRole = Role.builder(EMPTY_RESTRICTED_INDICES, "limited-role").add(IndexPrivilege.READ, "ind-1", "ind-2").build(); assertThat( - limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1")), + limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1"), null), is(true) ); assertThat( - limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11")), + limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11"), null), is(false) ); assertThat( - limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2")), + limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2"), null), is(true) ); Role role; @@ -553,18 +553,18 @@ public class LimitedRoleTests extends ESTestCase { } else { role = fromRole.limitedBy(limitedByRole); } - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1")), is(true)); - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11")), is(false)); - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2")), is(false)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1"), null), is(true)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11"), null), is(false)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2"), null), is(false)); } { Role limitedByRole = Role.builder(EMPTY_RESTRICTED_INDICES, "limited-role").add(IndexPrivilege.READ, "ind-*").build(); assertThat( - limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1")), + limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1"), null), is(true) ); assertThat( - limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2")), + limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2"), null), is(true) ); Role role; @@ -573,16 +573,16 @@ public class LimitedRoleTests extends ESTestCase { } else { role = fromRole.limitedBy(limitedByRole); } - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1")), is(true)); - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2")), is(false)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1"), null), is(true)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2"), null), is(false)); } } public void testAllowedIndicesMatcherWithNestedRole() { Role role = Role.builder(EMPTY_RESTRICTED_INDICES, "a-role").add(IndexPrivilege.READ, "ind-1*").build(); - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1")), is(true)); - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11")), is(true)); - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2")), is(false)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1"), null), is(true)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11"), null), is(true)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2"), null), is(false)); final int depth = randomIntBetween(2, 4); boolean index11Excluded = false; @@ -598,12 +598,12 @@ public class LimitedRoleTests extends ESTestCase { } else { role = role.limitedBy(limitedByRole); } - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1")), is(true)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1"), null), is(true)); assertThat( - role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11")), + role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11"), null), is(false == index11Excluded) ); - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2")), is(false)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2"), null), is(false)); } } @@ -646,6 +646,101 @@ public class LimitedRoleTests extends ESTestCase { assertThat(rolePredicate.test(TransportBulkAction.NAME), is(false)); } + public void testAllowedActionsMatcherWithSelectors() { + Role fromRole = Role.builder(EMPTY_RESTRICTED_INDICES, "fromRole") + .add(IndexPrivilege.READ_FAILURE_STORE, "ind*") + .add(IndexPrivilege.READ, "ind*") + .add(IndexPrivilege.READ_FAILURE_STORE, "metric") + .add(IndexPrivilege.READ, "logs") + .build(); + Automaton fromRoleAutomaton = fromRole.allowedActionsMatcher("index1"); + Predicate fromRolePredicate = Automatons.predicate(fromRoleAutomaton); + assertThat(fromRolePredicate.test(TransportSearchAction.TYPE.name()), is(true)); + + fromRoleAutomaton = fromRole.allowedActionsMatcher("index1::failures"); + fromRolePredicate = Automatons.predicate(fromRoleAutomaton); + assertThat(fromRolePredicate.test(TransportSearchAction.TYPE.name()), is(true)); + + fromRoleAutomaton = fromRole.allowedActionsMatcher("metric"); + fromRolePredicate = Automatons.predicate(fromRoleAutomaton); + assertThat(fromRolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + + fromRoleAutomaton = fromRole.allowedActionsMatcher("metric::failures"); + fromRolePredicate = Automatons.predicate(fromRoleAutomaton); + assertThat(fromRolePredicate.test(TransportSearchAction.TYPE.name()), is(true)); + + fromRoleAutomaton = fromRole.allowedActionsMatcher("logs"); + fromRolePredicate = Automatons.predicate(fromRoleAutomaton); + assertThat(fromRolePredicate.test(TransportSearchAction.TYPE.name()), is(true)); + + fromRoleAutomaton = fromRole.allowedActionsMatcher("logs::failures"); + fromRolePredicate = Automatons.predicate(fromRoleAutomaton); + assertThat(fromRolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + + Role limitedByRole = Role.builder(EMPTY_RESTRICTED_INDICES, "limitedRole") + .add(IndexPrivilege.READ, "index1", "index2") + .add(IndexPrivilege.READ_FAILURE_STORE, "index3") + .build(); + Automaton limitedByRoleAutomaton = limitedByRole.allowedActionsMatcher("index1"); + Predicate limitedByRolePredicated = Automatons.predicate(limitedByRoleAutomaton); + assertThat(limitedByRolePredicated.test(TransportSearchAction.TYPE.name()), is(true)); + + limitedByRoleAutomaton = limitedByRole.allowedActionsMatcher("index1"); + limitedByRolePredicated = Automatons.predicate(limitedByRoleAutomaton); + assertThat(limitedByRolePredicated.test(TransportSearchAction.TYPE.name()), is(true)); + + limitedByRoleAutomaton = limitedByRole.allowedActionsMatcher("index1::failures"); + limitedByRolePredicated = Automatons.predicate(limitedByRoleAutomaton); + assertThat(limitedByRolePredicated.test(TransportSearchAction.TYPE.name()), is(false)); + + limitedByRoleAutomaton = limitedByRole.allowedActionsMatcher("index3"); + limitedByRolePredicated = Automatons.predicate(limitedByRoleAutomaton); + assertThat(limitedByRolePredicated.test(TransportSearchAction.TYPE.name()), is(false)); + + limitedByRoleAutomaton = limitedByRole.allowedActionsMatcher("index3::failures"); + limitedByRolePredicated = Automatons.predicate(limitedByRoleAutomaton); + assertThat(limitedByRolePredicated.test(TransportSearchAction.TYPE.name()), is(true)); + + Role role; + if (randomBoolean()) { + role = limitedByRole.limitedBy(fromRole); + } else { + role = fromRole.limitedBy(limitedByRole); + } + + Automaton roleAutomaton = role.allowedActionsMatcher("index1"); + Predicate rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(true)); + + roleAutomaton = role.allowedActionsMatcher("index1::failures"); + rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + + roleAutomaton = role.allowedActionsMatcher("index3"); + rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + + roleAutomaton = role.allowedActionsMatcher("index3::failures"); + rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(true)); + + roleAutomaton = role.allowedActionsMatcher("metric"); + rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + + roleAutomaton = role.allowedActionsMatcher("metric::failures"); + rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + + roleAutomaton = role.allowedActionsMatcher("logs"); + rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + + roleAutomaton = role.allowedActionsMatcher("logs::failures"); + rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + } + public void testCheckClusterPrivilege() { Role fromRole = Role.builder(EMPTY_RESTRICTED_INDICES, "a-role") .cluster(Collections.singleton("manage_security"), Collections.emptyList()) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/InternalUsersTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/InternalUsersTests.java index 62dae84f30fc..40f66f457e67 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/InternalUsersTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/InternalUsersTests.java @@ -376,7 +376,7 @@ public class InternalUsersTests extends ESTestCase { final IndexAbstraction.ConcreteIndex index = new IndexAbstraction.ConcreteIndex(metadata); assertThat( "Role " + role + ", action " + action + " access to " + indexName, - role.allowedIndicesMatcher(action).test(index), + role.allowedIndicesMatcher(action).test(index, null), is(expectedValue) ); } diff --git a/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java b/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java index 8ea64ab38a7a..8486963a5dae 100644 --- a/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java +++ b/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java @@ -34,6 +34,7 @@ import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.IndexMetadata.DownsampleTaskStatus; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.MetadataCreateIndexService; import org.elasticsearch.cluster.metadata.ProjectId; import org.elasticsearch.cluster.metadata.ProjectMetadata; @@ -209,7 +210,8 @@ public class TransportDownsampleAction extends AcknowledgedTransportMasterNodeAc ) { long startTime = client.threadPool().relativeTimeInMillis(); String sourceIndexName = request.getSourceIndex(); - + IndexNameExpressionResolver.assertExpressionHasNullOrDataSelector(sourceIndexName); + IndexNameExpressionResolver.assertExpressionHasNullOrDataSelector(request.getTargetIndex()); final IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); if (indicesAccessControl != null) { final IndicesAccessControl.IndexAccessControl indexPermissions = indicesAccessControl.getIndexPermissions(sourceIndexName); diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 1ce2acac908e..bfdea27eee98 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -7,26 +7,57 @@ package org.elasticsearch.xpack.security.failurestore; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.SearchResponseUtils; +import org.elasticsearch.test.TestSecurityClient; import org.elasticsearch.test.cluster.ElasticsearchCluster; import org.elasticsearch.test.cluster.FeatureFlag; import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.test.rest.ObjectPath; import org.elasticsearch.xcontent.json.JsonXContent; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.SecurityOnTrialLicenseRestTestCase; +import org.junit.Before; import org.junit.ClassRule; import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; public class FailureStoreSecurityRestIT extends ESRestTestCase { + private TestSecurityClient securityClient; + + private Map apiKeys = new HashMap<>(); + @ClassRule public static ElasticsearchCluster cluster = ElasticsearchCluster.local() .apply(SecurityOnTrialLicenseRestTestCase.commonTrialSecurityClusterConfig) @@ -44,17 +75,38 @@ public class FailureStoreSecurityRestIT extends ESRestTestCase { return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); } - public void testGetUserPrivileges() throws IOException { - Request userRequest = new Request("PUT", "/_security/user/user"); - userRequest.setJsonEntity(""" - { - "password": "x-pack-test-password", - "roles": ["role"] - } - """); - assertOK(adminClient().performRequest(userRequest)); + private static final String ASYNC_SEARCH_TIMEOUT = "30s"; - putRole(""" + private static final String ADMIN_USER = "admin_user"; + + private static final String DATA_ACCESS = "data_access"; + private static final String BACKING_INDEX_DATA_ACCESS = "backing_index_data_access"; + private static final String BACKING_INDEX_FAILURE_ACCESS = "backing_index_failure_access"; + private static final String FAILURE_INDEX_DATA_ACCESS = "failure_index_data_access"; + private static final String FAILURE_INDEX_FAILURE_ACCESS = "failure_index_failure_access"; + private static final String STAR_READ_ONLY_ACCESS = "star_read_only"; + private static final String FAILURE_STORE_ACCESS = "failure_store_access"; + private static final String BOTH_ACCESS = "both_access"; + private static final String WRITE_ACCESS = "write_access"; + private static final String MANAGE_ACCESS = "manage_access"; + private static final String MANAGE_FAILURE_STORE_ACCESS = "manage_failure_store_access"; + private static final SecureString PASSWORD = new SecureString("admin-password"); + + @Before + public void setup() throws IOException { + apiKeys = new HashMap<>(); + createUser(WRITE_ACCESS, PASSWORD, WRITE_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["write", "auto_configure"]}] + }"""), WRITE_ACCESS); + } + + public void testGetUserPrivileges() throws IOException { + createUser("user", PASSWORD, "role"); + + upsertRole(""" { "cluster": ["all"], "indices": [ @@ -64,7 +116,7 @@ public class FailureStoreSecurityRestIT extends ESRestTestCase { } ] } - """); + """, "role"); expectUserPrivilegesResponse(""" { "cluster": ["all"], @@ -83,7 +135,7 @@ public class FailureStoreSecurityRestIT extends ESRestTestCase { "run_as": [] }"""); - putRole(""" + upsertRole(""" { "cluster": ["all"], "indices": [ @@ -93,7 +145,7 @@ public class FailureStoreSecurityRestIT extends ESRestTestCase { } ] } - """); + """, "role"); expectUserPrivilegesResponse(""" { "cluster": ["all"], @@ -108,7 +160,7 @@ public class FailureStoreSecurityRestIT extends ESRestTestCase { "run_as": [] }"""); - putRole(""" + upsertRole(""" { "cluster": ["all"], "indices": [ @@ -118,7 +170,7 @@ public class FailureStoreSecurityRestIT extends ESRestTestCase { } ] } - """); + """, "role"); expectUserPrivilegesResponse(""" { "cluster": ["all"], @@ -132,27 +184,1647 @@ public class FailureStoreSecurityRestIT extends ESRestTestCase { "applications": [], "run_as": [] }"""); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["*"], + "privileges": ["read", "read_failure_store"] + }, + { + "names": ["*"], + "privileges": ["write", "manage_failure_store"] + } + ] + } + """, "role"); + expectUserPrivilegesResponse(""" + { + "cluster": ["all"], + "global": [], + "indices": [ + { + "names": ["*"], + "privileges": ["read", "write"], + "allow_restricted_indices": false + }, + { + "names": ["*"], + "privileges": ["manage_failure_store", "read_failure_store"], + "allow_restricted_indices": false + }], + "applications": [], + "run_as": [] + }"""); + } + + public void testRoleWithSelectorInIndexPattern() throws Exception { + setupDataStream(); + + createUser("user", PASSWORD, "role"); + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["*::failures"], + "privileges": ["read"] + } + ] + }""", "role"); + createAndStoreApiKey("user", null); + + expectThrows("user", new Search("test1::failures"), 403); + expectSearch("user", new Search("*::failures")); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test1::failures"], + "privileges": ["read"] + } + ] + }""", "role"); + + expectThrows("user", new Search("test1::failures"), 403); + expectSearch("user", new Search("*::failures")); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["*::failures"], + "privileges": ["read_failure_store"] + } + ] + }""", "role"); + expectThrows("user", new Search("test1::failures"), 403); + expectSearch("user", new Search("*::failures")); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test1::failures"], + "privileges": ["read_failure_store"] + } + ] + }""", "role"); + expectThrows("user", new Search("test1::failures"), 403); + expectSearch("user", new Search("*::failures")); + } + + @SuppressWarnings("unchecked") + public void testFailureStoreAccess() throws Exception { + List docIds = setupDataStream(); + assertThat(docIds.size(), equalTo(2)); + assertThat(docIds, hasItem("1")); + String dataDocId = "1"; + String failuresDocId = docIds.stream().filter(id -> false == id.equals(dataDocId)).findFirst().get(); + + Tuple backingIndices = getSingleDataAndFailureIndices("test1"); + String dataIndexName = backingIndices.v1(); + String failureIndexName = backingIndices.v2(); + + createUser(DATA_ACCESS, PASSWORD, DATA_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read"]}] + }"""), DATA_ACCESS); + createAndStoreApiKey(DATA_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read"]}] + } + } + """); + + createUser(STAR_READ_ONLY_ACCESS, PASSWORD, STAR_READ_ONLY_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["*"], "privileges": ["read"]}] + }"""), STAR_READ_ONLY_ACCESS); + createAndStoreApiKey(STAR_READ_ONLY_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["*"], "privileges": ["read"]}] + } + } + """); + + createUser(FAILURE_STORE_ACCESS, PASSWORD, FAILURE_STORE_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read_failure_store"]}] + }"""), FAILURE_STORE_ACCESS); + createAndStoreApiKey(FAILURE_STORE_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read_failure_store"]}] + } + } + """); + + if (randomBoolean()) { + createUser(BOTH_ACCESS, PASSWORD, BOTH_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read", "read_failure_store"]}] + }"""), BOTH_ACCESS); + createAndStoreApiKey(BOTH_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read", "read_failure_store"]}] + } + } + """); + } else { + createUser(BOTH_ACCESS, PASSWORD, DATA_ACCESS, FAILURE_STORE_ACCESS); + createAndStoreApiKey(BOTH_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read", "read_failure_store"]}] + } + } + """); + } + + createAndStoreApiKey(WRITE_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["write", "auto_configure"]}] + } + } + """); + + createUser(BACKING_INDEX_DATA_ACCESS, PASSWORD, BACKING_INDEX_DATA_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["%s"], "privileges": ["read"]}] + }""", dataIndexName), BACKING_INDEX_DATA_ACCESS); + createAndStoreApiKey(BACKING_INDEX_DATA_ACCESS, null); + + createUser(BACKING_INDEX_FAILURE_ACCESS, PASSWORD, BACKING_INDEX_FAILURE_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["%s"], "privileges": ["read_failure_store"]}] + }""", dataIndexName), BACKING_INDEX_FAILURE_ACCESS); + createAndStoreApiKey(BACKING_INDEX_FAILURE_ACCESS, null); + + createUser(FAILURE_INDEX_DATA_ACCESS, PASSWORD, FAILURE_INDEX_DATA_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["%s"], "privileges": ["read"]}] + }""", failureIndexName), FAILURE_INDEX_DATA_ACCESS); + createAndStoreApiKey(FAILURE_INDEX_DATA_ACCESS, null); + + createUser(FAILURE_INDEX_FAILURE_ACCESS, PASSWORD, FAILURE_INDEX_FAILURE_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["%s"], "privileges": ["read_failure_store"]}] + }""", failureIndexName), FAILURE_INDEX_FAILURE_ACCESS); + createAndStoreApiKey(FAILURE_INDEX_FAILURE_ACCESS, null); + + Request aliasRequest = new Request("POST", "/_aliases"); + aliasRequest.setJsonEntity(""" + { + "actions": [ + { + "add": { + "index": "test1", + "alias": "test-alias" + } + } + ] + } + """); + assertOK(adminClient().performRequest(aliasRequest)); + + List users = List.of( + DATA_ACCESS, + FAILURE_STORE_ACCESS, + STAR_READ_ONLY_ACCESS, + BOTH_ACCESS, + ADMIN_USER, + BACKING_INDEX_DATA_ACCESS, + BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS + ); + + // search data + { + var request = new Search(randomFrom("test1::data", "test1")); + for (var user : users) { + switch (user) { + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + expectSearch(user, request, dataDocId); + break; + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS: + expectThrows(user, request, 403); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test1", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + expectSearch(user, request, dataDocId); + break; + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test-alias"); + for (var user : users) { + switch (user) { + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + expectSearch(user, request, dataDocId); + break; + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS: + expectThrows(user, request, 403); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test-alias", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + expectSearch(user, request, dataDocId); + break; + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test*"); + for (var user : users) { + switch (user) { + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + expectSearch(user, request, dataDocId); + break; + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("*1"); + for (var user : users) { + switch (user) { + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + expectSearch(user, request, dataDocId); + break; + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + // note expand_wildcards does not include hidden here + for (var request : List.of(new Search("*"), new Search("_all"), new Search(""))) { + for (var user : users) { + switch (user) { + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + expectSearch(user, request, dataDocId); + break; + case FAILURE_STORE_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + BACKING_INDEX_DATA_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(".ds*"); + for (var user : users) { + switch (user) { + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, BACKING_INDEX_DATA_ACCESS: + expectSearch(user, request, dataDocId); + break; + case FAILURE_STORE_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(dataIndexName); + for (var user : users) { + switch (user) { + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, BACKING_INDEX_DATA_ACCESS: + expectSearch(user, request, dataDocId); + break; + case FAILURE_STORE_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectThrows(user, request, 403); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(dataIndexName, "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, BACKING_INDEX_DATA_ACCESS: + expectSearch(user, request, dataDocId); + break; + case FAILURE_STORE_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test2"); + for (var user : users) { + switch (user) { + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + expectThrows(user, request, 404); + break; + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS: + expectThrows(user, request, 403); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test2", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, + BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + + // search failures + { + var request = new Search("test1::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectThrows(user, request, 403); + break; + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: + expectSearch(user, request, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test1::failures", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectSearch(user, request); + break; + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: + expectSearch(user, request, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test-alias::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectThrows(user, request, 403); + break; + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: + expectSearch(user, request, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test-alias::failures", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectSearch(user, request); + break; + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: + expectSearch(user, request, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test*::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectSearch(user, request); + break; + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: + expectSearch(user, request, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("*1::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectSearch(user, request); + break; + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: + expectSearch(user, request, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("*::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectSearch(user, request); + break; + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: + expectSearch(user, request, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(".fs*"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + case ADMIN_USER, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectSearch(user, request, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(failureIndexName); + for (var user : users) { + switch (user) { + case DATA_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectThrows(user, request, 403); + break; + case ADMIN_USER, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectSearch(user, request, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(failureIndexName, "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + case ADMIN_USER, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectSearch(user, request, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(failureIndexName + "::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS: + expectThrows(user, request, 403); + break; + case FAILURE_STORE_ACCESS, BOTH_ACCESS, ADMIN_USER, FAILURE_INDEX_FAILURE_ACCESS: + expectThrows(user, request, 404); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(failureIndexName + "::failures", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, FAILURE_STORE_ACCESS, ADMIN_USER, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, BACKING_INDEX_DATA_ACCESS, + BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(dataIndexName + "::failures"); + for (var user : users) { + switch (user) { + case STAR_READ_ONLY_ACCESS, BOTH_ACCESS, DATA_ACCESS, FAILURE_STORE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, BACKING_INDEX_DATA_ACCESS: + expectThrows(user, request, 403); + break; + case ADMIN_USER, BACKING_INDEX_FAILURE_ACCESS: + expectThrows(user, request, 404); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(dataIndexName + "::failures", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, FAILURE_STORE_ACCESS, ADMIN_USER, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, BACKING_INDEX_DATA_ACCESS, + BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(".fs*::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, FAILURE_STORE_ACCESS, ADMIN_USER, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, BACKING_INDEX_DATA_ACCESS, + BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(".ds*::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, FAILURE_STORE_ACCESS, ADMIN_USER, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, BACKING_INDEX_DATA_ACCESS, + BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test2::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectThrows(user, request, 403); + break; + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: + expectThrows(user, request, 404); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test2::failures", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, + BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + + // mixed access + { + var request = new Search("test1,test1::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectThrows(user, request, 403); + break; + case ADMIN_USER, BOTH_ACCESS: + expectSearch(user, request, dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test1,test1::failures", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + expectSearch(user, request, dataDocId); + break; + case FAILURE_STORE_ACCESS: + expectSearch(user, request, failuresDocId); + break; + case ADMIN_USER, BOTH_ACCESS: + expectSearch(user, request, dataDocId, failuresDocId); + break; + case BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test1," + failureIndexName); + for (var user : users) { + switch (user) { + case DATA_ACCESS, FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectThrows(user, request, 403); + break; + case ADMIN_USER, BOTH_ACCESS, STAR_READ_ONLY_ACCESS: + expectSearch(user, request, dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test1," + failureIndexName, "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS: + expectSearch(user, request, dataDocId); + break; + case FAILURE_STORE_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectSearch(user, request, failuresDocId); + break; + case ADMIN_USER, BOTH_ACCESS, STAR_READ_ONLY_ACCESS: + expectSearch(user, request, dataDocId, failuresDocId); + break; + case BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test1::failures," + dataIndexName); + for (var user : users) { + switch (user) { + case DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectThrows(user, request, 403); + break; + case ADMIN_USER, BOTH_ACCESS: + expectSearch(user, request, dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test1::failures," + dataIndexName, "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, BACKING_INDEX_DATA_ACCESS, STAR_READ_ONLY_ACCESS: + expectSearch(user, request, dataDocId); + break; + case FAILURE_STORE_ACCESS: + expectSearch(user, request, failuresDocId); + break; + case ADMIN_USER, BOTH_ACCESS: + expectSearch(user, request, dataDocId, failuresDocId); + break; + case BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test1,*::failures"); + for (var user : users) { + switch (user) { + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS: + expectThrows(user, request, 403); + break; + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + expectSearch(user, request, dataDocId); + break; + case ADMIN_USER, BOTH_ACCESS: + expectSearch(user, request, dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test1,*::failures", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case FAILURE_STORE_ACCESS: + expectSearch(user, request, failuresDocId); + break; + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + expectSearch(user, request, dataDocId); + break; + case ADMIN_USER, BOTH_ACCESS: + expectSearch(user, request, dataDocId, failuresDocId); + break; + case BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test1::failures,*"); + for (var user : users) { + switch (user) { + case FAILURE_STORE_ACCESS: + expectSearch(user, request, failuresDocId); + break; + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectThrows(user, request, 403); + break; + case ADMIN_USER, BOTH_ACCESS: + expectSearch(user, request, dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("test1::failures,*", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case FAILURE_STORE_ACCESS: + expectSearch(user, request, failuresDocId); + break; + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + expectSearch(user, request, dataDocId); + break; + case ADMIN_USER, BOTH_ACCESS: + expectSearch(user, request, dataDocId, failuresDocId); + break; + case BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search("*::failures,*"); + for (var user : users) { + switch (user) { + case FAILURE_STORE_ACCESS: + expectSearch(user, request, failuresDocId); + break; + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + expectSearch(user, request, dataDocId); + break; + case ADMIN_USER, BOTH_ACCESS: + expectSearch(user, request, dataDocId, failuresDocId); + break; + case BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS, BACKING_INDEX_DATA_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + } + + public void testWriteOperations() throws IOException { + setupDataStream(); + Tuple backingIndices = getSingleDataAndFailureIndices("test1"); + String dataIndexName = backingIndices.v1(); + String failureIndexName = backingIndices.v2(); + + createUser(MANAGE_ACCESS, PASSWORD, MANAGE_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["manage"]}] + }"""), MANAGE_ACCESS); + createAndStoreApiKey(MANAGE_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["manage"]}] + } + } + """); + + createUser(MANAGE_FAILURE_STORE_ACCESS, PASSWORD, MANAGE_FAILURE_STORE_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["manage_failure_store"]}] + }"""), MANAGE_FAILURE_STORE_ACCESS); + createAndStoreApiKey(MANAGE_FAILURE_STORE_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["manage_failure_store"]}] + } + } + """); + + // user with manage access to data stream does NOT get direct access to failure index + expectThrows(() -> deleteIndex(MANAGE_ACCESS, failureIndexName), 403); + expectThrows(() -> deleteIndex(MANAGE_ACCESS, dataIndexName), 400); + // manage_failure_store user COULD delete failure index (not valid because it's a write index, but allow security-wise) + expectThrows(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS, dataIndexName), 403); + expectThrows(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS, failureIndexName), 400); + expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, dataIndexName), 403); + + expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, "test1"), 403); + expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, "test1::failures"), 403); + + // manage user can delete data stream + deleteDataStream(MANAGE_ACCESS, "test1"); + + // deleting data stream deletes everything, including failure index + expectThrows(() -> adminClient().performRequest(new Request("GET", "/test1/_search")), 404); + expectThrows(() -> adminClient().performRequest(new Request("GET", "/" + dataIndexName + "/_search")), 404); + expectThrows(() -> adminClient().performRequest(new Request("GET", "/test1::failures/_search")), 404); + expectThrows(() -> adminClient().performRequest(new Request("GET", "/" + failureIndexName + "/_search")), 404); + } + + public void testFailureStoreAccessWithApiKeys() throws Exception { + List docIds = setupDataStream(); + assertThat(docIds.size(), equalTo(2)); + assertThat(docIds, hasItem("1")); + String dataDocId = "1"; + String failuresDocId = docIds.stream().filter(id -> false == id.equals(dataDocId)).findFirst().get(); + + Tuple backingIndices = getSingleDataAndFailureIndices("test1"); + String dataIndexName = backingIndices.v1(); + String failureIndexName = backingIndices.v2(); + + var user = "user"; + var role = "role"; + createUser(user, PASSWORD, role); + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["*"], + "privileges": ["read_failure_store"] + } + ] + } + """, role); + + String apiKey = createApiKey(user, """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test1"], "privileges": ["read_failure_store"]}] + } + }"""); + + expectSearchWithApiKey(apiKey, new Search("test1::failures"), failuresDocId); + expectSearchWithApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectThrowsWithApiKey(apiKey, new Search(dataIndexName), 403); + expectThrowsWithApiKey(apiKey, new Search("test1"), 403); + + apiKey = createApiKey(user, """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test1"], "privileges": ["read_failure_store", "read"]}] + } + }"""); + + expectSearchWithApiKey(apiKey, new Search("test1::failures"), failuresDocId); + expectSearchWithApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectThrowsWithApiKey(apiKey, new Search(dataIndexName), 403); + expectThrowsWithApiKey(apiKey, new Search("test1"), 403); + + apiKey = createApiKey(user, """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test2"], "privileges": ["read_failure_store", "read"]}] + } + }"""); + + expectThrowsWithApiKey(apiKey, new Search("test1::failures"), 403); + expectThrowsWithApiKey(apiKey, new Search(failureIndexName), 403); + expectThrowsWithApiKey(apiKey, new Search(dataIndexName), 403); + expectThrowsWithApiKey(apiKey, new Search("test1"), 403); + + apiKey = createApiKey(user, """ + { + "role": { + "cluster": ["all"], + "indices": [ + {"names": ["test1"], "privileges": ["read_failure_store"]}, + {"names": ["*"], "privileges": ["read"]} + ] + } + }"""); + + expectSearchWithApiKey(apiKey, new Search("test1::failures"), failuresDocId); + expectSearchWithApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectThrowsWithApiKey(apiKey, new Search(dataIndexName), 403); + expectThrowsWithApiKey(apiKey, new Search("test1"), 403); + + apiKey = createApiKey(user, """ + { + "role": { + "cluster": ["all"], + "indices": [ + {"names": ["*"], "privileges": ["read"]} + ] + } + }"""); + + expectThrowsWithApiKey(apiKey, new Search("test1::failures"), 403); + // funky but correct: assigned role descriptors grant direct access to failure index, limited-by to failure store + expectSearchWithApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectThrowsWithApiKey(apiKey, new Search(dataIndexName), 403); + expectThrowsWithApiKey(apiKey, new Search("test1"), 403); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["*"], + "privileges": ["read"] + } + ] + } + """, role); + apiKey = createApiKey(user, """ + { + "role": { + "cluster": ["all"], + "indices": [ + {"names": ["test1"], "privileges": ["read_failure_store"]} + ] + } + }"""); + expectThrowsWithApiKey(apiKey, new Search("test1::failures"), 403); + // funky but correct: limited-by role descriptors grant direct access to failure index, assigned to failure store + expectSearchWithApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectThrowsWithApiKey(apiKey, new Search(dataIndexName), 403); + expectThrowsWithApiKey(apiKey, new Search("test1"), 403); + } + + public void testPit() throws Exception { + List docIds = setupDataStream(); + String dataDocId = "1"; + String failuresDocId = docIds.stream().filter(id -> false == id.equals(dataDocId)).findFirst().get(); + + createUser("user", PASSWORD, "role"); + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test*"], + "privileges": ["read"] + } + ] + }""", "role"); + + { + expectThrows( + () -> performRequest("user", new Request("POST", Strings.format("/%s/_pit?keep_alive=1m", "test1::failures"))), + 403 + ); + Response pitResponse = performRequest("user", new Request("POST", Strings.format("/%s/_pit?keep_alive=1m", "test1"))); + assertOK(pitResponse); + String pitId = ObjectPath.createFromResponse(pitResponse).evaluate("id"); + assertThat(pitId, notNullValue()); + + var searchRequest = new Request("POST", "/_search"); + searchRequest.setJsonEntity(Strings.format(""" + { + "pit": { + "id": "%s" + } + } + """, pitId)); + Response searchResponse = performRequest("user", searchRequest); + expectSearch(searchResponse, dataDocId); + } + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test*"], + "privileges": ["read_failure_store"] + } + ] + }""", "role"); + + { + expectThrows(() -> performRequest("user", new Request("POST", Strings.format("/%s/_pit?keep_alive=1m", "test1"))), 403); + Response pitResponse = performRequest("user", new Request("POST", Strings.format("/%s/_pit?keep_alive=1m", "test1::failures"))); + assertOK(pitResponse); + String pitId = ObjectPath.createFromResponse(pitResponse).evaluate("id"); + assertThat(pitId, notNullValue()); + + var searchRequest = new Request("POST", "/_search"); + searchRequest.setJsonEntity(Strings.format(""" + { + "pit": { + "id": "%s" + } + } + """, pitId)); + Response searchResponse = performRequest("user", searchRequest); + expectSearch(searchResponse, failuresDocId); + } + } + + public void testDlsFls() throws Exception { + setupDataStream(); + + Tuple backingIndices = getSingleDataAndFailureIndices("test1"); + String dataIndexName = backingIndices.v1(); + String failureIndexName = backingIndices.v2(); + + String user = "user"; + String role = "role"; + createUser(user, PASSWORD, role); + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test*"], + "privileges": ["read", "read_failure_store"], + "field_security": { + "grant": ["@timestamp", "age"] + } + } + ] + }""", role); + + // FLS applies to regular data stream + assertSearchResponseContainsExpectedIndicesAndFields( + performRequest(user, new Search(randomFrom("test1", "test1::data")).toSearchRequest()), + Map.of(dataIndexName, Set.of("@timestamp", "age")) + ); + + // FLS sort of applies to failure store + // TODO this will change with FLS handling + assertSearchResponseContainsExpectedIndicesAndFields( + performRequest(user, new Search("test1::failures").toSearchRequest()), + Map.of(failureIndexName, Set.of("@timestamp")) + ); + + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["%s"], + "privileges": ["read"], + "field_security": { + "grant": ["@timestamp", "age"] + } + }, + { + "names": ["test*"], + "privileges": ["read_failure_store"], + "field_security": { + "grant": ["@timestamp", "age"] + } + } + ] + }""", randomFrom("test*", "test1")), role); + + // FLS applies to regular data stream + assertSearchResponseContainsExpectedIndicesAndFields( + performRequest(user, new Search(randomFrom("test1", "test1::data")).toSearchRequest()), + Map.of(dataIndexName, Set.of("@timestamp", "age")) + ); + + // FLS sort of applies to failure store + // TODO this will change with FLS handling + assertSearchResponseContainsExpectedIndicesAndFields( + performRequest(user, new Search("test1::failures").toSearchRequest()), + Map.of(failureIndexName, Set.of("@timestamp")) + ); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test*"], + "privileges": ["read"], + "field_security": { + "grant": ["@timestamp", "age"] + } + }, + { + "names": ["test*"], + "privileges": ["read_failure_store"] + } + ] + }""", role); + + // since there is a section without FLS, no FLS applies + assertSearchResponseContainsExpectedIndicesAndFields( + performRequest(user, new Search(randomFrom("test1", "test1::data")).toSearchRequest()), + Map.of(dataIndexName, Set.of("@timestamp", "age", "name", "email")) + ); + + assertSearchResponseContainsExpectedIndicesAndFields( + performRequest(user, new Search("test1::failures").toSearchRequest()), + Map.of(failureIndexName, Set.of("@timestamp", "document", "error")) + ); + + // DLS + String dataIndexDocId = "1"; + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test*"], + "privileges": ["read", "read_failure_store"], + "query":{"term":{"name":{"value":"not-jack"}}} + } + ] + }""", role); + // DLS applies and no docs match the query + expectSearch(user, new Search(randomFrom("test1", "test1::data"))); + expectSearch(user, new Search("test1::failures")); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test*"], + "privileges": ["read", "read_failure_store"], + "query":{"term":{"name":{"value":"jack"}}} + } + ] + }""", role); + // DLS applies and doc matches the query + expectSearch(user, new Search(randomFrom("test1", "test1::data")), dataIndexDocId); + expectSearch(user, new Search("test1::failures")); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test*"], + "privileges": ["read"], + "query":{"term":{"name":{"value":"not-jack"}}} + }, + { + "names": ["test*"], + "privileges": ["read_failure_store"] + } + ] + }""", role); + // DLS does not apply because there is a section without DLS + expectSearch(user, new Search(randomFrom("test1", "test1::data")), dataIndexDocId); + } + + private static void expectThrows(ThrowingRunnable runnable, int statusCode) { + var ex = expectThrows(ResponseException.class, runnable); + assertThat(ex.getResponse().getStatusLine().getStatusCode(), equalTo(statusCode)); + } + + private void expectThrows(String user, Search search, int statusCode) { + expectThrows(() -> performRequest(user, search.toSearchRequest()), statusCode); + expectThrows(() -> performRequest(user, search.toAsyncSearchRequest()), statusCode); + } + + private void expectSearch(String user, Search search, String... docIds) throws Exception { + expectSearch(performRequestMaybeUsingApiKey(user, search.toSearchRequest()), docIds); + expectAsyncSearch(performRequestMaybeUsingApiKey(user, search.toAsyncSearchRequest()), docIds); + } + + private void expectSearchWithApiKey(String apiKey, Search search, String... docIds) throws Exception { + expectSearch(performRequestWithApiKey(apiKey, search.toSearchRequest()), docIds); + expectAsyncSearch(performRequestWithApiKey(apiKey, search.toAsyncSearchRequest()), docIds); + } + + private void expectThrowsWithApiKey(String apiKey, Search search, int statusCode) { + expectThrows(() -> performRequestWithApiKey(apiKey, search.toSearchRequest()), statusCode); + expectThrows(() -> performRequestWithApiKey(apiKey, search.toAsyncSearchRequest()), statusCode); + } + + @SuppressWarnings("unchecked") + private static void expectAsyncSearch(Response response, String... docIds) throws IOException { + assertOK(response); + ObjectPath resp = ObjectPath.createFromResponse(response); + Boolean isRunning = resp.evaluate("is_running"); + Boolean isPartial = resp.evaluate("is_partial"); + assertThat(isRunning, is(false)); + assertThat(isPartial, is(false)); + + List hits = resp.evaluate("response.hits.hits"); + List actual = hits.stream().map(h -> (String) ((Map) h).get("_id")).toList(); + + assertThat(actual, containsInAnyOrder(docIds)); + } + + private static void expectSearch(Response response, String... docIds) throws IOException { + assertOK(response); + final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response)); + try { + SearchHit[] hits = searchResponse.getHits().getHits(); + assertThat(hits.length, equalTo(docIds.length)); + List actualDocIds = Arrays.stream(hits).map(SearchHit::getId).toList(); + assertThat(actualDocIds, containsInAnyOrder(docIds)); + } finally { + searchResponse.decRef(); + } + } + + private record Search(String searchTarget, String pathParamString) { + Search(String searchTarget) { + this(searchTarget, ""); + } + + Request toSearchRequest() { + return new Request("POST", Strings.format("/%s/_search%s", searchTarget, pathParamString)); + } + + Request toAsyncSearchRequest() { + var pathParam = pathParamString.isEmpty() + ? "?wait_for_completion_timeout=" + ASYNC_SEARCH_TIMEOUT + : pathParamString + "&wait_for_completion_timeout=" + ASYNC_SEARCH_TIMEOUT; + return new Request("POST", Strings.format("/%s/_async_search%s", searchTarget, pathParam)); + } + } + + private List setupDataStream() throws IOException { + createTemplates(); + return randomBoolean() ? populateDataStreamWithBulkRequest() : populateDataStreamWithDocRequests(); + } + + private void createTemplates() throws IOException { + var componentTemplateRequest = new Request("PUT", "/_component_template/component1"); + componentTemplateRequest.setJsonEntity(""" + { + "template": { + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "age": { + "type": "integer" + }, + "email": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "data_stream_options": { + "failure_store": { + "enabled": true + } + } + } + } + """); + assertOK(adminClient().performRequest(componentTemplateRequest)); + + var indexTemplateRequest = new Request("PUT", "/_index_template/template1"); + indexTemplateRequest.setJsonEntity(""" + { + "index_patterns": ["test*"], + "data_stream": {}, + "priority": 500, + "composed_of": ["component1"] + } + """); + assertOK(adminClient().performRequest(indexTemplateRequest)); + } + + private List populateDataStreamWithDocRequests() throws IOException { + List ids = new ArrayList<>(); + + var dataStreamName = "test1"; + var docRequest = new Request("PUT", "/" + dataStreamName + "/_doc/1?refresh=true&op_type=create"); + docRequest.setJsonEntity(""" + { + "@timestamp": 1, + "age" : 1, + "name" : "jack", + "email" : "jack@example.com" + } + """); + Response response = performRequest(WRITE_ACCESS, docRequest); + assertOK(response); + Map responseAsMap = responseAsMap(response); + ids.add((String) responseAsMap.get("_id")); + + docRequest = new Request("PUT", "/" + dataStreamName + "/_doc/2?refresh=true&op_type=create"); + docRequest.setJsonEntity(""" + { + "@timestamp": 2, + "age" : "this should be an int", + "name" : "jack", + "email" : "jack@example.com" + } + """); + response = performRequest(WRITE_ACCESS, docRequest); + assertOK(response); + responseAsMap = responseAsMap(response); + ids.add((String) responseAsMap.get("_id")); + + return ids; + } + + @SuppressWarnings("unchecked") + private List populateDataStreamWithBulkRequest() throws IOException { + var bulkRequest = new Request("POST", "/_bulk?refresh=true"); + bulkRequest.setJsonEntity(""" + { "create" : { "_index" : "test1", "_id" : "1" } } + { "@timestamp": 1, "age" : 1, "name" : "jack", "email" : "jack@example.com" } + { "create" : { "_index" : "test1", "_id" : "2" } } + { "@timestamp": 2, "age" : "this should be an int", "name" : "jack", "email" : "jack@example.com" } + """); + Response response = performRequest(WRITE_ACCESS, bulkRequest); + assertOK(response); + // we need this dance because the ID for the failed document is random, **not** 2 + Map stringObjectMap = responseAsMap(response); + List items = (List) stringObjectMap.get("items"); + List ids = new ArrayList<>(); + for (Object item : items) { + Map itemMap = (Map) item; + Map create = (Map) itemMap.get("create"); + assertThat(create.get("status"), equalTo(201)); + ids.add((String) create.get("_id")); + } + return ids; + } + + private void deleteDataStream(String user, String dataStreamName) throws IOException { + assertOK(performRequest(user, new Request("DELETE", "/_data_stream/" + dataStreamName))); + } + + private void deleteIndex(String user, String indexName) throws IOException { + assertOK(performRequest(user, new Request("DELETE", "/" + indexName))); + } + + private Response performRequest(String user, Request request) throws IOException { + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", basicAuthHeaderValue(user, PASSWORD)).build()); + return client().performRequest(request); + } + + private Response performRequestMaybeUsingApiKey(String user, Request request) throws IOException { + if (randomBoolean() && apiKeys.containsKey(user)) { + return performRequestWithApiKey(apiKeys.get(user), request); + } else { + return performRequest(user, request); + } + } + + private static Response performRequestWithApiKey(String apiKey, Request request) throws IOException { + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "ApiKey " + apiKey).build()); + return client().performRequest(request); } private static void expectUserPrivilegesResponse(String userPrivilegesResponse) throws IOException { Request request = new Request("GET", "/_security/user/_privileges"); - request.setOptions( - request.getOptions() - .toBuilder() - .addHeader("Authorization", basicAuthHeaderValue("user", new SecureString("x-pack-test-password".toCharArray()))) - ); + request.setOptions(request.getOptions().toBuilder().addHeader("Authorization", basicAuthHeaderValue("user", PASSWORD))); Response response = client().performRequest(request); assertOK(response); assertThat(responseAsMap(response), equalTo(mapFromJson(userPrivilegesResponse))); } - private static void putRole(String rolePayload) throws IOException { - Request roleRequest = new Request("PUT", "/_security/role/role"); - roleRequest.setJsonEntity(rolePayload); - assertOK(adminClient().performRequest(roleRequest)); - } - private static Map mapFromJson(String json) { return XContentHelper.convertToMap(JsonXContent.jsonXContent, json, false); } + + protected TestSecurityClient getSecurityClient() { + if (securityClient == null) { + securityClient = new TestSecurityClient(adminClient()); + } + return securityClient; + } + + protected void createUser(String username, SecureString password, String... roles) throws IOException { + getSecurityClient().putUser(new User(username, roles), password); + } + + protected String createAndStoreApiKey(String username, @Nullable String roleDescriptors) throws IOException { + assertThat("API key already registered for user: " + username, apiKeys.containsKey(username), is(false)); + apiKeys.put(username, createApiKey(username, roleDescriptors)); + return createApiKey(username, roleDescriptors); + } + + private String createApiKey(String username, String roleDescriptors) throws IOException { + var request = new Request("POST", "/_security/api_key"); + if (roleDescriptors == null) { + request.setJsonEntity(""" + { + "name": "test-api-key" + } + """); + } else { + request.setJsonEntity(Strings.format(""" + { + "name": "test-api-key", + "role_descriptors": %s + } + """, roleDescriptors)); + } + Response response = performRequest(username, request); + assertOK(response); + Map responseAsMap = responseAsMap(response); + return (String) responseAsMap.get("encoded"); + } + + protected void upsertRole(String roleDescriptor, String roleName) throws IOException { + Request createRoleRequest = roleRequest(roleDescriptor, roleName); + Response createRoleResponse = adminClient().performRequest(createRoleRequest); + assertOK(createRoleResponse); + } + + protected Request roleRequest(String roleDescriptor, String roleName) { + Request createRoleRequest; + if (randomBoolean()) { + createRoleRequest = new Request(randomFrom(HttpPut.METHOD_NAME, HttpPost.METHOD_NAME), "/_security/role/" + roleName); + createRoleRequest.setJsonEntity(roleDescriptor); + } else { + createRoleRequest = new Request(HttpPost.METHOD_NAME, "/_security/role"); + createRoleRequest.setJsonEntity(org.elasticsearch.core.Strings.format(""" + {"roles": {"%s": %s}} + """, roleName, roleDescriptor)); + } + return createRoleRequest; + } + + protected void assertSearchResponseContainsExpectedIndicesAndFields( + Response searchResponse, + Map> expectedIndicesAndFields + ) { + try { + assertOK(searchResponse); + var response = SearchResponseUtils.responseAsSearchResponse(searchResponse); + try { + final var searchResult = Arrays.stream(response.getHits().getHits()) + .collect(Collectors.toMap(SearchHit::getIndex, SearchHit::getSourceAsMap)); + + assertThat(searchResult.keySet(), equalTo(expectedIndicesAndFields.keySet())); + for (String index : expectedIndicesAndFields.keySet()) { + Set expectedFields = expectedIndicesAndFields.get(index); + assertThat(searchResult.get(index).keySet(), equalTo(expectedFields)); + } + } finally { + response.decRef(); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @SuppressWarnings("unchecked") + private Tuple, List> getDataAndFailureIndices(String dataStreamName) throws IOException { + Request dataStream = new Request("GET", "/_data_stream/" + dataStreamName); + Response response = adminClient().performRequest(dataStream); + Map dataStreams = entityAsMap(response); + assertEquals(Collections.singletonList("test1"), XContentMapValues.extractValue("data_streams.name", dataStreams)); + List dataIndexNames = (List) XContentMapValues.extractValue("data_streams.indices.index_name", dataStreams); + List failureIndexNames = (List) XContentMapValues.extractValue( + "data_streams.failure_store.indices.index_name", + dataStreams + ); + return new Tuple<>(dataIndexNames, failureIndexNames); + } + + private Tuple getSingleDataAndFailureIndices(String dataStreamName) throws IOException { + Tuple, List> indices = getDataAndFailureIndices(dataStreamName); + assertThat(indices.v1().size(), equalTo(1)); + assertThat(indices.v2().size(), equalTo(1)); + return new Tuple<>(indices.v1().get(0), indices.v2().get(0)); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index f3db0a3d2f63..b4035a36e15e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -2211,6 +2211,7 @@ public class Security extends Plugin return FieldPredicate.ACCEPT_ALL; } assert indicesAccessControl.isGranted(); + IndexNameExpressionResolver.assertExpressionHasNullOrDataSelector(index); IndicesAccessControl.IndexAccessControl indexPermissions = indicesAccessControl.getIndexPermissions(index); if (indexPermissions == null) { return FieldPredicate.ACCEPT_ALL; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index d2d50d14d4a3..a7fee63ff5b0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -48,8 +48,7 @@ import java.util.Optional; import java.util.Set; import java.util.SortedMap; import java.util.concurrent.CopyOnWriteArraySet; -import java.util.function.Predicate; -import java.util.function.Supplier; +import java.util.function.BiPredicate; import static org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER; @@ -323,7 +322,8 @@ class IndicesAndAliasesResolver { ); } if (indicesOptions.expandWildcardExpressions()) { - for (String authorizedIndex : authorizedIndices.all().get()) { + IndexComponentSelector selector = IndexComponentSelector.getByKeyOrThrow(allIndicesPatternSelector); + for (String authorizedIndex : authorizedIndices.all(selector)) { if (IndexAbstractionResolver.isIndexVisible( "*", allIndicesPatternSelector, @@ -352,7 +352,7 @@ class IndicesAndAliasesResolver { split.getLocal(), indicesOptions, projectMetadata, - authorizedIndices.all(), + authorizedIndices::all, authorizedIndices::check, indicesRequest.includeDataStreams() ); @@ -389,7 +389,7 @@ class IndicesAndAliasesResolver { if (aliasesRequest.expandAliasesWildcards()) { List aliases = replaceWildcardsWithAuthorizedAliases( aliasesRequest.aliases(), - loadAuthorizedAliases(authorizedIndices.all(), projectMetadata) + loadAuthorizedAliases(authorizedIndices, projectMetadata) ); aliasesRequest.replaceAliases(aliases.toArray(new String[aliases.size()])); } @@ -431,8 +431,13 @@ class IndicesAndAliasesResolver { * request's concrete index is not in the list of authorized indices, then we need to look to * see if this can be authorized against an alias */ - static String getPutMappingIndexOrAlias(PutMappingRequest request, Predicate isAuthorized, ProjectMetadata projectMetadata) { + static String getPutMappingIndexOrAlias( + PutMappingRequest request, + BiPredicate isAuthorized, + ProjectMetadata projectMetadata + ) { final String concreteIndexName = request.getConcreteIndex().getName(); + assert IndexNameExpressionResolver.hasSelectorSuffix(concreteIndexName) == false : "selectors are not allowed in this context"; // validate that the concrete index exists, otherwise there is no remapping that we could do final IndexAbstraction indexAbstraction = projectMetadata.getIndicesLookup().get(concreteIndexName); @@ -447,7 +452,8 @@ class IndicesAndAliasesResolver { + indexAbstraction.getType().getDisplayName() + "], but a concrete index is expected" ); - } else if (isAuthorized.test(concreteIndexName)) { + // we know this is implicit data access (as opposed to another selector) so the default selector check is correct + } else if (isAuthorized.test(concreteIndexName, IndexComponentSelector.DATA)) { // user is authorized to put mappings for this index resolvedAliasOrIndex = concreteIndexName; } else { @@ -456,7 +462,12 @@ class IndicesAndAliasesResolver { Map> foundAliases = projectMetadata.findAllAliases(new String[] { concreteIndexName }); List aliasMetadata = foundAliases.get(concreteIndexName); if (aliasMetadata != null) { - Optional foundAlias = aliasMetadata.stream().map(AliasMetadata::alias).filter(isAuthorized).filter(aliasName -> { + Optional foundAlias = aliasMetadata.stream().map(AliasMetadata::alias).filter(aliasName -> { + // we know this is implicit data access (as opposed to another selector) so the default selector check is correct + assert IndexNameExpressionResolver.hasSelectorSuffix(aliasName) == false : "selectors are not allowed in this context"; + if (false == isAuthorized.test(aliasName, IndexComponentSelector.DATA)) { + return false; + } IndexAbstraction alias = projectMetadata.getIndicesLookup().get(aliasName); List indices = alias.getIndices(); if (indices.size() == 1) { @@ -476,10 +487,13 @@ class IndicesAndAliasesResolver { return resolvedAliasOrIndex; } - private static List loadAuthorizedAliases(Supplier> authorizedIndices, ProjectMetadata projectMetadata) { + private static List loadAuthorizedAliases( + AuthorizationEngine.AuthorizedIndices authorizedIndices, + ProjectMetadata projectMetadata + ) { List authorizedAliases = new ArrayList<>(); SortedMap existingAliases = projectMetadata.getIndicesLookup(); - for (String authorizedIndex : authorizedIndices.get()) { + for (String authorizedIndex : authorizedIndices.all(IndexComponentSelector.DATA)) { IndexAbstraction indexAbstraction = existingAliases.get(authorizedIndex); if (indexAbstraction != null && indexAbstraction.getType() == IndexAbstraction.Type.ALIAS) { authorizedAliases.add(authorizedIndex); @@ -498,6 +512,7 @@ class IndicesAndAliasesResolver { } for (String aliasExpression : aliases) { + IndexNameExpressionResolver.assertExpressionHasNullOrDataSelector(aliasExpression); boolean include = true; if (aliasExpression.charAt(0) == '-') { include = false; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 771cc4185bbe..faedcacf21d8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -31,6 +31,7 @@ import org.elasticsearch.action.search.TransportClearScrollAction; import org.elasticsearch.action.search.TransportClosePointInTimeAction; import org.elasticsearch.action.search.TransportMultiSearchAction; import org.elasticsearch.action.search.TransportSearchScrollAction; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.action.support.SubscribableListener; import org.elasticsearch.action.termvectors.MultiTermVectorsAction; import org.elasticsearch.cluster.metadata.DataStream; @@ -107,6 +108,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.TreeSet; +import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; @@ -882,42 +884,76 @@ public class RBACEngine implements AuthorizationEngine { // TODO: can this be done smarter? I think there are usually more indices/aliases in the cluster then indices defined a roles? if (includeDataStreams) { for (IndexAbstraction indexAbstraction : lookup.values()) { - if (predicate.test(indexAbstraction)) { + // failure indices are special: when accessed directly (not through ::failures on parent data stream) they are accessed + // implicitly as data. However, authz to the parent data stream happens via the failures selector + if (indexAbstraction.isFailureIndexOfDataStream() + && predicate.test(indexAbstraction.getParentDataStream(), IndexComponentSelector.FAILURES)) { + indicesAndAliases.add(indexAbstraction.getName()); + // we know this is a failure index, and it's authorized so no need to check further + continue; + } + if (predicate.test(indexAbstraction, IndexComponentSelector.DATA)) { indicesAndAliases.add(indexAbstraction.getName()); if (indexAbstraction.getType() == IndexAbstraction.Type.DATA_STREAM) { // add data stream and its backing indices for any authorized data streams for (Index index : indexAbstraction.getIndices()) { indicesAndAliases.add(index.getName()); } - // TODO: We need to limit if a data stream's failure indices should return here. - for (Index index : ((DataStream) indexAbstraction).getFailureIndices()) { - indicesAndAliases.add(index.getName()); - } } } } } else { for (IndexAbstraction indexAbstraction : lookup.values()) { - if (indexAbstraction.getType() != IndexAbstraction.Type.DATA_STREAM && predicate.test(indexAbstraction)) { + if (indexAbstraction.getType() != IndexAbstraction.Type.DATA_STREAM + // data check is correct, even for failure indices -- in this context, we treat them as regular indices, and + // only include them if direct data access to them is granted (e.g., if a role has `read` over "*") + && predicate.test(indexAbstraction, IndexComponentSelector.DATA)) { indicesAndAliases.add(indexAbstraction.getName()); } } } timeChecker.accept(indicesAndAliases); return indicesAndAliases; - }, name -> { + }, () -> { + // TODO handle time checking in a follow-up + Set indicesAndAliases = new HashSet<>(); + if (includeDataStreams) { + for (IndexAbstraction indexAbstraction : lookup.values()) { + if (indexAbstraction.getType() == IndexAbstraction.Type.DATA_STREAM + && predicate.test(indexAbstraction, IndexComponentSelector.FAILURES)) { + indicesAndAliases.add(indexAbstraction.getName()); + // add data stream and its backing failure indices for any authorized data streams + for (Index index : ((DataStream) indexAbstraction).getFailureIndices()) { + indicesAndAliases.add(index.getName()); + } + } + } + } + return indicesAndAliases; + }, (name, selector) -> { final IndexAbstraction indexAbstraction = lookup.get(name); if (indexAbstraction == null) { // test access (by name) to a resource that does not currently exist // the action handler must handle the case of accessing resources that do not exist - return predicate.test(name, null); - } else { - // We check the parent data stream first if there is one. For testing requested indices, this is most likely - // more efficient than checking the index name first because we recommend grant privileges over data stream - // instead of backing indices. - return (indexAbstraction.getParentDataStream() != null && predicate.test(indexAbstraction.getParentDataStream())) - || predicate.test(indexAbstraction); + return predicate.test(name, null, selector); } + // We check the parent data stream first if there is one. For testing requested indices, this is most likely + // more efficient than checking the index name first because we recommend grant privileges over data stream + // instead of backing indices. + if (indexAbstraction.getParentDataStream() != null) { + if (indexAbstraction.isFailureIndexOfDataStream()) { + // access to failure indices is authorized via failures-based selectors on the parent data stream _not_ via data + // ones + if (predicate.test(indexAbstraction.getParentDataStream(), IndexComponentSelector.FAILURES)) { + return true; + } + } else if (IndexComponentSelector.DATA.equals(selector) || selector == null) { + if (predicate.test(indexAbstraction.getParentDataStream(), IndexComponentSelector.DATA)) { + return true; + } + } // we don't support granting access to a backing index with a failure selector via the parent data stream + } + return predicate.test(indexAbstraction, selector); }); } @@ -1042,23 +1078,32 @@ public class RBACEngine implements AuthorizationEngine { } static final class AuthorizedIndices implements AuthorizationEngine.AuthorizedIndices { + private final CachedSupplier> authorizedAndAvailableDataResources; + private final CachedSupplier> authorizedAndAvailableFailuresResources; + private final BiPredicate isAuthorizedPredicate; - private final CachedSupplier> allAuthorizedAndAvailableSupplier; - private final Predicate isAuthorizedPredicate; - - AuthorizedIndices(Supplier> allAuthorizedAndAvailableSupplier, Predicate isAuthorizedPredicate) { - this.allAuthorizedAndAvailableSupplier = CachedSupplier.wrap(allAuthorizedAndAvailableSupplier); + AuthorizedIndices( + Supplier> authorizedAndAvailableDataResources, + Supplier> authorizedAndAvailableFailuresResources, + BiPredicate isAuthorizedPredicate + ) { + this.authorizedAndAvailableDataResources = CachedSupplier.wrap(authorizedAndAvailableDataResources); + this.authorizedAndAvailableFailuresResources = CachedSupplier.wrap(authorizedAndAvailableFailuresResources); this.isAuthorizedPredicate = Objects.requireNonNull(isAuthorizedPredicate); } @Override - public Supplier> all() { - return allAuthorizedAndAvailableSupplier; + public Set all(IndexComponentSelector selector) { + Objects.requireNonNull(selector, "must specify a selector to get authorized indices"); + return IndexComponentSelector.FAILURES.equals(selector) + ? authorizedAndAvailableFailuresResources.get() + : authorizedAndAvailableDataResources.get(); } @Override - public boolean check(String name) { - return this.isAuthorizedPredicate.test(name); + public boolean check(String name, IndexComponentSelector selector) { + Objects.requireNonNull(selector, "must specify a selector for authorization check"); + return isAuthorizedPredicate.test(name, selector); } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java index 6f9cca010a82..13e7b5446d3c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java @@ -42,7 +42,7 @@ public class RestGetBuiltinPrivilegesAction extends SecurityBaseRestHandler { private static final Logger logger = LogManager.getLogger(RestGetBuiltinPrivilegesAction.class); // TODO remove this once we can update docs tests again - private static final Set FAILURE_STORE_PRIVILEGES_TO_EXCLUDE = Set.of("read_failure_store"); + private static final Set FAILURE_STORE_PRIVILEGES_TO_EXCLUDE = Set.of("read_failure_store", "manage_failure_store"); private final GetBuiltinPrivilegesResponseTranslator responseTranslator; public RestGetBuiltinPrivilegesAction( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index e91ed36331e7..4f712a22eddf 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.security.authz; import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction; import org.elasticsearch.action.search.TransportSearchAction; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.metadata.AliasMetadata; import org.elasticsearch.cluster.metadata.DataStream; @@ -55,7 +56,7 @@ public class AuthorizedIndicesTests extends ESTestCase { Metadata.EMPTY_METADATA.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertTrue(authorizedIndices.all().get().isEmpty()); + assertTrue(authorizedIndices.all(IndexComponentSelector.DATA).isEmpty()); } public void testAuthorizedIndicesUserWithSomeRoles() { @@ -115,15 +116,15 @@ public class AuthorizedIndicesTests extends ESTestCase { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all().get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); - assertThat(authorizedIndices.all().get(), not(contains("bbbbb"))); - assertThat(authorizedIndices.check("bbbbb"), is(false)); - assertThat(authorizedIndices.all().get(), not(contains("ba"))); - assertThat(authorizedIndices.check("ba"), is(false)); - assertThat(authorizedIndices.all().get(), not(contains(internalSecurityIndex))); - assertThat(authorizedIndices.check(internalSecurityIndex), is(false)); - assertThat(authorizedIndices.all().get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); - assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("ba"))); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.DATA), is(false)); } public void testAuthorizedIndicesUserWithSomeRolesEmptyMetadata() { @@ -134,7 +135,7 @@ public class AuthorizedIndicesTests extends ESTestCase { Metadata.EMPTY_METADATA.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertTrue(authorizedIndices.all().get().isEmpty()); + assertTrue(authorizedIndices.all(IndexComponentSelector.DATA).isEmpty()); } public void testSecurityIndicesAreRemovedFromRegularUser() { @@ -145,7 +146,7 @@ public class AuthorizedIndicesTests extends ESTestCase { Metadata.EMPTY_METADATA.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertTrue(authorizedIndices.all().get().isEmpty()); + assertTrue(authorizedIndices.all(IndexComponentSelector.DATA).isEmpty()); } public void testSecurityIndicesAreRestrictedForDefaultRole() { @@ -177,13 +178,13 @@ public class AuthorizedIndicesTests extends ESTestCase { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all().get(), containsInAnyOrder("an-index", "another-index")); - assertThat(authorizedIndices.check("an-index"), is(true)); - assertThat(authorizedIndices.check("another-index"), is(true)); - assertThat(authorizedIndices.all().get(), not(contains(internalSecurityIndex))); - assertThat(authorizedIndices.check(internalSecurityIndex), is(false)); - assertThat(authorizedIndices.all().get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); - assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), containsInAnyOrder("an-index", "another-index")); + assertThat(authorizedIndices.check("an-index", IndexComponentSelector.DATA), is(true)); + assertThat(authorizedIndices.check("another-index", IndexComponentSelector.DATA), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.DATA), is(false)); } public void testSecurityIndicesAreNotRemovedFromUnrestrictedRole() { @@ -216,7 +217,7 @@ public class AuthorizedIndicesTests extends ESTestCase { () -> ignore -> {} ); assertThat( - authorizedIndices.all().get(), + authorizedIndices.all(IndexComponentSelector.DATA), containsInAnyOrder("an-index", "another-index", SecuritySystemIndices.SECURITY_MAIN_ALIAS, internalSecurityIndex) ); @@ -227,7 +228,7 @@ public class AuthorizedIndicesTests extends ESTestCase { () -> ignore -> {} ); assertThat( - authorizedIndicesSuperUser.all().get(), + authorizedIndicesSuperUser.all(IndexComponentSelector.DATA), containsInAnyOrder("an-index", "another-index", SecuritySystemIndices.SECURITY_MAIN_ALIAS, internalSecurityIndex) ); } @@ -274,10 +275,92 @@ public class AuthorizedIndicesTests extends ESTestCase { true ) .put(new IndexMetadata.Builder(backingIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(DataStreamTestHelper.newInstance("adatastream1", List.of(new Index(backingIndex, "_na_")))) + .build(); + final PlainActionFuture future = new PlainActionFuture<>(); + final Set descriptors = Sets.newHashSet(aStarRole, bRole); + CompositeRolesStore.buildRoleFromDescriptors( + descriptors, + new FieldPermissionsCache(Settings.EMPTY), + null, + RESTRICTED_INDICES, + future + ); + Role roles = future.actionGet(); + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + roles, + getRequestInfo(TransportSearchAction.TYPE.name()), + metadata.getProject().getIndicesLookup(), + () -> ignore -> {} + ); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); + for (String resource : List.of("a1", "a2", "aaaaaa", "b", "ab")) { + assertThat(authorizedIndices.check(resource, IndexComponentSelector.DATA), is(true)); + } + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("ba"))); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.DATA), is(false)); + // due to context, datastreams are excluded from wildcard expansion + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("adatastream1"))); + // but they are authorized when explicitly tested (they are not "unavailable" for the Security filter) + assertThat(authorizedIndices.check("adatastream1", IndexComponentSelector.DATA), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.DATA), is(false)); + } + + public void testDataStreamsAreNotIncludedInAuthorizedIndicesWithFailuresSelectorAndAllPrivilege() { + assumeTrue("requires failure store", DataStream.isFailureStoreFeatureFlagEnabled()); + RoleDescriptor aStarRole = new RoleDescriptor( + "a_star", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a*").privileges("all").build() }, + null + ); + RoleDescriptor bRole = new RoleDescriptor( + "b", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("b").privileges("READ").build() }, + null + ); + Settings indexSettings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()).build(); + final String internalSecurityIndex = randomFrom( + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_6, + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7 + ); + String backingIndex = DataStream.getDefaultBackingIndexName("adatastream1", 1); + String failureIndex = DataStream.getDefaultFailureStoreName("adatastream1", 1, 1); + Metadata metadata = Metadata.builder() + .put(new IndexMetadata.Builder("a1").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("a2").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("aaaaaa").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("bbbbb").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + new IndexMetadata.Builder("b").settings(indexSettings) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(new AliasMetadata.Builder("ab").build()) + .putAlias(new AliasMetadata.Builder("ba").build()) + .build(), + true + ) + .put( + new IndexMetadata.Builder(internalSecurityIndex).settings(indexSettings) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(new AliasMetadata.Builder(SecuritySystemIndices.SECURITY_MAIN_ALIAS).build()) + .build(), + true + ) + .put(new IndexMetadata.Builder(backingIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder(failureIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) .put( DataStreamTestHelper.newInstance( "adatastream1", - List.of(new Index(DataStream.getDefaultBackingIndexName("adatastream1", 1), "_na_")) + List.of(new Index(backingIndex, "_na_")), + List.of(new Index(failureIndex, "_na_")) ) ) .build(); @@ -297,22 +380,279 @@ public class AuthorizedIndicesTests extends ESTestCase { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all().get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); - for (String resource : List.of("a1", "a2", "aaaaaa", "b", "ab")) { - assertThat(authorizedIndices.check(resource), is(true)); - } - assertThat(authorizedIndices.all().get(), not(contains("bbbbb"))); - assertThat(authorizedIndices.check("bbbbb"), is(false)); - assertThat(authorizedIndices.all().get(), not(contains("ba"))); - assertThat(authorizedIndices.check("ba"), is(false)); - // due to context, datastreams are excluded from wildcard expansion - assertThat(authorizedIndices.all().get(), not(contains("adatastream1"))); - // but they are authorized when explicitly tested (they are not "unavailable" for the Security filter) - assertThat(authorizedIndices.check("adatastream1"), is(true)); - assertThat(authorizedIndices.all().get(), not(contains(internalSecurityIndex))); - assertThat(authorizedIndices.check(internalSecurityIndex), is(false)); - assertThat(authorizedIndices.all().get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); - assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS), is(false)); + assertAuthorizedFor(authorizedIndices, IndexComponentSelector.DATA, "a1", "a2", "aaaaaa", "b", "ab"); + assertAuthorizedFor(authorizedIndices, IndexComponentSelector.FAILURES); + + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.FAILURES), is(false)); + + assertThat(authorizedIndices.check("ba", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.FAILURES), is(false)); + + // data are authorized when explicitly tested (they are not "unavailable" for the Security filter) + assertThat(authorizedIndices.check("adatastream1", IndexComponentSelector.DATA), is(true)); + assertThat(authorizedIndices.check("adatastream1", IndexComponentSelector.FAILURES), is(true)); + + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.FAILURES), is(false)); + } + + public void testDataStreamsAreIncludedInAuthorizedIndicesWithFailuresSelectorAndAllPrivilege() { + assumeTrue("requires failure store", DataStream.isFailureStoreFeatureFlagEnabled()); + RoleDescriptor aStarRole = new RoleDescriptor( + "a_star", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a*").privileges("all").build() }, + null + ); + RoleDescriptor bRole = new RoleDescriptor( + "b", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("b").privileges("READ").build() }, + null + ); + Settings indexSettings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()).build(); + final String internalSecurityIndex = randomFrom( + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_6, + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7 + ); + String backingIndex = DataStream.getDefaultBackingIndexName("adatastream1", 1); + String failureIndex = DataStream.getDefaultFailureStoreName("adatastream1", 1, 1); + Metadata metadata = Metadata.builder() + .put(new IndexMetadata.Builder("a1").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("a2").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("aaaaaa").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("bbbbb").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + new IndexMetadata.Builder("b").settings(indexSettings) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(new AliasMetadata.Builder("ab").build()) + .putAlias(new AliasMetadata.Builder("ba").build()) + .build(), + true + ) + .put( + new IndexMetadata.Builder(internalSecurityIndex).settings(indexSettings) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(new AliasMetadata.Builder(SecuritySystemIndices.SECURITY_MAIN_ALIAS).build()) + .build(), + true + ) + .put(new IndexMetadata.Builder(backingIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder(failureIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + DataStreamTestHelper.newInstance( + "adatastream1", + List.of(new Index(backingIndex, "_na_")), + List.of(new Index(failureIndex, "_na_")) + ) + ) + .build(); + final PlainActionFuture future = new PlainActionFuture<>(); + final Set descriptors = Sets.newHashSet(aStarRole, bRole); + CompositeRolesStore.buildRoleFromDescriptors( + descriptors, + new FieldPermissionsCache(Settings.EMPTY), + null, + RESTRICTED_INDICES, + future + ); + Role roles = future.actionGet(); + TransportRequest request = new ResolveIndexAction.Request(new String[] { "a*" }); + AuthorizationEngine.RequestInfo requestInfo = getRequestInfo(request, TransportSearchAction.TYPE.name()); + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + roles, + requestInfo, + metadata.getProject().getIndicesLookup(), + () -> ignore -> {} + ); + assertAuthorizedFor( + authorizedIndices, + IndexComponentSelector.DATA, + "a1", + "a2", + "aaaaaa", + "b", + "ab", + "adatastream1", + backingIndex, + failureIndex + ); + assertAuthorizedFor(authorizedIndices, IndexComponentSelector.FAILURES, "adatastream1", failureIndex); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.FAILURES), is(false)); + } + + public void testDataStreamsAreIncludedInAuthorizedIndicesWithFailuresSelector() { + assumeTrue("requires failure store", DataStream.isFailureStoreFeatureFlagEnabled()); + RoleDescriptor aReadFailuresStarRole = new RoleDescriptor( + "a_read_failure_store", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a*").privileges("read_failure_store").build() }, + null + ); + RoleDescriptor aReadRole = new RoleDescriptor( + "a_read", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a*").privileges("read").build() }, + null + ); + Settings indexSettings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()).build(); + final String internalSecurityIndex = randomFrom( + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_6, + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7 + ); + String backingIndex = DataStream.getDefaultBackingIndexName("adatastream1", 1); + String failureIndex = DataStream.getDefaultFailureStoreName("adatastream1", 1, 1); + Metadata metadata = Metadata.builder() + .put(new IndexMetadata.Builder("a1").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("a2").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("aaaaaa").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("bbbbb").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + new IndexMetadata.Builder(internalSecurityIndex).settings(indexSettings) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(new AliasMetadata.Builder(SecuritySystemIndices.SECURITY_MAIN_ALIAS).build()) + .build(), + true + ) + .put(new IndexMetadata.Builder(backingIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder(failureIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + DataStreamTestHelper.newInstance( + "adatastream1", + List.of(new Index(backingIndex, "_na_")), + List.of(new Index(failureIndex, "_na_")) + ) + ) + .build(); + final PlainActionFuture future = new PlainActionFuture<>(); + final Set descriptors = Sets.newHashSet(aReadFailuresStarRole, aReadRole); + CompositeRolesStore.buildRoleFromDescriptors( + descriptors, + new FieldPermissionsCache(Settings.EMPTY), + null, + RESTRICTED_INDICES, + future + ); + Role roles = future.actionGet(); + TransportRequest request = new ResolveIndexAction.Request(new String[] { "a*" }); + AuthorizationEngine.RequestInfo requestInfo = getRequestInfo(request, TransportSearchAction.TYPE.name()); + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + roles, + requestInfo, + metadata.getProject().getIndicesLookup(), + () -> ignore -> {} + ); + assertAuthorizedFor( + authorizedIndices, + IndexComponentSelector.DATA, + "a1", + "a2", + "aaaaaa", + "adatastream1", + backingIndex, + failureIndex + ); + assertAuthorizedFor(authorizedIndices, IndexComponentSelector.FAILURES, "adatastream1", failureIndex); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.FAILURES), is(false)); + } + + public void testDataStreamsAreNotIncludedInAuthorizedIndicesWithFailuresSelector() { + assumeTrue("requires failure store", DataStream.isFailureStoreFeatureFlagEnabled()); + RoleDescriptor aReadFailuresStarRole = new RoleDescriptor( + "a_read_failure_store", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a*").privileges("read_failure_store").build() }, + null + ); + RoleDescriptor aReadRole = new RoleDescriptor( + "a_read", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a*").privileges("read").build() }, + null + ); + Settings indexSettings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()).build(); + final String internalSecurityIndex = randomFrom( + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_6, + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7 + ); + String backingIndex = DataStream.getDefaultBackingIndexName("adatastream1", 1); + String failureIndex = DataStream.getDefaultFailureStoreName("adatastream1", 1, 1); + Metadata metadata = Metadata.builder() + .put(new IndexMetadata.Builder("a1").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("a2").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("aaaaaa").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("bbbbb").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + new IndexMetadata.Builder(internalSecurityIndex).settings(indexSettings) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(new AliasMetadata.Builder(SecuritySystemIndices.SECURITY_MAIN_ALIAS).build()) + .build(), + true + ) + .put(new IndexMetadata.Builder(backingIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder(failureIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + DataStreamTestHelper.newInstance( + "adatastream1", + List.of(new Index(backingIndex, "_na_")), + List.of(new Index(failureIndex, "_na_")) + ) + ) + .build(); + final PlainActionFuture future = new PlainActionFuture<>(); + final Set descriptors = Sets.newHashSet(aReadFailuresStarRole, aReadRole); + CompositeRolesStore.buildRoleFromDescriptors( + descriptors, + new FieldPermissionsCache(Settings.EMPTY), + null, + RESTRICTED_INDICES, + future + ); + Role roles = future.actionGet(); + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + roles, + getRequestInfo(TransportSearchAction.TYPE.name()), + metadata.getProject().getIndicesLookup(), + () -> ignore -> {} + ); + assertAuthorizedFor(authorizedIndices, IndexComponentSelector.DATA, "a1", "a2", "aaaaaa"); + assertAuthorizedFor(authorizedIndices, IndexComponentSelector.FAILURES); + + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.FAILURES), is(false)); + + assertThat(authorizedIndices.check("ba", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.FAILURES), is(false)); + + // data are authorized when explicitly tested (they are not "unavailable" for the Security filter) + assertThat(authorizedIndices.check("adatastream1", IndexComponentSelector.DATA), is(true)); + assertThat(authorizedIndices.check("adatastream1", IndexComponentSelector.FAILURES), is(true)); + + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.FAILURES), is(false)); } public void testDataStreamsAreIncludedInAuthorizedIndices() { @@ -357,12 +697,7 @@ public class AuthorizedIndicesTests extends ESTestCase { true ) .put(new IndexMetadata.Builder(backingIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) - .put( - DataStreamTestHelper.newInstance( - "adatastream1", - List.of(new Index(DataStream.getDefaultBackingIndexName("adatastream1", 1), "_na_")) - ) - ) + .put(DataStreamTestHelper.newInstance("adatastream1", List.of(new Index(backingIndex, "_na_")))) .build(); final PlainActionFuture future = new PlainActionFuture<>(); final Set descriptors = Sets.newHashSet(aStarRole, bRole); @@ -382,15 +717,18 @@ public class AuthorizedIndicesTests extends ESTestCase { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all().get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab", "adatastream1", backingIndex)); - assertThat(authorizedIndices.all().get(), not(contains("bbbbb"))); - assertThat(authorizedIndices.check("bbbbb"), is(false)); - assertThat(authorizedIndices.all().get(), not(contains("ba"))); - assertThat(authorizedIndices.check("ba"), is(false)); - assertThat(authorizedIndices.all().get(), not(contains(internalSecurityIndex))); - assertThat(authorizedIndices.check(internalSecurityIndex), is(false)); - assertThat(authorizedIndices.all().get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); - assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS), is(false)); + assertThat( + authorizedIndices.all(IndexComponentSelector.DATA), + containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab", "adatastream1", backingIndex) + ); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("ba"))); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.DATA), is(false)); } public static AuthorizationEngine.RequestInfo getRequestInfo(String action) { @@ -410,4 +748,15 @@ public class AuthorizedIndicesTests extends ESTestCase { null ); } + + private static void assertAuthorizedFor( + AuthorizedIndices authorizedIndices, + IndexComponentSelector selector, + String... expectedIndices + ) { + assertThat(authorizedIndices.all(selector), containsInAnyOrder(expectedIndices)); + for (String resource : expectedIndices) { + assertThat(authorizedIndices.check(resource, selector), is(true)); + } + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index d230aeeb5666..aea8528ef0d5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -30,6 +30,7 @@ import org.elasticsearch.action.search.SearchShardsRequest; import org.elasticsearch.action.search.TransportMultiSearchAction; import org.elasticsearch.action.search.TransportSearchAction; import org.elasticsearch.action.search.TransportSearchShardsAction; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.termvectors.MultiTermVectorsRequest; @@ -1941,7 +1942,11 @@ public class IndicesAndAliasesResolverTests extends ESTestCase { String index = "logs-00003"; // write index PutMappingRequest request = new PutMappingRequest(Strings.EMPTY_ARRAY).setConcreteIndex(new Index(index, UUIDs.base64UUID())); assert projectMetadata.getIndicesLookup().get("logs-alias").getIndices().size() == 3; - String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, "logs-alias"::equals, projectMetadata); + String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias( + request, + (i, s) -> i.equals("logs-alias"), + projectMetadata + ); String message = "user is authorized to access `logs-alias` and the put mapping request is for a write index" + "so this should have returned the alias name"; assertEquals(message, "logs-alias", putMappingIndexOrAlias); @@ -1951,7 +1956,11 @@ public class IndicesAndAliasesResolverTests extends ESTestCase { String index = "logs-00002"; // read index PutMappingRequest request = new PutMappingRequest(Strings.EMPTY_ARRAY).setConcreteIndex(new Index(index, UUIDs.base64UUID())); assert projectMetadata.getIndicesLookup().get("logs-alias").getIndices().size() == 3; - String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, "logs-alias"::equals, projectMetadata); + String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias( + request, + (i, s) -> i.equals("logs-alias"), + projectMetadata + ); String message = "user is authorized to access `logs-alias` and the put mapping request is for a read index" + "so this should have returned the concrete index as fallback"; assertEquals(message, index, putMappingIndexOrAlias); @@ -2225,14 +2234,14 @@ public class IndicesAndAliasesResolverTests extends ESTestCase { List dataStreams = List.of("logs-foo", "logs-foobar"); final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); for (String dsName : dataStreams) { - assertThat(authorizedIndices.all().get(), hasItem(dsName)); - assertThat(authorizedIndices.check(dsName), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName, IndexComponentSelector.DATA), is(true)); DataStream dataStream = projectMetadata.dataStreams().get(dsName); - assertThat(authorizedIndices.all().get(), hasItem(dsName)); - assertThat(authorizedIndices.check(dsName), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName, IndexComponentSelector.DATA), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } } @@ -2264,14 +2273,14 @@ public class IndicesAndAliasesResolverTests extends ESTestCase { // data streams and their backing indices should _not_ be in the authorized list since the backing indices // do not match the requested name final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, IndexComponentSelector.DATA), is(true)); DataStream dataStream = projectMetadata.dataStreams().get(dataStreamName); - assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, IndexComponentSelector.DATA), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } // neither data streams nor their backing indices will be in the resolved list since the backing indices do not match the @@ -2299,11 +2308,11 @@ public class IndicesAndAliasesResolverTests extends ESTestCase { final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, TransportSearchAction.TYPE.name(), request); for (String dsName : expectedDataStreams) { DataStream dataStream = projectMetadata.dataStreams().get(dsName); - assertThat(authorizedIndices.all().get(), hasItem(dsName)); - assertThat(authorizedIndices.check(dsName), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName, IndexComponentSelector.DATA), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } } @@ -2340,11 +2349,11 @@ public class IndicesAndAliasesResolverTests extends ESTestCase { final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, TransportSearchAction.TYPE.name(), request); // data streams and their backing indices should be in the authorized list - assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, IndexComponentSelector.DATA), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } ResolvedIndices resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases( @@ -2373,15 +2382,15 @@ public class IndicesAndAliasesResolverTests extends ESTestCase { final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, TransportSearchAction.TYPE.name(), request); for (String dsName : expectedDataStreams) { DataStream dataStream = projectMetadata.dataStreams().get(dsName); - assertThat(authorizedIndices.all().get(), hasItem(dsName)); - assertThat(authorizedIndices.check(dsName), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName, IndexComponentSelector.DATA), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } } @@ -2412,16 +2421,16 @@ public class IndicesAndAliasesResolverTests extends ESTestCase { // data streams and their backing indices should _not_ be in the authorized list since the backing indices // did not match the requested pattern and the request does not support data streams final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, IndexComponentSelector.DATA), is(true)); DataStream dataStream = projectMetadata.dataStreams().get(dataStreamName); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } // neither data streams nor their backing indices will be in the resolved list since the request does not support data streams @@ -2452,19 +2461,19 @@ public class IndicesAndAliasesResolverTests extends ESTestCase { // data streams should _not_ be in the authorized list but their backing indices that matched both the requested pattern // and the authorized pattern should be in the list final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all().get(), not(hasItem("logs-foobar"))); - assertThat(authorizedIndices.check("logs-foobar"), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar", IndexComponentSelector.DATA), is(false)); DataStream dataStream = projectMetadata.dataStreams().get("logs-foobar"); - assertThat(authorizedIndices.all().get(), not(hasItem(indexName))); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(hasItem(indexName))); // request pattern is subset of the authorized pattern, but be aware that patterns are never passed to #check in main code - assertThat(authorizedIndices.check(indexName), is(true)); + assertThat(authorizedIndices.check(indexName, IndexComponentSelector.DATA), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } // only the backing indices will be in the resolved list since the request does not support data streams @@ -2492,14 +2501,14 @@ public class IndicesAndAliasesResolverTests extends ESTestCase { // data streams should _not_ be in the authorized list but a single backing index that matched the requested pattern // and the authorized name should be in the list final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all().get(), not(hasItem("logs-foobar"))); - assertThat(authorizedIndices.check("logs-foobar"), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar", IndexComponentSelector.DATA), is(false)); String expectedIndex = failureStore ? DataStream.getDefaultFailureStoreName("logs-foobar", 1, System.currentTimeMillis()) : DataStream.getDefaultBackingIndexName("logs-foobar", 1); - assertThat(authorizedIndices.all().get(), hasItem(expectedIndex)); - assertThat(authorizedIndices.check(expectedIndex), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(expectedIndex)); + assertThat(authorizedIndices.check(expectedIndex, IndexComponentSelector.DATA), is(true)); // only the single backing index will be in the resolved list since the request does not support data streams // but one of the backing indices matched the requested pattern @@ -2524,19 +2533,19 @@ public class IndicesAndAliasesResolverTests extends ESTestCase { // data streams should _not_ be in the authorized list but their backing indices that matched both the requested pattern // and the authorized pattern should be in the list final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all().get(), not(hasItem("logs-foobar"))); - assertThat(authorizedIndices.check("logs-foobar"), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar", IndexComponentSelector.DATA), is(false)); DataStream dataStream = projectMetadata.dataStreams().get("logs-foobar"); - assertThat(authorizedIndices.all().get(), not(hasItem(indexName))); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(hasItem(indexName))); // request pattern is subset of the authorized pattern, but be aware that patterns are never passed to #check in main code - assertThat(authorizedIndices.check(indexName), is(true)); + assertThat(authorizedIndices.check(indexName, IndexComponentSelector.DATA), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } // only the backing indices will be in the resolved list since the request does not support data streams @@ -2567,10 +2576,10 @@ public class IndicesAndAliasesResolverTests extends ESTestCase { ? DataStream.getDefaultFailureStoreName("logs-foobar", 1, System.currentTimeMillis()) : DataStream.getDefaultBackingIndexName("logs-foobar", 1); final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all().get(), not(hasItem("logs-foobar"))); - assertThat(authorizedIndices.check("logs-foobar"), is(false)); - assertThat(authorizedIndices.all().get(), hasItem(expectedIndex)); - assertThat(authorizedIndices.check(expectedIndex), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(expectedIndex)); + assertThat(authorizedIndices.check(expectedIndex, IndexComponentSelector.DATA), is(true)); // only the single backing index will be in the resolved list since the request does not support data streams // but one of the backing indices matched the requested pattern diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 59fa2e417432..9ff64723704e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -19,6 +19,7 @@ import org.elasticsearch.action.delete.TransportDeleteAction; import org.elasticsearch.action.index.TransportIndexAction; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.TransportSearchAction; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.SubscribableListener; import org.elasticsearch.client.internal.Client; @@ -1444,14 +1445,14 @@ public class RBACEngineTests extends ESTestCase { lookup, () -> ignore -> {} ); - assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, IndexComponentSelector.DATA), is(true)); assertThat( - authorizedIndices.all().get(), - hasItems(backingIndices.stream().map(im -> im.getIndex().getName()).collect(Collectors.toList()).toArray(Strings.EMPTY_ARRAY)) + authorizedIndices.all(IndexComponentSelector.DATA), + hasItems(backingIndices.stream().map(im -> im.getIndex().getName()).toList().toArray(Strings.EMPTY_ARRAY)) ); for (String index : backingIndices.stream().map(im -> im.getIndex().getName()).toList()) { - assertThat(authorizedIndices.check(index), is(true)); + assertThat(authorizedIndices.check(index, IndexComponentSelector.DATA), is(true)); } } @@ -1487,7 +1488,8 @@ public class RBACEngineTests extends ESTestCase { lookup, () -> ignore -> {} ); - assertThat(authorizedIndices.all().get().isEmpty(), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA).isEmpty(), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.FAILURES).isEmpty(), is(true)); } public void testNoInfiniteRecursionForRBACAuthorizationInfoHashCode() { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java index d3ee1fb5fdc5..5ac5452fae90 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.TransportVersion; import org.elasticsearch.action.admin.indices.mapping.put.TransportAutoPutMappingAction; import org.elasticsearch.action.admin.indices.mapping.put.TransportPutMappingAction; import org.elasticsearch.action.search.TransportSearchAction; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.cluster.metadata.AliasMetadata; import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.cluster.metadata.DataStreamTestHelper; @@ -185,6 +186,269 @@ public class IndicesPermissionTests extends ESTestCase { } + public void testAuthorizeDataStreamAccessWithFailuresSelector() { + Metadata.Builder builder = Metadata.builder(); + String dataStreamName = randomAlphaOfLength(6); + int numBackingIndices = randomIntBetween(1, 3); + List backingIndices = new ArrayList<>(); + for (int backingIndexNumber = 1; backingIndexNumber <= numBackingIndices; backingIndexNumber++) { + backingIndices.add(createBackingIndexMetadata(DataStream.getDefaultBackingIndexName(dataStreamName, backingIndexNumber))); + } + DataStream ds = DataStreamTestHelper.newInstance( + dataStreamName, + backingIndices.stream().map(IndexMetadata::getIndex).collect(Collectors.toList()) + ); + builder.put(ds); + for (IndexMetadata index : backingIndices) { + builder.put(index, false); + } + var metadata = builder.build().getProject(); + FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY); + + for (var privilege : List.of(IndexPrivilege.ALL, IndexPrivilege.READ)) { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + privilege, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(randomFrom(dataStreamName, dataStreamName + "::data")), + metadata, + fieldPermissionsCache + ); + assertThat("for privilege " + privilege, permissions.isGranted(), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(false)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName), is(true)); + } + + for (var privilege : List.of(IndexPrivilege.ALL, IndexPrivilege.READ_FAILURE_STORE)) { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + privilege, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(dataStreamName + "::failures"), + metadata, + fieldPermissionsCache + ); + assertThat("for privilege " + privilege, permissions.isGranted(), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName), is(false)); + } + + for (var privilege : List.of(IndexPrivilege.ALL, IndexPrivilege.READ_FAILURE_STORE)) { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + privilege, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(dataStreamName + "::failures"), + metadata, + fieldPermissionsCache + ); + + assertThat("for privilege " + privilege, permissions.isGranted(), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName), is(false)); + } + + { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + IndexPrivilege.READ, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + IndexPrivilege.READ_FAILURE_STORE, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(randomFrom(dataStreamName, dataStreamName + "::data"), dataStreamName + "::failures"), + metadata, + fieldPermissionsCache + ); + assertThat(permissions.isGranted(), is(true)); + assertThat(permissions.hasIndexPermissions(dataStreamName + "::failures"), is(true)); + assertThat(permissions.hasIndexPermissions(dataStreamName), is(true)); + } + + { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + IndexPrivilege.ALL, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(randomFrom(dataStreamName, dataStreamName + "::data"), dataStreamName + "::failures"), + metadata, + fieldPermissionsCache + ); + assertThat("for privilege " + IndexPrivilege.ALL, permissions.isGranted(), is(true)); + assertThat("for privilege " + IndexPrivilege.ALL, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(true)); + assertThat("for privilege " + IndexPrivilege.ALL, permissions.hasIndexPermissions(dataStreamName), is(true)); + } + { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + IndexPrivilege.READ_FAILURE_STORE, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(randomFrom(dataStreamName, dataStreamName + "::data"), dataStreamName + "::failures"), + metadata, + fieldPermissionsCache + ); + assertThat("for privilege " + IndexPrivilege.READ_FAILURE_STORE, permissions.isGranted(), is(false)); + assertThat( + "for privilege " + IndexPrivilege.READ_FAILURE_STORE, + permissions.hasIndexPermissions(dataStreamName + "::failures"), + is(true) + ); + assertThat("for privilege " + IndexPrivilege.READ_FAILURE_STORE, permissions.hasIndexPermissions(dataStreamName), is(false)); + } + + { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + IndexPrivilege.READ, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(randomFrom(dataStreamName, dataStreamName + "::data"), dataStreamName + "::failures"), + metadata, + fieldPermissionsCache + ); + assertThat("for privilege " + IndexPrivilege.READ, permissions.isGranted(), is(false)); + assertThat("for privilege " + IndexPrivilege.READ, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(false)); + assertThat("for privilege " + IndexPrivilege.READ, permissions.hasIndexPermissions(dataStreamName), is(true)); + } + } + + public void testAuthorizeDataStreamFailureIndices() { + Metadata.Builder builder = Metadata.builder(); + String dataStreamName = randomAlphaOfLength(6); + int numBackingIndices = randomIntBetween(1, 3); + List backingIndices = new ArrayList<>(); + for (int backingIndexNumber = 1; backingIndexNumber <= numBackingIndices; backingIndexNumber++) { + backingIndices.add(createBackingIndexMetadata(DataStream.getDefaultBackingIndexName(dataStreamName, backingIndexNumber))); + } + List failureIndices = new ArrayList<>(); + int numFailureIndices = randomIntBetween(1, 3); + for (int failureIndexNumber = 1; failureIndexNumber <= numFailureIndices; failureIndexNumber++) { + failureIndices.add(createBackingIndexMetadata(DataStream.getDefaultFailureStoreName(dataStreamName, failureIndexNumber, 1L))); + } + DataStream ds = DataStreamTestHelper.newInstance( + dataStreamName, + backingIndices.stream().map(IndexMetadata::getIndex).collect(Collectors.toList()), + failureIndices.stream().map(IndexMetadata::getIndex).collect(Collectors.toList()) + ); + builder.put(ds); + for (IndexMetadata index : backingIndices) { + builder.put(index, false); + } + for (IndexMetadata index : failureIndices) { + builder.put(index, false); + } + var metadata = builder.build().getProject(); + FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY); + + for (var privilege : List.of(IndexPrivilege.READ)) { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + privilege, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + String failureIndex = randomFrom(failureIndices).getIndex().getName(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(failureIndex), + metadata, + fieldPermissionsCache + ); + assertThat("for privilege " + privilege, permissions.isGranted(), is(false)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(failureIndex), is(false)); + + String dataIndex = randomFrom(backingIndices).getIndex().getName(); + permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet(dataIndex), metadata, fieldPermissionsCache); + + assertThat("for privilege " + privilege, permissions.isGranted(), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataIndex), is(true)); + } + + for (var privilege : List.of(IndexPrivilege.READ_FAILURE_STORE)) { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + privilege, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + + String failureIndex = randomFrom(failureIndices).getIndex().getName(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(failureIndex), + metadata, + fieldPermissionsCache + ); + + assertThat("for privilege " + privilege, permissions.isGranted(), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(failureIndex), is(true)); + + String dataIndex = randomFrom(backingIndices).getIndex().getName(); + permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet(dataIndex), metadata, fieldPermissionsCache); + + assertThat("for privilege " + privilege, permissions.isGranted(), is(false)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataIndex), is(false)); + } + } + public void testAuthorizeMultipleGroupsMixedDls() { IndexMetadata.Builder imbBuilder = IndexMetadata.builder("_index") .settings(indexSettings(IndexVersion.current(), 1, 1)) @@ -705,14 +969,15 @@ public class IndicesPermissionTests extends ESTestCase { ); IndicesPermission.IsResourceAuthorizedPredicate predicate = new IndicesPermission.IsResourceAuthorizedPredicate( StringMatcher.of("other"), + StringMatcher.of(), StringMatcher.of(dataStreamName, backingIndex.getName(), concreteIndex.getName(), alias.getName()) ); assertThat(predicate.test(dataStream), is(false)); // test authorization for a missing resource with the datastream's name - assertThat(predicate.test(dataStream.getName(), null), is(true)); + assertThat(predicate.test(dataStream.getName(), null, IndexComponentSelector.DATA), is(true)); assertThat(predicate.test(backingIndex), is(false)); // test authorization for a missing resource with the backing index's name - assertThat(predicate.test(backingIndex.getName(), null), is(true)); + assertThat(predicate.test(backingIndex.getName(), null, IndexComponentSelector.DATA), is(true)); assertThat(predicate.test(concreteIndex), is(true)); assertThat(predicate.test(alias), is(true)); } @@ -720,10 +985,12 @@ public class IndicesPermissionTests extends ESTestCase { public void testResourceAuthorizedPredicateAnd() { IndicesPermission.IsResourceAuthorizedPredicate predicate1 = new IndicesPermission.IsResourceAuthorizedPredicate( StringMatcher.of("c", "a"), + StringMatcher.of(), StringMatcher.of("b", "d") ); IndicesPermission.IsResourceAuthorizedPredicate predicate2 = new IndicesPermission.IsResourceAuthorizedPredicate( StringMatcher.of("c", "b"), + StringMatcher.of(), StringMatcher.of("a", "d") ); Metadata.Builder mb = Metadata.builder( @@ -754,6 +1021,75 @@ public class IndicesPermissionTests extends ESTestCase { assertThat(predicate.test(concreteIndexD), is(true)); } + public void testResourceAuthorizedPredicateAndWithFailures() { + IndicesPermission.IsResourceAuthorizedPredicate predicate1 = new IndicesPermission.IsResourceAuthorizedPredicate( + StringMatcher.of("c", "a"), + StringMatcher.of("e", "f"), + StringMatcher.of("b", "d") + ); + IndicesPermission.IsResourceAuthorizedPredicate predicate2 = new IndicesPermission.IsResourceAuthorizedPredicate( + StringMatcher.of("c", "b"), + StringMatcher.of("a", "f", "g"), + StringMatcher.of("a", "d") + ); + Metadata.Builder mb = Metadata.builder( + DataStreamTestHelper.getClusterStateWithDataStreams( + List.of( + Tuple.tuple("a", 1), + Tuple.tuple("b", 1), + Tuple.tuple("c", 1), + Tuple.tuple("d", 1), + Tuple.tuple("e", 1), + Tuple.tuple("f", 1) + ), + List.of(), + Instant.now().toEpochMilli(), + builder().build(), + 1 + ).getMetadata() + ); + DataStream dataStreamA = mb.dataStream("a"); + DataStream dataStreamB = mb.dataStream("b"); + DataStream dataStreamC = mb.dataStream("c"); + DataStream dataStreamD = mb.dataStream("d"); + DataStream dataStreamE = mb.dataStream("e"); + DataStream dataStreamF = mb.dataStream("f"); + IndexAbstraction concreteIndexA = concreteIndexAbstraction("a"); + IndexAbstraction concreteIndexB = concreteIndexAbstraction("b"); + IndexAbstraction concreteIndexC = concreteIndexAbstraction("c"); + IndexAbstraction concreteIndexD = concreteIndexAbstraction("d"); + IndexAbstraction concreteIndexE = concreteIndexAbstraction("e"); + IndexAbstraction concreteIndexF = concreteIndexAbstraction("f"); + IndicesPermission.IsResourceAuthorizedPredicate predicate = predicate1.and(predicate2); + assertThat(predicate.test(dataStreamA), is(false)); + assertThat(predicate.test(dataStreamB), is(false)); + assertThat(predicate.test(dataStreamC), is(true)); + assertThat(predicate.test(dataStreamD), is(false)); + assertThat(predicate.test(dataStreamE), is(false)); + assertThat(predicate.test(dataStreamF), is(false)); + + assertThat(predicate.test(dataStreamA, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(dataStreamB, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(dataStreamC, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(dataStreamD, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(dataStreamE, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(dataStreamF, IndexComponentSelector.FAILURES), is(true)); + + assertThat(predicate.test(concreteIndexA), is(true)); + assertThat(predicate.test(concreteIndexB), is(true)); + assertThat(predicate.test(concreteIndexC), is(true)); + assertThat(predicate.test(concreteIndexD), is(true)); + assertThat(predicate.test(concreteIndexE), is(false)); + assertThat(predicate.test(concreteIndexF), is(false)); + + assertThat(predicate.test(concreteIndexA, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(concreteIndexB, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(concreteIndexC, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(concreteIndexD, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(concreteIndexE, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(concreteIndexF, IndexComponentSelector.FAILURES), is(true)); + } + private static IndexAbstraction concreteIndexAbstraction(String name) { return new IndexAbstraction.ConcreteIndex( IndexMetadata.builder(name).settings(indexSettings(IndexVersion.current(), 1, 0)).build()