Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/plugin_install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")

Expand Down
59 changes: 59 additions & 0 deletions release-notes/opensearch-security.release-notes-3.5.0.0.md
Original file line number Diff line number Diff line change
@@ -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))
Original file line number Diff line number Diff line change
@@ -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"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> CLAIM_USERNAME = List.of("preferred-username");
public static final List<String> 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();
}
}
}
17 changes: 12 additions & 5 deletions src/main/java/org/opensearch/security/auth/BackendRegistry.java
Original file line number Diff line number Diff line change
@@ -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.
*/

/*
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String, List<String>> extractHeaders(Metadata metadata) {
Map<String, List<String>> headerMap = new HashMap<>();
Map<String, List<String>> 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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ public Optional<TrustManagerFactory> trustManagerFactory() {
});
}

@Override
public Optional<SSLContext> buildSecureTransportContext(Settings settings) {
return sslSettingsManager.sslContextHandler(CertType.TRANSPORT).map(SslContextHandler::tryFetchSSLContext);
}

@Override
public Optional<SSLEngine> buildSecureServerTransportEngine(Settings settings, Transport transport) throws SSLException {
return sslSettingsManager.sslContextHandler(CertType.TRANSPORT).map(SslContextHandler::createSSLEngine);
Expand Down
Loading
Loading