Skip to content

Commit d8e3f40

Browse files
committed
[BUG] Getting DeadlineTimeoutException error during security tests after upgrading to 3.0
Signed-off-by: Andriy Redko <[email protected]>
1 parent 5b9ff98 commit d8e3f40

File tree

3 files changed

+316
-7
lines changed

3 files changed

+316
-7
lines changed
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.integtest;
7+
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
import java.nio.file.Files;
11+
import java.nio.file.LinkOption;
12+
import java.nio.file.Path;
13+
import java.nio.file.Paths;
14+
import java.security.GeneralSecurityException;
15+
import java.security.KeyStore;
16+
import java.util.ArrayList;
17+
import java.util.Arrays;
18+
19+
import javax.net.ssl.SSLContext;
20+
import javax.net.ssl.SSLEngine;
21+
22+
import org.apache.hc.client5.http.auth.AuthScope;
23+
import org.apache.hc.client5.http.auth.CredentialsProvider;
24+
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
25+
import org.apache.hc.client5.http.config.RequestConfig;
26+
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
27+
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
28+
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
29+
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
30+
import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder;
31+
import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy;
32+
import org.apache.hc.core5.function.Factory;
33+
import org.apache.hc.core5.http.HttpHost;
34+
import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
35+
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
36+
import org.apache.hc.core5.reactor.ssl.TlsDetails;
37+
import org.apache.hc.core5.ssl.SSLContextBuilder;
38+
import org.apache.hc.core5.util.Timeout;
39+
import org.apache.logging.log4j.LogManager;
40+
import org.apache.logging.log4j.Logger;
41+
import org.opensearch.OpenSearchException;
42+
import org.opensearch.client.RestClient;
43+
import org.opensearch.client.RestClientBuilder;
44+
import org.opensearch.common.Strings;
45+
import org.opensearch.common.settings.Settings;
46+
import org.opensearch.commons.ConfigConstants;
47+
import org.opensearch.commons.rest.TrustStore;
48+
49+
/**
50+
* Provides builder to create low-level and high-level REST client to make calls to OpenSearch.
51+
*
52+
* Sample usage:
53+
* SecureRestClientBuilder builder = new SecureRestClientBuilder(settings).build()
54+
* RestClient restClient = builder.build();
55+
*
56+
* Other usage:
57+
* RestClient restClient = new SecureRestClientBuilder("localhost", 9200, false)
58+
* .setUserPassword("admin", "admin")
59+
* .setTrustCerts(trustStorePath)
60+
* .build();
61+
*
62+
*
63+
* If https is enabled, creates RestClientBuilder using self-signed certificates or passed pem
64+
* as trusted.
65+
*
66+
* If https is not enabled, creates a http based client.
67+
*/
68+
public class SecureRestClientBuilder {
69+
70+
private final boolean httpSSLEnabled;
71+
private final String user;
72+
private final String passwd;
73+
private final ArrayList<HttpHost> hosts = new ArrayList<>();
74+
75+
private final Path configPath;
76+
private final Settings settings;
77+
78+
private int defaultConnectTimeOutMSecs = 5000;
79+
private int defaultSoTimeoutMSecs = 10000;
80+
private int defaultConnRequestTimeoutMSecs = 0;
81+
82+
private static final Logger log = LogManager.getLogger(SecureRestClientBuilder.class);
83+
84+
/**
85+
* ONLY for integration tests.
86+
* @param host
87+
* @param port
88+
* @param httpSSLEnabled
89+
* @param user
90+
* @param passWord
91+
*/
92+
public SecureRestClientBuilder(
93+
final String host,
94+
final int port,
95+
final boolean httpSSLEnabled,
96+
final String user,
97+
final String passWord
98+
) {
99+
if (Strings.isNullOrEmpty(user) || Strings.isNullOrEmpty(passWord)) {
100+
throw new IllegalArgumentException("Invalid user or password");
101+
}
102+
103+
this.httpSSLEnabled = httpSSLEnabled;
104+
this.user = user;
105+
this.passwd = passWord;
106+
this.settings = Settings.EMPTY;
107+
this.configPath = null;
108+
hosts.add(new HttpHost(httpSSLEnabled ? ConfigConstants.HTTPS : ConfigConstants.HTTP, host, port));
109+
}
110+
111+
/**
112+
* ONLY for integration tests.
113+
* @param httpHosts
114+
* @param httpSSLEnabled
115+
* @param user
116+
* @param passWord
117+
*/
118+
public SecureRestClientBuilder(HttpHost[] httpHosts, final boolean httpSSLEnabled, final String user, final String passWord) {
119+
120+
if (Strings.isNullOrEmpty(user) || Strings.isNullOrEmpty(passWord)) {
121+
throw new IllegalArgumentException("Invalid user or password");
122+
}
123+
124+
this.httpSSLEnabled = httpSSLEnabled;
125+
this.user = user;
126+
this.passwd = passWord;
127+
this.settings = Settings.EMPTY;
128+
this.configPath = null;
129+
hosts.addAll(Arrays.asList(httpHosts));
130+
}
131+
132+
public SecureRestClientBuilder(Settings settings, Path configPath) {
133+
134+
this.httpSSLEnabled = settings.getAsBoolean(ConfigConstants.OPENSEARCH_SECURITY_SSL_HTTP_ENABLED, false);
135+
this.settings = settings;
136+
this.configPath = configPath;
137+
this.user = null;
138+
this.passwd = null;
139+
String host = ConfigConstants.HOST_DEFAULT;
140+
int port = settings.getAsInt(ConfigConstants.HTTP_PORT, ConfigConstants.HTTP_PORT_DEFAULT);
141+
hosts.add(new HttpHost(httpSSLEnabled ? ConfigConstants.HTTPS : ConfigConstants.HTTP, host, port));
142+
}
143+
144+
public SecureRestClientBuilder(Settings settings, Path configPath, HttpHost[] httpHosts) {
145+
this.httpSSLEnabled = settings.getAsBoolean(ConfigConstants.OPENSEARCH_SECURITY_SSL_HTTP_ENABLED, false);
146+
this.settings = settings;
147+
this.configPath = configPath;
148+
this.user = null;
149+
this.passwd = null;
150+
hosts.addAll(Arrays.asList(httpHosts));
151+
}
152+
153+
/**
154+
* Creates a low-level Rest client.
155+
* @return
156+
* @throws IOException
157+
*/
158+
public RestClient build() throws IOException {
159+
return createRestClientBuilder().build();
160+
}
161+
162+
public SecureRestClientBuilder setConnectTimeout(int timeout) {
163+
this.defaultConnectTimeOutMSecs = timeout;
164+
return this;
165+
}
166+
167+
public SecureRestClientBuilder setSocketTimeout(int timeout) {
168+
this.defaultSoTimeoutMSecs = timeout;
169+
return this;
170+
}
171+
172+
public SecureRestClientBuilder setConnectionRequestTimeout(int timeout) {
173+
this.defaultConnRequestTimeoutMSecs = timeout;
174+
return this;
175+
}
176+
177+
private RestClientBuilder createRestClientBuilder() throws IOException {
178+
RestClientBuilder builder = RestClient.builder(hosts.toArray(new HttpHost[hosts.size()]));
179+
180+
builder.setRequestConfigCallback(new RestClientBuilder.RequestConfigCallback() {
181+
@Override
182+
public RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder requestConfigBuilder) {
183+
return requestConfigBuilder
184+
.setConnectTimeout(Timeout.ofMilliseconds(defaultConnectTimeOutMSecs))
185+
.setResponseTimeout(Timeout.ofMilliseconds(defaultSoTimeoutMSecs))
186+
.setConnectionRequestTimeout(Timeout.ofMilliseconds(defaultConnRequestTimeoutMSecs));
187+
}
188+
});
189+
190+
final SSLContext sslContext;
191+
try {
192+
sslContext = createSSLContext();
193+
} catch (GeneralSecurityException | IOException ex) {
194+
throw new IOException(ex);
195+
}
196+
final CredentialsProvider credentialsProvider = createCredsProvider();
197+
builder.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
198+
@Override
199+
public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) {
200+
if (sslContext != null) {
201+
TlsStrategy tlsStrategy = ClientTlsStrategyBuilder
202+
.create()
203+
.setSslContext(sslContext)
204+
// See please https://issues.apache.org/jira/browse/HTTPCLIENT-2219
205+
.setTlsDetailsFactory(new Factory<SSLEngine, TlsDetails>() {
206+
@Override
207+
public TlsDetails create(final SSLEngine sslEngine) {
208+
return new TlsDetails(sslEngine.getSession(), sslEngine.getApplicationProtocol());
209+
}
210+
})
211+
.build();
212+
PoolingAsyncClientConnectionManager connectionManager = PoolingAsyncClientConnectionManagerBuilder
213+
.create()
214+
.setMaxConnPerRoute(300)
215+
.setMaxConnTotal(1000)
216+
.setTlsStrategy(tlsStrategy)
217+
.build();
218+
httpClientBuilder.setConnectionManager(connectionManager);
219+
}
220+
if (credentialsProvider != null) {
221+
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
222+
}
223+
return httpClientBuilder;
224+
}
225+
});
226+
return builder;
227+
}
228+
229+
private SSLContext createSSLContext() throws IOException, GeneralSecurityException {
230+
SSLContextBuilder builder = new SSLContextBuilder();
231+
if (httpSSLEnabled) {
232+
// Handle trust store
233+
String pemFile = getTrustPem();
234+
if (Strings.isNullOrEmpty(pemFile)) {
235+
builder.loadTrustMaterial(null, new TrustSelfSignedStrategy());
236+
} else {
237+
String pem = resolve(pemFile, configPath);
238+
KeyStore trustStore = new TrustStore(pem).create();
239+
builder.loadTrustMaterial(trustStore, null);
240+
}
241+
242+
// Handle key store.
243+
KeyStore keyStore = getKeyStore();
244+
if (keyStore != null) {
245+
builder.loadKeyMaterial(keyStore, getKeystorePasswd().toCharArray());
246+
}
247+
248+
}
249+
return builder.build();
250+
}
251+
252+
private CredentialsProvider createCredsProvider() {
253+
if (Strings.isNullOrEmpty(user) || Strings.isNullOrEmpty(passwd))
254+
return null;
255+
256+
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
257+
credentialsProvider.setCredentials(new AuthScope(null, -1), new UsernamePasswordCredentials(user, passwd.toCharArray()));
258+
return credentialsProvider;
259+
}
260+
261+
private String resolve(final String originalFile, final Path configPath) {
262+
String path = null;
263+
if (originalFile != null && originalFile.length() > 0) {
264+
path = configPath.resolve(originalFile).toAbsolutePath().toString();
265+
log.debug("Resolved {} to {} against {}", originalFile, path, configPath.toAbsolutePath().toString());
266+
}
267+
268+
if (path == null || path.length() == 0) {
269+
throw new OpenSearchException("Empty file path for " + originalFile);
270+
}
271+
272+
if (Files.isDirectory(Paths.get(path), LinkOption.NOFOLLOW_LINKS)) {
273+
throw new OpenSearchException("Is a directory: " + path + " Expected a file for " + originalFile);
274+
}
275+
276+
if (!Files.isReadable(Paths.get(path))) {
277+
throw new OpenSearchException(
278+
"Unable to read "
279+
+ path
280+
+ " ("
281+
+ Paths.get(path)
282+
+ "). Please make sure this files exists and is readable regarding to permissions. Property: "
283+
+ originalFile
284+
);
285+
}
286+
if ("".equals(path)) {
287+
path = null;
288+
}
289+
return path;
290+
}
291+
292+
private String getTrustPem() {
293+
return settings.get(ConfigConstants.OPENSEARCH_SECURITY_SSL_HTTP_PEMCERT_FILEPATH, null);
294+
}
295+
296+
private String getKeystorePasswd() {
297+
return settings.get(ConfigConstants.OPENSEARCH_SECURITY_SSL_HTTP_KEYSTORE_KEYPASSWORD, null);
298+
}
299+
300+
private KeyStore getKeyStore() throws IOException, GeneralSecurityException {
301+
KeyStore keyStore = KeyStore.getInstance("jks");
302+
String keyStoreFile = settings.get(ConfigConstants.OPENSEARCH_SECURITY_SSL_HTTP_KEYSTORE_FILEPATH, null);
303+
String passwd = settings.get(ConfigConstants.OPENSEARCH_SECURITY_SSL_HTTP_KEYSTORE_PASSWORD, null);
304+
if (Strings.isNullOrEmpty(keyStoreFile) || Strings.isNullOrEmpty(passwd)) {
305+
return null;
306+
}
307+
String keyStorePath = resolve(keyStoreFile, configPath);
308+
try (InputStream is = Files.newInputStream(Paths.get(keyStorePath))) {
309+
keyStore.load(is, passwd.toCharArray());
310+
}
311+
return keyStore;
312+
}
313+
}

