From 2bc0e9925a2d421007e31b82c5ec8cf616197e13 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Wed, 19 Mar 2025 06:49:51 +0100 Subject: [PATCH 01/13] Prevent usage of :: selectors for remote cluster requests --- .../metadata/IndexNameExpressionResolver.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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 e221cb5c08e5b..64216cfb364a3 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -2356,6 +2356,7 @@ private static V splitSelectorExpression(String expression, BiFunction Date: Wed, 19 Mar 2025 19:36:22 +0100 Subject: [PATCH 02/13] fail only if ::failures selector is used --- .../metadata/IndexNameExpressionResolver.java | 28 ++++++++++--------- .../metadata/SelectorResolverTests.java | 21 ++++++++++++++ 2 files changed, 36 insertions(+), 13 deletions(-) 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 64216cfb364a3..74251deb33afc 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -2356,7 +2356,7 @@ private static V splitSelectorExpression(String expression, BiFunction resolve(selectorsAllowed, "::::data")); + expectThrows(InvalidIndexNameException.class, () -> resolve(selectorsAllowed, "::::failures")); + expectThrows(InvalidIndexNameException.class, () -> resolve(selectorsAllowed, ":::::failures")); // Suffix case is not supported because there is no component named with the empty string expectThrows(InvalidIndexNameException.class, () -> resolve(selectorsAllowed, "index::")); + + // remote cluster syntax is not allowed with ::failures selector + assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(selectorsAllowed, "cluster:index::failures"); + assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(noSelectors, "cluster:index::failures"); + assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(selectorsAllowed, "cluster-*:index::failures"); + assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(selectorsAllowed, "cluster-*:index-*::failures"); + assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(selectorsAllowed, "cluster-*:*::failures"); + assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(selectorsAllowed, "*:index-*::failures"); + assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(selectorsAllowed, "*:*::failures"); + // even with an empty index name + assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(selectorsAllowed, "cluster:::failures"); + } + + public void assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(Context context, String expression) { + var e = expectThrows(IllegalArgumentException.class, () -> resolve(context, expression)); + assertThat(e.getMessage(), containsString("failures selector is not supported with remote cluster expressions")); } public void testResolveMatchAllToSelectors() { From ba976a9742bd14b81209a3384812afa62c8ac2f6 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Wed, 19 Mar 2025 22:51:18 +0100 Subject: [PATCH 03/13] cleanup and extend existing rest IT --- .../metadata/IndexNameExpressionResolver.java | 14 +++++++------- .../cluster/metadata/SelectorResolverTests.java | 2 +- .../remotecluster/RemoteClusterSecurityRestIT.java | 8 ++++++++ 3 files changed, 16 insertions(+), 8 deletions(-) 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 74251deb33afc..0f92b294dfa57 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -2356,7 +2356,7 @@ private static V splitSelectorExpression(String expression, BiFunction resolve(context, expression)); - assertThat(e.getMessage(), containsString("failures selector is not supported with remote cluster expressions")); + assertThat(e.getMessage(), containsString("failures selector is not supported with cross-cluster expressions")); } public void testResolveMatchAllToSelectors() { diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRestIT.java index 307f59859c75a..afa8e9365fe93 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRestIT.java @@ -609,6 +609,14 @@ public void testCrossClusterSearch() throws Exception { assertThat(exception6.getResponse().getStatusLine().getStatusCode(), equalTo(401)); assertThat(exception6.getMessage(), containsString("invalid cross-cluster API key value")); } + + // check that ::failures selector is not supported with cross cluster search + final ResponseException exception7 = expectThrows( + ResponseException.class, + () -> performRequestWithRemoteSearchUser(new Request("GET", "/my_remote_cluster:index1::failures/_search")) + ); + assertThat(exception7.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat(exception7.getMessage(), containsString("failures selector is not supported with cross-cluster expressions")); } assertNoRcs1DeprecationWarnings(); } From 5bc46b552e001bc5bd53792d74ae13cbcc0819da Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Thu, 20 Mar 2025 10:41:12 +0100 Subject: [PATCH 04/13] test ccs with rcs1 --- ...moteClusterSecurityFailureStoreRestIT.java | 134 +++++++++++++ ...ClusterSecurityRCS1FailureStoreRestIT.java | 182 ++++++++++++++++++ .../RemoteClusterSecurityRestIT.java | 8 - 3 files changed, 316 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityFailureStoreRestIT.java create mode 100644 x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityFailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityFailureStoreRestIT.java new file mode 100644 index 0000000000000..d1cd64b4cd992 --- /dev/null +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityFailureStoreRestIT.java @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.remotecluster; + +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.Response; +import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.SearchResponseUtils; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; + +abstract class AbstractRemoteClusterSecurityFailureStoreRestIT extends AbstractRemoteClusterSecurityTestCase { + + protected void assertSearchResponseContainsIndices(Response response, String... expectedIndices) throws IOException { + assertOK(response); + final SearchResponse dataSearchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response)); + try { + final List actualIndices = Arrays.stream(dataSearchResponse.getHits().getHits()) + .map(SearchHit::getIndex) + .collect(Collectors.toList()); + assertThat(actualIndices, containsInAnyOrder(expectedIndices)); + } finally { + dataSearchResponse.decRef(); + } + } + + protected void setupTestDataStreamOnFulfillingCluster() throws IOException { + // Create data stream and index some documents + final Request createComponentTemplate = new Request("PUT", "/_component_template/component1"); + createComponentTemplate.setJsonEntity(""" + { + "template": { + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "age": { + "type": "integer" + }, + "email": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "data_stream_options": { + "failure_store": { + "enabled": true + } + } + } + }"""); + assertOK(performRequestAgainstFulfillingCluster(createComponentTemplate)); + + final Request createTemplate = new Request("PUT", "/_index_template/template1"); + createTemplate.setJsonEntity(""" + { + "index_patterns": ["test*"], + "data_stream": {}, + "priority": 500, + "composed_of": ["component1"] + }"""); + assertOK(performRequestAgainstFulfillingCluster(createTemplate)); + + final Request createDoc1 = new Request("PUT", "/test1/_doc/1?refresh=true&op_type=create"); + createDoc1.setJsonEntity(""" + { + "@timestamp": 1, + "age" : 1, + "name" : "jack", + "email" : "jack@example.com" + }"""); + assertOK(performRequestAgainstFulfillingCluster(createDoc1)); + + final Request createDoc2 = new Request("PUT", "/test1/_doc/2?refresh=true&op_type=create"); + createDoc2.setJsonEntity(""" + { + "@timestamp": 2, + "age" : "this should be an int", + "name" : "jack", + "email" : "jack@example.com" + }"""); + assertOK(performRequestAgainstFulfillingCluster(createDoc2)); + } + + protected Response performRequestWithRemoteSearchUser(final Request request) throws IOException { + request.setOptions( + RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", headerFromRandomAuthMethod(REMOTE_SEARCH_USER, PASS)) + ); + return client().performRequest(request); + } + + @SuppressWarnings("unchecked") + protected Tuple, List> getDataAndFailureIndices(String dataStreamName) throws IOException { + Request dataStream = new Request("GET", "/_data_stream/" + dataStreamName); + Response response = performRequestAgainstFulfillingCluster(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); + } + + protected 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/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java new file mode 100644 index 0000000000000..33ce15adabe79 --- /dev/null +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.remotecluster; + +import org.elasticsearch.client.Request; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.FeatureFlag; +import org.elasticsearch.test.cluster.local.distribution.DistributionType; +import org.elasticsearch.test.cluster.util.resource.Resource; +import org.junit.ClassRule; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.util.Locale; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +public class RemoteClusterSecurityRCS1FailureStoreRestIT extends AbstractRemoteClusterSecurityFailureStoreRestIT { + + static { + fulfillingCluster = ElasticsearchCluster.local() + .distribution(DistributionType.DEFAULT) + .name("fulfilling-cluster") + .nodes(3) + .apply(commonClusterConfig) + .feature(FeatureFlag.FAILURE_STORE_ENABLED) + .rolesFile(Resource.fromClasspath("roles.yml")) + .build(); + + queryCluster = ElasticsearchCluster.local() + .distribution(DistributionType.DEFAULT) + .name("query-cluster") + .apply(commonClusterConfig) + .feature(FeatureFlag.FAILURE_STORE_ENABLED) + .rolesFile(Resource.fromClasspath("roles.yml")) + .build(); + } + + @ClassRule + public static TestRule clusterRule = RuleChain.outerRule(fulfillingCluster).around(queryCluster); + + public void testCrossClusterSearch() throws Exception { + // configure remote cluster using certificate-based authentication + configureRemoteCluster(REMOTE_CLUSTER_ALIAS, fulfillingCluster, true, randomBoolean(), randomBoolean()); + + // fulfilling cluster setup + setupRoleAndUserOnFulfillingCluster(); + setupTestDataStreamOnFulfillingCluster(); + + // query cluster setup + setupLocalDataOnQueryCluster(); + setupUserAndRoleOnQueryCluster(); + + final Tuple backingIndices = getSingleDataAndFailureIndices("test1"); + final String backingDataIndexName = backingIndices.v1(); + final String backingFailureIndexName = backingIndices.v2(); + { + // query remote cluster using ::data selector should succeed + final boolean alsoSearchLocally = randomBoolean(); + final Request dataSearchRequest = new Request( + "GET", + String.format( + Locale.ROOT, + "/%s%s:%s/_search?ccs_minimize_roundtrips=%s&ignore_unavailable=false", + alsoSearchLocally ? "local_index," : "", + randomFrom("my_remote_cluster", "*", "my_remote_*"), + randomFrom("test1::data", "test1", "test*", "test*::data", "*", "*::data", backingDataIndexName), + randomBoolean() + ) + ); + final String[] expectedIndices = alsoSearchLocally + ? new String[] { "local_index", backingDataIndexName } + : new String[] { backingDataIndexName }; + assertSearchResponseContainsIndices(performRequestWithRemoteSearchUser(dataSearchRequest), expectedIndices); + } + { + // query remote cluster using ::failures selector should fail + final ResponseException exception = expectThrows( + ResponseException.class, + () -> performRequestWithRemoteSearchUser( + new Request( + "GET", + String.format( + Locale.ROOT, + "/my_remote_cluster:%s/_search?ccs_minimize_roundtrips=%s", + randomFrom("test1::failures", "test*::failures", "*::failures"), + randomBoolean() + ) + ) + ) + ); + assertThat(exception.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat(exception.getMessage(), containsString("failures selector is not supported with cross-cluster expressions")); + } + { + // direct access to backing failure index is subject to the user's permissions and is allowed + assertSearchResponseContainsIndices( + performRequestWithRemoteSearchUser( + new Request( + "GET", + String.format( + Locale.ROOT, + "/my_remote_cluster:%s/_search?ccs_minimize_roundtrips=%s", + backingFailureIndexName, + randomBoolean() + ) + ) + ), + backingFailureIndexName + ); + } + } + + private static void setupLocalDataOnQueryCluster() throws IOException { + // Index some documents, to use them in a mixed-cluster search + final var indexDocRequest = new Request("POST", "/local_index/_doc?refresh=true"); + indexDocRequest.setJsonEntity("{\"local_foo\": \"local_bar\"}"); + assertOK(client().performRequest(indexDocRequest)); + } + + private static void setupUserAndRoleOnQueryCluster() throws IOException { + // Create user role with privileges for remote and local indices + final var putRoleRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE); + putRoleRequest.setJsonEntity(""" + { + "description": "Role with privileges for remote and local indices.", + "indices": [ + { + "names": ["local_index"], + "privileges": ["read"] + } + ], + "remote_indices": [ + { + "names": ["test*"], + "privileges": ["read", "read_cross_cluster"], + "clusters": ["my_remote_cluster"] + } + ] + }"""); + assertOK(adminClient().performRequest(putRoleRequest)); + final var putUserRequest = new Request("PUT", "/_security/user/" + REMOTE_SEARCH_USER); + putUserRequest.setJsonEntity(""" + { + "password": "x-pack-test-password", + "roles" : ["remote_search"] + }"""); + assertOK(adminClient().performRequest(putUserRequest)); + } + + private static void setupRoleAndUserOnFulfillingCluster() throws IOException { + var putRoleOnFulfillingClusterRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE); + putRoleOnFulfillingClusterRequest.setJsonEntity(""" + { + "indices": [ + { + "names": ["test*"], + "privileges": ["read", "read_cross_cluster", "read_failure_store"] + } + ] + }"""); + assertOK(performRequestAgainstFulfillingCluster(putRoleOnFulfillingClusterRequest)); + + var putUserOnFulfillingClusterRequest = new Request("PUT", "/_security/user/" + REMOTE_SEARCH_USER); + putUserOnFulfillingClusterRequest.setJsonEntity(""" + { + "password": "x-pack-test-password", + "roles" : ["remote_search"] + }"""); + assertOK(performRequestAgainstFulfillingCluster(putUserOnFulfillingClusterRequest)); + } + +} diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRestIT.java index afa8e9365fe93..307f59859c75a 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRestIT.java @@ -609,14 +609,6 @@ public void testCrossClusterSearch() throws Exception { assertThat(exception6.getResponse().getStatusLine().getStatusCode(), equalTo(401)); assertThat(exception6.getMessage(), containsString("invalid cross-cluster API key value")); } - - // check that ::failures selector is not supported with cross cluster search - final ResponseException exception7 = expectThrows( - ResponseException.class, - () -> performRequestWithRemoteSearchUser(new Request("GET", "/my_remote_cluster:index1::failures/_search")) - ); - assertThat(exception7.getResponse().getStatusLine().getStatusCode(), equalTo(403)); - assertThat(exception7.getMessage(), containsString("failures selector is not supported with cross-cluster expressions")); } assertNoRcs1DeprecationWarnings(); } From e92ffde72b3bdf36d5cdc986abebcf0b27b4630d Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Thu, 20 Mar 2025 11:27:18 +0100 Subject: [PATCH 05/13] nit --- ...ClusterSecurityRCS1FailureStoreRestIT.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java index 33ce15adabe79..008d83a7af57d 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java @@ -103,20 +103,16 @@ public void testCrossClusterSearch() throws Exception { } { // direct access to backing failure index is subject to the user's permissions and is allowed - assertSearchResponseContainsIndices( - performRequestWithRemoteSearchUser( - new Request( - "GET", - String.format( - Locale.ROOT, - "/my_remote_cluster:%s/_search?ccs_minimize_roundtrips=%s", - backingFailureIndexName, - randomBoolean() - ) - ) - ), - backingFailureIndexName + Request failureIndexSearchRequest = new Request( + "GET", + String.format( + Locale.ROOT, + "/my_remote_cluster:%s/_search?ccs_minimize_roundtrips=%s", + backingFailureIndexName, + randomBoolean() + ) ); + assertSearchResponseContainsIndices(performRequestWithRemoteSearchUser(failureIndexSearchRequest), backingFailureIndexName); } } From 29de66983c7695f2fa127473ceb0bf25a55fb37f Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Thu, 20 Mar 2025 11:47:01 +0100 Subject: [PATCH 06/13] remove remote_indices - not relevant for RCS1 test case --- .../RemoteClusterSecurityRCS1FailureStoreRestIT.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java index 008d83a7af57d..37dc421e97c82 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java @@ -134,13 +134,6 @@ private static void setupUserAndRoleOnQueryCluster() throws IOException { "names": ["local_index"], "privileges": ["read"] } - ], - "remote_indices": [ - { - "names": ["test*"], - "privileges": ["read", "read_cross_cluster"], - "clusters": ["my_remote_cluster"] - } ] }"""); assertOK(adminClient().performRequest(putRoleRequest)); From eaf9a01b3a07b1126ec04e6967c04b20012546f5 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Thu, 20 Mar 2025 12:18:18 +0100 Subject: [PATCH 07/13] test CCS with RCS2 --- ...ClusterSecurityRCS2FailureStoreRestIT.java | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java new file mode 100644 index 0000000000000..fb8f47d23c32c --- /dev/null +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.remotecluster; + +import org.elasticsearch.client.Request; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.FeatureFlag; +import org.elasticsearch.test.cluster.local.distribution.DistributionType; +import org.elasticsearch.test.cluster.util.resource.Resource; +import org.junit.ClassRule; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +public class RemoteClusterSecurityRCS2FailureStoreRestIT extends AbstractRemoteClusterSecurityFailureStoreRestIT { + + private static final AtomicReference> API_KEY_MAP_REF = new AtomicReference<>(); + + static { + fulfillingCluster = ElasticsearchCluster.local() + .distribution(DistributionType.DEFAULT) + .name("fulfilling-cluster") + .apply(commonClusterConfig) + .feature(FeatureFlag.FAILURE_STORE_ENABLED) + .setting("remote_cluster_server.enabled", "true") + .setting("remote_cluster.port", "0") + .setting("xpack.security.remote_cluster_server.ssl.enabled", "true") + .setting("xpack.security.remote_cluster_server.ssl.key", "remote-cluster.key") + .setting("xpack.security.remote_cluster_server.ssl.certificate", "remote-cluster.crt") + .setting("xpack.security.authc.token.enabled", "true") + .keystore("xpack.security.remote_cluster_server.ssl.secure_key_passphrase", "remote-cluster-password") + .build(); + + queryCluster = ElasticsearchCluster.local() + .distribution(DistributionType.DEFAULT) + .name("query-cluster") + .apply(commonClusterConfig) + .feature(FeatureFlag.FAILURE_STORE_ENABLED) + .setting("xpack.security.remote_cluster_client.ssl.enabled", "true") + .setting("xpack.security.remote_cluster_client.ssl.certificate_authorities", "remote-cluster-ca.crt") + .setting("xpack.security.authc.token.enabled", "true") + .keystore("cluster.remote.my_remote_cluster.credentials", () -> { + API_KEY_MAP_REF.compareAndSet(null, createCrossClusterAccessApiKey(""" + { + "search": [ + { + "names": ["test*"] + } + ] + }""")); + return (String) API_KEY_MAP_REF.get().get("encoded"); + }) + .rolesFile(Resource.fromClasspath("roles.yml")) + .build(); + } + + @ClassRule + // Use a RuleChain to ensure that fulfilling cluster is started before query cluster + public static TestRule clusterRule = RuleChain.outerRule(fulfillingCluster).around(queryCluster); + + public void testCrossClusterSearch() throws Exception { + // configure remote cluster using API Key-based authentication + configureRemoteCluster(); + + // fulfilling cluster setup + setupTestDataStreamOnFulfillingCluster(); + + // query cluster setup + setupLocalDataOnQueryCluster(); + setupUserAndRoleOnQueryCluster(); + + final Tuple backingIndices = getSingleDataAndFailureIndices("test1"); + final String backingDataIndexName = backingIndices.v1(); + final String backingFailureIndexName = backingIndices.v2(); + { + // query remote cluster using ::data selector should succeed + final boolean alsoSearchLocally = randomBoolean(); + final Request dataSearchRequest = new Request( + "GET", + String.format( + Locale.ROOT, + "/%s%s:%s/_search?ccs_minimize_roundtrips=%s&ignore_unavailable=false", + alsoSearchLocally ? "local_index," : "", + randomFrom("my_remote_cluster", "*", "my_remote_*"), + randomFrom("test1::data", "test1", "test*", "test*::data", "*", "*::data", backingDataIndexName), + randomBoolean() + ) + ); + final String[] expectedIndices = alsoSearchLocally + ? new String[] { "local_index", backingDataIndexName } + : new String[] { backingDataIndexName }; + assertSearchResponseContainsIndices(performRequestWithRemoteSearchUser(dataSearchRequest), expectedIndices); + } + { + // query remote cluster using ::failures selector should fail + final ResponseException exception = expectThrows( + ResponseException.class, + () -> performRequestWithRemoteSearchUser( + new Request( + "GET", + String.format( + Locale.ROOT, + "/my_remote_cluster:%s/_search?ccs_minimize_roundtrips=%s", + randomFrom("test1::failures", "test*::failures", "*::failures"), + randomBoolean() + ) + ) + ) + ); + assertThat(exception.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat(exception.getMessage(), containsString("failures selector is not supported with cross-cluster expressions")); + } + { + // direct access to backing failure index is subject to the user's permissions and is allowed + Request failureIndexSearchRequest = new Request( + "GET", + String.format( + Locale.ROOT, + "/my_remote_cluster:%s/_search?ccs_minimize_roundtrips=%s", + backingFailureIndexName, + randomBoolean() + ) + ); + assertSearchResponseContainsIndices(performRequestWithRemoteSearchUser(failureIndexSearchRequest), backingFailureIndexName); + } + } + + private static void setupLocalDataOnQueryCluster() throws IOException { + // Index some documents, to use them in a mixed-cluster search + final var indexDocRequest = new Request("POST", "/local_index/_doc?refresh=true"); + indexDocRequest.setJsonEntity("{\"local_foo\": \"local_bar\"}"); + assertOK(client().performRequest(indexDocRequest)); + } + + private static void setupUserAndRoleOnQueryCluster() throws IOException { + // Create user role with privileges for remote and local indices + final var putRoleRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE); + putRoleRequest.setJsonEntity(""" + { + "description": "Role with privileges for remote and local indices.", + "indices": [ + { + "names": ["local_index"], + "privileges": ["read"] + } + ], + "remote_indices": [ + { + "names": ["test*"], + "privileges": ["read", "read_cross_cluster"], + "clusters": ["my_remote_cluster"] + } + ] + }"""); + assertOK(adminClient().performRequest(putRoleRequest)); + final var putUserRequest = new Request("PUT", "/_security/user/" + REMOTE_SEARCH_USER); + putUserRequest.setJsonEntity(""" + { + "password": "x-pack-test-password", + "roles" : ["remote_search"] + }"""); + assertOK(adminClient().performRequest(putUserRequest)); + } + +} From 4e366eda595be1b85cf5189064c63f9c8cb2a45a Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Fri, 21 Mar 2025 21:21:34 +0100 Subject: [PATCH 08/13] cleanup tests, handle edge cases with ccs_minimize_roundtrips --- ...moteClusterSecurityFailureStoreRestIT.java | 6 +-- ...ClusterSecurityRCS1FailureStoreRestIT.java | 54 ++++++++++++++----- ...ClusterSecurityRCS2FailureStoreRestIT.java | 22 ++++++-- 3 files changed, 64 insertions(+), 18 deletions(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityFailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityFailureStoreRestIT.java index d1cd64b4cd992..2d169098fff6f 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityFailureStoreRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityFailureStoreRestIT.java @@ -30,14 +30,14 @@ abstract class AbstractRemoteClusterSecurityFailureStoreRestIT extends AbstractR protected void assertSearchResponseContainsIndices(Response response, String... expectedIndices) throws IOException { assertOK(response); - final SearchResponse dataSearchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response)); + final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response)); try { - final List actualIndices = Arrays.stream(dataSearchResponse.getHits().getHits()) + final List actualIndices = Arrays.stream(searchResponse.getHits().getHits()) .map(SearchHit::getIndex) .collect(Collectors.toList()); assertThat(actualIndices, containsInAnyOrder(expectedIndices)); } finally { - dataSearchResponse.decRef(); + searchResponse.decRef(); } } diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java index 37dc421e97c82..fa0c4b0b192c3 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java @@ -48,9 +48,13 @@ public class RemoteClusterSecurityRCS1FailureStoreRestIT extends AbstractRemoteC @ClassRule public static TestRule clusterRule = RuleChain.outerRule(fulfillingCluster).around(queryCluster); - public void testCrossClusterSearch() throws Exception { - // configure remote cluster using certificate-based authentication - configureRemoteCluster(REMOTE_CLUSTER_ALIAS, fulfillingCluster, true, randomBoolean(), randomBoolean()); + public void testRCS1CrossClusterSearch() throws Exception { + final boolean rcs1Security = true; + final boolean isProxyMode = randomBoolean(); + final boolean skipUnavailable = false; // we want to get actual failures and not skip and get empty results + final boolean ccsMinimizeRoundtrips = randomBoolean(); + + configureRemoteCluster(REMOTE_CLUSTER_ALIAS, fulfillingCluster, rcs1Security, isProxyMode, skipUnavailable); // fulfilling cluster setup setupRoleAndUserOnFulfillingCluster(); @@ -70,11 +74,11 @@ public void testCrossClusterSearch() throws Exception { "GET", String.format( Locale.ROOT, - "/%s%s:%s/_search?ccs_minimize_roundtrips=%s&ignore_unavailable=false", + "/%s%s:%s/_search?ccs_minimize_roundtrips=%s", alsoSearchLocally ? "local_index," : "", randomFrom("my_remote_cluster", "*", "my_remote_*"), randomFrom("test1::data", "test1", "test*", "test*::data", "*", "*::data", backingDataIndexName), - randomBoolean() + ccsMinimizeRoundtrips ) ); final String[] expectedIndices = alsoSearchLocally @@ -91,9 +95,9 @@ public void testCrossClusterSearch() throws Exception { "GET", String.format( Locale.ROOT, - "/my_remote_cluster:%s/_search?ccs_minimize_roundtrips=%s", + "/my_remote_cluster:%s/_search?ccs_minimize_roundtrips=%s&ignore_unavailable=true", randomFrom("test1::failures", "test*::failures", "*::failures"), - randomBoolean() + ccsMinimizeRoundtrips ) ) ) @@ -102,17 +106,45 @@ public void testCrossClusterSearch() throws Exception { assertThat(exception.getMessage(), containsString("failures selector is not supported with cross-cluster expressions")); } { - // direct access to backing failure index is subject to the user's permissions and is allowed + // direct access to backing failure index is subject to the user's permissions + // it might fail in some cases and work in others Request failureIndexSearchRequest = new Request( "GET", String.format( Locale.ROOT, "/my_remote_cluster:%s/_search?ccs_minimize_roundtrips=%s", backingFailureIndexName, - randomBoolean() + ccsMinimizeRoundtrips ) ); - assertSearchResponseContainsIndices(performRequestWithRemoteSearchUser(failureIndexSearchRequest), backingFailureIndexName); + if (ccsMinimizeRoundtrips) { + // this is a special case where indices:data/read/search will be sent to a remote cluster + // and the request to backing failure store index will be authorized based on the datastream + // which grants access to backing failure store indices (granted by read_failure_store privilege) + // from a security perspective, this is a valid use case and there is no way to prevent this with RCS1 security model + // since from the fulfilling cluster perspective this request is no different from any other local search request + assertSearchResponseContainsIndices(performRequestWithRemoteSearchUser(failureIndexSearchRequest), backingFailureIndexName); + } else { + // in this case, the user does not have the necessary permissions to search the backing failure index + // the request to failure store backing index is authorized based on the datastream + // which does not grant access to the indices:admin/search/search_shards action + // this action is granted by read_cross_cluster privilege which is currently + // not supporting the failure backing indices (only data backing indices) + final ResponseException exception = expectThrows( + ResponseException.class, + () -> performRequestWithRemoteSearchUser(failureIndexSearchRequest) + ); + assertThat(exception.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat( + exception.getMessage(), + containsString( + "action [indices:admin/search/search_shards] is unauthorized for user [remote_search_user] " + + "with effective roles [remote_search] on indices [" + + backingFailureIndexName + + "], this action is granted by the index privileges [view_index_metadata,manage,read_cross_cluster,all]" + ) + ); + } } } @@ -124,11 +156,9 @@ private static void setupLocalDataOnQueryCluster() throws IOException { } private static void setupUserAndRoleOnQueryCluster() throws IOException { - // Create user role with privileges for remote and local indices final var putRoleRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE); putRoleRequest.setJsonEntity(""" { - "description": "Role with privileges for remote and local indices.", "indices": [ { "names": ["local_index"], diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java index fb8f47d23c32c..bc2866bc63b65 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java @@ -72,9 +72,10 @@ public class RemoteClusterSecurityRCS2FailureStoreRestIT extends AbstractRemoteC // Use a RuleChain to ensure that fulfilling cluster is started before query cluster public static TestRule clusterRule = RuleChain.outerRule(fulfillingCluster).around(queryCluster); - public void testCrossClusterSearch() throws Exception { + public void testRCS2CrossClusterSearch() throws Exception { // configure remote cluster using API Key-based authentication configureRemoteCluster(); + final String crossClusterAccessApiKeyId = (String) API_KEY_MAP_REF.get().get("id"); // fulfilling cluster setup setupTestDataStreamOnFulfillingCluster(); @@ -125,7 +126,7 @@ public void testCrossClusterSearch() throws Exception { assertThat(exception.getMessage(), containsString("failures selector is not supported with cross-cluster expressions")); } { - // direct access to backing failure index is subject to the user's permissions and is allowed + // direct access to backing failure index is not allowed - no explicit read privileges over .fs-* indices Request failureIndexSearchRequest = new Request( "GET", String.format( @@ -135,7 +136,22 @@ public void testCrossClusterSearch() throws Exception { randomBoolean() ) ); - assertSearchResponseContainsIndices(performRequestWithRemoteSearchUser(failureIndexSearchRequest), backingFailureIndexName); + final ResponseException exception = expectThrows( + ResponseException.class, + () -> performRequestWithRemoteSearchUser(failureIndexSearchRequest) + ); + assertThat(exception.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat( + exception.getMessage(), + containsString( + "action [indices:data/read/search] towards remote cluster is unauthorized for user [remote_search_user] " + + "with assigned roles [remote_search] authenticated by API key id [" + + crossClusterAccessApiKeyId + + "] of user [test_user] on indices [" + + backingFailureIndexName + + "], this action is granted by the index privileges [read,all]" + ) + ); } } From 816d919983076dd1bbaac37c9fce7bada918b607 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Fri, 21 Mar 2025 22:18:43 +0100 Subject: [PATCH 09/13] more test users --- ...moteClusterSecurityFailureStoreRestIT.java | 5 + ...ClusterSecurityRCS1FailureStoreRestIT.java | 118 ++++++++++++++---- ...ClusterSecurityRCS2FailureStoreRestIT.java | 11 +- 3 files changed, 107 insertions(+), 27 deletions(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityFailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityFailureStoreRestIT.java index 2d169098fff6f..b4265d2b9e3ad 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityFailureStoreRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityFailureStoreRestIT.java @@ -110,6 +110,11 @@ protected Response performRequestWithRemoteSearchUser(final Request request) thr return client().performRequest(request); } + protected Response performRequestWithUser(final String user, final Request request) throws IOException { + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", headerFromRandomAuthMethod(user, PASS))); + return client().performRequest(request); + } + @SuppressWarnings("unchecked") protected Tuple, List> getDataAndFailureIndices(String dataStreamName) throws IOException { Request dataStream = new Request("GET", "/_data_stream/" + dataStreamName); diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java index fa0c4b0b192c3..c121c2ee0f8b9 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java @@ -9,6 +9,9 @@ import org.elasticsearch.client.Request; import org.elasticsearch.client.ResponseException; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.core.Strings; import org.elasticsearch.core.Tuple; import org.elasticsearch.test.cluster.ElasticsearchCluster; import org.elasticsearch.test.cluster.FeatureFlag; @@ -48,6 +51,10 @@ public class RemoteClusterSecurityRCS1FailureStoreRestIT extends AbstractRemoteC @ClassRule public static TestRule clusterRule = RuleChain.outerRule(fulfillingCluster).around(queryCluster); + private static final String ALL_ACCESS = "all_access"; + private static final String DATA_ACCESS = "data_access"; + private static final String FAILURE_STORE_ACCESS = "failure_store_access"; + public void testRCS1CrossClusterSearch() throws Exception { final boolean rcs1Security = true; final boolean isProxyMode = randomBoolean(); @@ -67,7 +74,9 @@ public void testRCS1CrossClusterSearch() throws Exception { final Tuple backingIndices = getSingleDataAndFailureIndices("test1"); final String backingDataIndexName = backingIndices.v1(); final String backingFailureIndexName = backingIndices.v2(); - { + + final String[] users = { FAILURE_STORE_ACCESS, DATA_ACCESS, ALL_ACCESS }; + for (String user : users) { // query remote cluster using ::data selector should succeed final boolean alsoSearchLocally = randomBoolean(); final Request dataSearchRequest = new Request( @@ -84,13 +93,14 @@ public void testRCS1CrossClusterSearch() throws Exception { final String[] expectedIndices = alsoSearchLocally ? new String[] { "local_index", backingDataIndexName } : new String[] { backingDataIndexName }; - assertSearchResponseContainsIndices(performRequestWithRemoteSearchUser(dataSearchRequest), expectedIndices); + assertSearchResponseContainsIndices(performRequestWithUser(user, dataSearchRequest), expectedIndices); } - { - // query remote cluster using ::failures selector should fail + for (String user : users) { + // query remote cluster using ::failures selector should fail (regardless of the user's permissions) final ResponseException exception = expectThrows( ResponseException.class, - () -> performRequestWithRemoteSearchUser( + () -> performRequestWithUser( + user, new Request( "GET", String.format( @@ -123,7 +133,10 @@ public void testRCS1CrossClusterSearch() throws Exception { // which grants access to backing failure store indices (granted by read_failure_store privilege) // from a security perspective, this is a valid use case and there is no way to prevent this with RCS1 security model // since from the fulfilling cluster perspective this request is no different from any other local search request - assertSearchResponseContainsIndices(performRequestWithRemoteSearchUser(failureIndexSearchRequest), backingFailureIndexName); + assertSearchResponseContainsIndices( + performRequestWithUser(FAILURE_STORE_ACCESS, failureIndexSearchRequest), + backingFailureIndexName + ); } else { // in this case, the user does not have the necessary permissions to search the backing failure index // the request to failure store backing index is authorized based on the datastream @@ -132,7 +145,7 @@ public void testRCS1CrossClusterSearch() throws Exception { // not supporting the failure backing indices (only data backing indices) final ResponseException exception = expectThrows( ResponseException.class, - () -> performRequestWithRemoteSearchUser(failureIndexSearchRequest) + () -> performRequestWithUser(FAILURE_STORE_ACCESS, failureIndexSearchRequest) ); assertThat(exception.getResponse().getStatusLine().getStatusCode(), equalTo(403)); assertThat( @@ -156,8 +169,19 @@ private static void setupLocalDataOnQueryCluster() throws IOException { } private static void setupUserAndRoleOnQueryCluster() throws IOException { - final var putRoleRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE); - putRoleRequest.setJsonEntity(""" + createRole(adminClient(), ALL_ACCESS, """ + { + "indices": [ + { + "names": ["*"], + "privileges": ["all"] + } + ] + }"""); + createUser(adminClient(), ALL_ACCESS, PASS, ALL_ACCESS); + // the role must simply exist on query cluster, the access is irrelevant, + // but we here grant the access to local_index only to test mixed search + createRole(adminClient(), FAILURE_STORE_ACCESS, """ { "indices": [ { @@ -166,19 +190,48 @@ private static void setupUserAndRoleOnQueryCluster() throws IOException { } ] }"""); - assertOK(adminClient().performRequest(putRoleRequest)); - final var putUserRequest = new Request("PUT", "/_security/user/" + REMOTE_SEARCH_USER); - putUserRequest.setJsonEntity(""" + createUser(adminClient(), FAILURE_STORE_ACCESS, PASS, FAILURE_STORE_ACCESS); + createRole(adminClient(), DATA_ACCESS, """ { - "password": "x-pack-test-password", - "roles" : ["remote_search"] + "indices": [ + { + "names": ["local_index"], + "privileges": ["read"] + } + ] }"""); - assertOK(adminClient().performRequest(putUserRequest)); + createUser(adminClient(), DATA_ACCESS, PASS, DATA_ACCESS); + } + + private static void createRole(RestClient client, String role, String roleDescriptor) throws IOException { + final Request putRoleRequest = new Request("PUT", "/_security/role/" + role); + putRoleRequest.setJsonEntity(roleDescriptor); + assertOK(client.performRequest(putRoleRequest)); + } + + private static void createUser(RestClient client, String user, SecureString password, String role) throws IOException { + final Request putUserRequest = new Request("PUT", "/_security/user/" + user); + putUserRequest.setJsonEntity(Strings.format(""" + { + "password": "%s", + "roles" : ["%s"] + }""", password.toString(), role)); + assertOK(client.performRequest(putUserRequest)); } private static void setupRoleAndUserOnFulfillingCluster() throws IOException { - var putRoleOnFulfillingClusterRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE); - putRoleOnFulfillingClusterRequest.setJsonEntity(""" + putRoleOnFulfillingCluster(DATA_ACCESS, """ + { + "indices": [ + { + "names": ["test*"], + "privileges": ["read", "read_cross_cluster"] + } + ] + }"""); + putUserOnFulfillingCluster(DATA_ACCESS, DATA_ACCESS); + + putRoleOnFulfillingCluster(FAILURE_STORE_ACCESS, """ { "indices": [ { @@ -187,15 +240,34 @@ private static void setupRoleAndUserOnFulfillingCluster() throws IOException { } ] }"""); - assertOK(performRequestAgainstFulfillingCluster(putRoleOnFulfillingClusterRequest)); + putUserOnFulfillingCluster(FAILURE_STORE_ACCESS, FAILURE_STORE_ACCESS); - var putUserOnFulfillingClusterRequest = new Request("PUT", "/_security/user/" + REMOTE_SEARCH_USER); - putUserOnFulfillingClusterRequest.setJsonEntity(""" + putRoleOnFulfillingCluster(ALL_ACCESS, """ { - "password": "x-pack-test-password", - "roles" : ["remote_search"] + "indices": [ + { + "names": ["*"], + "privileges": ["all"] + } + ] }"""); - assertOK(performRequestAgainstFulfillingCluster(putUserOnFulfillingClusterRequest)); + putUserOnFulfillingCluster(ALL_ACCESS, ALL_ACCESS); + } + + private static void putRoleOnFulfillingCluster(String roleName, String roleDescriptor) throws IOException { + Request request = new Request("PUT", "/_security/role/" + roleName); + request.setJsonEntity(roleDescriptor); + assertOK(performRequestAgainstFulfillingCluster(request)); + } + + private static void putUserOnFulfillingCluster(String user, String role) throws IOException { + Request request = new Request("PUT", "/_security/user/" + user); + request.setJsonEntity(Strings.format(""" + { + "password": "%s", + "roles" : ["%s"] + }""", PASS.toString(), role)); + assertOK(performRequestAgainstFulfillingCluster(request)); } } diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java index bc2866bc63b65..9204c13b388b7 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java @@ -76,6 +76,7 @@ public void testRCS2CrossClusterSearch() throws Exception { // configure remote cluster using API Key-based authentication configureRemoteCluster(); final String crossClusterAccessApiKeyId = (String) API_KEY_MAP_REF.get().get("id"); + final boolean ccsMinimizeRoundtrips = randomBoolean(); // fulfilling cluster setup setupTestDataStreamOnFulfillingCluster(); @@ -98,7 +99,7 @@ public void testRCS2CrossClusterSearch() throws Exception { alsoSearchLocally ? "local_index," : "", randomFrom("my_remote_cluster", "*", "my_remote_*"), randomFrom("test1::data", "test1", "test*", "test*::data", "*", "*::data", backingDataIndexName), - randomBoolean() + ccsMinimizeRoundtrips ) ); final String[] expectedIndices = alsoSearchLocally @@ -117,7 +118,7 @@ public void testRCS2CrossClusterSearch() throws Exception { Locale.ROOT, "/my_remote_cluster:%s/_search?ccs_minimize_roundtrips=%s", randomFrom("test1::failures", "test*::failures", "*::failures"), - randomBoolean() + ccsMinimizeRoundtrips ) ) ) @@ -133,7 +134,7 @@ public void testRCS2CrossClusterSearch() throws Exception { Locale.ROOT, "/my_remote_cluster:%s/_search?ccs_minimize_roundtrips=%s", backingFailureIndexName, - randomBoolean() + ccsMinimizeRoundtrips ) ); final ResponseException exception = expectThrows( @@ -144,7 +145,9 @@ public void testRCS2CrossClusterSearch() throws Exception { assertThat( exception.getMessage(), containsString( - "action [indices:data/read/search] towards remote cluster is unauthorized for user [remote_search_user] " + "action [" + + (ccsMinimizeRoundtrips ? "indices:data/read/search" : "indices:admin/search/search_shards") + + "] towards remote cluster is unauthorized for user [remote_search_user] " + "with assigned roles [remote_search] authenticated by API key id [" + crossClusterAccessApiKeyId + "] of user [test_user] on indices [" From 7dc89d4e47fd9ea17c0e2f72847a5e6034f54cec Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Fri, 21 Mar 2025 22:20:09 +0100 Subject: [PATCH 10/13] fix assertion --- .../RemoteClusterSecurityRCS1FailureStoreRestIT.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java index c121c2ee0f8b9..ae20283fc343d 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java @@ -151,8 +151,12 @@ public void testRCS1CrossClusterSearch() throws Exception { assertThat( exception.getMessage(), containsString( - "action [indices:admin/search/search_shards] is unauthorized for user [remote_search_user] " - + "with effective roles [remote_search] on indices [" + "action [indices:admin/search/search_shards] is unauthorized for user [" + + FAILURE_STORE_ACCESS + + "] " + + "with effective roles [" + + FAILURE_STORE_ACCESS + + "] on indices [" + backingFailureIndexName + "], this action is granted by the index privileges [view_index_metadata,manage,read_cross_cluster,all]" ) From 26e8ae36385645544d8bddff1a5d74106f736a66 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Fri, 21 Mar 2025 22:35:40 +0100 Subject: [PATCH 11/13] test direct access to backing failure index for other users --- ...ClusterSecurityRCS1FailureStoreRestIT.java | 30 +++++++++++++++++++ ...ClusterSecurityRCS2FailureStoreRestIT.java | 1 - 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java index ae20283fc343d..f1ef686bafd08 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java @@ -127,6 +127,36 @@ public void testRCS1CrossClusterSearch() throws Exception { ccsMinimizeRoundtrips ) ); + + // user with access to all should be able to search the backing failure index + assertSearchResponseContainsIndices(performRequestWithUser(ALL_ACCESS, failureIndexSearchRequest), backingFailureIndexName); + + // user with data only access should not be able to search the backing failure index + { + final ResponseException exception = expectThrows( + ResponseException.class, + () -> performRequestWithUser(DATA_ACCESS, failureIndexSearchRequest) + ); + assertThat(exception.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat( + exception.getMessage(), + containsString( + "action [" + + (ccsMinimizeRoundtrips ? "indices:data/read/search" : "indices:admin/search/search_shards") + + "] is unauthorized for user [" + + DATA_ACCESS + + "] " + + "with effective roles [" + + DATA_ACCESS + + "] on indices [" + + backingFailureIndexName + + "], this action is granted by the index privileges [view_index_metadata,manage,read_cross_cluster,all]" + + ) + ); + } + + // for user with access to failure store, it depends on the underlying action that is being sent to the remote cluster if (ccsMinimizeRoundtrips) { // this is a special case where indices:data/read/search will be sent to a remote cluster // and the request to backing failure store index will be authorized based on the datastream diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java index 9204c13b388b7..dcae5fd97301d 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java @@ -166,7 +166,6 @@ private static void setupLocalDataOnQueryCluster() throws IOException { } private static void setupUserAndRoleOnQueryCluster() throws IOException { - // Create user role with privileges for remote and local indices final var putRoleRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE); putRoleRequest.setJsonEntity(""" { From 9c3d0d7adb1ead842b461ee02d95ebfdad89f050 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Fri, 21 Mar 2025 22:44:23 +0100 Subject: [PATCH 12/13] fix assertion --- .../RemoteClusterSecurityRCS2FailureStoreRestIT.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java index dcae5fd97301d..9b6176292ca4b 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java @@ -152,7 +152,9 @@ public void testRCS2CrossClusterSearch() throws Exception { + crossClusterAccessApiKeyId + "] of user [test_user] on indices [" + backingFailureIndexName - + "], this action is granted by the index privileges [read,all]" + + "], this action is granted by the index privileges [" + + (ccsMinimizeRoundtrips ? "read,all" : "view_index_metadata,manage,read_cross_cluster,all") + + "]" ) ); } From e32510420089c412bba18180c978d114cc3c5daf Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Fri, 21 Mar 2025 23:21:32 +0100 Subject: [PATCH 13/13] support ::failures with ccs --- .../metadata/IndexNameExpressionResolver.java | 24 --------- .../metadata/SelectorResolverTests.java | 27 ++++------ ...ossClusterApiKeyRoleDescriptorBuilder.java | 6 ++- .../role/RoleDescriptorRequestValidator.java | 9 +--- .../authz/privilege/IndexPrivilege.java | 9 +++- ...ClusterSecurityRCS1FailureStoreRestIT.java | 53 ++++--------------- ...ClusterSecurityRCS2FailureStoreRestIT.java | 40 +++----------- 7 files changed, 41 insertions(+), 127 deletions(-) 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 027b3f677c5df..fa359794166fa 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -2373,7 +2373,6 @@ private static V splitSelectorExpression(String expression, BiFunction resolve(selectorsAllowed, "index::")); - // remote cluster syntax is not allowed with ::failures selector - assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(selectorsAllowed, "cluster:index::failures"); - assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(noSelectors, "cluster:index::failures"); - assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(selectorsAllowed, "cluster-*:index::failures"); - assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(selectorsAllowed, "cluster-*:index-*::failures"); - assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(selectorsAllowed, "cluster-*:*::failures"); - assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(selectorsAllowed, "*:index-*::failures"); - assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(selectorsAllowed, "*:*::failures"); - // even with an empty index name - assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(selectorsAllowed, "cluster:::failures"); - } - - public void assertFailuresSelectorNotSupportedWithRemoteClusterExpressions(Context context, String expression) { - var e = expectThrows(IllegalArgumentException.class, () -> resolve(context, expression)); - assertThat(e.getMessage(), containsString("failures selector is not supported with cross-cluster expressions")); + assertThat(resolve(selectorsAllowed, "cluster:index::failures"), equalTo(new ResolvedExpression("cluster:index", FAILURES))); + expectThrows(IllegalArgumentException.class, () -> resolve(noSelectors, "cluster:index::failures")); + assertThat(resolve(selectorsAllowed, "cluster-*:index::failures"), equalTo(new ResolvedExpression("cluster-*:index", FAILURES))); + assertThat( + resolve(selectorsAllowed, "cluster-*:index-*::failures"), + equalTo(new ResolvedExpression("cluster-*:index-*", FAILURES)) + ); + assertThat(resolve(selectorsAllowed, "cluster-*:*::failures"), equalTo(new ResolvedExpression("cluster-*:*", FAILURES))); + assertThat(resolve(selectorsAllowed, "*:index-*::failures"), equalTo(new ResolvedExpression("*:index-*", FAILURES))); + assertThat(resolve(selectorsAllowed, "*:*::failures"), equalTo(new ResolvedExpression("*:*", FAILURES))); + assertThat(resolve(selectorsAllowed, "cluster:::failures"), equalTo(new ResolvedExpression("cluster:", FAILURES))); } public void testResolveMatchAllToSelectors() { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java index 95ad898140c21..d22ed149e87cf 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java @@ -31,7 +31,11 @@ public class CrossClusterApiKeyRoleDescriptorBuilder { Arrays.stream(CCS_CLUSTER_PRIVILEGE_NAMES), Arrays.stream(CCR_CLUSTER_PRIVILEGE_NAMES) ).toArray(String[]::new); - public static final String[] CCS_INDICES_PRIVILEGE_NAMES = { "read", "read_cross_cluster", "view_index_metadata" }; + public static final String[] CCS_INDICES_PRIVILEGE_NAMES = { + "read", + "read_cross_cluster", + "view_index_metadata", + "read_failure_store" }; public static final String[] CCR_INDICES_PRIVILEGE_NAMES = { "cross_cluster_replication", "cross_cluster_replication_internal" }; public static final String ROLE_DESCRIPTOR_NAME = "cross_cluster"; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java index 761c2258d3d3f..f695d7da9eeaf 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java @@ -11,7 +11,6 @@ import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPredicate; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.restriction.WorkflowResolver; import org.elasticsearch.xpack.core.security.support.MetadataUtils; @@ -61,13 +60,7 @@ public static ActionRequestValidationException validate( validationException = addValidationError("remote index cluster alias cannot be an empty string", validationException); } try { - Set privileges = IndexPrivilege.resolveBySelectorAccess(Set.of(ridp.indicesPrivileges().getPrivileges())); - if (privileges.stream().anyMatch(p -> p.getSelectorPredicate() == IndexComponentSelectorPredicate.FAILURES)) { - validationException = addValidationError( - "remote index privileges cannot contain privileges that grant access to the failure store", - validationException - ); - } + IndexPrivilege.resolveBySelectorAccess(Set.of(ridp.indicesPrivileges().getPrivileges())); } catch (IllegalArgumentException ile) { validationException = addValidationError(ile.getMessage(), validationException); } 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 5da75d5717edf..24fa45fb887dc 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 @@ -188,7 +188,11 @@ 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 = 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 READ_CROSS_CLUSTER = new IndexPrivilege( + "read_cross_cluster", + READ_CROSS_CLUSTER_AUTOMATON, + IndexComponentSelectorPredicate.ALL + ); public static final IndexPrivilege CREATE = new IndexPrivilege("create", CREATE_AUTOMATON); public static final IndexPrivilege INDEX = new IndexPrivilege("index", INDEX_AUTOMATON); public static final IndexPrivilege DELETE = new IndexPrivilege("delete", DELETE_AUTOMATON); @@ -383,6 +387,9 @@ private static Set resolve(Set name) { dataSelectorAccessPrivileges.add(indexPrivilege); } else if (indexPrivilege.selectorPredicate == IndexComponentSelectorPredicate.FAILURES) { failuresSelectorAccessPrivileges.add(indexPrivilege); + } else if (indexPrivilege.selectorPredicate == IndexComponentSelectorPredicate.ALL) { + failuresSelectorAccessPrivileges.add(indexPrivilege); + dataSelectorAccessPrivileges.add(indexPrivilege); } else { String errorMessage = "unexpected selector [" + indexPrivilege.selectorPredicate + "]"; assert false : errorMessage; diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java index f1ef686bafd08..d545758fbeac5 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1FailureStoreRestIT.java @@ -95,11 +95,10 @@ public void testRCS1CrossClusterSearch() throws Exception { : new String[] { backingDataIndexName }; assertSearchResponseContainsIndices(performRequestWithUser(user, dataSearchRequest), expectedIndices); } - for (String user : users) { + for (String user : new String[] { FAILURE_STORE_ACCESS, ALL_ACCESS }) { // query remote cluster using ::failures selector should fail (regardless of the user's permissions) - final ResponseException exception = expectThrows( - ResponseException.class, - () -> performRequestWithUser( + assertSearchResponseContainsIndices( + performRequestWithUser( user, new Request( "GET", @@ -110,10 +109,9 @@ public void testRCS1CrossClusterSearch() throws Exception { ccsMinimizeRoundtrips ) ) - ) + ), + backingFailureIndexName ); - assertThat(exception.getResponse().getStatusLine().getStatusCode(), equalTo(403)); - assertThat(exception.getMessage(), containsString("failures selector is not supported with cross-cluster expressions")); } { // direct access to backing failure index is subject to the user's permissions @@ -155,43 +153,10 @@ public void testRCS1CrossClusterSearch() throws Exception { ) ); } - - // for user with access to failure store, it depends on the underlying action that is being sent to the remote cluster - if (ccsMinimizeRoundtrips) { - // this is a special case where indices:data/read/search will be sent to a remote cluster - // and the request to backing failure store index will be authorized based on the datastream - // which grants access to backing failure store indices (granted by read_failure_store privilege) - // from a security perspective, this is a valid use case and there is no way to prevent this with RCS1 security model - // since from the fulfilling cluster perspective this request is no different from any other local search request - assertSearchResponseContainsIndices( - performRequestWithUser(FAILURE_STORE_ACCESS, failureIndexSearchRequest), - backingFailureIndexName - ); - } else { - // in this case, the user does not have the necessary permissions to search the backing failure index - // the request to failure store backing index is authorized based on the datastream - // which does not grant access to the indices:admin/search/search_shards action - // this action is granted by read_cross_cluster privilege which is currently - // not supporting the failure backing indices (only data backing indices) - final ResponseException exception = expectThrows( - ResponseException.class, - () -> performRequestWithUser(FAILURE_STORE_ACCESS, failureIndexSearchRequest) - ); - assertThat(exception.getResponse().getStatusLine().getStatusCode(), equalTo(403)); - assertThat( - exception.getMessage(), - containsString( - "action [indices:admin/search/search_shards] is unauthorized for user [" - + FAILURE_STORE_ACCESS - + "] " - + "with effective roles [" - + FAILURE_STORE_ACCESS - + "] on indices [" - + backingFailureIndexName - + "], this action is granted by the index privileges [view_index_metadata,manage,read_cross_cluster,all]" - ) - ); - } + assertSearchResponseContainsIndices( + performRequestWithUser(FAILURE_STORE_ACCESS, failureIndexSearchRequest), + backingFailureIndexName + ); } } diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java index 9b6176292ca4b..d0969b5401e67 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2FailureStoreRestIT.java @@ -8,7 +8,6 @@ package org.elasticsearch.xpack.remotecluster; import org.elasticsearch.client.Request; -import org.elasticsearch.client.ResponseException; import org.elasticsearch.core.Tuple; import org.elasticsearch.test.cluster.ElasticsearchCluster; import org.elasticsearch.test.cluster.FeatureFlag; @@ -23,9 +22,6 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; - public class RemoteClusterSecurityRCS2FailureStoreRestIT extends AbstractRemoteClusterSecurityFailureStoreRestIT { private static final AtomicReference> API_KEY_MAP_REF = new AtomicReference<>(); @@ -108,10 +104,9 @@ public void testRCS2CrossClusterSearch() throws Exception { assertSearchResponseContainsIndices(performRequestWithRemoteSearchUser(dataSearchRequest), expectedIndices); } { - // query remote cluster using ::failures selector should fail - final ResponseException exception = expectThrows( - ResponseException.class, - () -> performRequestWithRemoteSearchUser( + // query remote cluster using ::failures selector should succeed + assertSearchResponseContainsIndices( + performRequestWithRemoteSearchUser( new Request( "GET", String.format( @@ -121,13 +116,11 @@ public void testRCS2CrossClusterSearch() throws Exception { ccsMinimizeRoundtrips ) ) - ) + ), + backingFailureIndexName ); - assertThat(exception.getResponse().getStatusLine().getStatusCode(), equalTo(403)); - assertThat(exception.getMessage(), containsString("failures selector is not supported with cross-cluster expressions")); } { - // direct access to backing failure index is not allowed - no explicit read privileges over .fs-* indices Request failureIndexSearchRequest = new Request( "GET", String.format( @@ -137,26 +130,7 @@ public void testRCS2CrossClusterSearch() throws Exception { ccsMinimizeRoundtrips ) ); - final ResponseException exception = expectThrows( - ResponseException.class, - () -> performRequestWithRemoteSearchUser(failureIndexSearchRequest) - ); - assertThat(exception.getResponse().getStatusLine().getStatusCode(), equalTo(403)); - assertThat( - exception.getMessage(), - containsString( - "action [" - + (ccsMinimizeRoundtrips ? "indices:data/read/search" : "indices:admin/search/search_shards") - + "] towards remote cluster is unauthorized for user [remote_search_user] " - + "with assigned roles [remote_search] authenticated by API key id [" - + crossClusterAccessApiKeyId - + "] of user [test_user] on indices [" - + backingFailureIndexName - + "], this action is granted by the index privileges [" - + (ccsMinimizeRoundtrips ? "read,all" : "view_index_metadata,manage,read_cross_cluster,all") - + "]" - ) - ); + assertSearchResponseContainsIndices(performRequestWithRemoteSearchUser(failureIndexSearchRequest), backingFailureIndexName); } } @@ -181,7 +155,7 @@ private static void setupUserAndRoleOnQueryCluster() throws IOException { "remote_indices": [ { "names": ["test*"], - "privileges": ["read", "read_cross_cluster"], + "privileges": ["read", "read_cross_cluster", "read_failure_store"], "clusters": ["my_remote_cluster"] } ]