From ba0d56f61264b988dbc5600b51fb1db4a1d0c73b Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Wed, 12 Mar 2025 11:33:00 +0100 Subject: [PATCH 1/8] Prevent access for users with DLS/FLS to the failure store Failure store collects ingestion and mapping related failures when documents are written to a data stream. Indexing can fail and be captured in the failure store at any point in the ingest process. The fields may not have been dropped or sanitized during ingestion processing, or the document may not be in the form expected by document/field-level security rules, either of which may lead to the document exposing sensitive information that would otherwise not be exposed if the document was successfully processed and ingested. Since the DLS/FLS may not be applicable in the expected way, we here prevent access to the failure store for all users that have DLS/FLS restrictions. --- .../xpack/security/Security.java | 5 ++ .../FailureStoreRequestInterceptor.java | 68 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java 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 f3db0a3d2f630..9565c86585926 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 @@ -29,6 +29,7 @@ import org.elasticsearch.bootstrap.BootstrapCheck; import org.elasticsearch.client.internal.Client; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.IndexTemplateMetadata; import org.elasticsearch.cluster.metadata.ProjectId; @@ -328,6 +329,7 @@ import org.elasticsearch.xpack.security.authz.accesscontrol.OptOutQueryCache; import org.elasticsearch.xpack.security.authz.interceptor.BulkShardRequestInterceptor; import org.elasticsearch.xpack.security.authz.interceptor.DlsFlsLicenseRequestInterceptor; +import org.elasticsearch.xpack.security.authz.interceptor.FailureStoreRequestInterceptor; import org.elasticsearch.xpack.security.authz.interceptor.IndicesAliasesRequestInterceptor; import org.elasticsearch.xpack.security.authz.interceptor.RequestInterceptor; import org.elasticsearch.xpack.security.authz.interceptor.ResizeRequestInterceptor; @@ -1139,6 +1141,9 @@ Collection createComponents( new ValidateRequestInterceptor(threadPool, getLicenseState()) ) ); + if (DataStream.isFailureStoreFeatureFlagEnabled()) { + requestInterceptors.add(new FailureStoreRequestInterceptor(threadPool, getLicenseState())); + } } requestInterceptors = Collections.unmodifiableSet(requestInterceptors); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java new file mode 100644 index 0000000000000..fba62d1c733e4 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java @@ -0,0 +1,68 @@ +/* + * 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.security.authz.interceptor; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.support.IndexComponentSelector; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; + +import java.util.Arrays; +import java.util.Map; + +public class FailureStoreRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor { + + public FailureStoreRequestInterceptor(ThreadPool threadPool, XPackLicenseState licenseState) { + super(threadPool.getThreadContext(), licenseState); + } + + @Override + void disableFeatures( + IndicesRequest indicesRequest, + Map indicesAccessControlByIndex, + ActionListener listener + ) { + System.out.println("FailureStoreRequestInterceptor: " + indicesAccessControlByIndex); + if (indicesAccessControlByIndex.entrySet() + .stream() + .anyMatch(iac -> hasFailureStoreSelectorSuffix(iac.getKey()) && hasDlsFlsPermissions(iac.getValue()))) { + listener.onFailure( + new ElasticsearchSecurityException( + "Failure store access is not allowed for users who have field or document level security enabled on one of the indices", + RestStatus.BAD_REQUEST + ) + ); + } else { + listener.onResponse(null); + } + } + + @Override + boolean supports(IndicesRequest request) { + // TODO: check if this is the right approach or should we only intercept search requests + return request.indicesOptions().allowSelectors() && Arrays.stream(request.indices()).anyMatch(this::hasFailureStoreSelectorSuffix); + } + + private boolean hasFailureStoreSelectorSuffix(String name) { + return IndexNameExpressionResolver.hasSelectorSuffix(name) + && IndexComponentSelector.getByKey( + IndexNameExpressionResolver.splitSelectorExpression(name).v2() + ) == IndexComponentSelector.FAILURES; + } + + private boolean hasDlsFlsPermissions(IndicesAccessControl.IndexAccessControl indexAccessControl) { + return indexAccessControl.getDocumentPermissions().hasDocumentLevelPermissions() + || indexAccessControl.getFieldPermissions().hasFieldLevelSecurity(); + } + +} From dbe80a05cef276c7faf2b864db35fd09dd021cb5 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Wed, 12 Mar 2025 12:28:44 +0100 Subject: [PATCH 2/8] remove debug logs --- .../authz/interceptor/FailureStoreRequestInterceptor.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java index fba62d1c733e4..4a08c7f289072 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java @@ -32,7 +32,6 @@ void disableFeatures( Map indicesAccessControlByIndex, ActionListener listener ) { - System.out.println("FailureStoreRequestInterceptor: " + indicesAccessControlByIndex); if (indicesAccessControlByIndex.entrySet() .stream() .anyMatch(iac -> hasFailureStoreSelectorSuffix(iac.getKey()) && hasDlsFlsPermissions(iac.getValue()))) { From 4d8ea21a9ce63eb2d1279fd5a4f105dac917a512 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Wed, 12 Mar 2025 12:42:54 +0100 Subject: [PATCH 3/8] refactor away from using streams --- .../FailureStoreRequestInterceptor.java | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java index 4a08c7f289072..bcef45b1b15f6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java @@ -17,7 +17,6 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; -import java.util.Arrays; import java.util.Map; public class FailureStoreRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor { @@ -32,24 +31,32 @@ void disableFeatures( Map indicesAccessControlByIndex, ActionListener listener ) { - if (indicesAccessControlByIndex.entrySet() - .stream() - .anyMatch(iac -> hasFailureStoreSelectorSuffix(iac.getKey()) && hasDlsFlsPermissions(iac.getValue()))) { - listener.onFailure( - new ElasticsearchSecurityException( - "Failure store access is not allowed for users who have field or document level security enabled on one of the indices", - RestStatus.BAD_REQUEST - ) - ); - } else { - listener.onResponse(null); + for (var indexAccessControl : indicesAccessControlByIndex.entrySet()) { + if (hasFailureStoreSelectorSuffix(indexAccessControl.getKey()) && hasDlsFlsPermissions(indexAccessControl.getValue())) { + listener.onFailure( + new ElasticsearchSecurityException( + "Failure store access is not allowed for users who have " + + "field or document level security enabled on one of the indices", + RestStatus.BAD_REQUEST + ) + ); + return; + } } + listener.onResponse(null); } @Override boolean supports(IndicesRequest request) { // TODO: check if this is the right approach or should we only intercept search requests - return request.indicesOptions().allowSelectors() && Arrays.stream(request.indices()).anyMatch(this::hasFailureStoreSelectorSuffix); + if (request.indicesOptions().allowSelectors()) { + for (String index : request.indices()) { + if (hasFailureStoreSelectorSuffix(index)) { + return true; + } + } + } + return false; } private boolean hasFailureStoreSelectorSuffix(String name) { From 88436794835039db2ebe968117532fe0c9f9178d Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Tue, 18 Mar 2025 16:31:43 +0100 Subject: [PATCH 4/8] remove todo --- .../authz/interceptor/FailureStoreRequestInterceptor.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java index bcef45b1b15f6..1c7d4815e1206 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java @@ -48,7 +48,6 @@ void disableFeatures( @Override boolean supports(IndicesRequest request) { - // TODO: check if this is the right approach or should we only intercept search requests if (request.indicesOptions().allowSelectors()) { for (String index : request.indices()) { if (hasFailureStoreSelectorSuffix(index)) { From 80e904a698c6865e56594d62c3abc27a343fcbd0 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 24 Mar 2025 11:20:06 +0100 Subject: [PATCH 5/8] adjust IT tests --- .../FailureStoreSecurityRestIT.java | 33 +++++++++++-------- .../FailureStoreRequestInterceptor.java | 6 ++-- 2 files changed, 22 insertions(+), 17 deletions(-) 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 bfdea27eee98c..31fe3e8f33479 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 @@ -47,6 +47,7 @@ import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; @@ -1388,12 +1389,8 @@ public void testDlsFls() throws Exception { 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")) - ); + // FLS does not apply to failure store + expectFlsDlsError(() -> performRequest(user, new Search("test1::failures").toSearchRequest())); upsertRole(Strings.format(""" { @@ -1422,12 +1419,8 @@ public void testDlsFls() throws Exception { 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")) - ); + // FLS does not apply to failure store + expectFlsDlsError(() -> performRequest(user, new Search("test1::failures").toSearchRequest())); upsertRole(""" { @@ -1473,7 +1466,8 @@ public void testDlsFls() throws Exception { }""", role); // DLS applies and no docs match the query expectSearch(user, new Search(randomFrom("test1", "test1::data"))); - expectSearch(user, new Search("test1::failures")); + // DLS is not applicable to failure store + expectFlsDlsError(() -> performRequest(user, new Search("test1::failures").toSearchRequest())); upsertRole(""" { @@ -1488,7 +1482,8 @@ public void testDlsFls() throws Exception { }""", role); // DLS applies and doc matches the query expectSearch(user, new Search(randomFrom("test1", "test1::data")), dataIndexDocId); - expectSearch(user, new Search("test1::failures")); + // DLS is not applicable to failure store + expectFlsDlsError(() -> performRequest(user, new Search("test1::failures").toSearchRequest())); upsertRole(""" { @@ -1827,4 +1822,14 @@ private Tuple getSingleDataAndFailureIndices(String dataStreamNa assertThat(indices.v2().size(), equalTo(1)); return new Tuple<>(indices.v1().get(0), indices.v2().get(0)); } + + private static void expectFlsDlsError(ThrowingRunnable runnable) { + var exception = expectThrows(ResponseException.class, runnable); + assertThat( + exception.getMessage(), + containsString( + "Failure store access is not allowed for users who have field or document level security enabled on one of the indices" + ) + ); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java index 1c7d4815e1206..08aec464a0023 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java @@ -32,7 +32,7 @@ void disableFeatures( ActionListener listener ) { for (var indexAccessControl : indicesAccessControlByIndex.entrySet()) { - if (hasFailureStoreSelectorSuffix(indexAccessControl.getKey()) && hasDlsFlsPermissions(indexAccessControl.getValue())) { + if (hasFailuresSelectorSuffix(indexAccessControl.getKey()) && hasDlsFlsPermissions(indexAccessControl.getValue())) { listener.onFailure( new ElasticsearchSecurityException( "Failure store access is not allowed for users who have " @@ -50,7 +50,7 @@ void disableFeatures( boolean supports(IndicesRequest request) { if (request.indicesOptions().allowSelectors()) { for (String index : request.indices()) { - if (hasFailureStoreSelectorSuffix(index)) { + if (hasFailuresSelectorSuffix(index)) { return true; } } @@ -58,7 +58,7 @@ boolean supports(IndicesRequest request) { return false; } - private boolean hasFailureStoreSelectorSuffix(String name) { + private boolean hasFailuresSelectorSuffix(String name) { return IndexNameExpressionResolver.hasSelectorSuffix(name) && IndexComponentSelector.getByKey( IndexNameExpressionResolver.splitSelectorExpression(name).v2() From 58110d4f7b0f5568d4f48af6efe726dcf1fff6f4 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Tue, 25 Mar 2025 10:47:34 +0100 Subject: [PATCH 6/8] also prevent direct access to backing failure indices with FLS/DLS --- .../FailureStoreSecurityRestIT.java | 53 ++++++++++++++++++- .../xpack/security/Security.java | 2 +- .../FailureStoreRequestInterceptor.java | 29 ++++++++-- 3 files changed, 78 insertions(+), 6 deletions(-) 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 31fe3e8f33479..0f46ad24f6a4a 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 @@ -17,6 +17,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.support.XContentMapValues; @@ -1448,7 +1449,21 @@ public void testDlsFls() throws Exception { assertSearchResponseContainsExpectedIndicesAndFields( performRequest(user, new Search("test1::failures").toSearchRequest()), - Map.of(failureIndexName, Set.of("@timestamp", "document", "error")) + Map.of( + failureIndexName, + Set.of( + "@timestamp", + "document.id", + "document.index", + "document.source.@timestamp", + "document.source.age", + "document.source.email", + "document.source.name", + "error.message", + "error.stack_trace", + "error.type" + ) + ) ); // DLS @@ -1502,6 +1517,40 @@ public void testDlsFls() throws Exception { }""", role); // DLS does not apply because there is a section without DLS expectSearch(user, new Search(randomFrom("test1", "test1::data")), dataIndexDocId); + + // check that direct access to backing failure store indices is not allowed + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["%s"], + "privileges": ["read"], + "field_security": { + "grant": ["@timestamp", "document.source.name"] + } + } + ] + }""", failureIndexName), role); + // FLS is not applicable to backing failure store indices + expectFlsDlsError(() -> performRequest(user, new Search(failureIndexName).toSearchRequest())); + expectFlsDlsError(() -> performRequest(user, new Search(".fs-*").toSearchRequest())); + + // DLS is not applicable to backing failure store, even when granted directly + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["%s"], + "privileges": ["read"], + "query":{"term":{"name":{"value":"jack"}}} + } + ] + }""", failureIndexName), role); + expectFlsDlsError(() -> performRequest(user, new Search(failureIndexName).toSearchRequest())); + expectFlsDlsError(() -> performRequest(user, new Search(".fs-*").toSearchRequest())); + } private static void expectThrows(ThrowingRunnable runnable, int statusCode) { @@ -1792,7 +1841,7 @@ protected void assertSearchResponseContainsExpectedIndicesAndFields( assertThat(searchResult.keySet(), equalTo(expectedIndicesAndFields.keySet())); for (String index : expectedIndicesAndFields.keySet()) { Set expectedFields = expectedIndicesAndFields.get(index); - assertThat(searchResult.get(index).keySet(), equalTo(expectedFields)); + assertThat(Maps.flatten(searchResult.get(index), false, true).keySet(), equalTo(expectedFields)); } } finally { response.decRef(); 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 16581cab8e3e0..e9c3cb3fbd51f 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 @@ -1142,7 +1142,7 @@ Collection createComponents( ) ); if (DataStream.isFailureStoreFeatureFlagEnabled()) { - requestInterceptors.add(new FailureStoreRequestInterceptor(threadPool, getLicenseState())); + requestInterceptors.add(new FailureStoreRequestInterceptor(clusterService, projectResolver, threadPool, getLicenseState())); } } requestInterceptors = Collections.unmodifiableSet(requestInterceptors); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java index 08aec464a0023..1b439d7ca63d8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java @@ -11,7 +11,11 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.support.IndexComponentSelector; +import org.elasticsearch.cluster.metadata.DataStreamFailureStoreDefinition; +import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.project.ProjectResolver; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.threadpool.ThreadPool; @@ -21,8 +25,18 @@ public class FailureStoreRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor { - public FailureStoreRequestInterceptor(ThreadPool threadPool, XPackLicenseState licenseState) { + private final ClusterService clusterService; + private final ProjectResolver projectResolver; + + public FailureStoreRequestInterceptor( + ClusterService clusterService, + ProjectResolver projectResolver, + ThreadPool threadPool, + XPackLicenseState licenseState + ) { super(threadPool.getThreadContext(), licenseState); + this.clusterService = clusterService; + this.projectResolver = projectResolver; } @Override @@ -32,7 +46,8 @@ void disableFeatures( ActionListener listener ) { for (var indexAccessControl : indicesAccessControlByIndex.entrySet()) { - if (hasFailuresSelectorSuffix(indexAccessControl.getKey()) && hasDlsFlsPermissions(indexAccessControl.getValue())) { + if ((hasFailuresSelectorSuffix(indexAccessControl.getKey()) || isBackingFailureStoreIndex(indexAccessControl.getKey())) + && hasDlsFlsPermissions(indexAccessControl.getValue())) { listener.onFailure( new ElasticsearchSecurityException( "Failure store access is not allowed for users who have " @@ -50,7 +65,7 @@ void disableFeatures( boolean supports(IndicesRequest request) { if (request.indicesOptions().allowSelectors()) { for (String index : request.indices()) { - if (hasFailuresSelectorSuffix(index)) { + if (hasFailuresSelectorSuffix(index) || isBackingFailureStoreIndex(index)) { return true; } } @@ -70,4 +85,12 @@ private boolean hasDlsFlsPermissions(IndicesAccessControl.IndexAccessControl ind || indexAccessControl.getFieldPermissions().hasFieldLevelSecurity(); } + private boolean isBackingFailureStoreIndex(String index) { + IndexMetadata indexMetadata = clusterService.state().metadata().getProject(projectResolver.getProjectId()).index(index); + if (indexMetadata == null) { + return false; + } + return indexMetadata.getSettings().hasValue(DataStreamFailureStoreDefinition.INDEX_FAILURE_STORE_VERSION_SETTING_NAME); + } + } From a9efba4cde36ab1fe9aba794477eec4dd25899ef Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Tue, 25 Mar 2025 10:55:11 +0100 Subject: [PATCH 7/8] direct access to backing data indices should work with FLS --- .../FailureStoreSecurityRestIT.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 0f46ad24f6a4a..3912a585dcb57 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 @@ -1523,6 +1523,13 @@ public void testDlsFls() throws Exception { { "cluster": ["all"], "indices": [ + { + "names": ["%s"], + "privileges": ["read"], + "field_security": { + "grant": ["@timestamp", "age"] + } + }, { "names": ["%s"], "privileges": ["read"], @@ -1531,7 +1538,17 @@ public void testDlsFls() throws Exception { } } ] - }""", failureIndexName), role); + }""", dataIndexName, failureIndexName), role); + + // FLS applies to backing data index + assertSearchResponseContainsExpectedIndicesAndFields( + performRequest(user, new Search(dataIndexName).toSearchRequest()), + Map.of(dataIndexName, Set.of("@timestamp", "age")) + ); + assertSearchResponseContainsExpectedIndicesAndFields( + performRequest(user, new Search(".ds-*").toSearchRequest()), + Map.of(dataIndexName, Set.of("@timestamp", "age")) + ); // FLS is not applicable to backing failure store indices expectFlsDlsError(() -> performRequest(user, new Search(failureIndexName).toSearchRequest())); expectFlsDlsError(() -> performRequest(user, new Search(".fs-*").toSearchRequest())); From f0eb840d4267a40ec00522ff7959c9caba20b6af Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Tue, 25 Mar 2025 15:29:06 +0100 Subject: [PATCH 8/8] use getIndicesLookup instead of index metadata --- .../interceptor/FailureStoreRequestInterceptor.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java index 1b439d7ca63d8..d831ae8235bba 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FailureStoreRequestInterceptor.java @@ -11,8 +11,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.support.IndexComponentSelector; -import org.elasticsearch.cluster.metadata.DataStreamFailureStoreDefinition; -import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.project.ProjectResolver; import org.elasticsearch.cluster.service.ClusterService; @@ -86,11 +85,15 @@ private boolean hasDlsFlsPermissions(IndicesAccessControl.IndexAccessControl ind } private boolean isBackingFailureStoreIndex(String index) { - IndexMetadata indexMetadata = clusterService.state().metadata().getProject(projectResolver.getProjectId()).index(index); - if (indexMetadata == null) { + final IndexAbstraction indexAbstraction = clusterService.state() + .metadata() + .getProject(projectResolver.getProjectId()) + .getIndicesLookup() + .get(index); + if (indexAbstraction == null) { return false; } - return indexMetadata.getSettings().hasValue(DataStreamFailureStoreDefinition.INDEX_FAILURE_STORE_VERSION_SETTING_NAME); + return indexAbstraction.isFailureIndexOfDataStream(); } }