Przeglądaj źródła

SONAR-14853 Elasticsearch http encryption

tags/10.2.0.77647
Eric Giffon 11 miesięcy temu
rodzic
commit
0811ba15c3

+ 1
- 0
build.gradle Wyświetl plik

@@ -311,6 +311,7 @@ subprojects {
dependencySet(group: 'com.squareup.okhttp3', version: '4.10.0') {
entry 'okhttp'
entry 'mockwebserver'
entry 'okhttp-tls'
}
dependency 'org.json:json:20230227'
dependency 'com.tngtech.java:junit-dataprovider:1.13.1'

+ 1
- 0
server/sonar-main/build.gradle Wyświetl plik

@@ -34,6 +34,7 @@ dependencies {
testImplementation 'org.awaitility:awaitility'
testImplementation 'org.mockito:mockito-core'
testImplementation 'com.squareup.okhttp3:mockwebserver'
testImplementation 'com.squareup.okhttp3:okhttp-tls'
testImplementation 'commons-logging:commons-logging:1.2'
testImplementation project(':sonar-testing-harness')
}

+ 8
- 1
server/sonar-main/src/main/java/org/sonar/application/AppStateFactory.java Wyświetl plik

@@ -20,7 +20,10 @@
package org.sonar.application;

import com.google.common.net.HostAndPort;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.sonar.application.cluster.AppNodesClusterHostsConsistency;
@@ -34,6 +37,8 @@ import org.sonar.process.Props;
import org.sonar.process.cluster.hz.HazelcastMember;
import org.sonar.process.cluster.hz.HazelcastMemberBuilder;

import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE_PASSWORD;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_HZ_HOSTS;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_KUBERNETES;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_NODE_HOST;
@@ -78,6 +83,8 @@ public class AppStateFactory {
.map(HostAndPort::fromString)
.collect(Collectors.toSet());
String searchPassword = props.value(CLUSTER_SEARCH_PASSWORD.getKey());
return new EsConnectorImpl(hostAndPorts, searchPassword);
Path keyStorePath = Optional.ofNullable(props.value(CLUSTER_ES_HTTP_KEYSTORE.getKey())).map(Paths::get).orElse(null);
String keyStorePassword = props.value(CLUSTER_ES_HTTP_KEYSTORE_PASSWORD.getKey());
return new EsConnectorImpl(hostAndPorts, searchPassword, keyStorePath, keyStorePassword);
}
}

+ 34
- 17
server/sonar-main/src/main/java/org/sonar/application/ProcessLauncherImpl.java Wyświetl plik

