From 1127b1c513b40342091aeb76e6e9f6475d303f6b Mon Sep 17 00:00:00 2001 From: Ralph Ursprung Date: Fri, 17 Jul 2026 11:11:11 +0200 Subject: [PATCH 1/3] simplify gRPC tests by centralizing more utilities the plugin configuration and the authorization handling is always the same => these can be centralized in `GrpcHelpers` as well. Signed-off-by: Ralph Ursprung --- .../security/grpc/BasicAuthGrpcTest.java | 69 ++++--------------- .../security/grpc/GrpcAnonymousAuthTest.java | 43 ++---------- .../opensearch/security/grpc/GrpcHelpers.java | 48 +++++++++++++ .../grpc/JWTGrpcDefaultAuthHeaderTest.java | 33 +-------- .../grpc/JWTGrpcDisabledAuthDomainTest.java | 33 +-------- .../security/grpc/JWTGrpcInterceptorTest.java | 35 +--------- 6 files changed, 73 insertions(+), 188 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/grpc/BasicAuthGrpcTest.java b/src/integrationTest/java/org/opensearch/security/grpc/BasicAuthGrpcTest.java index b493a39fde..6f9fa11d4f 100644 --- a/src/integrationTest/java/org/opensearch/security/grpc/BasicAuthGrpcTest.java +++ b/src/integrationTest/java/org/opensearch/security/grpc/BasicAuthGrpcTest.java @@ -11,22 +11,14 @@ package org.opensearch.security.grpc; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Collections; -import java.util.List; import java.util.Map; import org.junit.ClassRule; import org.junit.Test; -import org.opensearch.Version; -import org.opensearch.plugins.PluginInfo; -import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.transport.grpc.GrpcPlugin; import io.grpc.Channel; import io.grpc.ClientInterceptor; @@ -36,8 +28,11 @@ import static org.opensearch.security.grpc.GrpcHelpers.GRPC_INDEX_ROLE; import static org.opensearch.security.grpc.GrpcHelpers.GRPC_INDEX_USER; +import static org.opensearch.security.grpc.GrpcHelpers.SECURITY_WITH_GRPC_PLUGIN; import static org.opensearch.security.grpc.GrpcHelpers.SINGLE_NODE_SECURE_AUTH_GRPC_TRANSPORT_SETTINGS; import static org.opensearch.security.grpc.GrpcHelpers.TEST_CERTIFICATES; +import static org.opensearch.security.grpc.GrpcHelpers.createBasicAuthHeader; +import static org.opensearch.security.grpc.GrpcHelpers.createChannelWithBasicAuthorization; import static org.opensearch.security.grpc.GrpcHelpers.createHeaderInterceptor; import static org.opensearch.security.grpc.GrpcHelpers.doBulk; import static org.opensearch.security.grpc.GrpcHelpers.getSecureGrpcEndpoint; @@ -63,55 +58,19 @@ public class BasicAuthGrpcTest { public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) .certificates(TEST_CERTIFICATES) .nodeSettings(SINGLE_NODE_SECURE_AUTH_GRPC_TRANSPORT_SETTINGS) - .plugin( - new PluginInfo( - GrpcPlugin.class.getName(), - "classpath plugin", - "NA", - Version.CURRENT, - "21", - GrpcPlugin.class.getName(), - null, - Collections.emptyList(), - false - ) - ) - .plugin( - new PluginInfo( - OpenSearchSecurityPlugin.class.getName(), - "classpath plugin", - "NA", - Version.CURRENT, - "21", - OpenSearchSecurityPlugin.class.getName(), - null, - List.of("org.opensearch.transport.grpc.GrpcPlugin"), - false - ) - ) + .plugin(SECURITY_WITH_GRPC_PLUGIN) .users(GRPC_INDEX_USER) .roles(GRPC_INDEX_ROLE) .rolesMapping(new TestSecurityConfig.RoleMapping(GRPC_INDEX_ROLE.getName()).users(GRPC_INDEX_USER.getName())) .authc(BASIC_AUTH_DOMAIN) .build(); - /** - * Creates a Basic Auth header value: "Basic base64(username:password)" - */ - private String createBasicAuthHeader(String username, String password) { - String credentials = username + ":" + password; - String base64Credentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); - return "Basic " + base64Credentials; - } - @Test public void testBasicAuthenticationWrongPassword() throws Exception { - String authHeader = createBasicAuthHeader(GRPC_INDEX_USER.getName(), "wrong-password"); - ManagedChannel channel = secureChannel(getSecureGrpcEndpoint(cluster)); + final var channel = secureChannel(getSecureGrpcEndpoint(cluster)); try { - ClientInterceptor authInterceptor = createHeaderInterceptor(Map.of("Authorization", authHeader)); - Channel channelWithAuth = io.grpc.ClientInterceptors.intercept(channel, authInterceptor); + final var channelWithAuth = createChannelWithBasicAuthorization(channel, GRPC_INDEX_USER.getName(), "wrong-password"); try { doBulk(channelWithAuth, "test-grpc-basic-wrong-pass", 2); @@ -127,12 +86,10 @@ public void testBasicAuthenticationWrongPassword() throws Exception { @Test public void testBasicAuthenticationUnknownUser() throws Exception { - String authHeader = createBasicAuthHeader("nonexistent-user", "any-password"); - ManagedChannel channel = secureChannel(getSecureGrpcEndpoint(cluster)); + final var channel = secureChannel(getSecureGrpcEndpoint(cluster)); try { - ClientInterceptor authInterceptor = createHeaderInterceptor(Map.of("Authorization", authHeader)); - Channel channelWithAuth = io.grpc.ClientInterceptors.intercept(channel, authInterceptor); + final var channelWithAuth = createChannelWithBasicAuthorization(channel, "nonexistent-user", "any-password"); try { doBulk(channelWithAuth, "test-grpc-basic-unknown-user", 2); @@ -148,12 +105,14 @@ public void testBasicAuthenticationUnknownUser() throws Exception { @Test public void testBasicAuthenticationSuccess() throws Exception { - String authHeader = createBasicAuthHeader(GRPC_INDEX_USER.getName(), GRPC_INDEX_USER.getPassword()); - ManagedChannel channel = secureChannel(getSecureGrpcEndpoint(cluster)); + final var channel = secureChannel(getSecureGrpcEndpoint(cluster)); try { - ClientInterceptor authInterceptor = createHeaderInterceptor(Map.of("Authorization", authHeader)); - Channel channelWithAuth = io.grpc.ClientInterceptors.intercept(channel, authInterceptor); + final var channelWithAuth = createChannelWithBasicAuthorization( + channel, + GRPC_INDEX_USER.getName(), + GRPC_INDEX_USER.getPassword() + ); var bulkResp = doBulk(channelWithAuth, "test-grpc-basic-auth", 2); assertNotNull(bulkResp); assertFalse("Bulk request should succeed with valid Basic Auth", bulkResp.getErrors()); diff --git a/src/integrationTest/java/org/opensearch/security/grpc/GrpcAnonymousAuthTest.java b/src/integrationTest/java/org/opensearch/security/grpc/GrpcAnonymousAuthTest.java index 48121a22a1..e3aa39e335 100644 --- a/src/integrationTest/java/org/opensearch/security/grpc/GrpcAnonymousAuthTest.java +++ b/src/integrationTest/java/org/opensearch/security/grpc/GrpcAnonymousAuthTest.java @@ -12,30 +12,23 @@ package org.opensearch.security.grpc; import java.util.Arrays; -import java.util.Collections; -import java.util.List; import java.util.Map; import org.junit.ClassRule; import org.junit.Test; -import org.opensearch.Version; -import org.opensearch.plugins.PluginInfo; -import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.transport.grpc.GrpcPlugin; -import io.grpc.Channel; -import io.grpc.ClientInterceptor; import io.grpc.ManagedChannel; import io.grpc.Status; import io.grpc.StatusRuntimeException; +import static org.opensearch.security.grpc.GrpcHelpers.SECURITY_WITH_GRPC_PLUGIN; import static org.opensearch.security.grpc.GrpcHelpers.SINGLE_NODE_SECURE_AUTH_GRPC_TRANSPORT_SETTINGS; import static org.opensearch.security.grpc.GrpcHelpers.TEST_CERTIFICATES; -import static org.opensearch.security.grpc.GrpcHelpers.createHeaderInterceptor; +import static org.opensearch.security.grpc.GrpcHelpers.createChannelWithAuthorization; import static org.opensearch.security.grpc.GrpcHelpers.doBulk; import static org.opensearch.security.grpc.GrpcHelpers.getSecureGrpcEndpoint; import static org.opensearch.security.grpc.GrpcHelpers.secureChannel; @@ -54,32 +47,7 @@ public class GrpcAnonymousAuthTest { .certificates(TEST_CERTIFICATES) .nodeSettings(SINGLE_NODE_SECURE_AUTH_GRPC_TRANSPORT_SETTINGS) .nodeSettings(Map.of("plugins.security.authcz.admin_dn", Arrays.asList(TEST_CERTIFICATES.getAdminDNs()))) - .plugin( - new PluginInfo( - GrpcPlugin.class.getName(), - "classpath plugin", - "NA", - Version.CURRENT, - "21", - GrpcPlugin.class.getName(), - null, - Collections.emptyList(), - false - ) - ) - .plugin( - new PluginInfo( - OpenSearchSecurityPlugin.class.getName(), - "classpath plugin", - "NA", - Version.CURRENT, - "21", - OpenSearchSecurityPlugin.class.getName(), - null, - List.of("org.opensearch.transport.grpc.GrpcPlugin"), - false - ) - ) + .plugin(SECURITY_WITH_GRPC_PLUGIN) .anonymousAuth(true) .roles(ANONYMOUS_BULK_ROLE) .rolesMapping(new TestSecurityConfig.RoleMapping(ANONYMOUS_BULK_ROLE.getName()).users("*")) @@ -103,10 +71,9 @@ public void testAnonymousAuthRejectedForGrpc() throws Exception { @Test public void testInvalidAuthHeaderRejected() throws Exception { - ManagedChannel channel = secureChannel(getSecureGrpcEndpoint(cluster)); + final var channel = secureChannel(getSecureGrpcEndpoint(cluster)); try { - ClientInterceptor mockAuthInterceptor = createHeaderInterceptor(Map.of("Authorization", "mock-auth-header")); - Channel channelWithAuth = io.grpc.ClientInterceptors.intercept(channel, mockAuthInterceptor); + final var channelWithAuth = createChannelWithAuthorization(channel, "mock-auth-header"); try { doBulk(channelWithAuth, "test-invalid-auth", 2); diff --git a/src/integrationTest/java/org/opensearch/security/grpc/GrpcHelpers.java b/src/integrationTest/java/org/opensearch/security/grpc/GrpcHelpers.java index eea314b2fe..b1b2ee7d3d 100644 --- a/src/integrationTest/java/org/opensearch/security/grpc/GrpcHelpers.java +++ b/src/integrationTest/java/org/opensearch/security/grpc/GrpcHelpers.java @@ -10,15 +10,19 @@ package org.opensearch.security.grpc; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.opensearch.Version; import org.opensearch.common.transport.PortsRange; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.plugins.PluginInfo; import org.opensearch.protobufs.BulkRequest; import org.opensearch.protobufs.BulkRequestBody; import org.opensearch.protobufs.BulkResponse; @@ -32,11 +36,13 @@ import org.opensearch.protobufs.SearchResponse; import org.opensearch.protobufs.services.DocumentServiceGrpc; import org.opensearch.protobufs.services.SearchServiceGrpc; +import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.certificate.TestCertificates; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.LocalOpenSearchCluster; +import org.opensearch.transport.grpc.GrpcPlugin; import org.opensearch.transport.grpc.spi.GrpcInterceptorProvider; import org.opensearch.transport.grpc.ssl.SecureNetty4GrpcServerTransport; @@ -59,6 +65,30 @@ import static io.grpc.internal.GrpcUtil.NOOP_PROXY_DETECTOR; public class GrpcHelpers { + public static final PluginInfo[] SECURITY_WITH_GRPC_PLUGIN = { + new PluginInfo( + GrpcPlugin.class.getName(), + "classpath plugin", + "NA", + Version.CURRENT, + "21", + GrpcPlugin.class.getName(), + null, + Collections.emptyList(), + false + ), + new PluginInfo( + OpenSearchSecurityPlugin.class.getName(), + "classpath plugin", + "NA", + Version.CURRENT, + "21", + OpenSearchSecurityPlugin.class.getName(), + null, + List.of("org.opensearch.transport.grpc.GrpcPlugin"), + false + ) }; + protected static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); protected static final TestCertificates UN_TRUSTED_TEST_CERTIFICATES = new TestCertificates(); @@ -109,6 +139,24 @@ public void request(int numMessages) { }; } + /** + * Creates a Basic Auth header value: "Basic base64(username:password)" + */ + public static String createBasicAuthHeader(String username, String password) { + String credentials = username + ":" + password; + String base64Credentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + return "Basic " + base64Credentials; + } + + public static Channel createChannelWithAuthorization(final ManagedChannel channel, final String authorizationHeader) { + final var authInterceptor = createHeaderInterceptor(Map.of("Authorization", authorizationHeader)); + return io.grpc.ClientInterceptors.intercept(channel, authInterceptor); + } + + public static Channel createChannelWithBasicAuthorization(final ManagedChannel channel, final String username, final String password) { + return createChannelWithAuthorization(channel, createBasicAuthHeader(username, password)); + } + protected static final Map CLIENT_AUTH_NONE = Map.of( "plugins.security.ssl.aux.secure-transport-grpc.clientauth_mode", "NONE" diff --git a/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcDefaultAuthHeaderTest.java b/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcDefaultAuthHeaderTest.java index 78e68936d7..d8819eafa4 100644 --- a/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcDefaultAuthHeaderTest.java +++ b/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcDefaultAuthHeaderTest.java @@ -13,7 +13,6 @@ import java.security.KeyPair; import java.util.Base64; -import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; @@ -21,14 +20,10 @@ import org.junit.ClassRule; import org.junit.Test; -import org.opensearch.Version; -import org.opensearch.plugins.PluginInfo; -import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.test.framework.JwtConfigBuilder; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.transport.grpc.GrpcPlugin; import io.grpc.Channel; import io.grpc.ClientInterceptor; @@ -40,6 +35,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII; import static org.opensearch.security.grpc.GrpcHelpers.GRPC_INDEX_ROLE; import static org.opensearch.security.grpc.GrpcHelpers.GRPC_INDEX_USER; +import static org.opensearch.security.grpc.GrpcHelpers.SECURITY_WITH_GRPC_PLUGIN; import static org.opensearch.security.grpc.GrpcHelpers.SINGLE_NODE_SECURE_AUTH_GRPC_TRANSPORT_SETTINGS; import static org.opensearch.security.grpc.GrpcHelpers.TEST_CERTIFICATES; import static org.opensearch.security.grpc.GrpcHelpers.createHeaderInterceptor; @@ -79,32 +75,7 @@ private String createValidJwtToken(String username, String... roles) { public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) .certificates(TEST_CERTIFICATES) .nodeSettings(SINGLE_NODE_SECURE_AUTH_GRPC_TRANSPORT_SETTINGS) - .plugin( - new PluginInfo( - GrpcPlugin.class.getName(), - "classpath plugin", - "NA", - Version.CURRENT, - "21", - GrpcPlugin.class.getName(), - null, - Collections.emptyList(), - false - ) - ) - .plugin( - new PluginInfo( - OpenSearchSecurityPlugin.class.getName(), - "classpath plugin", - "NA", - Version.CURRENT, - "21", - OpenSearchSecurityPlugin.class.getName(), - null, - List.of("org.opensearch.transport.grpc.GrpcPlugin"), - false - ) - ) + .plugin(SECURITY_WITH_GRPC_PLUGIN) .users(GRPC_INDEX_USER) .roles(GRPC_INDEX_ROLE) .rolesMapping(new TestSecurityConfig.RoleMapping(GRPC_INDEX_ROLE.getName()).backendRoles("grpc_index_role")) diff --git a/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcDisabledAuthDomainTest.java b/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcDisabledAuthDomainTest.java index b6d9ed103e..9dd0cefca5 100644 --- a/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcDisabledAuthDomainTest.java +++ b/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcDisabledAuthDomainTest.java @@ -13,7 +13,6 @@ import java.security.KeyPair; import java.util.Base64; -import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; @@ -21,14 +20,10 @@ import org.junit.ClassRule; import org.junit.Test; -import org.opensearch.Version; -import org.opensearch.plugins.PluginInfo; -import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.test.framework.JwtConfigBuilder; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.transport.grpc.GrpcPlugin; import io.grpc.Channel; import io.grpc.ClientInterceptor; @@ -42,6 +37,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII; import static org.opensearch.security.grpc.GrpcHelpers.GRPC_INDEX_ROLE; import static org.opensearch.security.grpc.GrpcHelpers.GRPC_INDEX_USER; +import static org.opensearch.security.grpc.GrpcHelpers.SECURITY_WITH_GRPC_PLUGIN; import static org.opensearch.security.grpc.GrpcHelpers.SINGLE_NODE_SECURE_AUTH_GRPC_TRANSPORT_SETTINGS; import static org.opensearch.security.grpc.GrpcHelpers.TEST_CERTIFICATES; import static org.opensearch.security.grpc.GrpcHelpers.createHeaderInterceptor; @@ -90,32 +86,7 @@ private String createValidJwtToken(String username, String... roles) { public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) .certificates(TEST_CERTIFICATES) .nodeSettings(SINGLE_NODE_SECURE_AUTH_GRPC_TRANSPORT_SETTINGS) - .plugin( - new PluginInfo( - GrpcPlugin.class.getName(), - "classpath plugin", - "NA", - Version.CURRENT, - "21", - GrpcPlugin.class.getName(), - null, - Collections.emptyList(), - false - ) - ) - .plugin( - new PluginInfo( - OpenSearchSecurityPlugin.class.getName(), - "classpath plugin", - "NA", - Version.CURRENT, - "21", - OpenSearchSecurityPlugin.class.getName(), - null, - List.of("org.opensearch.transport.grpc.GrpcPlugin"), - false - ) - ) + .plugin(SECURITY_WITH_GRPC_PLUGIN) .users(GRPC_INDEX_USER) .roles(GRPC_INDEX_ROLE) .rolesMapping(new TestSecurityConfig.RoleMapping(GRPC_INDEX_ROLE.getName()).backendRoles("grpc_index_role")) diff --git a/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcInterceptorTest.java b/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcInterceptorTest.java index b684fa6115..9ea210ab1b 100644 --- a/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcInterceptorTest.java +++ b/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcInterceptorTest.java @@ -14,7 +14,6 @@ import java.security.KeyPair; import java.util.Arrays; import java.util.Base64; -import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; @@ -22,15 +21,11 @@ import org.junit.ClassRule; import org.junit.Test; -import org.opensearch.Version; -import org.opensearch.plugins.PluginInfo; import org.opensearch.protobufs.BulkResponse; -import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.test.framework.JwtConfigBuilder; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.transport.grpc.GrpcPlugin; import io.grpc.Channel; import io.grpc.ClientInterceptor; @@ -48,6 +43,7 @@ import static org.opensearch.security.grpc.GrpcHelpers.GRPC_INDEX_USER_NO_MAPPING; import static org.opensearch.security.grpc.GrpcHelpers.GRPC_SEARCH_ROLE; import static org.opensearch.security.grpc.GrpcHelpers.GRPC_SEARCH_USER; +import static org.opensearch.security.grpc.GrpcHelpers.SECURITY_WITH_GRPC_PLUGIN; import static org.opensearch.security.grpc.GrpcHelpers.SINGLE_NODE_SECURE_AUTH_GRPC_TRANSPORT_SETTINGS; import static org.opensearch.security.grpc.GrpcHelpers.TEST_CERTIFICATES; import static org.opensearch.security.grpc.GrpcHelpers.createHeaderInterceptor; @@ -136,34 +132,7 @@ private String createWrongClaimsJwtToken(String username, String... roles) { Arrays.asList("grpc_search_user", "grpc_user") ) ) - .plugin( - // Add GrpcPlugin - new PluginInfo( - GrpcPlugin.class.getName(), - "classpath plugin", - "NA", - Version.CURRENT, - "21", - GrpcPlugin.class.getName(), - null, - Collections.emptyList(), - false - ) - ) - .plugin( - // Override the default security plugin with one that declares extension relationship - new PluginInfo( - OpenSearchSecurityPlugin.class.getName(), - "classpath plugin", - "NA", - Version.CURRENT, - "21", - OpenSearchSecurityPlugin.class.getName(), - null, - List.of("org.opensearch.transport.grpc.GrpcPlugin"), // Extends GrpcPlugin - false - ) - ) + .plugin(SECURITY_WITH_GRPC_PLUGIN) .anonymousAuth(false) .users(GRPC_INDEX_USER, GRPC_INDEX_USER_NO_MAPPING, GRPC_SEARCH_USER, GRPC_IMPERSONATING_USER) .roles(GRPC_INDEX_ROLE, GRPC_INDEX_ROLE_NO_MAPPING, GRPC_SEARCH_ROLE) From ca3cbc612247e454cd9d584eb0c9ca9dd1f0e638 Mon Sep 17 00:00:00 2001 From: Ralph Ursprung Date: Tue, 21 Jul 2026 09:58:22 +0200 Subject: [PATCH 2/3] `WhoAmITests`: make test failure more explicit if one of the lists didn't contain any entries it'd just crash. now it causes a proper test failure, which is clearer. Signed-off-by: Ralph Ursprung --- .../java/org/opensearch/security/rest/WhoAmITests.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java b/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java index c0ac25ce38..38e1a3dca9 100644 --- a/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java +++ b/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java @@ -49,6 +49,7 @@ import static org.opensearch.test.framework.audit.AuditMessagePredicate.grantedPrivilege; import static org.opensearch.test.framework.audit.AuditMessagePredicate.privilegePredicateRESTLayer; import static org.opensearch.test.framework.audit.AuditMessagePredicate.userAuthenticatedPredicate; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class WhoAmITests { @@ -202,6 +203,8 @@ private void verifyAuditLogSimilarity(List currentTestAuditMessage transportSet.add(auditMessage); } } + assertFalse(restSet.isEmpty()); + assertFalse(transportSet.isEmpty()); // We pass 1 message from each layer to check for similarity checkForStructuralSimilarity(restSet.get(0), transportSet.get(0)); } From e8f930484aaeb4b0c7377de7bb389772da9b0ad7 Mon Sep 17 00:00:00 2001 From: Ralph Ursprung Date: Wed, 15 Jul 2026 18:41:35 +0200 Subject: [PATCH 3/3] support attributes from request headers in DLS with this it is now possible to specify request headers which should be available as substitutions in DLS queries. both HTTP and gRPC headers are supported. the headers have to be configured under the config key `plugins.security.unsupported.dls.allowed_request_headers`. this is a map (the key doesn't matter) with the following content for each entry: * `name`: the actual name of the gRPC / HTTP header (case-insensitive) * `isMultiValue`: whether the header can have more than one value (default: `false`) * `validationRegex`: a regex to ensure that the header value cannot be used for code injection into the DLS query * `maxValueLength`: the maximum length each header value is allowed to have (default: 256) since DLS query substitution is pure string substitution and headers, unlike other user attributes, are fully under the control of the caller (and thus a potential attacker) the content must be carefully validated to ensure that it does not pose a risk. for this the `validationRegex` needs to be used - by default it rejects all content, thus it must be configured explicitly. it should be configured to only allow explicitly the patterns which are absolutely needed. due to the risk associated with this feature it is currently being treated as unsupported/experimental and will also not be documented. if you, dear reader, stumble upon this PR / commit please beware: only use this is if you are absolutely sure that you know what you are doing! you have been warned! the substitution is done using `${attr.header.[header name]}`, e.g. `${attr.header.x-example-header}`. if `isMultiValue` is set to `true` then the substitution will always contain quotes around the values and has to be treated as a list, i.e. you should enclose it in `[]`: ``` { "terms": { "testfield": [${attr.header.x-example-header-mv}] } } ``` while if `isMultiValue` is set to `false` then the value will be added verbatim and you need to quote it: ``` { "term": { "testfield": "${attr.header.x-example-header}" } } ``` to prevent the risk of DOS attacks through the header forwarding both a limit on the length of header values has been introduced (configurable per header, see `maxValueLength`; default: 256 characters) as well as a global limit on the amount of headers (see `DlsRequestHeadersUtil#MAX_HEADER_COUNT`; arbitrarily set to 256) has been introduced. the config options can only be set via the config file (they are intentionally not marked as `Dynamic`) so that it has to be a clear decision to set this. this restriction can be lifted at a later point. once #6311 is implemented the risk posed by this feature will go down since then it will no longer be possible to modify the query with a crafted request (which this feature tries to prevent by having the admin specify a regex for the validation). resolves #6265 Signed-off-by: Ralph Ursprung --- .../HeaderAttrInDlsIntegrationTest.java | 198 +++++++++++++++++ .../opensearch/security/grpc/GrpcHelpers.java | 6 +- ...rdsMultitenancySystemIndexHandlerTest.java | 14 +- .../dlsfls/DocumentPrivilegesTest.java | 9 +- ...MockPrivilegeEvaluationContextBuilder.java | 4 +- .../security/OpenSearchSecurityPlugin.java | 5 + .../security/filter/SecurityGrpcFilter.java | 10 +- .../security/filter/SecurityRestFilter.java | 3 + .../PrivilegesEvaluationContext.java | 21 +- .../security/privileges/UserAttributes.java | 6 +- .../legacy/PrivilegesEvaluatorImpl.java | 4 +- .../nextgen/PrivilegesEvaluatorImpl.java | 5 +- .../dlsfls/DlsRequestHeadersUtil.java | 208 ++++++++++++++++++ .../security/support/ConfigConstants.java | 4 + .../transport/SecurityInterceptor.java | 5 + .../transport/SecurityRequestHandler.java | 16 ++ .../RestLayerPrivilegesEvaluatorTest.java | 4 +- .../privileges/UserAttributesUnitTest.java | 3 +- .../SystemIndexAccessEvaluatorTest.java | 3 +- 19 files changed, 507 insertions(+), 21 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/dlsfls/HeaderAttrInDlsIntegrationTest.java create mode 100644 src/main/java/org/opensearch/security/privileges/dlsfls/DlsRequestHeadersUtil.java diff --git a/src/integrationTest/java/org/opensearch/security/dlsfls/HeaderAttrInDlsIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/dlsfls/HeaderAttrInDlsIntegrationTest.java new file mode 100644 index 0000000000..8fd17f0a42 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/dlsfls/HeaderAttrInDlsIntegrationTest.java @@ -0,0 +1,198 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlsfls; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Stream; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import org.opensearch.security.grpc.GrpcHelpers; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.opensearch.security.grpc.GrpcHelpers.SECURITY_WITH_GRPC_PLUGIN; +import static org.opensearch.security.grpc.GrpcHelpers.createChannelWithBasicAuthorization; +import static org.opensearch.security.grpc.GrpcHelpers.createHeaderInterceptor; +import static org.opensearch.security.grpc.GrpcHelpers.getSecureGrpcEndpoint; +import static org.opensearch.security.grpc.GrpcHelpers.secureChannel; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +public class HeaderAttrInDlsIntegrationTest { + private static final String DLS_INDEX = "testindex"; + + private static final String DLS_INDEX_SETTINGS = """ + { + "mappings": { + "properties": { + "testfield": { + "type": "text" + } + } + } + }"""; + private static final String SINGLE_VALUE_DLS_QUERY = "{ \"term\": { \"testfield\": \"${attr.header.x-example-header}\" } }"; + private static final String MULTI_VALUE_DLS_QUERY = "{ \"terms\": { \"testfield\": [${attr.header.x-example-header-mv}] } }"; + private static final String SHORT_VALUE_DLS_QUERY = "{ \"term\": { \"testfield\": \"${attr.header.x-short-header}\" } }"; + + static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + static final TestSecurityConfig.User SINGLE_VALUE_DLS_USER = new TestSecurityConfig.User("sv_dls_user").roles( + new TestSecurityConfig.Role("sv_dls_role").clusterPermissions("*").indexPermissions("*").dls(SINGLE_VALUE_DLS_QUERY).on(DLS_INDEX) + ); + + static final TestSecurityConfig.User MULTI_VALUE_DLS_USER = new TestSecurityConfig.User("mv_dls_user").roles( + new TestSecurityConfig.Role("mv_dls_role").clusterPermissions("*").indexPermissions("*").dls(MULTI_VALUE_DLS_QUERY).on(DLS_INDEX) + ); + + static final TestSecurityConfig.User SHORT_VALUE_DLS_USER = new TestSecurityConfig.User("short_dls_user").roles( + new TestSecurityConfig.Role("short_dls_role").clusterPermissions("*").indexPermissions("*").dls(SHORT_VALUE_DLS_QUERY).on(DLS_INDEX) + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.DEFAULT) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER, SINGLE_VALUE_DLS_USER, MULTI_VALUE_DLS_USER, SHORT_VALUE_DLS_USER) + .nodeSetting("plugins.security.unsupported.dls.allowed_request_headers.x-example-header.name", "X-Example-Header") + .nodeSetting("plugins.security.unsupported.dls.allowed_request_headers.x-example-header.isMultiValue", "false") + .nodeSetting("plugins.security.unsupported.dls.allowed_request_headers.x-example-header.validationRegex", "[a-z\\-]+") + .nodeSetting("plugins.security.unsupported.dls.allowed_request_headers.x-example-header-mv.name", "X-Example-Header-MV") + .nodeSetting("plugins.security.unsupported.dls.allowed_request_headers.x-example-header-mv.isMultiValue", "true") + .nodeSetting("plugins.security.unsupported.dls.allowed_request_headers.x-example-header-mv.validationRegex", "[a-z\\-]+") + .nodeSetting("plugins.security.unsupported.dls.allowed_request_headers.x-invalid-header.name", "X-Invalid-Header") + .nodeSetting("plugins.security.unsupported.dls.allowed_request_headers.x-short-header.name", "X-Short-Header") + .nodeSetting("plugins.security.unsupported.dls.allowed_request_headers.x-short-header.validationRegex", "[a-z\\-]+") + .nodeSetting("plugins.security.unsupported.dls.allowed_request_headers.x-short-header.maxValueLength", "3") + .nodeSettings(GrpcHelpers.SINGLE_NODE_SECURE_AUTH_GRPC_TRANSPORT_SETTINGS) + .nodeSettings(GrpcHelpers.CLIENT_AUTH_REQUIRE) + .plugin(SECURITY_WITH_GRPC_PLUGIN) + .build(); + + @BeforeClass + public static void createTestData() { + try (final var client = cluster.getRestClient(ADMIN_USER)) { + client.putJson(DLS_INDEX, DLS_INDEX_SETTINGS); + client.postJson(DLS_INDEX + "/_doc?refresh=true", "{\"testfield\": \"foobar\"}"); + client.postJson(DLS_INDEX + "/_doc?refresh=true", "{\"testfield\": \"foo\"}"); + client.postJson(DLS_INDEX + "/_doc?refresh=true", "{\"testfield\": \"baz\"}"); + } + } + + @Test + public void testQueryWithSingleValueHeaderMatches() { + assertThat(runSearchAndGetTotalHitsForSingleValueHeader("foobar"), is(1)); + assertThat(runSearchAndGetTotalHitsForSingleValueHeader("does-not-exist"), is(0)); + } + + @Test + public void testQueryWithMultiValueHeaderMatches() { + assertThat(runSearchAndGetTotalHitsForMultiValueHeader("foobar"), is(1)); + assertThat(runSearchAndGetTotalHitsForMultiValueHeader("does-not-exist"), is(0)); + assertThat(runSearchAndGetTotalHitsForMultiValueHeader("foobar", "foo"), is(2)); + } + + @Test + public void testFailureOnValueNotMatchingRegex() { + try (final var client = cluster.getRestClient(SINGLE_VALUE_DLS_USER)) { + final var response = client.get(DLS_INDEX + "/_search", new BasicHeader("X-Example-Header", "UPPERCASE-IS-INVALID")); + assertThat(response.getStatusCode(), is(HttpStatus.SC_BAD_REQUEST)); + } + } + + @Test + public void testFailureOnUndefinedRegex() { + try (final var client = cluster.getRestClient(SINGLE_VALUE_DLS_USER)) { + final var response = client.get(DLS_INDEX + "/_search", new BasicHeader("X-Invalid-Header", "nothing matches here")); + assertThat(response.getStatusCode(), is(HttpStatus.SC_BAD_REQUEST)); + } + } + + @Test + public void testLengthLimit() { + // short works + runSearchAndGetTotalHits(SHORT_VALUE_DLS_USER, new BasicHeader("X-Short-Header", "foo")); + // exceeding the limit fails it + try (final var client = cluster.getRestClient(SHORT_VALUE_DLS_USER)) { + final var response = client.get(DLS_INDEX + "/_search", new BasicHeader("X-Short-Header", "foobar")); + assertThat(response.getStatusCode(), is(HttpStatus.SC_BAD_REQUEST)); + } + } + + @Test + public void testFailureOnMultipleValuesOnSingleValueConfig() { + try (final var client = cluster.getRestClient(SINGLE_VALUE_DLS_USER)) { + final var response = client.get( + DLS_INDEX + "/_search", + new BasicHeader("X-Example-Header", "a"), + new BasicHeader("X-Example-Header", "b") + ); + assertThat(response.getStatusCode(), is(HttpStatus.SC_BAD_REQUEST)); + } + } + + @Test + public void testFailureOnTooManyHeadersOverall() { + try (final var client = cluster.getRestClient(MULTI_VALUE_DLS_USER)) { + final var headers = Stream.iterate(new BasicHeader("X-Example-Header-MV", "a"), h -> h).limit(257).toArray(Header[]::new); + final var response = client.get(DLS_INDEX + "/_search", headers); + assertThat(response.getStatusCode(), is(HttpStatus.SC_BAD_REQUEST)); + } + } + + @Test + public void testGrpcChannel() throws Exception { + final var channel = secureChannel(getSecureGrpcEndpoint(cluster)); + try { + final var channelWithAuth = createChannelWithBasicAuthorization( + channel, + SINGLE_VALUE_DLS_USER.getName(), + SINGLE_VALUE_DLS_USER.getPassword() + ); + final var authInterceptor = createHeaderInterceptor(Map.of("X-Example-Header", "foobar")); + final var channelWithHeader = io.grpc.ClientInterceptors.intercept(channelWithAuth, authInterceptor); + + final var searchResp = GrpcHelpers.doMatchAll(channelWithHeader, DLS_INDEX, 10); + assertThat(searchResp, notNullValue()); + assertThat(searchResp.getHits().getTotal().getTotalHits().getValue(), is(1L)); + } finally { + channel.shutdown(); + } + } + + int runSearchAndGetTotalHitsForSingleValueHeader(final String headerValue) { + return runSearchAndGetTotalHits(SINGLE_VALUE_DLS_USER, new BasicHeader("X-Example-Header", headerValue)); + } + + int runSearchAndGetTotalHitsForMultiValueHeader(final String... headerValues) { + final var headers = Arrays.stream(headerValues).map(s -> new BasicHeader("X-Example-Header-MV", s)).toArray(Header[]::new); + return runSearchAndGetTotalHits(MULTI_VALUE_DLS_USER, headers); + } + + int runSearchAndGetTotalHits(final TestSecurityConfig.User user, final Header... headers) { + try (final var client = cluster.getRestClient(user)) { + final var response = client.get(DLS_INDEX + "/_search", headers); + assertThat(response.getStatusCode(), is(HttpStatus.SC_OK)); + return response.getIntFromJsonBody("/hits/total/value"); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/grpc/GrpcHelpers.java b/src/integrationTest/java/org/opensearch/security/grpc/GrpcHelpers.java index b1b2ee7d3d..dd38c4e9fa 100644 --- a/src/integrationTest/java/org/opensearch/security/grpc/GrpcHelpers.java +++ b/src/integrationTest/java/org/opensearch/security/grpc/GrpcHelpers.java @@ -157,17 +157,17 @@ public static Channel createChannelWithBasicAuthorization(final ManagedChannel c return createChannelWithAuthorization(channel, createBasicAuthHeader(username, password)); } - protected static final Map CLIENT_AUTH_NONE = Map.of( + public static final Map CLIENT_AUTH_NONE = Map.of( "plugins.security.ssl.aux.secure-transport-grpc.clientauth_mode", "NONE" ); - protected static final Map CLIENT_AUTH_OPT = Map.of( + public static final Map CLIENT_AUTH_OPT = Map.of( "plugins.security.ssl.aux.secure-transport-grpc.clientauth_mode", "OPTIONAL" ); - protected static final Map CLIENT_AUTH_REQUIRE = Map.of( + public static final Map CLIENT_AUTH_REQUIRE = Map.of( "plugins.security.ssl.aux.secure-transport-grpc.clientauth_mode", "REQUIRE" ); diff --git a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/nextgen/DashboardsMultitenancySystemIndexHandlerTest.java b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/nextgen/DashboardsMultitenancySystemIndexHandlerTest.java index 517d72bd33..49f163765a 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/nextgen/DashboardsMultitenancySystemIndexHandlerTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/nextgen/DashboardsMultitenancySystemIndexHandlerTest.java @@ -10,6 +10,8 @@ */ package org.opensearch.security.privileges.actionlevel.nextgen; +import java.util.List; + import com.google.common.collect.ImmutableSet; import org.junit.Test; @@ -78,7 +80,8 @@ public void handle_multitenancyDisabled() { resolver, new IndicesRequestResolver(resolver), () -> clusterState, - ActionPrivileges.EMPTY + ActionPrivileges.EMPTY, + List.of() ); assertNull(subject.handle(searchRequest, "indices:data/read/search", user, ctx)); @@ -118,7 +121,8 @@ public void handle_privateTenantDisabled() { resolver, new IndicesRequestResolver(resolver), () -> clusterState, - ActionPrivileges.EMPTY + ActionPrivileges.EMPTY, + List.of() ); PrivilegesEvaluatorResponse result = subject.handle(searchRequest, "indices:data/read/search", user, ctx); @@ -158,7 +162,8 @@ public void handle_dashboardsServerUser() { resolver, new IndicesRequestResolver(resolver), () -> clusterState, - ActionPrivileges.EMPTY + ActionPrivileges.EMPTY, + List.of() ); PrivilegesEvaluatorResponse result = subject.handle(searchRequest, "indices:data/read/search", user, ctx); @@ -222,7 +227,8 @@ public void handle_unsupportedRequest() { resolver, new IndicesRequestResolver(resolver), () -> clusterState, - ActionPrivileges.EMPTY + ActionPrivileges.EMPTY, + List.of() ); PrivilegesEvaluatorResponse result = subject.handle(unsupportedRequest, "indices:data/read/search", user, ctx); diff --git a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java index 7497c3f796..11a0c5f7ee 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/dlsfls/DocumentPrivilegesTest.java @@ -541,7 +541,8 @@ public IndicesAndAliases_getRestriction( null, null, () -> CLUSTER_STATE, - ActionPrivileges.EMPTY + ActionPrivileges.EMPTY, + List.of() ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -844,7 +845,8 @@ public IndicesAndAliases_isUnrestricted( INDEX_NAME_EXPRESSION_RESOLVER, null, () -> CLUSTER_STATE, - ActionPrivileges.EMPTY + ActionPrivileges.EMPTY, + List.of() ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; @@ -1146,7 +1148,8 @@ public DataStreams_getRestriction( null, () -> CLUSTER_STATE, - ActionPrivileges.EMPTY + ActionPrivileges.EMPTY, + List.of() ); this.statefulness = statefulness; this.dfmEmptyOverridesAll = dfmEmptyOverridesAll == DfmEmptyOverridesAll.DFM_EMPTY_OVERRIDES_ALL_TRUE; diff --git a/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java b/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java index 0e3816ba1d..6fcfb61e83 100644 --- a/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java +++ b/src/integrationTest/java/org/opensearch/security/util/MockPrivilegeEvaluationContextBuilder.java @@ -14,6 +14,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -100,7 +101,8 @@ public PrivilegesEvaluationContext get() { indexNameExpressionResolver, new IndicesRequestResolver(indexNameExpressionResolver), () -> clusterState, - this.actionPrivileges + this.actionPrivileges, + List.of() ); } } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 28761a4666..aa716c2846 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -2628,6 +2628,11 @@ public List> getSettings() { settings.add(SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING); settings.add(SecuritySettings.DLS_WRITE_BLOCKED); + + settings.add(Setting.groupSetting(ConfigConstants.OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS_CONFIG + ".", Property.NodeScope + // do not make this Property.Dynamic - as a security measure, + // this can only be changed with access to the config file. + )); } return settings; diff --git a/src/main/java/org/opensearch/security/filter/SecurityGrpcFilter.java b/src/main/java/org/opensearch/security/filter/SecurityGrpcFilter.java index 3bdebbae21..d75667a1b2 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityGrpcFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityGrpcFilter.java @@ -21,6 +21,7 @@ import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auth.BackendRegistry; +import org.opensearch.security.privileges.dlsfls.DlsRequestHeadersUtil; import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin; import org.opensearch.security.ssl.util.SSLRequestHelper; import org.opensearch.security.support.ConfigConstants; @@ -77,7 +78,8 @@ public ServerInterceptor getInterceptor() { return new AuthNGrpcInterceptor( threadContext, OpenSearchSecurityPlugin.GuiceHolder.getBackendRegistry(), - OpenSearchSecurityPlugin.GuiceHolder.getAuditLog() + OpenSearchSecurityPlugin.GuiceHolder.getAuditLog(), + settings ); } }); @@ -92,11 +94,13 @@ private static class AuthNGrpcInterceptor implements ServerInterceptor { private final ThreadContext threadContext; private final BackendRegistry backendRegistry; private final AuditLog auditLog; + private final Settings settings; - public AuthNGrpcInterceptor(ThreadContext threadContext, BackendRegistry backendRegistry, AuditLog auditLog) { + public AuthNGrpcInterceptor(ThreadContext threadContext, BackendRegistry backendRegistry, AuditLog auditLog, Settings settings) { this.threadContext = threadContext; this.backendRegistry = backendRegistry; this.auditLog = auditLog; + this.settings = settings; } @Override @@ -174,6 +178,8 @@ private ServerCall.Listener handleCall( auditLog.logSucceededLogin(user.getName(), false, null, requestChannel); } + DlsRequestHeadersUtil.extractAndStoreDlsRequestHeaders(requestChannel, threadContext, settings); + // Caller was authorized - Proceed with request return serverCallHandler.startCall(serverCall, metadata); } catch (Exception e) { diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index 210d3f20c5..0fde11edad 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -62,6 +62,7 @@ import org.opensearch.security.dlic.rest.api.AllowlistApiAction; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; +import org.opensearch.security.privileges.dlsfls.DlsRequestHeadersUtil; import org.opensearch.security.securityconf.impl.AllowlistingSettings; import org.opensearch.security.ssl.http.netty.Netty4HttpRequestHeaderVerifier; import org.opensearch.security.ssl.transport.PrincipalExtractor; @@ -182,6 +183,8 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c // for audit logging final SecurityRequestChannel filteredRequestChannel = SecurityRequestFactory.from(filteredRequest, channel); + DlsRequestHeadersUtil.extractAndStoreDlsRequestHeaders(requestChannel, threadContext, settings); + // Authenticate request if (!NettyAttribute.popFrom(request, Netty4HttpRequestHeaderVerifier.IS_AUTHENTICATED).orElse(false)) { // we aren't authenticated so we should skip this step diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index 92f9205e6a..da0f57f9b9 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -11,9 +11,11 @@ package org.opensearch.security.privileges; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; +import java.util.stream.Collectors; import com.google.common.collect.ImmutableSet; @@ -23,6 +25,7 @@ import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.metadata.OptionallyResolvedIndices; +import org.opensearch.security.privileges.dlsfls.DlsRequestHeadersUtil; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; @@ -72,6 +75,11 @@ public class PrivilegesEvaluationContext { private final ActionRequestMetadata actionRequestMetadata; + /** + * allowlisted headers can be accessed in DLS queries. + */ + private final List headersForDls; + public PrivilegesEvaluationContext( User user, ImmutableSet mappedRoles, @@ -82,7 +90,8 @@ public PrivilegesEvaluationContext( IndexNameExpressionResolver indexNameExpressionResolver, IndicesRequestResolver indicesRequestResolver, Supplier clusterStateSupplier, - ActionPrivileges actionPrivileges + ActionPrivileges actionPrivileges, + List headersForDls ) { this.user = user; this.mappedRoles = mappedRoles; @@ -94,6 +103,7 @@ public PrivilegesEvaluationContext( this.task = task; this.actionRequestMetadata = actionRequestMetadata; this.actionPrivileges = actionPrivileges; + this.headersForDls = (headersForDls == null) ? List.of() : headersForDls; } public User getUser() { @@ -179,6 +189,10 @@ public ActionPrivileges getActionPrivileges() { @Override public String toString() { + final var headersForDlsOutput = headersForDls.isEmpty() + ? "" + : ", headersForDls[keys]=" + + headersForDls.stream().map(DlsRequestHeadersUtil.DlsRequestHeader::name).collect(Collectors.joining(", ")); return "PrivilegesEvaluationContext{" + "user=" + user @@ -191,10 +205,15 @@ public String toString() { + resolvedIndices + ", mappedRoles=" + mappedRoles + + headersForDlsOutput + '}'; } public ConcurrentHashMap hasFlsOrFieldMaskingCache() { return hasFlsOrFieldMaskingCache; } + + public List getHeadersForDls() { + return headersForDls; + } } diff --git a/src/main/java/org/opensearch/security/privileges/UserAttributes.java b/src/main/java/org/opensearch/security/privileges/UserAttributes.java index f8cf02f898..15b951f61a 100644 --- a/src/main/java/org/opensearch/security/privileges/UserAttributes.java +++ b/src/main/java/org/opensearch/security/privileges/UserAttributes.java @@ -12,7 +12,6 @@ import java.util.HashMap; import java.util.List; -import java.util.Set; import java.util.regex.Pattern; import com.google.common.base.Joiner; @@ -52,6 +51,9 @@ public static String replaceProperties(String orig, PrivilegesEvaluationContext ); replacementsWithDots.putAll(user.getCustomAttributesMap()); + context.getHeadersForDls() + .forEach(header -> replacementsWithDots.put("attr.header." + header.name().toLowerCase(), header.serialize())); + // we also support referencing variables with underscores instead of dots => we need both in our lookup table. final var replacements = new HashMap<>(replacementsWithDots); replacementsWithDots.forEach((k, v) -> replacements.put(k.replace(".", "_"), v)); @@ -61,7 +63,7 @@ public static String replaceProperties(String orig, PrivilegesEvaluationContext return orig; } - private static String toQuotedCommaSeparatedString(final Set roles) { + private static String toQuotedCommaSeparatedString(final Iterable roles) { return Joiner.on(',').join(Iterables.transform(roles, s -> { return new StringBuilder(s.length() + 2).append('"').append(s).append('"').toString(); })); diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorImpl.java b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorImpl.java index f6004924a7..001c262d7c 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorImpl.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/legacy/PrivilegesEvaluatorImpl.java @@ -104,6 +104,7 @@ import org.opensearch.threadpool.ThreadPool; import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; +import static org.opensearch.security.support.ConfigConstants.OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS; /** * The current implementation for action privilege evaluation. @@ -303,7 +304,8 @@ public PrivilegesEvaluationContext createContext( resolver, indicesRequestResolver, clusterStateSupplier, - actionPrivileges + actionPrivileges, + threadContext.getTransient(OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS) ); } diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/PrivilegesEvaluatorImpl.java b/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/PrivilegesEvaluatorImpl.java index 3676500f99..ddce072aac 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/PrivilegesEvaluatorImpl.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/nextgen/PrivilegesEvaluatorImpl.java @@ -73,6 +73,8 @@ import org.opensearch.tasks.Task; import org.opensearch.threadpool.ThreadPool; +import static org.opensearch.security.support.ConfigConstants.OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS; + /** * A next generation implementation of PrivilegesEvaluator with the following properties: *
    @@ -240,7 +242,8 @@ public PrivilegesEvaluationContext createContext( indexNameExpressionResolver, indicesRequestResolver, clusterStateSupplier, - actionPrivileges + actionPrivileges, + threadContext.getTransient(OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS) ); } diff --git a/src/main/java/org/opensearch/security/privileges/dlsfls/DlsRequestHeadersUtil.java b/src/main/java/org/opensearch/security/privileges/dlsfls/DlsRequestHeadersUtil.java new file mode 100644 index 0000000000..f9d2fa4b3e --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/dlsfls/DlsRequestHeadersUtil.java @@ -0,0 +1,208 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.privileges.dlsfls; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.filter.SecurityRequestChannel; + +import tools.jackson.core.type.TypeReference; + +import static java.util.function.Function.identity; +import static org.opensearch.security.support.ConfigConstants.OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS; +import static org.opensearch.security.support.ConfigConstants.OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS_CONFIG; + +/** + * Utilities to handle request headers which are made available as attributes for substitution in DLS queries. + */ +public class DlsRequestHeadersUtil { + private static final Logger log = LogManager.getLogger(DlsRequestHeadersUtil.class); + + private DlsRequestHeadersUtil() {} + + /** + * Arbitrary upper limit on the amount of supported headers to prevent a DOS attack. + * Since the headers are forwarded to requests to other shards sending a too big amount of headers could + * negatively impact other shards. + */ + private static final int MAX_HEADER_COUNT = 256; + + /** + * It is possible to reference HTTP / gRPC headers in DLS queries. To achieve this, they are extracted from the + * request and stored in the thread context in a manner where they will also be available if the request is + * forwarded. + */ + public static void extractAndStoreDlsRequestHeaders( + final SecurityRequestChannel securityRequestChannel, + final ThreadContext threadContext, + final Settings settings + ) { + final var allowedDlsRequestHeaders = getDlsRequestHeaderSettings(settings); + + final Map> rawDlsRequestHeaders = securityRequestChannel.getHeaders() == null + ? Map.of() + : securityRequestChannel.getHeaders() + .entrySet() + .stream() + .filter(e -> allowedDlsRequestHeaders.keySet().stream().anyMatch(a -> a.equalsIgnoreCase(e.getKey()))) + .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); + + final var dlsRequestHeaders = rawDlsRequestHeaders.entrySet() + .stream() + .map(e -> toDlsRequestHeader(allowedDlsRequestHeaders.get(e.getKey().toLowerCase()), e.getKey(), e.getValue())) + .toList(); + + final var totalHeaderCount = rawDlsRequestHeaders.values().stream().mapToInt(List::size).sum(); + + if (totalHeaderCount > MAX_HEADER_COUNT) { + throw new IllegalArgumentException( + String.format( + "found %d headers for DLS variables which exceeds the global maximum of %d", + totalHeaderCount, + MAX_HEADER_COUNT + ) + ); + } + + log.debug( + "found {} headers with a total of {} values which matched one of the {} allowlisted headers for DLS variables: {}", + dlsRequestHeaders::size, + () -> totalHeaderCount, + allowedDlsRequestHeaders::size, + rawDlsRequestHeaders::keySet + ); + log.trace("allowlisted headers: {}", () -> allowedDlsRequestHeaders); + + // store it transiently for usage in this thread + threadContext.putTransient(OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS, dlsRequestHeaders); + + // and as a header so that it is passed on to other threads / instances, where it will be restored into a transient entry + // See SecurityRequestHandler#messageReceivedDecorate + if (!dlsRequestHeaders.isEmpty()) { + final var serializedDlsRequestHeaders = DefaultObjectMapper.objectMapper().writerFor(new TypeReference>() { + }).writeValueAsString(dlsRequestHeaders); + threadContext.putHeader(OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS, serializedDlsRequestHeaders); + } + } + + /** + * Validates the request header and, if it is valid, turns it into a {@link DlsRequestHeader}. + * @throws IllegalArgumentException if the header value does not match the configured regex or has more than + * one value if it is flagged as single-value + */ + private static DlsRequestHeader toDlsRequestHeader( + final DlsRequestHeaderSettings dlsRequestHeaderSettings, + final String headerName, + final List headerValues + ) { + for (final var headerValue : headerValues) { + if (!dlsRequestHeaderSettings.validationPattern().matcher(headerValue).matches()) { + throw new IllegalArgumentException( + String.format("header %s does not match the specified pattern and has thus been rejected!", headerName) + ); + } + if (headerValue.length() > dlsRequestHeaderSettings.maxValueLength()) { + throw new IllegalArgumentException( + String.format( + "header %s exceeds the maximum defined length! (%d > %d)", + headerName, + headerValue.length(), + dlsRequestHeaderSettings.maxValueLength() + ) + ); + } + } + + if (dlsRequestHeaderSettings.isMultiValue()) { + return new MultiValueDlsRequestHeader(headerName, headerValues); + } else { + if (headerValues.size() != 1) { + throw new IllegalArgumentException( + String.format( + "Received %d entries for header %s, but expected it to be a single value!", + headerValues.size(), + headerName + ) + ); + } + return new SingleValueDlsRequestHeader(headerName, headerValues.getFirst()); + } + } + + public static Map getDlsRequestHeaderSettings(final Settings settings) { + final var allowedDlsRequestHeaderSettings = settings.getGroups(OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS_CONFIG); + return allowedDlsRequestHeaderSettings.values() + .stream() + .map( + s -> new DlsRequestHeaderSettings( + s.get("name"), + s.getAsBoolean("isMultiValue", false), + // by default only match empty values => regex *must* be specified by admin + Pattern.compile(s.get("validationRegex", "^$")), + s.getAsInt("maxValueLength", 256) + ) + ) + .collect(Collectors.toUnmodifiableMap(e -> e.name.toLowerCase(), identity())); + } + + /** + * Represents the settings for a request header as specified in the config. + */ + public record DlsRequestHeaderSettings(String name, boolean isMultiValue, Pattern validationPattern, int maxValueLength) { + } + + /** + * Represents an actual request header which matched one of the configured DLS request headers. + * Jackson serialisation is used for the transport between different nodes. + */ + @JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME, include = JsonTypeInfo.As.PROPERTY, property = "class") + @JsonSubTypes({ + @JsonSubTypes.Type(value = SingleValueDlsRequestHeader.class), + @JsonSubTypes.Type(value = MultiValueDlsRequestHeader.class), }) + public interface DlsRequestHeader { + /** + * @return the name of the request header. + */ + String name(); + + /** + * @return the header value(s) formatted for usage in a DLS query. + */ + String serialize(); + } + + public record SingleValueDlsRequestHeader(String name, String value) implements DlsRequestHeader, Serializable { + @Override + public String serialize() { + return this.value; + } + } + + public record MultiValueDlsRequestHeader(String name, List values) implements DlsRequestHeader, Serializable { + @Override + public String serialize() { + return values.parallelStream().map(s -> "\"" + s + "\"").collect(Collectors.joining(",")); + } + } +} diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 8ef4db809f..54622d8922 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -379,6 +379,10 @@ public class ConfigConstants { public static final String SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES = SECURITY_SETTINGS_PREFIX + "unsupported.load_static_resources"; public static final String SECURITY_UNSUPPORTED_ACCEPT_INVALID_CONFIG = SECURITY_SETTINGS_PREFIX + "unsupported.accept_invalid_config"; + public static final String OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS_CONFIG = SECURITY_SETTINGS_PREFIX + + "unsupported.dls.allowed_request_headers"; + public static final String OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS = OPENSEARCH_SECURITY_CONFIG_PREFIX + "dls_request_headers"; + public static final String SECURITY_PROTECTED_INDICES_ENABLED_KEY = SECURITY_SETTINGS_PREFIX + "protected_indices.enabled"; public static final Boolean SECURITY_PROTECTED_INDICES_ENABLED_DEFAULT = false; public static final String SECURITY_PROTECTED_INDICES_KEY = SECURITY_SETTINGS_PREFIX + "protected_indices.indices"; diff --git a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java index 904f3c8208..0f0773b9a3 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java +++ b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java @@ -37,6 +37,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import com.google.common.base.Strings; import com.google.common.collect.Maps; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -178,6 +179,10 @@ public void sendRequestDecorate( requestHeadersToCopy.removeAll(Task.REQUEST_HEADERS); // Special case where this header is preserved during stashContext. } + if (!Strings.isNullOrEmpty(getThreadContext().getHeader(ConfigConstants.OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS))) { + requestHeadersToCopy.add(ConfigConstants.OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS); + } + final Supplier restorableContextSupplier = getThreadContext().newRestorableContext(true); try (ThreadContext.StoredContext stashedContext = getThreadContext().stashContext()) { final TransportResponseHandler restoringHandler = new RestoringTransportResponseHandler( diff --git a/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java b/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java index be24ea3d70..8b089dfa7a 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java +++ b/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java @@ -28,6 +28,7 @@ import java.net.InetSocketAddress; import java.security.cert.X509Certificate; +import java.util.List; import java.util.UUID; import java.util.stream.Collectors; @@ -42,10 +43,12 @@ import org.opensearch.core.common.transport.TransportAddress; import org.opensearch.extensions.ExtensionsManager; import org.opensearch.search.internal.ShardSearchRequest; +import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auditlog.AuditLog.Origin; import org.opensearch.security.auth.UserSubjectImpl; +import org.opensearch.security.privileges.dlsfls.DlsRequestHeadersUtil; import org.opensearch.security.ssl.SslExceptionHandler; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.transport.SSLConfig; @@ -63,6 +66,8 @@ import org.opensearch.transport.TransportRequest; import org.opensearch.transport.TransportRequestHandler; +import tools.jackson.core.type.TypeReference; + import static org.opensearch.security.OpenSearchSecurityPlugin.isActionTraceEnabled; public class SecurityRequestHandler extends SecuritySSLRequestHandler { @@ -120,6 +125,17 @@ protected void messageReceivedDecorate( getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, originHeader); } + // restore headers used for DLS + final var dlsRequestHeadersAsString = getThreadContext().getHeader(ConfigConstants.OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS); + if (!Strings.isNullOrEmpty(dlsRequestHeadersAsString)) { + final List dlsRequestHeaders = DefaultObjectMapper.readValue( + dlsRequestHeadersAsString, + new TypeReference<>() { + } + ); + getThreadContext().putTransient(ConfigConstants.OPENSEARCH_SECURITY_DLS_REQUEST_HEADERS, dlsRequestHeaders); + } + try { if (transportChannel.getChannelType() == null) { diff --git a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index d472e1dab1..bdf202c53d 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -11,6 +11,7 @@ package org.opensearch.security.privileges; +import java.util.List; import java.util.Set; import java.util.function.Supplier; @@ -163,7 +164,8 @@ public PrivilegesEvaluationContext createContext( null, null, null, - actionPrivileges + actionPrivileges, + List.of() ); } diff --git a/src/test/java/org/opensearch/security/privileges/UserAttributesUnitTest.java b/src/test/java/org/opensearch/security/privileges/UserAttributesUnitTest.java index 5097cba679..fee4c734fb 100644 --- a/src/test/java/org/opensearch/security/privileges/UserAttributesUnitTest.java +++ b/src/test/java/org/opensearch/security/privileges/UserAttributesUnitTest.java @@ -48,7 +48,8 @@ public void testReplaceProperties() { null, null, null, - null + null, + List.of() ); String stringWithPlaceholders = """ diff --git a/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluatorTest.java index bc4b1baa89..0cf0b5d39c 100644 --- a/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/actionlevel/legacy/SystemIndexAccessEvaluatorTest.java @@ -182,7 +182,8 @@ PrivilegesEvaluationContext ctx(String action) { indexNameExpressionResolver, new IndicesRequestResolver(indexNameExpressionResolver), () -> clusterState, - actionPrivileges + actionPrivileges, + List.of() ); }