From df28c5d95838beec4bb98059baab9b2cd70022fe Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 19 Apr 2021 11:30:44 +0200 Subject: [PATCH] SONAR-14583 Use Elasticsearch password in HTTP clients --- .../sonar/application/AppStateFactory.java | 4 +- .../application/ProcessLauncherImpl.java | 3 +- .../sonar/application/es/EsConnectorImpl.java | 27 +++++++++++- .../application/es/EsConnectorImplTest.java | 14 ++++++- .../java/org/sonar/server/es/EsClient.java | 41 +++++++++++++++++-- .../org/sonar/server/es/EsClientProvider.java | 5 ++- .../org/sonar/server/es/EsClientTest.java | 22 ++++++++++ 7 files changed, 105 insertions(+), 11 deletions(-) diff --git a/server/sonar-main/src/main/java/org/sonar/application/AppStateFactory.java b/server/sonar-main/src/main/java/org/sonar/application/AppStateFactory.java index 28c2e0001a4..936dcb64996 100644 --- a/server/sonar-main/src/main/java/org/sonar/application/AppStateFactory.java +++ b/server/sonar-main/src/main/java/org/sonar/application/AppStateFactory.java @@ -41,6 +41,7 @@ import static org.sonar.process.ProcessProperties.Property.CLUSTER_NODE_HOST; import static org.sonar.process.ProcessProperties.Property.CLUSTER_NODE_HZ_PORT; import static org.sonar.process.ProcessProperties.Property.CLUSTER_NODE_NAME; import static org.sonar.process.ProcessProperties.Property.CLUSTER_SEARCH_HOSTS; +import static org.sonar.process.ProcessProperties.Property.CLUSTER_SEARCH_PASSWORD; public class AppStateFactory { private final AppSettings settings; @@ -74,6 +75,7 @@ public class AppStateFactory { Set hostAndPorts = Arrays.stream(searchHosts.split(",")) .map(HostAndPort::fromString) .collect(Collectors.toSet()); - return new EsConnectorImpl(hostAndPorts); + String searchPassword = props.value(CLUSTER_SEARCH_PASSWORD.getKey()); + return new EsConnectorImpl(hostAndPorts, searchPassword); } } diff --git a/server/sonar-main/src/main/java/org/sonar/application/ProcessLauncherImpl.java b/server/sonar-main/src/main/java/org/sonar/application/ProcessLauncherImpl.java index 657d3a693e8..de47af4f884 100644 --- a/server/sonar-main/src/main/java/org/sonar/application/ProcessLauncherImpl.java +++ b/server/sonar-main/src/main/java/org/sonar/application/ProcessLauncherImpl.java @@ -32,7 +32,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Properties; import java.util.function.Supplier; import org.slf4j.Logger; @@ -107,7 +106,7 @@ 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.getHttpPort())), esInstallation.getBootstrapPassword()); return new EsManagedProcess(process, processId, esConnector); } else { ProcessCommands commands = allProcessesCommands.createAfterClean(processId.getIpcIndex()); diff --git a/server/sonar-main/src/main/java/org/sonar/application/es/EsConnectorImpl.java b/server/sonar-main/src/main/java/org/sonar/application/es/EsConnectorImpl.java index f5dbca10dcc..1c5a3263df1 100644 --- a/server/sonar-main/src/main/java/org/sonar/application/es/EsConnectorImpl.java +++ b/server/sonar-main/src/main/java/org/sonar/application/es/EsConnectorImpl.java @@ -26,11 +26,16 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; +import javax.annotation.Nullable; 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.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.slf4j.Logger; @@ -39,14 +44,17 @@ import org.slf4j.LoggerFactory; import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; public class EsConnectorImpl implements EsConnector { + private static final String ES_USERNAME = "elastic"; private static final Logger LOG = LoggerFactory.getLogger(EsConnectorImpl.class); private final AtomicReference restClient = new AtomicReference<>(null); private final Set hostAndPorts; + private final String searchPassword; - public EsConnectorImpl(Set hostAndPorts) { + public EsConnectorImpl(Set hostAndPorts, @Nullable String searchPassword) { this.hostAndPorts = hostAndPorts; + this.searchPassword = searchPassword; } @Override @@ -95,7 +103,22 @@ public class EsConnectorImpl implements EsConnector { .collect(Collectors.joining(", ")); LOG.debug("Connected to Elasticsearch node: [{}]", addresses); } - return new RestHighLevelClient(RestClient.builder(httpHosts)); + + RestClientBuilder builder = RestClient.builder(httpHosts) + .setHttpClientConfigCallback(httpClientBuilder -> { + if (searchPassword != null) { + BasicCredentialsProvider provider = getBasicCredentialsProvider(searchPassword); + httpClientBuilder.setDefaultCredentialsProvider(provider); + } + return httpClientBuilder; + }); + return new RestHighLevelClient(builder); + } + + private static BasicCredentialsProvider getBasicCredentialsProvider(String searchPassword) { + BasicCredentialsProvider provider = new BasicCredentialsProvider(); + provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(ES_USERNAME, searchPassword)); + return provider; } } diff --git a/server/sonar-main/src/test/java/org/sonar/application/es/EsConnectorImplTest.java b/server/sonar-main/src/test/java/org/sonar/application/es/EsConnectorImplTest.java index d92dfb3f0b2..9c5e3d0d914 100644 --- a/server/sonar-main/src/test/java/org/sonar/application/es/EsConnectorImplTest.java +++ b/server/sonar-main/src/test/java/org/sonar/application/es/EsConnectorImplTest.java @@ -59,7 +59,7 @@ public class EsConnectorImplTest { @Rule public MockWebServer mockWebServer = new MockWebServer(); - EsConnectorImpl underTest = new EsConnectorImpl(Sets.newHashSet(HostAndPort.fromParts(mockWebServer.getHostName(), mockWebServer.getPort()))); + EsConnectorImpl underTest = new EsConnectorImpl(Sets.newHashSet(HostAndPort.fromParts(mockWebServer.getHostName(), mockWebServer.getPort())), null); @After public void after() { @@ -82,6 +82,18 @@ public class EsConnectorImplTest { .hasValue(ClusterHealthStatus.YELLOW); } + @Test + public void should_add_authentication_header() throws InterruptedException { + mockServerResponse(200, JSON_SUCCESS_RESPONSE); + String password = "test-password"; + + EsConnectorImpl underTest = new EsConnectorImpl(Sets.newHashSet(HostAndPort.fromParts(mockWebServer.getHostName(), mockWebServer.getPort())), password); + + assertThat(underTest.getClusterHealthStatus()) + .hasValue(ClusterHealthStatus.YELLOW); + assertThat(mockWebServer.takeRequest().getHeader("Authorization")).isEqualTo("Basic ZWxhc3RpYzp0ZXN0LXBhc3N3b3Jk"); + } + private void mockServerResponse(int httpCode, String jsonResponse) { mockWebServer.enqueue(new MockResponse() .setResponseCode(httpCode) diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/es/EsClient.java b/server/sonar-server-common/src/main/java/org/sonar/server/es/EsClient.java index cb82f3a5161..eeb66afc1ac 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/es/EsClient.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/es/EsClient.java @@ -27,7 +27,11 @@ import java.io.Closeable; import java.io.IOException; import java.util.Arrays; import java.util.function.Supplier; +import javax.annotation.Nullable; 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.util.EntityUtils; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; @@ -65,6 +69,7 @@ import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Requests; import org.elasticsearch.client.Response; import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.client.indices.CreateIndexResponse; @@ -72,6 +77,7 @@ import org.elasticsearch.client.indices.GetIndexRequest; import org.elasticsearch.client.indices.GetIndexResponse; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.common.Priority; +import org.jetbrains.annotations.NotNull; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.api.utils.log.Profiler; @@ -86,13 +92,18 @@ import static org.sonar.server.es.EsRequestDetails.computeDetailsAsString; * with context) and profiling of requests. */ public class EsClient implements Closeable { + private static final String ES_USERNAME = "elastic"; private final RestHighLevelClient restHighLevelClient; private final Gson gson; public static final Logger LOGGER = Loggers.get("es"); public EsClient(HttpHost... hosts) { - this(new MinimalRestHighLevelClient(hosts)); + this(new MinimalRestHighLevelClient(null, hosts)); + } + + public EsClient(@Nullable String searchPassword, HttpHost... hosts) { + this(new MinimalRestHighLevelClient(searchPassword, hosts)); } EsClient(RestHighLevelClient restHighLevelClient) { @@ -256,9 +267,33 @@ public class EsClient implements Closeable { } static class MinimalRestHighLevelClient extends RestHighLevelClient { + 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)); + } + + @NotNull + private static RestClientBuilder buildHttpClient(@Nullable String searchPassword, + HttpHost[] hosts) { + return RestClient.builder(hosts) + .setRequestConfigCallback(r -> r + .setConnectTimeout(CONNECT_TIMEOUT) + .setSocketTimeout(SOCKET_TIMEOUT)) + .setHttpClientConfigCallback(httpClientBuilder -> { + if (searchPassword != null) { + BasicCredentialsProvider provider = getBasicCredentialsProvider(searchPassword); + httpClientBuilder.setDefaultCredentialsProvider(provider); + } + return httpClientBuilder; + }); + } - public MinimalRestHighLevelClient(HttpHost... hosts) { - super(RestClient.builder(hosts)); + private static BasicCredentialsProvider getBasicCredentialsProvider(String searchPassword) { + BasicCredentialsProvider provider = new BasicCredentialsProvider(); + provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(ES_USERNAME, searchPassword)); + return provider; } MinimalRestHighLevelClient(RestClient restClient) { diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/es/EsClientProvider.java b/server/sonar-server-common/src/main/java/org/sonar/server/es/EsClientProvider.java index 051cd170936..7c1dd55de9e 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/es/EsClientProvider.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/es/EsClientProvider.java @@ -40,6 +40,7 @@ import static org.sonar.process.ProcessProperties.Property.CLUSTER_ENABLED; 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; +import static org.sonar.process.ProcessProperties.Property.CLUSTER_SEARCH_PASSWORD; import static org.sonar.process.ProcessProperties.Property.SEARCH_HOST; import static org.sonar.process.ProcessProperties.Property.SEARCH_PORT; import static org.sonar.process.cluster.NodeType.SEARCH; @@ -67,7 +68,7 @@ public class EsClientProvider extends ProviderAdapter { LOGGER.info("Connected to remote Elasticsearch: [{}]", displayedAddresses(httpHosts)); } else { - //defaults provided in: + // defaults provided in: // * 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()); @@ -75,7 +76,7 @@ public class EsClientProvider extends ProviderAdapter { LOGGER.info("Connected to local Elasticsearch: [{}]", displayedAddresses(httpHosts)); } - cache = new EsClient(httpHosts.toArray(new HttpHost[0])); + cache = new EsClient(config.get(CLUSTER_SEARCH_PASSWORD.getKey()).orElse(null), httpHosts.toArray(new HttpHost[0])); } return cache; } diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/es/EsClientTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/es/EsClientTest.java index 3f048ed39c8..7c9cc59da2c 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/es/EsClientTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/es/EsClientTest.java @@ -22,11 +22,15 @@ package org.sonar.server.es; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.Objects; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestHighLevelClient; +import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentMatcher; @@ -144,6 +148,9 @@ public class EsClientTest { " }" + "}"; + @Rule + public MockWebServer mockWebServer = new MockWebServer(); + RestClient restClient = mock(RestClient.class); RestHighLevelClient client = new EsClient.MinimalRestHighLevelClient(restClient); @@ -231,6 +238,21 @@ public class EsClientTest { underTest.clusterStats(); } + @Test + public void should_add_authentication_header() throws InterruptedException { + mockWebServer.enqueue(new MockResponse() + .setResponseCode(200) + .setBody(EXAMPLE_CLUSTER_STATS_JSON) + .setHeader("Content-Type", "application/json")); + + String password = "test-password"; + EsClient underTest = new EsClient(password, new HttpHost(mockWebServer.getHostName(), mockWebServer.getPort())); + + underTest.clusterStats(); + + assertThat(mockWebServer.takeRequest().getHeader("Authorization")).isEqualTo("Basic ZWxhc3RpYzp0ZXN0LXBhc3N3b3Jk"); + } + static class RawRequestMatcher implements ArgumentMatcher { String endpoint; String method; -- 2.39.5