diff --git a/docs/changelog/105709.yaml b/docs/changelog/105709.yaml new file mode 100644 index 000000000000..568d60a86334 --- /dev/null +++ b/docs/changelog/105709.yaml @@ -0,0 +1,6 @@ +pr: 105709 +summary: Disable validate when rewrite parameter is sent and the index access control + list is non-null +area: Security +type: bug +issues: [] 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 d3898cc510d7..3d27e9ee06dd 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 @@ -303,6 +303,7 @@ import org.elasticsearch.xpack.security.authz.interceptor.SearchRequestCacheDisa import org.elasticsearch.xpack.security.authz.interceptor.SearchRequestInterceptor; import org.elasticsearch.xpack.security.authz.interceptor.ShardSearchRequestInterceptor; import org.elasticsearch.xpack.security.authz.interceptor.UpdateRequestInterceptor; +import org.elasticsearch.xpack.security.authz.interceptor.ValidateRequestInterceptor; import org.elasticsearch.xpack.security.authz.restriction.WorkflowService; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.authz.store.DeprecationRoleDescriptorConsumer; @@ -999,7 +1000,8 @@ public class Security extends Plugin new UpdateRequestInterceptor(threadPool, getLicenseState()), new BulkShardRequestInterceptor(threadPool, getLicenseState()), new DlsFlsLicenseRequestInterceptor(threadPool.getThreadContext(), getLicenseState()), - new SearchRequestCacheDisablingInterceptor(threadPool, getLicenseState()) + new SearchRequestCacheDisablingInterceptor(threadPool, getLicenseState()), + new ValidateRequestInterceptor(threadPool, getLicenseState()) ) ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/ValidateRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/ValidateRequestInterceptor.java new file mode 100644 index 000000000000..1705ea7d1490 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/ValidateRequestInterceptor.java @@ -0,0 +1,61 @@ +/* + * 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.admin.indices.validate.query.ValidateQueryRequest; +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 ValidateRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor { + + public ValidateRequestInterceptor(ThreadPool threadPool, XPackLicenseState licenseState) { + super(threadPool.getThreadContext(), licenseState); + } + + @Override + void disableFeatures( + IndicesRequest indicesRequest, + Map indexAccessControlByIndex, + ActionListener listener + ) { + final ValidateQueryRequest request = (ValidateQueryRequest) indicesRequest; + if (indexAccessControlByIndex.values().stream().anyMatch(iac -> iac.getDocumentPermissions().hasDocumentLevelPermissions())) { + if (hasRewrite(request)) { + listener.onFailure( + new ElasticsearchSecurityException( + "Validate with rewrite isn't supported if document level security is enabled", + RestStatus.BAD_REQUEST + ) + ); + } else { + listener.onResponse(null); + } + } else { + listener.onResponse(null); + } + } + + @Override + public boolean supports(IndicesRequest request) { + if (request instanceof ValidateQueryRequest validateQueryRequest) { + return hasRewrite(validateQueryRequest); + } else { + return false; + } + } + + private static boolean hasRewrite(ValidateQueryRequest validateQueryRequest) { + return validateQueryRequest.rewrite(); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/ValidateRequestInterceptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/ValidateRequestInterceptorTests.java new file mode 100644 index 000000000000..bb494b7855db --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/ValidateRequestInterceptorTests.java @@ -0,0 +1,94 @@ +/* + * 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.admin.indices.validate.query.ValidateQueryRequestBuilder; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.client.internal.ElasticsearchClient; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.license.MockLicenseState; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.authz.permission.DocumentPermissions; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; +import org.junit.After; +import org.junit.Before; + +import java.util.Map; +import java.util.Set; + +import static org.elasticsearch.xpack.core.security.SecurityField.DOCUMENT_LEVEL_SECURITY_FEATURE; +import static org.hamcrest.Matchers.containsString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ValidateRequestInterceptorTests extends ESTestCase { + + private ThreadPool threadPool; + private MockLicenseState licenseState; + private ValidateRequestInterceptor interceptor; + + @Before + public void init() { + threadPool = new TestThreadPool("validate request interceptor tests"); + licenseState = mock(MockLicenseState.class); + when(licenseState.isAllowed(DOCUMENT_LEVEL_SECURITY_FEATURE)).thenReturn(true); + interceptor = new ValidateRequestInterceptor(threadPool, licenseState); + } + + @After + public void stopThreadPool() { + terminate(threadPool); + } + + public void testValidateRequestWithDLS() { + final DocumentPermissions documentPermissions = DocumentPermissions.filteredBy(Set.of(new BytesArray(""" + {"term":{"username":"foo"}}"""))); // value does not matter + ElasticsearchClient client = mock(ElasticsearchClient.class); + ValidateQueryRequestBuilder builder = new ValidateQueryRequestBuilder(client); + final String index = randomAlphaOfLengthBetween(3, 8); + final PlainActionFuture listener1 = new PlainActionFuture<>(); + Map accessControlMap = Map.of( + index, + new IndicesAccessControl.IndexAccessControl(FieldPermissions.DEFAULT, documentPermissions) + ); + // with DLS and rewrite enabled + interceptor.disableFeatures(builder.setRewrite(true).request(), accessControlMap, listener1); + ElasticsearchSecurityException exception = expectThrows(ElasticsearchSecurityException.class, () -> listener1.actionGet()); + assertThat(exception.getMessage(), containsString("Validate with rewrite isn't supported if document level security is enabled")); + + // with DLS and rewrite disabled + final PlainActionFuture listener2 = new PlainActionFuture<>(); + interceptor.disableFeatures(builder.setRewrite(false).request(), accessControlMap, listener2); + assertNull(listener2.actionGet()); + + } + + public void testValidateRequestWithOutDLS() { + final DocumentPermissions documentPermissions = null; // no DLS + ElasticsearchClient client = mock(ElasticsearchClient.class); + ValidateQueryRequestBuilder builder = new ValidateQueryRequestBuilder(client); + final String index = randomAlphaOfLengthBetween(3, 8); + final PlainActionFuture listener1 = new PlainActionFuture<>(); + Map accessControlMap = Map.of( + index, + new IndicesAccessControl.IndexAccessControl(FieldPermissions.DEFAULT, documentPermissions) + ); + // without DLS and rewrite enabled + interceptor.disableFeatures(builder.setRewrite(true).request(), accessControlMap, listener1); + assertNull(listener1.actionGet()); + + // without DLS and rewrite disabled + final PlainActionFuture listener2 = new PlainActionFuture<>(); + interceptor.disableFeatures(builder.setRewrite(false).request(), accessControlMap, listener2); + assertNull(listener2.actionGet()); + } +}