@@ -54,6 +54,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
import static java.util.Collections.singleton;
import static org.sonar.application.es.EsKeyStoreCli.BOOTSTRAP_PASSWORD_PROPERTY_KEY;
import static org.sonar.application.es.EsKeyStoreCli.HTTP_KEYSTORE_PASSWORD_PROPERTY_KEY;
import static org.sonar.application.es.EsKeyStoreCli.KEYSTORE_PASSWORD_PROPERTY_KEY;
import static org.sonar.application.es.EsKeyStoreCli.TRUSTSTORE_PASSWORD_PROPERTY_KEY;
import static org.sonar.process.ProcessEntryPoint.PROPERTY_GRACEFUL_STOP_TIMEOUT_MS;
@@ -102,7 +103,8 @@ public class ProcessLauncherImpl implements ProcessLauncher {
if (processId == ProcessId.ELASTICSEARCH) {
checkArgument(esInstallation != null, "Incorrect configuration EsInstallation is null");
EsConnectorImpl esConnector = new EsConnectorImpl(singleton(HostAndPort.fromParts(esInstallation.getHost(),
esInstallation.getHttpPort())), esInstallation.getBootstrapPassword());
esInstallation.getHttpPort())), esInstallation.getBootstrapPassword(), esInstallation.getHttpKeyStoreLocation(),
esInstallation.getHttpKeyStorePassword().orElse(null));
return new EsManagedProcess(process, processId, esConnector);
} else {
ProcessCommands commands = allProcessesCommands.createAfterClean(processId.getIpcIndex());
@@ -140,7 +142,7 @@ public class ProcessLauncherImpl implements ProcessLauncher {

pruneElasticsearchConfDirectory(confDir);
createElasticsearchConfDirectory(confDir);
setupElasticsearchAuthentication(esInstallation);
setupElasticsearchSecurity(esInstallation);

esInstallation.getEsYmlSettings().writeToYmlSettingsFile(esInstallation.getElasticsearchYml());
esInstallation.getEsJvmOptions().writeToJvmOptionFile(esInstallation.getJvmOptions());
@@ -163,26 +165,41 @@ public class ProcessLauncherImpl implements ProcessLauncher {
}
}

private void setupElasticsearchAuthentication(EsInstallation esInstallation) {
private void setupElasticsearchSecurity(EsInstallation esInstallation) {
if (esInstallation.isSecurityEnabled()) {
EsKeyStoreCli keyStoreCli = EsKeyStoreCli.getInstance(esInstallation)
.store(BOOTSTRAP_PASSWORD_PROPERTY_KEY, esInstallation.getBootstrapPassword());
EsKeyStoreCli keyStoreCli = EsKeyStoreCli.getInstance(esInstallation);

String esConfPath = esInstallation.getConfDirectory().getAbsolutePath();
setupElasticsearchAuthentication(esInstallation, keyStoreCli);
setupElasticsearchHttpEncryption(esInstallation, keyStoreCli);

Path trustStoreLocation = esInstallation.getTrustStoreLocation();
Path keyStoreLocation = esInstallation.getKeyStoreLocation();
if (trustStoreLocation.equals(keyStoreLocation)) {
copyFile(trustStoreLocation, Paths.get(esConfPath, trustStoreLocation.toFile().getName()));
} else {
copyFile(trustStoreLocation, Paths.get(esConfPath, trustStoreLocation.toFile().getName()));
copyFile(keyStoreLocation, Paths.get(esConfPath, keyStoreLocation.toFile().getName()));
}
keyStoreCli.executeWith(this::launchJava);
}
}

esInstallation.getTrustStorePassword().ifPresent(s -> keyStoreCli.store(TRUSTSTORE_PASSWORD_PROPERTY_KEY, s));
esInstallation.getKeyStorePassword().ifPresent(s -> keyStoreCli.store(KEYSTORE_PASSWORD_PROPERTY_KEY, s));
private static void setupElasticsearchAuthentication(EsInstallation esInstallation, EsKeyStoreCli keyStoreCli) {
keyStoreCli.store(BOOTSTRAP_PASSWORD_PROPERTY_KEY, esInstallation.getBootstrapPassword());

keyStoreCli.executeWith(this::launchJava);
String esConfPath = esInstallation.getConfDirectory().getAbsolutePath();

Path trustStoreLocation = esInstallation.getTrustStoreLocation();
Path keyStoreLocation = esInstallation.getKeyStoreLocation();
if (trustStoreLocation.equals(keyStoreLocation)) {
copyFile(trustStoreLocation, Paths.get(esConfPath, trustStoreLocation.toFile().getName()));
} else {
copyFile(trustStoreLocation, Paths.get(esConfPath, trustStoreLocation.toFile().getName()));
copyFile(keyStoreLocation, Paths.get(esConfPath, keyStoreLocation.toFile().getName()));
}

esInstallation.getTrustStorePassword().ifPresent(s -> keyStoreCli.store(TRUSTSTORE_PASSWORD_PROPERTY_KEY, s));
esInstallation.getKeyStorePassword().ifPresent(s -> keyStoreCli.store(KEYSTORE_PASSWORD_PROPERTY_KEY, s));
}

private static void setupElasticsearchHttpEncryption(EsInstallation esInstallation, EsKeyStoreCli keyStoreCli) {
if (esInstallation.isHttpEncryptionEnabled()) {
String esConfPath = esInstallation.getConfDirectory().getAbsolutePath();
Path httpKeyStoreLocation = esInstallation.getHttpKeyStoreLocation();
copyFile(httpKeyStoreLocation, Paths.get(esConfPath, httpKeyStoreLocation.toFile().getName()));
esInstallation.getHttpKeyStorePassword().ifPresent(s -> keyStoreCli.store(HTTP_KEYSTORE_PASSWORD_PROPERTY_KEY, s));
}
}


+ 44
- 2
server/sonar-main/src/main/java/org/sonar/application/es/EsConnectorImpl.java Wyświetl plik

@@ -21,16 +21,26 @@ package org.sonar.application.es;

import com.google.common.net.HostAndPort;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.client.RequestOptions;
@@ -51,10 +61,15 @@ public class EsConnectorImpl implements EsConnector {
private final AtomicReference<RestHighLevelClient> restClient = new AtomicReference<>(null);
private final Set<HostAndPort> hostAndPorts;
private final String searchPassword;
private final Path keyStorePath;
private final String keyStorePassword;

public EsConnectorImpl(Set<HostAndPort> hostAndPorts, @Nullable String searchPassword) {
public EsConnectorImpl(Set<HostAndPort> hostAndPorts, @Nullable String searchPassword, @Nullable Path keyStorePath,
@Nullable String keyStorePassword) {
this.hostAndPorts = hostAndPorts;
this.searchPassword = searchPassword;
this.keyStorePath = keyStorePath;
this.keyStorePassword = keyStorePassword;
}

@Override
@@ -94,7 +109,7 @@ public class EsConnectorImpl implements EsConnector {

private RestHighLevelClient buildRestHighLevelClient() {
HttpHost[] httpHosts = hostAndPorts.stream()
.map(hostAndPort -> new HttpHost(hostAndPort.getHost(), hostAndPort.getPortOrDefault(9001)))
.map(this::toHttpHost)
.toArray(HttpHost[]::new);

if (LOG.isDebugEnabled()) {
@@ -110,15 +125,42 @@ public class EsConnectorImpl implements EsConnector {
BasicCredentialsProvider provider = getBasicCredentialsProvider(searchPassword);
httpClientBuilder.setDefaultCredentialsProvider(provider);
}

if (keyStorePath != null) {
SSLContext sslContext = getSSLContext(keyStorePath, keyStorePassword);
httpClientBuilder.setSSLContext(sslContext);
}

return httpClientBuilder;
});
return new RestHighLevelClient(builder);
}

private HttpHost toHttpHost(HostAndPort host) {
try {
String scheme = keyStorePath != null ? "https" : HttpHost.DEFAULT_SCHEME_NAME;
return new HttpHost(InetAddress.getByName(host.getHost()), host.getPortOrDefault(9001), scheme);
} catch (UnknownHostException e) {
throw new IllegalStateException("Can not resolve host [" + host + "]", e);
}
}

private static BasicCredentialsProvider getBasicCredentialsProvider(String searchPassword) {
BasicCredentialsProvider provider = new BasicCredentialsProvider();
provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(ES_USERNAME, searchPassword));
return provider;
}

private static SSLContext getSSLContext(Path keyStorePath, @Nullable String keyStorePassword) {
try {
KeyStore keyStore = KeyStore.getInstance("pkcs12");
try (InputStream is = Files.newInputStream(keyStorePath)) {
keyStore.load(is, keyStorePassword == null ? null : keyStorePassword.toCharArray());
}
SSLContextBuilder sslBuilder = SSLContexts.custom().loadTrustMaterial(keyStore, null);
return sslBuilder.build();
} catch (IOException | GeneralSecurityException e) {
throw new IllegalStateException("Failed to setup SSL context on ES client", e);
}
}
}

+ 21
- 0
server/sonar-main/src/main/java/org/sonar/application/es/EsInstallation.java Wyświetl plik

@@ -34,6 +34,8 @@ import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.process.Props;

import static org.sonar.process.ProcessProperties.Property.CLUSTER_ENABLED;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE_PASSWORD;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_KEYSTORE_PASSWORD;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_TRUSTSTORE;
@@ -73,6 +75,10 @@ public class EsInstallation {
private final String keyStorePassword;
@Nullable
private final String trustStorePassword;
private final Path httpKeyStoreLocation;
@Nullable
private final String httpKeyStorePassword;
private final boolean httpEncryptionEnabled;

public EsInstallation(Props props) {
File sqHomeDir = props.nonNullValueAsFile(PATH_HOME.getKey());
@@ -89,6 +95,9 @@ public class EsInstallation {
this.keyStorePassword = props.value(CLUSTER_ES_KEYSTORE_PASSWORD.getKey());
this.trustStoreLocation = getPath(props.value(CLUSTER_ES_TRUSTSTORE.getKey()));
this.trustStorePassword = props.value(CLUSTER_ES_TRUSTSTORE_PASSWORD.getKey());
this.httpKeyStoreLocation = getPath(props.value(CLUSTER_ES_HTTP_KEYSTORE.getKey()));
this.httpKeyStorePassword = props.value(CLUSTER_ES_HTTP_KEYSTORE_PASSWORD.getKey());
this.httpEncryptionEnabled = securityEnabled && httpKeyStoreLocation != null;
}

private static Path getPath(@Nullable String path) {
@@ -224,4 +233,16 @@ public class EsInstallation {
public Optional<String> getTrustStorePassword() {
return Optional.ofNullable(trustStorePassword);
}

public Path getHttpKeyStoreLocation() {
return httpKeyStoreLocation;
}

public Optional<String> getHttpKeyStorePassword() {
return Optional.ofNullable(httpKeyStorePassword);
}

public boolean isHttpEncryptionEnabled() {
return httpEncryptionEnabled;
}
}

+ 1
- 0
server/sonar-main/src/main/java/org/sonar/application/es/EsKeyStoreCli.java Wyświetl plik

@@ -41,6 +41,7 @@ public class EsKeyStoreCli {
public static final String BOOTSTRAP_PASSWORD_PROPERTY_KEY = "bootstrap.password";
public static final String KEYSTORE_PASSWORD_PROPERTY_KEY = "xpack.security.transport.ssl.keystore.secure_password";
public static final String TRUSTSTORE_PASSWORD_PROPERTY_KEY = "xpack.security.transport.ssl.truststore.secure_password";
public static final String HTTP_KEYSTORE_PASSWORD_PROPERTY_KEY = "xpack.security.http.ssl.keystore.secure_password";

private static final String MAIN_CLASS_NAME = "org.elasticsearch.launcher.CliToolLauncher";


+ 10
- 2
server/sonar-main/src/main/java/org/sonar/application/es/EsSettings.java Wyświetl plik

@@ -38,6 +38,7 @@ import static java.lang.String.valueOf;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ENABLED;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_DISCOVERY_SEED_HOSTS;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HOSTS;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_TRUSTSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_NAME;
@@ -98,7 +99,7 @@ public class EsSettings {
configureFileSystem(builder);
configureNetwork(builder);
configureCluster(builder);
configureAuthentication(builder);
configureSecurity(builder);
configureOthers(builder);
LOGGER.info("Elasticsearch listening on [HTTP: {}:{}, TCP: {}:{}]",
builder.get(ES_HTTP_HOST_KEY), builder.get(ES_HTTP_PORT_KEY),
@@ -111,7 +112,7 @@ public class EsSettings {
builder.put("path.logs", fileSystem.getLogDirectory().getAbsolutePath());
}

private void configureAuthentication(Map<String, String> builder) {
private void configureSecurity(Map<String, String> builder) {
if (clusterEnabled && props.value((CLUSTER_SEARCH_PASSWORD.getKey())) != null) {

String clusterESKeystoreFileName = getFileNameFromPathProperty(CLUSTER_ES_KEYSTORE);
@@ -123,6 +124,13 @@ public class EsSettings {
builder.put("xpack.security.transport.ssl.verification_mode", "certificate");
builder.put("xpack.security.transport.ssl.keystore.path", clusterESKeystoreFileName);
builder.put("xpack.security.transport.ssl.truststore.path", clusterESTruststoreFileName);

if (props.value(CLUSTER_ES_HTTP_KEYSTORE.getKey()) != null) {
String clusterESHttpKeystoreFileName = getFileNameFromPathProperty(CLUSTER_ES_HTTP_KEYSTORE);

builder.put("xpack.security.http.ssl.enabled", Boolean.TRUE.toString());
builder.put("xpack.security.http.ssl.keystore.path", clusterESHttpKeystoreFileName);
}
} else {
builder.put("xpack.security.autoconfiguration.enabled", Boolean.FALSE.toString());
builder.put("xpack.security.enabled", Boolean.FALSE.toString());

+ 33
- 0
server/sonar-main/src/test/java/org/sonar/application/ProcessLauncherImplTest.java Wyświetl plik

@@ -182,6 +182,39 @@ public class ProcessLauncherImplTest {
assertThat(Paths.get(esInstallation.getConfDirectory().getAbsolutePath(), "keystore.pk12")).exists();
}

@Test
public void launch_whenEnablingEsWithHttpEncryption_shouldCopyKeystoreToEsConf() throws Exception {
File tempDir = temp.newFolder();
File certificateFile = temp.newFile("certificate.pk12");
File httpCertificateFile = temp.newFile("httpCertificate.pk12");
TestProcessBuilder processBuilder = new TestProcessBuilder();
ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder);

EsInstallation esInstallation = createEsInstallation(new Props(new Properties())
.set("sonar.cluster.enabled", "true")
.set("sonar.cluster.search.password", "bootstrap-password")
.set("sonar.cluster.es.ssl.keystore", certificateFile.getAbsolutePath())
.set("sonar.cluster.es.ssl.truststore", certificateFile.getAbsolutePath())
.set("sonar.cluster.es.http.ssl.keystore", httpCertificateFile.getAbsolutePath())
.set("sonar.cluster.es.http.ssl.keystorePassword", "keystore-password"));

JavaCommand<JvmOptions> command = new JavaCommand<>(ProcessId.ELASTICSEARCH, temp.newFolder());
command.addClasspath("lib/*.class");
command.addClasspath("lib/*.jar");
command.setArgument("foo", "bar");
command.setClassName("org.sonarqube.Main");
command.setEnvVariable("VAR1", "valueOfVar1");
command.setJvmOptions(new JvmOptions<>()
.add("-Dfoo=bar")
.add("-Dfoo2=bar2"));
command.setEsInstallation(esInstallation);

ManagedProcess monitor = underTest.launch(command);
assertThat(monitor).isNotNull();
assertThat(Paths.get(esInstallation.getConfDirectory().getAbsolutePath(), "certificate.pk12")).exists();
assertThat(Paths.get(esInstallation.getConfDirectory().getAbsolutePath(), "httpCertificate.pk12")).exists();
}

@Test
public void properties_are_passed_to_command_via_a_temporary_properties_file() throws Exception {
File tempDir = temp.newFolder();

+ 55
- 2
server/sonar-main/src/test/java/org/sonar/application/es/EsConnectorImplTest.java Wyświetl plik

@@ -21,13 +21,23 @@ package org.sonar.application.es;

import com.google.common.collect.Sets;
import com.google.common.net.HostAndPort;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.tls.HandshakeCertificates;
import okhttp3.tls.HeldCertificate;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -76,8 +86,11 @@ public class EsConnectorImplTest {

@Rule
public MockWebServer mockWebServer = new MockWebServer();
@Rule
public TemporaryFolder temp = new TemporaryFolder();

EsConnectorImpl underTest = new EsConnectorImpl(Sets.newHashSet(HostAndPort.fromParts(mockWebServer.getHostName(), mockWebServer.getPort())), null);
EsConnectorImpl underTest = new EsConnectorImpl(Sets.newHashSet(HostAndPort.fromParts(mockWebServer.getHostName(),
mockWebServer.getPort())), null, null, null);

@After
public void after() {
@@ -105,13 +118,53 @@ public class EsConnectorImplTest {
mockServerResponse(200, JSON_SUCCESS_RESPONSE);
String password = "test-password";

EsConnectorImpl underTest = new EsConnectorImpl(Sets.newHashSet(HostAndPort.fromParts(mockWebServer.getHostName(), mockWebServer.getPort())), password);
EsConnectorImpl underTest = new EsConnectorImpl(Sets.newHashSet(HostAndPort.fromParts(mockWebServer.getHostName(), mockWebServer.getPort())),
password, null, null);

assertThat(underTest.getClusterHealthStatus())
.hasValue(ClusterHealthStatus.YELLOW);
assertThat(mockWebServer.takeRequest().getHeader("Authorization")).isEqualTo("Basic ZWxhc3RpYzp0ZXN0LXBhc3N3b3Jk");
}

@Test
public void newInstance_whenKeyStorePassed_shouldCreateClient() throws GeneralSecurityException, IOException {
mockServerResponse(200, JSON_SUCCESS_RESPONSE);

Path keyStorePath = temp.newFile("keystore.p12").toPath();
String password = "password";

HandshakeCertificates certificate = createCertificate(mockWebServer.getHostName(), keyStorePath, password);
mockWebServer.useHttps(certificate.sslSocketFactory(), false);

EsConnectorImpl underTest = new EsConnectorImpl(Sets.newHashSet(HostAndPort.fromParts(mockWebServer.getHostName(),
mockWebServer.getPort())), null, keyStorePath, password);

assertThat(underTest.getClusterHealthStatus()).hasValue(ClusterHealthStatus.YELLOW);
}

private HandshakeCertificates createCertificate(String hostName, Path keyStorePath, String password)
throws GeneralSecurityException, IOException {
HeldCertificate localhostCertificate = new HeldCertificate.Builder()
.addSubjectAlternativeName(hostName)
.build();

try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(null);
ks.setKeyEntry("alias", localhostCertificate.keyPair().getPrivate(), password.toCharArray(),
new java.security.cert.Certificate[]{localhostCertificate.certificate()});
ks.store(baos, password.toCharArray());

try (OutputStream outputStream = Files.newOutputStream(keyStorePath)) {
outputStream.write(baos.toByteArray());
}
}

return new HandshakeCertificates.Builder()
.heldCertificate(localhostCertificate)
.build();
}

private void mockServerResponse(int httpCode, String jsonResponse) {
mockWebServer.enqueue(new MockResponse()
.setResponseCode(200)

+ 31
- 0
server/sonar-main/src/test/java/org/sonar/application/es/EsInstallationTest.java Wyświetl plik

@@ -29,6 +29,9 @@ import org.sonar.process.Props;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ENABLED;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_SEARCH_PASSWORD;
import static org.sonar.process.ProcessProperties.Property.PATH_DATA;
import static org.sonar.process.ProcessProperties.Property.PATH_HOME;
import static org.sonar.process.ProcessProperties.Property.PATH_LOGS;
@@ -201,4 +204,32 @@ public class EsInstallationTest {

assertThat(underTest.getJvmOptions()).isEqualTo(new File(tempDir, "conf/es/jvm.options"));
}

@Test
public void isHttpEncryptionEnabled_shouldReturnCorrectValue() throws IOException {
File sqHomeDir = temp.newFolder();
Props props = new Props(new Properties());
props.set(PATH_DATA.getKey(), temp.newFolder().getAbsolutePath());
props.set(PATH_HOME.getKey(), temp.newFolder().getAbsolutePath());
props.set(PATH_TEMP.getKey(), sqHomeDir.getAbsolutePath());
props.set(PATH_LOGS.getKey(), temp.newFolder().getAbsolutePath());
props.set(CLUSTER_ENABLED.getKey(), "true");
props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "password");
props.set(CLUSTER_ES_HTTP_KEYSTORE.getKey(), sqHomeDir.getAbsolutePath());

EsInstallation underTest = new EsInstallation(props);
assertThat(underTest.isHttpEncryptionEnabled()).isTrue();

props.set(CLUSTER_ENABLED.getKey(), "false");
props.set(CLUSTER_ES_HTTP_KEYSTORE.getKey(), sqHomeDir.getAbsolutePath());

underTest = new EsInstallation(props);
assertThat(underTest.isHttpEncryptionEnabled()).isFalse();

props.set(CLUSTER_ENABLED.getKey(), "true");
props.rawProperties().remove(CLUSTER_ES_HTTP_KEYSTORE.getKey());

underTest = new EsInstallation(props);
assertThat(underTest.isHttpEncryptionEnabled()).isFalse();
}
}

+ 61
- 5
server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java Wyświetl plik

@@ -50,6 +50,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HOSTS;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_TRUSTSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_NAME;
@@ -367,7 +368,7 @@ public class EsSettingsTest {
}

@Test
public void configureAuthentication_givenClusterSearchPasswordNotProvided_dontAddXpackParameters() throws Exception {
public void configureSecurity_givenClusterSearchPasswordNotProvided_dontAddXpackParameters() throws Exception {
Props props = minProps(true);

EsSettings settings = new EsSettings(props, new EsInstallation(props), system);
@@ -378,7 +379,7 @@ public class EsSettingsTest {
}

@Test
public void configureAuthentication_givenClusterSearchPasswordProvided_addXpackParameters_file_exists() throws Exception {
public void configureSecurity_givenClusterSearchPasswordProvided_addXpackParameters_file_exists() throws Exception {
Props props = minProps(true);
props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "qwerty");
File keystore = temp.newFile("keystore.p12");
@@ -399,7 +400,7 @@ public class EsSettingsTest {
}

@Test
public void configureAuthentication_givenClusterSearchPasswordProvidedButKeystorePathMissing_throwException() throws Exception {
public void configureSecurity_givenClusterSearchPasswordProvidedButKeystorePathMissing_throwException() throws Exception {
Props props = minProps(true);
props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "qwerty");

@@ -411,7 +412,7 @@ public class EsSettingsTest {
}

@Test
public void configureAuthentication_givenClusterModeFalse_dontAddXpackParameters() throws Exception {
public void configureSecurity_givenClusterModeFalse_dontAddXpackParameters() throws Exception {
Props props = minProps(false);
props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "qwerty");

@@ -423,7 +424,7 @@ public class EsSettingsTest {
}

@Test
public void configureAuthentication_givenFileNotExist_throwException() throws Exception {
public void configureSecurity_givenFileNotExist_throwException() throws Exception {
Props props = minProps(true);
props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "qwerty");
File truststore = temp.newFile("truststore.p12");
@@ -438,6 +439,61 @@ public class EsSettingsTest {
.hasMessage("Unable to configure: sonar.cluster.es.ssl.keystore. File specified in [not-existing-file] does not exist");
}

@Test
public void configureSecurity_whenHttpKeystoreProvided_shouldAddHttpProperties() throws Exception {
Props props = minProps(true);
File keystore = temp.newFile("keystore.p12");
File truststore = temp.newFile("truststore.p12");
File httpKeystore = temp.newFile("http-keystore.p12");
props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "qwerty");
props.set(CLUSTER_ES_KEYSTORE.getKey(), keystore.getAbsolutePath());
props.set(CLUSTER_ES_TRUSTSTORE.getKey(), truststore.getAbsolutePath());
props.set(CLUSTER_ES_HTTP_KEYSTORE.getKey(), httpKeystore.getAbsolutePath());

EsSettings settings = new EsSettings(props, new EsInstallation(props), system);

Map<String, String> outputParams = settings.build();

assertThat(outputParams)
.containsEntry("xpack.security.http.ssl.enabled", "true")
.containsEntry("xpack.security.http.ssl.keystore.path", httpKeystore.getName());
}

@Test
public void configureSecurity_whenHttpKeystoreNotProvided_shouldNotAddHttpProperties() throws Exception {
Props props = minProps(true);
File keystore = temp.newFile("keystore.p12");
File truststore = temp.newFile("truststore.p12");
props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "qwerty");
props.set(CLUSTER_ES_KEYSTORE.getKey(), keystore.getAbsolutePath());
props.set(CLUSTER_ES_TRUSTSTORE.getKey(), truststore.getAbsolutePath());

EsSettings settings = new EsSettings(props, new EsInstallation(props), system);

Map<String, String> outputParams = settings.build();

assertThat(outputParams)
.doesNotContainKey("xpack.security.http.ssl.enabled")
.doesNotContainKey("xpack.security.http.ssl.keystore.path");
}

@Test
public void configureSecurity_whenHttpKeystoreProvided_shouldFailIfNotExists() throws Exception {
Props props = minProps(true);
File keystore = temp.newFile("keystore.p12");
File truststore = temp.newFile("truststore.p12");
props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "qwerty");
props.set(CLUSTER_ES_KEYSTORE.getKey(), keystore.getAbsolutePath());
props.set(CLUSTER_ES_TRUSTSTORE.getKey(), truststore.getAbsolutePath());
props.set(CLUSTER_ES_HTTP_KEYSTORE.getKey(), "not-existing-file");

EsSettings settings = new EsSettings(props, new EsInstallation(props), system);

assertThatThrownBy(settings::build)
.isInstanceOf(MessageException.class)
.hasMessage("Unable to configure: sonar.cluster.es.http.ssl.keystore. File specified in [not-existing-file] does not exist");
}

@DataProvider
public static Object[][] clusterEnabledOrNot() {
return new Object[][] {

+ 2
- 0
server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java Wyświetl plik

@@ -135,6 +135,8 @@ public class ProcessProperties {
CLUSTER_ES_TRUSTSTORE("sonar.cluster.es.ssl.truststore"),
CLUSTER_ES_KEYSTORE_PASSWORD("sonar.cluster.es.ssl.keystorePassword"),
CLUSTER_ES_TRUSTSTORE_PASSWORD("sonar.cluster.es.ssl.truststorePassword"),
CLUSTER_ES_HTTP_KEYSTORE("sonar.cluster.es.http.ssl.keystore"),
CLUSTER_ES_HTTP_KEYSTORE_PASSWORD("sonar.cluster.es.http.ssl.keystorePassword"),
// search node only settings
CLUSTER_ES_HOSTS("sonar.cluster.es.hosts"),
CLUSTER_ES_DISCOVERY_SEED_HOSTS("sonar.cluster.es.discovery.seed.hosts"),

+ 1
- 0
server/sonar-server-common/build.gradle Wyświetl plik

@@ -31,6 +31,7 @@ dependencies {
testImplementation 'com.google.code.findbugs:jsr305'
testImplementation 'org.subethamail:subethasmtp'
testImplementation 'com.squareup.okhttp3:mockwebserver'
testImplementation 'com.squareup.okhttp3:okhttp-tls'
testImplementation 'com.squareup.okio:okio'
testImplementation 'com.tngtech.java:junit-dataprovider'
testImplementation 'junit:junit'

+ 37
- 10
server/sonar-server-common/src/main/java/org/sonar/server/es/EsClient.java Wyświetl plik

@@ -25,13 +25,21 @@ import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
@@ -41,9 +49,6 @@ import org.elasticsearch.action.admin.indices.cache.clear.ClearIndicesCacheRespo
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequest;
import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeResponse;
import org.elasticsearch.client.indices.GetMappingsRequest;
import org.elasticsearch.client.indices.GetMappingsResponse;
import org.elasticsearch.client.indices.PutMappingRequest;
import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
import org.elasticsearch.action.admin.indices.refresh.RefreshResponse;
import org.elasticsearch.action.admin.indices.settings.get.GetSettingsRequest;
@@ -75,6 +80,9 @@ import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.client.indices.GetIndexResponse;
import org.elasticsearch.client.indices.GetMappingsRequest;
import org.elasticsearch.client.indices.GetMappingsResponse;
import org.elasticsearch.client.indices.PutMappingRequest;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.elasticsearch.common.Priority;
import org.jetbrains.annotations.NotNull;
@@ -98,11 +106,11 @@ public class EsClient implements Closeable {
private final Gson gson;

public EsClient(HttpHost... hosts) {
this(new MinimalRestHighLevelClient(null, hosts));
this(new MinimalRestHighLevelClient(null, null, null, hosts));
}

public EsClient(@Nullable String searchPassword, HttpHost... hosts) {
this(new MinimalRestHighLevelClient(searchPassword, hosts));
public EsClient(@Nullable String searchPassword, @Nullable String keyStorePath, @Nullable String keyStorePassword, HttpHost... hosts) {
this(new MinimalRestHighLevelClient(searchPassword, keyStorePath, keyStorePassword, hosts));
}

EsClient(RestHighLevelClient restHighLevelClient) {
@@ -265,8 +273,9 @@ public class EsClient implements Closeable {
private static final int CONNECT_TIMEOUT = 5000;
private static final int SOCKET_TIMEOUT = 60000;

public MinimalRestHighLevelClient(@Nullable String searchPassword, HttpHost... hosts) {
super(buildHttpClient(searchPassword, hosts).build(), RestClient::close, Lists.newArrayList(), true);
public MinimalRestHighLevelClient(@Nullable String searchPassword, @Nullable String keyStorePath,
@Nullable String keyStorePassword, HttpHost... hosts) {
super(buildHttpClient(searchPassword, keyStorePath, keyStorePassword, hosts).build(), RestClient::close, Lists.newArrayList(), true);
}

MinimalRestHighLevelClient(RestClient restClient) {
@@ -274,8 +283,8 @@ public class EsClient implements Closeable {
}

@NotNull
private static RestClientBuilder buildHttpClient(@Nullable String searchPassword,
HttpHost[] hosts) {
private static RestClientBuilder buildHttpClient(@Nullable String searchPassword, @Nullable String keyStorePath,
@Nullable String keyStorePassword, HttpHost[] hosts) {
return RestClient.builder(hosts)
.setRequestConfigCallback(r -> r
.setConnectTimeout(CONNECT_TIMEOUT)
@@ -285,6 +294,12 @@ public class EsClient implements Closeable {
BasicCredentialsProvider provider = getBasicCredentialsProvider(searchPassword);
httpClientBuilder.setDefaultCredentialsProvider(provider);
}

if (keyStorePath != null) {
SSLContext sslContext = getSSLContext(keyStorePath, keyStorePassword);
httpClientBuilder.setSSLContext(sslContext);
}

return httpClientBuilder;
});
}
@@ -295,6 +310,18 @@ public class EsClient implements Closeable {
return provider;
}

private static SSLContext getSSLContext(String keyStorePath, @Nullable String keyStorePassword) {
try {
KeyStore keyStore = KeyStore.getInstance("pkcs12");
try (InputStream is = Files.newInputStream(Paths.get(keyStorePath))) {
keyStore.load(is, keyStorePassword == null ? null : keyStorePassword.toCharArray());
}
SSLContextBuilder sslBuilder = SSLContexts.custom().loadTrustMaterial(keyStore, null);
return sslBuilder.build();
} catch (IOException | GeneralSecurityException e) {
throw new IllegalStateException("Failed to setup SSL context on ES client", e);
}
}
}

<R> R execute(EsRequestExecutor<R> executor) {

+ 11
- 5
server/sonar-server-common/src/main/java/org/sonar/server/es/EsClientProvider.java Wyświetl plik

@@ -37,6 +37,8 @@ import org.sonar.process.cluster.NodeType;
import org.springframework.context.annotation.Bean;

import static org.sonar.process.ProcessProperties.Property.CLUSTER_ENABLED;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE_PASSWORD;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_NAME;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_NODE_TYPE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_SEARCH_HOSTS;
@@ -69,23 +71,27 @@ public class EsClientProvider {
// * in org.sonar.process.ProcessProperties.Property.SEARCH_HOST
// * in org.sonar.process.ProcessProperties.Property.SEARCH_PORT
HostAndPort host = HostAndPort.fromParts(config.get(SEARCH_HOST.getKey()).get(), config.getInt(SEARCH_PORT.getKey()).get());
httpHosts = Collections.singletonList(toHttpHost(host));
httpHosts = Collections.singletonList(toHttpHost(host, config));
LOGGER.info("Connected to local Elasticsearch: [{}]", displayedAddresses(httpHosts));
}

return new EsClient(config.get(CLUSTER_SEARCH_PASSWORD.getKey()).orElse(null), httpHosts.toArray(new HttpHost[0]));
return new EsClient(config.get(CLUSTER_SEARCH_PASSWORD.getKey()).orElse(null),
config.get(CLUSTER_ES_HTTP_KEYSTORE.getKey()).orElse(null),
config.get(CLUSTER_ES_HTTP_KEYSTORE_PASSWORD.getKey()).orElse(null),
httpHosts.toArray(new HttpHost[0]));
}

private static List<HttpHost> getHttpHosts(Configuration config) {
return Arrays.stream(config.getStringArray(CLUSTER_SEARCH_HOSTS.getKey()))
.map(HostAndPort::fromString)
.map(EsClientProvider::toHttpHost)
.map(host -> toHttpHost(host, config))
.toList();
}

private static HttpHost toHttpHost(HostAndPort host) {
private static HttpHost toHttpHost(HostAndPort host, Configuration config) {
try {
return new HttpHost(InetAddress.getByName(host.getHost()), host.getPortOrDefault(9001));
String scheme = config.get(CLUSTER_ES_HTTP_KEYSTORE.getKey()).isPresent() ? "https" : HttpHost.DEFAULT_SCHEME_NAME;
return new HttpHost(InetAddress.getByName(host.getHost()), host.getPortOrDefault(9001), scheme);
} catch (UnknownHostException e) {
throw new IllegalStateException("Can not resolve host [" + host + "]", e);
}

+ 26
- 0
server/sonar-server-common/src/test/java/org/sonar/server/es/EsClientProviderTest.java Wyświetl plik

@@ -19,13 +19,17 @@
*/
package org.sonar.server.es;

import java.io.IOException;
import java.net.InetAddress;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import org.assertj.core.api.Condition;
import org.elasticsearch.client.Node;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.slf4j.event.Level;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.testfixtures.log.LogTester;
@@ -34,6 +38,7 @@ import static java.lang.String.format;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ENABLED;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_NAME;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_NODE_TYPE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_SEARCH_HOSTS;
@@ -44,6 +49,8 @@ import static org.sonar.process.ProcessProperties.Property.SEARCH_PORT;
public class EsClientProviderTest {
@Rule
public LogTester logTester = new LogTester();
@Rule
public TemporaryFolder temp = new TemporaryFolder();

private MapSettings settings = new MapSettings();
private EsClientProvider underTest = new EsClientProvider();
@@ -140,4 +147,23 @@ public class EsClientProviderTest {
assertThat(logTester.logs(Level.INFO))
.has(new Condition<>(s -> s.contains("Connected to remote Elasticsearch: [http://" + localhostHostname + ":9001, http://" + localhostHostname + ":8081]"), ""));
}

@Test
public void provide_whenHttpEncryptionEnabled_shouldUseHttps() throws GeneralSecurityException, IOException {
settings.setProperty(CLUSTER_ENABLED.getKey(), true);
Path keyStorePath = temp.newFile("keystore.p12").toPath();
EsClientTest.createCertificate("localhost", keyStorePath, "password");
settings.setProperty(CLUSTER_ES_HTTP_KEYSTORE.getKey(), keyStorePath.toString());
settings.setProperty(CLUSTER_NODE_TYPE.getKey(), "application");
settings.setProperty(CLUSTER_SEARCH_HOSTS.getKey(), format("%s,%s:8081", localhostHostname, localhostHostname));

EsClient client = underTest.provide(settings.asConfig());
RestHighLevelClient nativeClient = client.nativeClient();

Node node = nativeClient.getLowLevelClient().getNodes().get(0);
assertThat(node.getHost().getSchemeName()).isEqualTo("https");

assertThat(logTester.logs(Level.INFO))
.has(new Condition<>(s -> s.contains("Connected to remote Elasticsearch: [https://" + localhostHostname + ":9001, https://" + localhostHostname + ":8081]"), ""));
}
}

+ 60
- 8
server/sonar-server-common/src/test/java/org/sonar/server/es/EsClientTest.java Wyświetl plik

@@ -20,10 +20,18 @@
package org.sonar.server.es;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Objects;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.tls.HandshakeCertificates;
import okhttp3.tls.HeldCertificate;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.elasticsearch.client.Request;
@@ -32,6 +40,7 @@ import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.ArgumentMatcher;

import static org.assertj.core.api.Assertions.assertThat;
@@ -151,6 +160,8 @@ public class EsClientTest {

@Rule
public MockWebServer mockWebServer = new MockWebServer();
@Rule
public TemporaryFolder temp = new TemporaryFolder();

RestClient restClient = mock(RestClient.class);
RestHighLevelClient client = new EsClient.MinimalRestHighLevelClient(restClient);
@@ -179,7 +190,7 @@ public class EsClientTest {
when(restClient.performRequest(argThat(new RawRequestMatcher(
"GET",
"/_nodes/stats/fs,process,jvm,indices,breaker"))))
.thenReturn(response);
.thenReturn(response);

assertThat(underTest.nodesStats()).isNotNull();
}
@@ -189,7 +200,7 @@ public class EsClientTest {
when(restClient.performRequest(argThat(new RawRequestMatcher(
"GET",
"/_nodes/stats/fs,process,jvm,indices,breaker"))))
.thenThrow(IOException.class);
.thenThrow(IOException.class);

assertThatThrownBy(() -> underTest.nodesStats())
.isInstanceOf(ElasticsearchException.class);
@@ -204,7 +215,7 @@ public class EsClientTest {
when(restClient.performRequest(argThat(new RawRequestMatcher(
"GET",
"/_stats"))))
.thenReturn(response);
.thenReturn(response);

assertThat(underTest.indicesStats()).isNotNull();
}
@@ -214,7 +225,7 @@ public class EsClientTest {
when(restClient.performRequest(argThat(new RawRequestMatcher(
"GET",
"/_stats"))))
.thenThrow(IOException.class);
.thenThrow(IOException.class);

assertThatThrownBy(() -> underTest.indicesStats())
.isInstanceOf(ElasticsearchException.class);
@@ -230,7 +241,7 @@ public class EsClientTest {
when(restClient.performRequest(argThat(new RawRequestMatcher(
"GET",
"/_cluster/stats"))))
.thenReturn(response);
.thenReturn(response);

assertThat(underTest.clusterStats()).isNotNull();
}
@@ -240,7 +251,7 @@ public class EsClientTest {
when(restClient.performRequest(argThat(new RawRequestMatcher(
"GET",
"/_cluster/stats"))))
.thenThrow(IOException.class);
.thenThrow(IOException.class);

assertThatThrownBy(() -> underTest.clusterStats())
.isInstanceOf(ElasticsearchException.class);
@@ -254,13 +265,54 @@ public class EsClientTest {
.setHeader("Content-Type", "application/json"));

String password = "test-password";
EsClient underTest = new EsClient(password, new HttpHost(mockWebServer.getHostName(), mockWebServer.getPort()));
EsClient underTest = new EsClient(password, null, null, new HttpHost(mockWebServer.getHostName(), mockWebServer.getPort()));

underTest.clusterStats();
assertThat(underTest.clusterStats()).isNotNull();

assertThat(mockWebServer.takeRequest().getHeader("Authorization")).isEqualTo("Basic ZWxhc3RpYzp0ZXN0LXBhc3N3b3Jk");
}

@Test
public void newInstance_whenKeyStorePassed_shouldCreateClient() throws GeneralSecurityException, IOException {
mockWebServer.enqueue(new MockResponse()
.setResponseCode(200)
.setBody(EXAMPLE_CLUSTER_STATS_JSON)
.setHeader("Content-Type", "application/json"));

Path keyStorePath = temp.newFile("keystore.p12").toPath();
String password = "password";

HandshakeCertificates certificate = createCertificate(mockWebServer.getHostName(), keyStorePath, password);
mockWebServer.useHttps(certificate.sslSocketFactory(), false);

EsClient underTest = new EsClient(null, keyStorePath.toString(), password,
new HttpHost(mockWebServer.getHostName(), mockWebServer.getPort(), "https"));

assertThat(underTest.clusterStats()).isNotNull();
}

static HandshakeCertificates createCertificate(String hostName, Path keyStorePath, String password) throws GeneralSecurityException, IOException {
HeldCertificate localhostCertificate = new HeldCertificate.Builder()
.addSubjectAlternativeName(hostName)
.build();

try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(null);
ks.setKeyEntry("alias", localhostCertificate.keyPair().getPrivate(), password.toCharArray(),
new java.security.cert.Certificate[]{localhostCertificate.certificate()});
ks.store(baos, password.toCharArray());

try (OutputStream outputStream = Files.newOutputStream(keyStorePath)) {
outputStream.write(baos.toByteArray());
}
}

return new HandshakeCertificates.Builder()
.heldCertificate(localhostCertificate)
.build();
}

static class RawRequestMatcher implements ArgumentMatcher<Request> {
String endpoint;
String method;

Ładowanie…
Anuluj
Zapisz