notifications/notifications/src/test/kotlin/org/opensearch/integtest/PluginRestTestCase.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import org.opensearch.common.xcontent.NamedXContentRegistry
2222
import org.opensearch.common.xcontent.XContentType
2323
import org.opensearch.commons.ConfigConstants
2424
import org.opensearch.commons.notifications.model.ConfigType
25-
import org.opensearch.commons.rest.SecureRestClientBuilder
2625
import org.opensearch.notifications.NotificationPlugin
2726
import org.opensearch.rest.RestRequest
2827
import org.opensearch.rest.RestStatus

notifications/notifications/src/test/kotlin/org/opensearch/integtest/SecurityNotificationIT.kt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,14 @@ class SecurityNotificationIT : PluginRestTestCase() {
3535

3636
@Before
3737
fun create() {
38-
39-
if (userClient == null) {
40-
createUser(user, user, arrayOf())
41-
userClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), user, user).setSocketTimeout(60000).build()
42-
}
38+
createUser(user, user, arrayOf())
39+
userClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), user, user).setSocketTimeout(60000).build()
4340
}
4441

4542
@After
4643
fun cleanup() {
47-
4844
userClient?.close()
45+
userClient = null
4946
}
5047

5148
fun `test Create slack notification config with user that has create Notification permission`() {

0 commit comments

Comments
 (0)