From 2cb891d7fb74aa649dc3767d2742a83da62f6f44 Mon Sep 17 00:00:00 2001 From: Finn Date: Thu, 29 Jan 2026 16:51:27 -0800 Subject: [PATCH 1/5] [Backport 3.5] Make gRPC JWT header keys case insensitive (#5929) (#5930) Signed-off-by: Finn Carroll --- CHANGELOG.md | 2 +- .../security/grpc/GrpcAnonymousAuthTest.java | 2 +- .../grpc/JWTGrpcDefaultAuthHeaderTest.java | 144 ++++++++++++++++++ .../security/auth/BackendRegistry.java | 17 ++- .../security/filter/GrpcRequestChannel.java | 8 +- .../filter/GrpcRequestChannelTest.java | 28 ++++ 6 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcDefaultAuthHeaderTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 67c5b7c5f8..82b573df25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Enhancements - Introduce new dynamic setting (plugins.security.dls.write_blocked) to block all writes when restrictions apply ([#5828](https://github.com/opensearch-project/security/pull/5828)) - +- Fix JWT case-sensitive header parsing over gRPC transport ([#5929](https://github.com/opensearch-project/security/issues/5929)) - Support nested JWT claims in role DLS queries ([#5687](https://github.com/opensearch-project/security/issues/5687)) - Support creation of client SSL engine with a given SNI ([#5894](https://github.com/opensearch-project/security/pull/5894)) - Enable audit logging of document contents for DELETE operations ([#5914](https://github.com/opensearch-project/security/pull/5914)) diff --git a/src/integrationTest/java/org/opensearch/security/grpc/GrpcAnonymousAuthTest.java b/src/integrationTest/java/org/opensearch/security/grpc/GrpcAnonymousAuthTest.java index 217868aa90..48121a22a1 100644 --- a/src/integrationTest/java/org/opensearch/security/grpc/GrpcAnonymousAuthTest.java +++ b/src/integrationTest/java/org/opensearch/security/grpc/GrpcAnonymousAuthTest.java @@ -112,7 +112,7 @@ public void testInvalidAuthHeaderRejected() throws Exception { doBulk(channelWithAuth, "test-invalid-auth", 2); fail("Expected authentication failure - invalid auth header"); } catch (StatusRuntimeException e) { - assertEquals(Status.Code.INVALID_ARGUMENT, e.getStatus().getCode()); + assertEquals(Status.Code.UNAUTHENTICATED, e.getStatus().getCode()); } } finally { channel.shutdown(); diff --git a/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcDefaultAuthHeaderTest.java b/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcDefaultAuthHeaderTest.java new file mode 100644 index 0000000000..78e68936d7 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/grpc/JWTGrpcDefaultAuthHeaderTest.java @@ -0,0 +1,144 @@ +/* + * 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.grpc; + +import java.security.KeyPair; +import java.util.Base64; +import java.util.Collections; +import java.util.Date; +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.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; +import io.grpc.ManagedChannel; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +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.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.doBulk; +import static org.opensearch.security.grpc.GrpcHelpers.getSecureGrpcEndpoint; +import static org.opensearch.security.grpc.GrpcHelpers.secureChannel; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +public class JWTGrpcDefaultAuthHeaderTest { + + public static final List CLAIM_USERNAME = List.of("preferred-username"); + public static final List CLAIM_ROLES = List.of("backend-user-roles"); + private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); + private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII); + + private String createValidJwtToken(String username, String... roles) { + Date now = new Date(); + return Jwts.builder() + .claim(CLAIM_USERNAME.get(0), username) + .claim(CLAIM_ROLES.get(0), String.join(",", roles)) + .setIssuer("test-issuer") + .setSubject(username) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + 3600 * 1000)) + .signWith(KEY_PAIR.getPrivate(), SignatureAlgorithm.RS256) + .compact(); + } + + // JWT auth domain with default Authorization header + public static final TestSecurityConfig.AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig.AuthcDomain("jwt", 1, true) + .jwtHttpAuthenticator(new JwtConfigBuilder().signingKey(List.of(PUBLIC_KEY)).subjectKey(CLAIM_USERNAME).rolesKey(CLAIM_ROLES)) + .backend("noop"); + + @ClassRule + 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 + ) + ) + .users(GRPC_INDEX_USER) + .roles(GRPC_INDEX_ROLE) + .rolesMapping(new TestSecurityConfig.RoleMapping(GRPC_INDEX_ROLE.getName()).backendRoles("grpc_index_role")) + .authc(JWT_AUTH_DOMAIN) + .build(); + + @Test + public void testAuthorizationHeaderCaseInsensitive() throws Exception { + String jwtToken = createValidJwtToken("grpc_user", "grpc_index_role"); + ManagedChannel channel = secureChannel(getSecureGrpcEndpoint(cluster)); + + try { + ClientInterceptor authInterceptor = createHeaderInterceptor(Map.of("authorization", "Bearer " + jwtToken)); + Channel channelWithAuth = io.grpc.ClientInterceptors.intercept(channel, authInterceptor); + var bulkResp = doBulk(channelWithAuth, "test-grpc-index-lower", 2); + assertNotNull(bulkResp); + assertFalse(bulkResp.getErrors()); + assertEquals(2, bulkResp.getItemsCount()); + + authInterceptor = createHeaderInterceptor(Map.of("Authorization", "Bearer " + jwtToken)); + channelWithAuth = io.grpc.ClientInterceptors.intercept(channel, authInterceptor); + bulkResp = doBulk(channelWithAuth, "test-grpc-index-standard", 2); + assertNotNull(bulkResp); + assertFalse(bulkResp.getErrors()); + assertEquals(2, bulkResp.getItemsCount()); + + authInterceptor = createHeaderInterceptor(Map.of("AutHoRiZaTION", "Bearer " + jwtToken)); + channelWithAuth = io.grpc.ClientInterceptors.intercept(channel, authInterceptor); + bulkResp = doBulk(channelWithAuth, "test-grpc-index-mixed", 2); + assertNotNull(bulkResp); + assertFalse(bulkResp.getErrors()); + assertEquals(2, bulkResp.getItemsCount()); + } finally { + channel.shutdown(); + } + } +} diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index c63bfea9b1..250911e72c 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -1,9 +1,16 @@ /* - * SPDX-License-Identifier: Apache-2.0 + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ /* @@ -262,7 +269,7 @@ public boolean authenticate(final SecurityRequestChannel request) { */ ThreadContext threadContext = this.threadPool.getThreadContext(); final String sslPrincipal = (String) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL); - if (!gRPC && adminDns.isAdminDN(sslPrincipal)) { + if (adminDns.isAdminDN(sslPrincipal)) { // PKI authenticated REST call User superuser = new User(sslPrincipal); UserSubject subject = new UserSubjectImpl(threadPool, superuser); diff --git a/src/main/java/org/opensearch/security/filter/GrpcRequestChannel.java b/src/main/java/org/opensearch/security/filter/GrpcRequestChannel.java index adb040c2b1..e563d934fc 100644 --- a/src/main/java/org/opensearch/security/filter/GrpcRequestChannel.java +++ b/src/main/java/org/opensearch/security/filter/GrpcRequestChannel.java @@ -14,11 +14,11 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.TreeMap; import javax.net.ssl.SSLEngine; import org.opensearch.rest.RestRequest.Method; @@ -44,8 +44,12 @@ public GrpcRequestChannel(ServerCall serverCall, Metadata metadata) { this.headers = extractHeaders(metadata); } + /** + * @param metadata gRPC metadata object. + * @return case-insensitive header map - excluding binary headers. + */ private Map> extractHeaders(Metadata metadata) { - Map> headerMap = new HashMap<>(); + Map> headerMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); for (String key : metadata.keys()) { if (!key.endsWith("-bin")) { // Skip binary headers String value = metadata.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER)); diff --git a/src/test/java/org/opensearch/security/filter/GrpcRequestChannelTest.java b/src/test/java/org/opensearch/security/filter/GrpcRequestChannelTest.java index c445490671..ca8eb754ee 100644 --- a/src/test/java/org/opensearch/security/filter/GrpcRequestChannelTest.java +++ b/src/test/java/org/opensearch/security/filter/GrpcRequestChannelTest.java @@ -83,6 +83,34 @@ public void testHeaderExtraction() { assertEquals("Bearer jwt456", channel.header("jwt-auth")); } + @Test + public void testHeaderExtractionCaseInsensitive() { + ServerCall serverCall = createMockServerCall("org.opensearch.protobufs.services.DocumentService/Bulk"); + + Metadata metadata = new Metadata(); + Metadata.Key authKey = Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key contentKey = Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key customKey = Metadata.Key.of("x-custom-header", Metadata.ASCII_STRING_MARSHALLER); + metadata.put(authKey, "Bearer mytoken123"); + metadata.put(contentKey, "application/json"); + metadata.put(customKey, "custom-value"); + + GrpcRequestChannel channel = new GrpcRequestChannel(serverCall, metadata); + + assertEquals("Bearer mytoken123", channel.header("authorization")); + assertEquals("Bearer mytoken123", channel.header("Authorization")); + assertEquals("Bearer mytoken123", channel.header("AUTHORIZATION")); + assertEquals("Bearer mytoken123", channel.header("AuThOrIzAtIoN")); + + assertEquals("application/json", channel.header("content-type")); + assertEquals("application/json", channel.header("Content-Type")); + assertEquals("application/json", channel.header("CONTENT-TYPE")); + + assertEquals("custom-value", channel.header("x-custom-header")); + assertEquals("custom-value", channel.header("X-Custom-Header")); + assertEquals("custom-value", channel.header("X-CUSTOM-HEADER")); + } + @Test public void testPathAndUri() { ServerCall serverCall = createMockServerCall("org.opensearch.protobufs.services.DocumentService/Bulk"); From 5fddfffe0b2a0242900eddc61d579c55188448ff Mon Sep 17 00:00:00 2001 From: opensearch-ci <83309141+opensearch-ci-bot@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:14:56 -0500 Subject: [PATCH 2/5] [AUTO] Add release notes for 3.5.0 (#5941) Signed-off-by: opensearch-ci --- ...ensearch-security.release-notes-3.5.0.0.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 release-notes/opensearch-security.release-notes-3.5.0.0.md diff --git a/release-notes/opensearch-security.release-notes-3.5.0.0.md b/release-notes/opensearch-security.release-notes-3.5.0.0.md new file mode 100644 index 0000000000..6036030fef --- /dev/null +++ b/release-notes/opensearch-security.release-notes-3.5.0.0.md @@ -0,0 +1,59 @@ +## Version 3.5.0 Release Notes + +Compatible with OpenSearch and OpenSearch Dashboards version 3.5.0 + +### Features + +* Allow configuring the timezone for audit log - Feature #5867 ([#5901](https://github.com/opensearch-project/security/pull/5901)) +* Introduce new dynamic setting (`plugins.security.dls.write_blocked`) to block all writes when restrictions apply ([#5828](https://github.com/opensearch-project/security/pull/5828)) +* JWT authentication for gRPC transport ([#5916](https://github.com/opensearch-project/security/pull/5916)) +* Support for HTTP/3 (server side) ([#5886](https://github.com/opensearch-project/security/pull/5886)) + +### Enhancements + +* Enable audit logging of document contents for DELETE operations ([#5914](https://github.com/opensearch-project/security/pull/5914)) +* Skip hasExplicitIndexPrivilege check for plugin users accessing their own system indices ([#5858](https://github.com/opensearch-project/security/pull/5858)) +* Fix-issue-5687 allow access to nested JWT claims via dot notation ([#5891](https://github.com/opensearch-project/security/pull/5891)) +* Implement buildSecureClientTransportEngine with serverName parameter ([#5894](https://github.com/opensearch-project/security/pull/5894)) +* Serialize Search Request object in DLS Filter Level Handler only when… ([#5883](https://github.com/opensearch-project/security/pull/5883)) + +### Bug Fixes + +* Bug fix: Fixing partial cache update post snapshot restore ([#5478](https://github.com/opensearch-project/security/pull/5478)) +* Fix IllegalArgumentException when resolved indices are empty ([#5797](https://github.com/opensearch-project/security/pull/5797)) +* Fix test failure related to change in core to add content-encoding to response headers ([#5897](https://github.com/opensearch-project/security/pull/5897)) +* Fixed NPE in LDAP recursive role search ([#5861](https://github.com/opensearch-project/security/pull/5861)) +* Make gRPC JWT header keys case insensitive ([#5929](https://github.com/opensearch-project/security/pull/5929)) + +### Infrastructure + +* Clear CHANGELOG post 3.4 release ([#5864](https://github.com/opensearch-project/security/pull/5864)) + +### Maintenance + +* Bump at.yawk.lz4:lz4-java from 1.10.1 to 1.10.2 ([#5874](https://github.com/opensearch-project/security/pull/5874)) +* Bump ch.qos.logback:logback-classic from 1.5.21 to 1.5.23 ([#5888](https://github.com/opensearch-project/security/pull/5888)) +* Bump ch.qos.logback:logback-classic from 1.5.23 to 1.5.24 ([#5902](https://github.com/opensearch-project/security/pull/5902)) +* Bump ch.qos.logback:logback-classic from 1.5.24 to 1.5.25 ([#5912](https://github.com/opensearch-project/security/pull/5912)) +* Bump ch.qos.logback:logback-classic from 1.5.25 to 1.5.26 ([#5919](https://github.com/opensearch-project/security/pull/5919)) +* Bump com.nimbusds:nimbus-jose-jwt from 10.6 to 10.7 ([#5904](https://github.com/opensearch-project/security/pull/5904)) +* Bump io.dropwizard.metrics:metrics-core from 4.2.37 to 4.2.38 ([#5922](https://github.com/opensearch-project/security/pull/5922)) +* Bump io.projectreactor:reactor-core from 3.8.1 to 3.8.2 ([#5910](https://github.com/opensearch-project/security/pull/5910)) +* Bump net.bytebuddy:byte-buddy from 1.18.2 to 1.18.3 ([#5877](https://github.com/opensearch-project/security/pull/5877)) +* Bump net.bytebuddy:byte-buddy from 1.18.3 to 1.18.4 ([#5913](https://github.com/opensearch-project/security/pull/5913)) +* Bump org.checkerframework:checker-qual from 3.52.1 to 3.53.0 ([#5906](https://github.com/opensearch-project/security/pull/5906)) +* Bump org.cryptacular:cryptacular from 1.2.7 to 1.3.0 ([#5921](https://github.com/opensearch-project/security/pull/5921)) +* Bump org.junit.jupiter:junit-jupiter-api from 5.14.1 to 5.14.2 ([#5903](https://github.com/opensearch-project/security/pull/5903)) +* Bump org.mockito:mockito-core from 5.20.0 to 5.21.0 ([#5875](https://github.com/opensearch-project/security/pull/5875)) +* Bump org.ow2.asm:asm from 9.9 to 9.9.1 ([#5876](https://github.com/opensearch-project/security/pull/5876)) +* Bump org.springframework.kafka:spring-kafka-test from 4.0.0 to 4.0.1 ([#5873](https://github.com/opensearch-project/security/pull/5873)) +* Bump org.springframework.kafka:spring-kafka-test from 4.0.1 to 4.0.2 ([#5918](https://github.com/opensearch-project/security/pull/5918)) +* Bump spring_version from 7.0.2 to 7.0.3 ([#5911](https://github.com/opensearch-project/security/pull/5911)) +* Refer to version of error_prone_annotations from core's version catalog (2.45.0) ([#5890](https://github.com/opensearch-project/security/pull/5890)) +* Remove MakeJava9Happy class that's not applicable in OS 3.X ([#5896](https://github.com/opensearch-project/security/pull/5896)) +* Update Jackson to 2.20.1 ([#5892](https://github.com/opensearch-project/security/pull/5892)) +* Upgrade eclipse dependencies ([#5863](https://github.com/opensearch-project/security/pull/5863)) + +### Refactoring + +* Refactor plugin system index tests to use parameterized test pattern ([#5895](https://github.com/opensearch-project/security/pull/5895)) \ No newline at end of file From a9fb014165b33c1a8063af1402885f73b3edc345 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:22:15 -0500 Subject: [PATCH 3/5] [Backport 3.5] Fix the issue of unprocessed X-Request-Id (#5958) Signed-off-by: Shawn Qiang <814238703@qq.com> Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] --- CHANGELOG.md | 1 + .../security/RequestHeadersTests.java | 70 +++++++++++++++++++ .../transport/SecurityInterceptor.java | 2 +- .../org/opensearch/security/TaskTests.java | 26 +++++++ 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/integrationTest/java/org/opensearch/security/RequestHeadersTests.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 82b573df25..e29c0c8b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Fix test failure related to change in core to add content-encoding to response headers ([#5897](https://github.com/opensearch-project/security/pull/5897)) - Fix partial cache update post snapshot restore[#5478](https://github.com/opensearch-project/security/pull/5478) +- Fix the issue of unprocessed X-Request-Id ([#5954](https://github.com/opensearch-project/security/pull/5954)) ### Refactoring - Refactor plugin system index tests to use parameterized test pattern ([#5895](https://github.com/opensearch-project/security/pull/5895)) diff --git a/src/integrationTest/java/org/opensearch/security/RequestHeadersTests.java b/src/integrationTest/java/org/opensearch/security/RequestHeadersTests.java new file mode 100644 index 0000000000..5206454b5f --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/RequestHeadersTests.java @@ -0,0 +1,70 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.apache.hc.core5.http.message.BasicHeader; +import org.junit.ClassRule; +import org.junit.Test; + +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.tasks.Task; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + +public class RequestHeadersTests { + + public static final AuthcDomain AUTHC_DOMAIN = new AuthcDomain("basic", 0).httpAuthenticatorWithChallenge("basic").backend("internal"); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authc(AUTHC_DOMAIN) + .users(USER_ADMIN) + .nodeSettings(Map.of(SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + USER_ADMIN.getName() + "__" + ALL_ACCESS.getName()))) + .build(); + + @Test + public void testRequestHeadersArePassedThrough() throws IOException, InterruptedException { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + client.put("test-index"); + + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + builder.field("field1", "foo"); + builder.endObject(); + + HttpResponse indexDocResponse = client.putJson( + "test-index/_doc/2", + builder.toString(), + new BasicHeader(Task.X_OPAQUE_ID, "2"), + new BasicHeader(Task.X_REQUEST_ID, "a1b2c3d4e5f67890abcdef1234567890") + ); + + assertThat(indexDocResponse.getStatusCode(), equalTo(RestStatus.CREATED.getStatus())); + assertThat(indexDocResponse.getHeader(Task.X_OPAQUE_ID).getValue(), equalTo("2")); + assertThat(indexDocResponse.getHeader(Task.X_REQUEST_ID).getValue(), equalTo("a1b2c3d4e5f67890abcdef1234567890")); + } + } +} diff --git a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java index 0a21c23881..7bccd487bf 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java +++ b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java @@ -175,7 +175,7 @@ public void sendRequestDecorate( requestHeadersToCopy, getThreadContext().getHeader(ConfigConstants.OPENSEARCH_SECURITY_REQUEST_HEADERS).split(",") ); - requestHeadersToCopy.remove(Task.X_OPAQUE_ID); // Special case where this header is preserved during stashContext. + requestHeadersToCopy.removeAll(Task.REQUEST_HEADERS); // Special case where this header is preserved during stashContext. } try (ThreadContext.StoredContext stashedContext = getThreadContext().stashContext()) { diff --git a/src/test/java/org/opensearch/security/TaskTests.java b/src/test/java/org/opensearch/security/TaskTests.java index daae9631d0..e87aa66329 100644 --- a/src/test/java/org/opensearch/security/TaskTests.java +++ b/src/test/java/org/opensearch/security/TaskTests.java @@ -53,4 +53,30 @@ public void testXOpaqueIdHeader() throws Exception { Assert.assertTrue(res.getBody().split("X-Opaque-Id").length > 2); Assert.assertTrue(!res.getBody().contains("failures")); } + + @Test + public void testTaskRequestHeadersAreRemoved() throws Exception { + setup(Settings.EMPTY, new DynamicSecurityConfig(), Settings.EMPTY); + + RestHelper rh = nonSslRestHelper(); + HttpResponse res; + + // Test with X-Opaque-Id and X-Request-Id headers (32 hex chars for X-Request-Id) + assertThat( + HttpStatus.SC_OK, + is( + (res = rh.executeGetRequest( + "_tasks?group_by=parents&pretty", + encodeBasicHeader("nagilum", "nagilum"), + new BasicHeader(Task.X_OPAQUE_ID, "testOpaqueId"), + new BasicHeader(Task.X_REQUEST_ID, "abcd1234abcd1234abcd1234abcd1234") + )).getStatusCode() + ) + ); + + // Verify the response contains the headers in task headers + Assert.assertTrue(res.getBody().contains("X-Opaque-Id")); + Assert.assertTrue(res.getBody().contains("testOpaqueId")); + Assert.assertTrue(!res.getBody().contains("failures")); + } } From d581e726f6327341b29c0819269821ab1bc43cd5 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:56:03 -0500 Subject: [PATCH 4/5] [Backport 3.5] Add buildSecureTransportContext() to SecureTransportSettingsProvider for Arrow Flight TLS cert reload (#5972) Signed-off-by: Rishabh Maurya Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] --- .../security/ssl/OpenSearchSecureSettingsFactory.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java index 6e2ea7544f..2361586662 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java @@ -121,6 +121,11 @@ public Optional trustManagerFactory() { }); } + @Override + public Optional buildSecureTransportContext(Settings settings) { + return sslSettingsManager.sslContextHandler(CertType.TRANSPORT).map(SslContextHandler::tryFetchSSLContext); + } + @Override public Optional buildSecureServerTransportEngine(Settings settings, Transport transport) throws SSLException { return sslSettingsManager.sslContextHandler(CertType.TRANSPORT).map(SslContextHandler::createSSLEngine); From 02a87bc6bb131ec608bcf398234bc30548a9b737 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:54:43 -0400 Subject: [PATCH 5/5] [AUTO] Increment version to 3.5.1-SNAPSHOT (#6041) Signed-off-by: opensearch-ci-bot Co-authored-by: opensearch-ci-bot --- .github/workflows/plugin_install.yml | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/plugin_install.yml b/.github/workflows/plugin_install.yml index 280489f1d0..a851901079 100644 --- a/.github/workflows/plugin_install.yml +++ b/.github/workflows/plugin_install.yml @@ -3,7 +3,7 @@ name: Plugin Install on: [push, pull_request, workflow_dispatch] env: - OPENSEARCH_VERSION: 3.5.0 + OPENSEARCH_VERSION: 3.5.1 PLUGIN_NAME: opensearch-security jobs: diff --git a/build.gradle b/build.gradle index 0e148a8f2f..8a1334288a 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ import groovy.json.JsonBuilder buildscript { ext { - opensearch_version = System.getProperty("opensearch.version", "3.5.0-SNAPSHOT") + opensearch_version = System.getProperty("opensearch.version", "3.5.1-SNAPSHOT") isSnapshot = "true" == System.getProperty("build.snapshot", "true") buildVersionQualifier = System.getProperty("build.version_qualifier", "")