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/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..dd38c4e9fa 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,17 +139,35 @@ public void request(int numMessages) { }; } - protected static final Map CLIENT_AUTH_NONE = Map.of( + /** + * 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)); + } + + 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/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) 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/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)); } 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() ); }