From 392d41cdfc989d08cf5b79ea9a20e1f82665b447 Mon Sep 17 00:00:00 2001 From: "mark.j.leonard" Date: Wed, 26 Sep 2018 11:58:30 +0100 Subject: [PATCH] Implement client authentication to ElasticSearch Add configuration to the existing Elastic Search properties to allow Basic Authentication and/or TLS (SSL) connectivity using HTTPS. The new configuration is optional and this commit is intended to be backwards compatible with existing deployments. Change-Id: I19ec3da9ff810c3f6eabd6f5faf71adde182c861 Issue-ID: AAI-1650 Signed-off-by: mark.j.leonard --- README.md | 17 ++- .../elasticsearch/config/ElasticSearchConfig.java | 134 ++++++++++++++++++- .../dao/ElasticSearchHttpController.java | 9 +- .../dao/ElasticSearchHttpsController.java | 148 +++++++++++++++++++++ .../dao/ElasticSearchHttpControllerTest.java | 17 ++- 5 files changed, 310 insertions(+), 15 deletions(-) create mode 100644 src/main/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/dao/ElasticSearchHttpsController.java diff --git a/README.md b/README.md index 506cc05..ae2127d 100644 --- a/README.md +++ b/README.md @@ -100,13 +100,20 @@ Create this file with exactly the contents below: _elastic-search.properties_ -This file tells the _Search Data Service_ how to communicate with the ElasticSearch data store which it will use for its back end. +This properties file configures the _Search Data Service_ for communication with ElasticSearch. The contents of this file will be determined by your ElasticSearch deployment: - es-cluster-name=<> - es-ip-address=<> - ex.http-port=9200 - + es.cluster-name=<> + es.ip-address=<> + es.http-port=9200 + # Optional parameters + es.uri-scheme=<> + es.trust-store=<> + es.trust-store-password=<> + es.key-store=<> + es.key-store-password=<> + es.auth-user=<> + es.auth-password=<> ##### Contents of the /opt/app/search-data-service/app-config/auth Directory diff --git a/src/main/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/config/ElasticSearchConfig.java b/src/main/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/config/ElasticSearchConfig.java index 1bf1db7..f5cb9da 100644 --- a/src/main/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/config/ElasticSearchConfig.java +++ b/src/main/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/config/ElasticSearchConfig.java @@ -20,9 +20,22 @@ */ package org.onap.aai.sa.searchdbabstraction.elasticsearch.config; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Optional; import java.util.Properties; +import org.eclipse.jetty.util.security.Password; +import org.onap.aai.sa.searchdbabstraction.util.SearchDbConstants; public class ElasticSearchConfig { + + private String uriScheme; + private String trustStore; + private String trustStorePassword; + private String keyStore; + private String keyStorePassword; + private String authUser; + private String authPassword; private String ipAddress; private String httpPort; private String javaApiPort; @@ -31,15 +44,33 @@ public class ElasticSearchConfig { public static final String ES_CLUSTER_NAME = "es.cluster-name"; public static final String ES_IP_ADDRESS = "es.ip-address"; public static final String ES_HTTP_PORT = "es.http-port"; - + public static final String ES_URI_SCHEME = "es.uri-scheme"; + public static final String ES_TRUST_STORE = "es.trust-store"; + public static final String ES_TRUST_STORE_ENC = "es.trust-store-password"; + public static final String ES_KEY_STORE = "es.key-store"; + public static final String ES_KEY_STORE_ENC = "es.key-store-password"; + public static final String ES_AUTH_USER = "es.auth-user"; + public static final String ES_AUTH_ENC = "es.auth-password"; + + private static final String DEFAULT_URI_SCHEME = "http"; private static final String JAVA_API_PORT_DEFAULT = "9300"; + private String authValue; public ElasticSearchConfig(Properties props) { - + setUriScheme(props.getProperty(ES_URI_SCHEME)); + if (getUriScheme().equals("https")) { + initializeHttpsProperties(props); + } setClusterName(props.getProperty(ES_CLUSTER_NAME)); setIpAddress(props.getProperty(ES_IP_ADDRESS)); setHttpPort(props.getProperty(ES_HTTP_PORT)); setJavaApiPort(JAVA_API_PORT_DEFAULT); + initializeAuthValues(props); + } + + + public String getUriScheme() { + return this.uriScheme; } public String getIpAddress() { @@ -74,10 +105,105 @@ public class ElasticSearchConfig { this.clusterName = clusterName; } + public void setKeyStore(String keyStore) { + this.keyStore = keyStore; + } + + public void setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + public String getKeyStorePath() { + return keyStore; + } + + public String getKeyStorePassword() { + return keyStorePassword; + } + + public String getTrustStorePath() { + return trustStore; + } + + public void setTrustStore(String trustStore) { + this.trustStore = trustStore; + } + + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + public String getTrustStorePassword() { + return trustStorePassword; + } + + public void setAuthUser(String authUser) { + this.authUser = authUser; + } + + public String getAuthUser() { + return authUser; + } + + public void setAuthPassword(String authPassword) { + this.authPassword = authPassword; + } + + public String getAuthPassword() { + return authPassword; + } + + public boolean useAuth() { + return getAuthUser() != null || getAuthPassword() != null; + } + + public String getAuthValue() { + return authValue; + } + @Override public String toString() { - return "ElasticSearchConfig [ipAddress=" + ipAddress + ", httpPort=" + httpPort + ", javaApiPort=" + javaApiPort - + ", clusterName=" + clusterName + "]"; + return String.format( + "%s://%s:%s (cluster=%s) (API port=%s)%nauth=%s%ntrustStore=%s (passwd %s)%nkeyStore=%s (passwd %s)", + uriScheme, ipAddress, httpPort, clusterName, javaApiPort, useAuth(), trustStore, + trustStorePassword != null, keyStore, keyStorePassword != null); } + private void initializeAuthValues(Properties props) { + setAuthUser(props.getProperty(ES_AUTH_USER)); + Optional passwordValue = Optional.ofNullable(props.getProperty(ES_AUTH_ENC)); + if (passwordValue.isPresent()) { + setAuthPassword(Password.deobfuscate(passwordValue.get())); + } + if (useAuth()) { + authValue = "Basic " + Base64.getEncoder() + .encodeToString((getAuthUser() + ":" + getAuthPassword()).getBytes(StandardCharsets.UTF_8)); + } + } + + private void initializeHttpsProperties(Properties props) { + Optional trustStoreFile = Optional.ofNullable(props.getProperty(ES_TRUST_STORE)); + if (trustStoreFile.isPresent()) { + setTrustStore(SearchDbConstants.SDB_SPECIFIC_CONFIG + trustStoreFile.get()); + } + + Optional passwordValue = Optional.ofNullable(props.getProperty(ES_TRUST_STORE_ENC)); + if (passwordValue.isPresent()) { + setTrustStorePassword(Password.deobfuscate(passwordValue.get())); + } + + Optional keyStoreFile = Optional.ofNullable(props.getProperty(ES_KEY_STORE)); + if (keyStoreFile.isPresent()) { + setKeyStore(SearchDbConstants.SDB_SPECIFIC_CONFIG + keyStoreFile.get()); + } + + passwordValue = Optional.ofNullable(props.getProperty(ES_KEY_STORE_ENC)); + if (passwordValue.isPresent()) { + setKeyStorePassword(Password.deobfuscate(passwordValue.get())); + } + } + + private void setUriScheme(String uriScheme) { + this.uriScheme = Optional.ofNullable(uriScheme).orElse(DEFAULT_URI_SCHEME); + } } diff --git a/src/main/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/dao/ElasticSearchHttpController.java b/src/main/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/dao/ElasticSearchHttpController.java index 6d08c1d..6087488 100644 --- a/src/main/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/dao/ElasticSearchHttpController.java +++ b/src/main/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/dao/ElasticSearchHttpController.java @@ -132,13 +132,15 @@ public class ElasticSearchHttpController implements DocumentStoreInterface { protected AnalysisConfiguration analysisConfig; - public ElasticSearchHttpController(ElasticSearchConfig config) { this.config = config; analysisConfig = new AnalysisConfiguration(); String rootUrl = null; try { + if ("https".equals(config.getUriScheme())) { + new ElasticSearchHttpsController(config); + } rootUrl = buildUrl(createUriBuilder("")).toString(); logger.info(SearchDbMsgs.ELASTIC_SEARCH_CONNECTION_ATTEMPT, rootUrl); checkConnection(); @@ -728,6 +730,9 @@ public class ElasticSearchHttpController implements DocumentStoreInterface { conn = (HttpURLConnection) url.openConnection(); conn.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON); conn.setDoOutput(true); + if (config.useAuth()) { + conn.setRequestProperty("Authorization", config.getAuthValue()); + } } catch (IOException e) { shutdownConnection(conn); throw new DocumentStoreOperationException("Failed to open connection to URL " + url, e); @@ -849,7 +854,7 @@ public class ElasticSearchHttpController implements DocumentStoreInterface { builder.host(config.getIpAddress()); String port = Optional.ofNullable(config.getHttpPort()).orElse("0"); builder.port(Integer.valueOf(port)); - builder.scheme("http"); + builder.scheme(config.getUriScheme()); return builder; } diff --git a/src/main/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/dao/ElasticSearchHttpsController.java b/src/main/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/dao/ElasticSearchHttpsController.java new file mode 100644 index 0000000..51b8952 --- /dev/null +++ b/src/main/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/dao/ElasticSearchHttpsController.java @@ -0,0 +1,148 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * Copyright © 2017-2018 Amdocs + * ================================================================================ + * 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 + * + * 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. + * ============LICENSE_END========================================================= + */ +package org.onap.aai.sa.searchdbabstraction.elasticsearch.dao; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import org.onap.aai.cl.api.Logger; +import org.onap.aai.cl.eelf.LoggerFactory; +import org.onap.aai.sa.searchdbabstraction.elasticsearch.config.ElasticSearchConfig; + +/** + * HTTPS (TLS) specific configuration. + */ +public class ElasticSearchHttpsController { + + private static final Logger logger = + LoggerFactory.getInstance().getLogger(ElasticSearchHttpsController.class.getName()); + + private static final String SSL_PROTOCOL = "TLS"; + private static final String KEYSTORE_ALGORITHM = "SunX509"; + private static final String KEYSTORE_TYPE = "PKCS12"; + + public ElasticSearchHttpsController(ElasticSearchConfig config) throws NoSuchAlgorithmException, KeyStoreException, + CertificateException, IOException, KeyManagementException, UnrecoverableKeyException { + logger.debug("Initialising HTTPS configuration"); + + SSLContext ctx = SSLContext.getInstance(SSL_PROTOCOL); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KEYSTORE_ALGORITHM); + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); + + String clientCertPassword = config.getKeyStorePassword(); + + char[] pwd = null; + if (clientCertPassword != null) { + pwd = clientCertPassword.toCharArray(); + } else { + logger.debug("No key store password is defined"); + } + + TrustManager[] trustManagers = getTrustManagers(config); + KeyManager[] keyManagers = null; + + String clientCertFileName = config.getKeyStorePath(); + if (clientCertFileName != null) { + InputStream fin = Files.newInputStream(Paths.get(clientCertFileName)); + keyStore.load(fin, pwd); + kmf.init(keyStore, pwd); + keyManagers = kmf.getKeyManagers(); + } + + ctx.init(keyManagers, trustManagers, null); + logger.debug("Initialised SSL context"); + + HttpsURLConnection.setDefaultSSLSocketFactory(ctx.getSocketFactory()); + HttpsURLConnection.setDefaultHostnameVerifier((host, session) -> host.equalsIgnoreCase(session.getPeerHost())); + } + + private TrustManager[] getTrustManagers(ElasticSearchConfig config) + throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + // Using null here initializes the TMF with the default trust store. + tmf.init((KeyStore) null); + + // Find the default trust manager. + final X509TrustManager defaultTrustManager = findX509TrustManager(tmf); + + String trustStoreFile = config.getTrustStorePath(); + if (trustStoreFile == null) { + logger.debug("No trust store defined"); + return new TrustManager[] {defaultTrustManager}; + } + + // Create a new Trust Manager from the local trust store. + try (InputStream myKeys = Files.newInputStream(Paths.get(trustStoreFile))) { + KeyStore myTrustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + char[] pwdArray = null; + if (config.getTrustStorePassword() != null) { + pwdArray = config.getTrustStorePassword().toCharArray(); + } + myTrustStore.load(myKeys, pwdArray); + tmf.init(myTrustStore); + } + + // Create a custom trust manager that wraps both our trust store and the default. + final X509TrustManager finalLocalTm = findX509TrustManager(tmf); + + return new TrustManager[] {new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return defaultTrustManager.getAcceptedIssuers(); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + try { + finalLocalTm.checkServerTrusted(chain, authType); + } catch (CertificateException e) { + defaultTrustManager.checkServerTrusted(chain, authType); + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + defaultTrustManager.checkClientTrusted(chain, authType); + } + }}; + } + + private X509TrustManager findX509TrustManager(TrustManagerFactory tmf) { + return (X509TrustManager) Arrays.asList(tmf.getTrustManagers()).stream() + .filter(tm -> tm instanceof X509TrustManager).findFirst().orElse(null); + } +} diff --git a/src/test/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/dao/ElasticSearchHttpControllerTest.java b/src/test/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/dao/ElasticSearchHttpControllerTest.java index 3cfb58a..1c75af2 100644 --- a/src/test/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/dao/ElasticSearchHttpControllerTest.java +++ b/src/test/java/org/onap/aai/sa/searchdbabstraction/elasticsearch/dao/ElasticSearchHttpControllerTest.java @@ -1,4 +1,4 @@ -/** +/** * ============LICENSE_START======================================================= * org.onap.aai * ================================================================================ @@ -29,6 +29,7 @@ import static org.hamcrest.CoreMatchers.not; import static org.junit.Assert.assertThat; import java.util.Properties; +import org.eclipse.jetty.util.security.Password; import org.json.JSONObject; import org.junit.Before; import org.junit.Ignore; @@ -62,6 +63,9 @@ public class ElasticSearchHttpControllerTest { Properties properties = new Properties(); properties.put(ElasticSearchConfig.ES_IP_ADDRESS, "127.0.0.1"); properties.put(ElasticSearchConfig.ES_HTTP_PORT, "9200"); + properties.put(ElasticSearchConfig.ES_URI_SCHEME, "http"); + properties.put(ElasticSearchConfig.ES_AUTH_USER, "your_user_here"); + properties.put(ElasticSearchConfig.ES_AUTH_ENC, Password.obfuscate("your_password_here")); elasticSearch = new ElasticSearchHttpController(new ElasticSearchConfig(properties)); testDocument = new AAIEntityTestObject(); @@ -155,7 +159,12 @@ public class ElasticSearchHttpControllerTest { @Test public void testDeleteDocument() throws Exception { - OperationResult result = elasticSearch.deleteDocument(TEST_INDEX_NAME, testDocument); + OperationResult result = elasticSearch.getDocument(TEST_INDEX_NAME, testDocument); + if (result.getResultCode() == 404) { + testCreateDocument(); + } + + result = elasticSearch.deleteDocument(TEST_INDEX_NAME, testDocument); assertThat(result.getResult(), containsString(TEST_INDEX_NAME)); result = elasticSearch.getDocument(TEST_INDEX_NAME, testDocument); @@ -173,8 +182,8 @@ public class ElasticSearchHttpControllerTest { doc.setSearchTagIDs("" + i); doc.setSearchTags("service-instance-id"); - // OperationResult result = elasticSearch.createDocument(TEST_INDEX_NAME, doc, false); - // assertThat(result.getResultCode(), anyOf(equalTo(201), equalTo(400))); + OperationResult result = elasticSearch.createDocument(TEST_INDEX_NAME, doc, false); + assertThat(result.getResultCode(), anyOf(equalTo(201), equalTo(400))); } } -- 2.16.6