-
Notifications
You must be signed in to change notification settings - Fork 25.6k
Prevent access for users with DLS/FLS to the failure store #124634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ba0d56f
dbe80a0
4d8ea21
8843679
8e20b39
a2e1b9e
80e904a
58110d4
50c937b
a9efba4
f0eb840
feee7da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -47,6 +48,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 +1390,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 +1420,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(""" | ||
| { | ||
|
|
@@ -1455,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", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting, do you know why this changed? I.e., why are we getting flattened fields now, instead of top-level field? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've changed the test assertion to flatten the results in order to make sure that FLS is properly respected for nested fields. Previously we were only asserting the top level fields. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah sorry, this was an old comment I forgot to delete -- plz ignore, this is a good improvement |
||
| "document.index", | ||
| "document.source.@timestamp", | ||
| "document.source.age", | ||
| "document.source.email", | ||
| "document.source.name", | ||
| "error.message", | ||
| "error.stack_trace", | ||
| "error.type" | ||
| ) | ||
| ) | ||
| ); | ||
|
|
||
| // DLS | ||
|
|
@@ -1473,7 +1481,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 +1497,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(""" | ||
| { | ||
|
|
@@ -1507,6 +1517,57 @@ 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", "age"] | ||
| } | ||
| }, | ||
| { | ||
| "names": ["%s"], | ||
| "privileges": ["read"], | ||
| "field_security": { | ||
| "grant": ["@timestamp", "document.source.name"] | ||
| } | ||
| } | ||
| ] | ||
| }""", 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())); | ||
|
|
||
| // 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) { | ||
|
|
@@ -1797,7 +1858,7 @@ protected void assertSearchResponseContainsExpectedIndicesAndFields( | |
| assertThat(searchResult.keySet(), equalTo(expectedIndicesAndFields.keySet())); | ||
| for (String index : expectedIndicesAndFields.keySet()) { | ||
| Set<String> 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(); | ||
|
|
@@ -1827,4 +1888,14 @@ private Tuple<String, String> 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" | ||
| ) | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| /* | ||
| * 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.IndexAbstraction; | ||
| 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; | ||
| import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| public class FailureStoreRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor { | ||
|
|
||
| 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 | ||
| void disableFeatures( | ||
| IndicesRequest indicesRequest, | ||
| Map<String, IndicesAccessControl.IndexAccessControl> indicesAccessControlByIndex, | ||
| ActionListener<Void> listener | ||
| ) { | ||
| for (var indexAccessControl : indicesAccessControlByIndex.entrySet()) { | ||
| if ((hasFailuresSelectorSuffix(indexAccessControl.getKey()) || isBackingFailureStoreIndex(indexAccessControl.getKey())) | ||
| && hasDlsFlsPermissions(indexAccessControl.getValue())) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd consider flipping this since most roles won't have FLS/DLS so we'll short-circuit quicker |
||
| 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) { | ||
| if (request.indicesOptions().allowSelectors()) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure we want There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch! This started with intention to prevent access with |
||
| for (String index : request.indices()) { | ||
| if (hasFailuresSelectorSuffix(index) || isBackingFailureStoreIndex(index)) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private boolean hasFailuresSelectorSuffix(String name) { | ||
| return IndexNameExpressionResolver.hasSelectorSuffix(name) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Fine to include the bigger check with splitting as an assertion since it does additional validation, but There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. Will make the change. |
||
| && IndexComponentSelector.getByKey( | ||
| IndexNameExpressionResolver.splitSelectorExpression(name).v2() | ||
| ) == IndexComponentSelector.FAILURES; | ||
| } | ||
|
|
||
| private boolean hasDlsFlsPermissions(IndicesAccessControl.IndexAccessControl indexAccessControl) { | ||
| return indexAccessControl.getDocumentPermissions().hasDocumentLevelPermissions() | ||
| || indexAccessControl.getFieldPermissions().hasFieldLevelSecurity(); | ||
| } | ||
|
|
||
| private boolean isBackingFailureStoreIndex(String index) { | ||
| final IndexAbstraction indexAbstraction = clusterService.state() | ||
| .metadata() | ||
| .getProject(projectResolver.getProjectId()) | ||
| .getIndicesLookup() | ||
| .get(index); | ||
| if (indexAbstraction == null) { | ||
| return false; | ||
| } | ||
| return indexAbstraction.isFailureIndexOfDataStream(); | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've hit the ready button too soon. I still want to test cover the case when users have direct access to
.fs-*indices and accessing without::failuresselector.