This is an automated email from the ASF dual-hosted git repository.
bbotella pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/trunk by this push:
new 48be0f6ca0 CASSANDRA-17559: FQLTool Replay should support full set of
login options
48be0f6ca0 is described below
commit 48be0f6ca077675b853e3b42bc98025c75e89527
Author: Arvind Kandpal <[email protected]>
AuthorDate: Wed Aug 26 16:32:55 2026 +0530
CASSANDRA-17559: FQLTool Replay should support full set of login options
* Added support for SSL encryption (--ssl, --ssl-truststore,
--ssl-keystore, passwords)
* Added support for custom Auth Providers (--auth-provider)
* Implemented proper password masking for target hosts in logs and result
paths
* Added fail-fast validation for SSL and AuthProvider configurations on
startup
* Added unit tests for invalid configurations and edge cases in password
masking
Address review comments: Move password masking to ParsedTargetHost and fix
AuthProvider initialization
Address review: encapsulate connection options, fix password parsing,
validate SSL config
Address review: Preserve Replay public API overload
Address review: support one-way TLS by building SSLContext directly
Address review: mask replay labels in QueryReplayer, use
IllegalArgumentException for bad auth provider class, add --ssl-store-type
patch by Arvind Kandpal; reviewed by Bernardo Botella, Brandon Williams
---
CHANGES.txt | 1 +
.../cassandra/fqltool/ConnectionOptions.java | 253 +++++++++++++++++++++
.../apache/cassandra/fqltool/QueryReplayer.java | 80 +++++--
.../apache/cassandra/fqltool/commands/Replay.java | 47 +++-
.../apache/cassandra/fqltool/FQLReplayTest.java | 247 ++++++++++++++++++++
5 files changed, 609 insertions(+), 19 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 5a82fd77d5..24a881ef89 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
7.0
+ * FQLTool Replay support for full set of login options (CASSANDRA-17559)
* Extract reusable HttpService from cloud metadata connector (CASSANDRA-21195)
* Redact hash and _hash suffixed parameters in system_views.settings
(CASSANDRA-21686)
* Allow CQLSSTableWriter to specify SSTable id generator to use
(CASSANDRA-21012)
diff --git
a/tools/fqltool/src/org/apache/cassandra/fqltool/ConnectionOptions.java
b/tools/fqltool/src/org/apache/cassandra/fqltool/ConnectionOptions.java
new file mode 100644
index 0000000000..38cb23c311
--- /dev/null
+++ b/tools/fqltool/src/org/apache/cassandra/fqltool/ConnectionOptions.java
@@ -0,0 +1,253 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+package org.apache.cassandra.fqltool;
+
+import java.io.IOException;
+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 javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+
+import com.datastax.driver.core.AuthProvider;
+import com.datastax.driver.core.RemoteEndpointAwareJdkSSLOptions;
+import com.datastax.driver.core.SSLOptions;
+
+import org.apache.cassandra.io.util.FileInputStreamPlus;
+
+/**
+ * Holds the SSL and authentication settings used to connect to target hosts
during fqltool replay.
+ * Note that providing any SSL-related configuration option implicitly enables
SSL.
+ */
+public class ConnectionOptions
+{
+ private final boolean ssl;
+ private final SSLOptions sslOptions;
+ private final String authProviderClass;
+
+ private ConnectionOptions(boolean ssl, SSLOptions sslOptions, String
authProviderClass)
+ {
+ this.ssl = ssl;
+ this.sslOptions = sslOptions;
+ this.authProviderClass = authProviderClass;
+ }
+
+ public boolean ssl()
+ {
+ return ssl;
+ }
+
+ public SSLOptions sslOptions()
+ {
+ return sslOptions;
+ }
+
+ public String authProviderClass()
+ {
+ return authProviderClass;
+ }
+
+ /**
+ * Builds the configured AuthProvider: a (String,String) constructor when
credentials are present, otherwise a no-arg constructor.
+ */
+ @SuppressWarnings("unchecked")
+ public AuthProvider instantiateAuthProvider(String user, String password)
+ {
+ try
+ {
+ Class<? extends AuthProvider> clazz = (Class<? extends
AuthProvider>) Class.forName(authProviderClass);
+
+ if (user != null && password != null)
+ return clazz.getConstructor(String.class,
String.class).newInstance(user, password);
+
+ return clazz.getDeclaredConstructor().newInstance();
+ }
+ catch (NoSuchMethodException e)
+ {
+ throw new RuntimeException("Auth provider " + authProviderClass +
" does not support plain text credentials", e);
+ }
+ catch (ReflectiveOperationException e)
+ {
+ throw new RuntimeException("Could not instantiate auth provider: "
+ authProviderClass, e);
+ }
+ }
+
+ public static Builder builder()
+ {
+ return new Builder();
+ }
+
+ public static class Builder
+ {
+ private static final String DEFAULT_STORE_TYPE = "JKS";
+
+ private boolean ssl;
+ private String truststorePath;
+ private String truststorePassword;
+ private String keystorePath;
+ private String keystorePassword;
+ private String storeType;
+ private String authProviderClass;
+
+ public Builder withSsl(boolean ssl)
+ {
+ this.ssl = ssl;
+ return this;
+ }
+
+ public Builder withTruststore(String truststorePath)
+ {
+ this.truststorePath = truststorePath;
+ return this;
+ }
+
+ public Builder withTruststorePassword(String truststorePassword)
+ {
+ this.truststorePassword = truststorePassword;
+ return this;
+ }
+
+ public Builder withKeystore(String keystorePath)
+ {
+ this.keystorePath = keystorePath;
+ return this;
+ }
+
+ public Builder withKeystorePassword(String keystorePassword)
+ {
+ this.keystorePassword = keystorePassword;
+ return this;
+ }
+
+ public Builder withStoreType(String storeType)
+ {
+ this.storeType = storeType;
+ return this;
+ }
+
+ public Builder withAuthProviderClass(String authProviderClass)
+ {
+ this.authProviderClass = authProviderClass;
+ return this;
+ }
+
+ public ConnectionOptions build()
+ {
+ if (truststorePassword != null && truststorePath == null)
+ throw new IllegalArgumentException("--ssl-truststore-password
requires --ssl-truststore to be set");
+ if (keystorePassword != null && keystorePath == null)
+ throw new IllegalArgumentException("--ssl-keystore-password
requires --ssl-keystore to be set");
+ if (storeType != null && truststorePath == null && keystorePath ==
null)
+ throw new IllegalArgumentException("--ssl-store-type requires
--ssl-truststore or --ssl-keystore to be set");
+
+ // any SSL-related option implicitly enables SSL
+ boolean effectiveSsl = ssl || truststorePath != null ||
keystorePath != null;
+
+ if (authProviderClass != null)
+ validateAuthProviderClass();
+
+ SSLOptions sslOptions = effectiveSsl ? buildSSLOptions() : null;
+
+ return new ConnectionOptions(effectiveSsl, sslOptions,
authProviderClass);
+ }
+
+ private void validateAuthProviderClass()
+ {
+ try
+ {
+ Class<?> clazz = Class.forName(authProviderClass);
+ if (!AuthProvider.class.isAssignableFrom(clazz))
+ throw new IllegalArgumentException(authProviderClass + "
does not implement " + AuthProvider.class.getName());
+ }
+ catch (ClassNotFoundException e)
+ {
+ throw new IllegalArgumentException("Could not find auth
provider class: " + authProviderClass, e);
+ }
+ }
+
+ private SSLOptions buildSSLOptions()
+ {
+ try
+ {
+ TrustManagerFactory tmf = buildTrustManagerFactory();
+ KeyManagerFactory kmf = keystorePath != null ?
buildKeyManagerFactory() : null;
+
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(kmf != null ? kmf.getKeyManagers() : null,
+ tmf.getTrustManagers(),
+ null);
+
+ return RemoteEndpointAwareJdkSSLOptions.builder()
+
.withSSLContext(sslContext)
+ .build();
+ }
+ catch (NoSuchAlgorithmException | KeyManagementException |
KeyStoreException | CertificateException | IOException |
UnrecoverableKeyException e)
+ {
+ throw new RuntimeException("Could not configure SSL for
fqltool replay", e);
+ }
+ }
+
+ private TrustManagerFactory buildTrustManagerFactory() throws
NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException
+ {
+ TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+
+ if (truststorePath != null)
+ {
+ KeyStore ts = KeyStore.getInstance(storeType());
+ char[] password = truststorePassword != null ?
truststorePassword.toCharArray() : null;
+ try (FileInputStreamPlus fis = new
FileInputStreamPlus(truststorePath))
+ {
+ ts.load(fis, password);
+ }
+ tmf.init(ts);
+ }
+ else
+ {
+ tmf.init((KeyStore) null);
+ }
+
+ return tmf;
+ }
+
+ private KeyManagerFactory buildKeyManagerFactory() throws
NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException,
UnrecoverableKeyException
+ {
+ KeyStore ks = KeyStore.getInstance(storeType());
+ char[] password = keystorePassword != null ?
keystorePassword.toCharArray() : null;
+ try (FileInputStreamPlus fis = new
FileInputStreamPlus(keystorePath))
+ {
+ ks.load(fis, password);
+ }
+
+ KeyManagerFactory kmf =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(ks, password);
+
+ return kmf;
+ }
+
+ private String storeType()
+ {
+ return storeType != null ? storeType : DEFAULT_STORE_TYPE;
+ }
+ }
+}
diff --git a/tools/fqltool/src/org/apache/cassandra/fqltool/QueryReplayer.java
b/tools/fqltool/src/org/apache/cassandra/fqltool/QueryReplayer.java
index a0f0303229..09727d0270 100644
--- a/tools/fqltool/src/org/apache/cassandra/fqltool/QueryReplayer.java
+++ b/tools/fqltool/src/org/apache/cassandra/fqltool/QueryReplayer.java
@@ -76,6 +76,20 @@ public class QueryReplayer implements Closeable
this(queryIterator, targetHosts, resultPaths, filters,
queryFilePathString, new DefaultSessionProvider(), null);
}
+ /**
+ * Constructor that takes SSL and auth provider settings via
ConnectionOptions.
+ */
+ public QueryReplayer(Iterator<List<FQLQuery>> queryIterator,
+ List<String> targetHosts,
+ List<File> resultPaths,
+ List<Predicate<FQLQuery>> filters,
+ String queryFilePathString,
+ ConnectionOptions connectionOptions)
+ {
+ this(queryIterator, targetHosts, resultPaths, filters,
queryFilePathString,
+ new DefaultSessionProvider(connectionOptions), null);
+ }
+
/**
* Constructor public to allow external users to build their own session
provider
*
@@ -94,7 +108,8 @@ public class QueryReplayer implements Closeable
this.filters = filters;
sessions =
targetHosts.stream().map(sessionProvider::connect).collect(Collectors.toList());
File queryFilePath = queryFilePathString != null ? new
File(queryFilePathString) : null;
- resultHandler = new ResultHandler(targetHosts, resultPaths,
queryFilePath, mismatchListener);
+ List<String> maskedTargetHosts =
targetHosts.stream().map(ParsedTargetHost::maskPassword).collect(Collectors.toList());
+ resultHandler = new ResultHandler(maskedTargetHosts, resultPaths,
queryFilePath, mismatchListener);
}
public void replay()
@@ -186,7 +201,7 @@ public class QueryReplayer implements Closeable
resultHandler.close();
}
- static class ParsedTargetHost
+ public static class ParsedTargetHost
{
final int port;
final String user;
@@ -201,26 +216,42 @@ public class QueryReplayer implements Closeable
this.password = password;
}
- static ParsedTargetHost fromString(String s)
+ /**
+ * Masks the password in a target host string so it's never logged or
written to result paths.
+ */
+ public static String maskPassword(String target)
{
- String [] userInfoHostPort = s.split("@");
+ int at = target.lastIndexOf('@');
+ if (at < 0)
+ return target;
+ String userInfo = target.substring(0, at);
+ int colon = userInfo.indexOf(':');
+ if (colon < 0)
+ return target;
+ return userInfo.substring(0, colon) + ":*****@" +
target.substring(at + 1);
+ }
- String hostPort = null;
+ static ParsedTargetHost fromString(String s)
+ {
+ int at = s.lastIndexOf('@');
+ String hostPort;
String user = null;
String password = null;
- if (userInfoHostPort.length == 2)
+
+ if (at >= 0)
{
- String [] userPassword = userInfoHostPort[0].split(":");
- if (userPassword.length != 2)
+ String userInfo = s.substring(0, at);
+ hostPort = s.substring(at + 1);
+ int colon = userInfo.indexOf(':');
+ if (colon < 0)
throw new RuntimeException("Username provided but no
password");
- hostPort = userInfoHostPort[1];
- user = userPassword[0];
- password = userPassword[1];
+ user = userInfo.substring(0, colon);
+ password = userInfo.substring(colon + 1);
}
- else if (userInfoHostPort.length == 1)
- hostPort = userInfoHostPort[0];
else
- throw new RuntimeException("Malformed target host: "+s);
+ {
+ hostPort = s;
+ }
String[] splitHostPort = hostPort.split(":");
int port = 9042;
@@ -241,6 +272,18 @@ public class QueryReplayer implements Closeable
{
private final static Map<String, Session> sessionCache = new
HashMap<>();
+ private final ConnectionOptions connectionOptions;
+
+ DefaultSessionProvider()
+ {
+ this(ConnectionOptions.builder().build());
+ }
+
+ DefaultSessionProvider(ConnectionOptions connectionOptions)
+ {
+ this.connectionOptions = connectionOptions;
+ }
+
public synchronized Session connect(String connectionString)
{
if (sessionCache.containsKey(connectionString))
@@ -249,8 +292,15 @@ public class QueryReplayer implements Closeable
ParsedTargetHost pth =
ParsedTargetHost.fromString(connectionString);
builder.addContactPoint(pth.host);
builder.withPort(pth.port);
- if (pth.user != null)
+
+ if (connectionOptions.sslOptions() != null)
+ builder.withSSL(connectionOptions.sslOptions());
+
+ if (connectionOptions.authProviderClass() != null)
+
builder.withAuthProvider(connectionOptions.instantiateAuthProvider(pth.user,
pth.password));
+ else if (pth.user != null)
builder.withCredentials(pth.user, pth.password);
+
Cluster c = builder.build();
sessionCache.put(connectionString, c.connect());
return sessionCache.get(connectionString);
diff --git
a/tools/fqltool/src/org/apache/cassandra/fqltool/commands/Replay.java
b/tools/fqltool/src/org/apache/cassandra/fqltool/commands/Replay.java
index 39b72e3e4b..b4daae2351 100644
--- a/tools/fqltool/src/org/apache/cassandra/fqltool/commands/Replay.java
+++ b/tools/fqltool/src/org/apache/cassandra/fqltool/commands/Replay.java
@@ -34,6 +34,7 @@ import
net.openhft.chronicle.queue.impl.single.SingleChronicleQueueBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.apache.cassandra.fqltool.ConnectionOptions;
import org.apache.cassandra.fqltool.FQLQuery;
import org.apache.cassandra.fqltool.FQLQueryIterator;
import org.apache.cassandra.fqltool.QueryReplayer;
@@ -55,7 +56,7 @@ public class Replay implements Runnable
@Parameters(paramLabel = "path", description = "Paths containing the full
query logs to replay.", arity = "1..*")
private List<String> arguments = new ArrayList<>();
- @Option(paramLabel = "target", names = { "--target" }, description =
"Hosts to replay the logs to, can be repeated to replay to more hosts.",
required = true)
+ @Option(paramLabel = "target", names = { "--target" }, description =
"Hosts to replay the logs to, can be repeated to replay to more hosts. Format:
[username:password@]host[:port], for example
'cassandra:[email protected]:9042'.", required = true)
private List<String> targetHosts;
@Option(paramLabel = "results", names = { "--results" }, description =
"Where to store the results of the queries, this should be a directory. Leave
this option out to avoid storing results.")
@@ -70,6 +71,27 @@ public class Replay implements Runnable
@Option(paramLabel = "replay_ddl_statements", names = {
"--replay-ddl-statements" }, description = "If specified, replays DDL
statements as well, they are excluded from replaying by default.")
private boolean replayDDLStatements;
+ @Option(paramLabel = "ssl", names = { "--ssl" }, description = "Use SSL
for connecting to the target hosts.")
+ private boolean ssl;
+
+ @Option(paramLabel = "ssl_truststore", names = { "--ssl-truststore" },
description = "Path to the SSL truststore.")
+ private String truststorePath;
+
+ @Option(paramLabel = "ssl_truststore_password", names = {
"--ssl-truststore-password" }, description = "Password for the SSL truststore.")
+ private String truststorePassword;
+
+ @Option(paramLabel = "ssl_keystore", names = { "--ssl-keystore" },
description = "Path to the SSL keystore, required for two-way SSL.")
+ private String keystorePath;
+
+ @Option(paramLabel = "ssl_keystore_password", names = {
"--ssl-keystore-password" }, description = "Password for the SSL keystore.")
+ private String keystorePassword;
+
+ @Option(paramLabel = "ssl_store_type", names = { "--ssl-store-type" },
description = "Type of the SSL truststore and keystore, for example JKS or
PKCS12. Applies to both stores. Default: JKS.")
+ private String storeType;
+
+ @Option(paramLabel = "auth_provider", names = { "--auth-provider" },
description = "Fully qualified class name of a custom
com.datastax.driver.core.AuthProvider implementation (e.g. for Kerberos).")
+ private String authProviderClass;
+
@Override
public void run()
{
@@ -84,14 +106,25 @@ public class Replay implements Runnable
System.err.println("The results path (" + basePath + ")
should be an existing directory");
throw new IllegalArgumentException("The results path (" +
basePath + ") should be an existing directory");
}
- resultPaths = targetHosts.stream().map(target -> new
File(basePath, target)).collect(Collectors.toList());
+ resultPaths = targetHosts.stream().map(target -> new
File(basePath,
QueryReplayer.ParsedTargetHost.maskPassword(target))).collect(Collectors.toList());
resultPaths.forEach(File::mkdir);
}
if (targetHosts.size() < 1)
{
throw new IllegalArgumentException("You need to state at least
one --target host to replay the query against");
}
- replay(keyspace, arguments, targetHosts, resultPaths,
queryStorePath, replayDDLStatements);
+
+ ConnectionOptions connectionOptions = ConnectionOptions.builder()
+
.withSsl(ssl)
+
.withTruststore(truststorePath)
+
.withTruststorePassword(truststorePassword)
+
.withKeystore(keystorePath)
+
.withKeystorePassword(keystorePassword)
+
.withStoreType(storeType)
+
.withAuthProviderClass(authProviderClass)
+ .build();
+
+ replay(keyspace, arguments, targetHosts, resultPaths,
queryStorePath, replayDDLStatements, connectionOptions);
}
catch (Exception e)
{
@@ -100,6 +133,12 @@ public class Replay implements Runnable
}
public static void replay(String keyspace, List<String> arguments,
List<String> targetHosts, List<File> resultPaths, String queryStorePath,
boolean replayDDLStatements)
+ {
+ replay(keyspace, arguments, targetHosts, resultPaths, queryStorePath,
replayDDLStatements, ConnectionOptions.builder().build());
+ }
+
+ public static void replay(String keyspace, List<String> arguments,
List<String> targetHosts, List<File> resultPaths, String queryStorePath,
boolean replayDDLStatements,
+ ConnectionOptions connectionOptions)
{
int readAhead = 200; // how many fql queries should we read in to
memory to be able to sort them?
List<ChronicleQueue> readQueues = null;
@@ -124,7 +163,7 @@ public class Replay implements Runnable
readQueues = arguments.stream().map(s ->
SingleChronicleQueueBuilder.single(s).readOnly(true).build()).collect(Collectors.toList());
iterators =
readQueues.stream().map(ChronicleQueue::createTailer).map(tailer -> new
FQLQueryIterator(tailer, readAhead)).collect(Collectors.toList());
try (MergeIterator<FQLQuery, List<FQLQuery>> iter =
MergeIterator.get(iterators, FQLQuery::compareTo, new Reducer());
- QueryReplayer replayer = new QueryReplayer(iter, targetHosts,
resultPaths, filters, queryStorePath))
+ QueryReplayer replayer = new QueryReplayer(iter, targetHosts,
resultPaths, filters, queryStorePath, connectionOptions))
{
replayer.replay();
}
diff --git
a/tools/fqltool/test/unit/org/apache/cassandra/fqltool/FQLReplayTest.java
b/tools/fqltool/test/unit/org/apache/cassandra/fqltool/FQLReplayTest.java
index 734029330a..94aa1f18ea 100644
--- a/tools/fqltool/test/unit/org/apache/cassandra/fqltool/FQLReplayTest.java
+++ b/tools/fqltool/test/unit/org/apache/cassandra/fqltool/FQLReplayTest.java
@@ -19,15 +19,21 @@
package org.apache.cassandra.fqltool;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Random;
+import java.util.UUID;
import java.util.stream.Collectors;
import com.datastax.driver.core.CodecRegistry;
@@ -578,6 +584,247 @@ public class FQLReplayTest
assertEquals(9042, pth.port );
assertEquals("aaa", pth.user);
assertEquals("bbb", pth.password);
+
+ pth = fromString("user:p@[email protected]:9042");
+ assertEquals("127.0.0.1", pth.host);
+ assertEquals(9042, pth.port);
+ assertEquals("user", pth.user);
+ assertEquals("p@ssword", pth.password);
+ }
+
+ @Test
+ public void testInvalidAuthProviderClassThrows()
+ {
+ try
+ {
+
ConnectionOptions.builder().withAuthProviderClass("com.not.a.real.AuthProviderClass").build();
+ throw new AssertionError("Expected IllegalArgumentException for
invalid auth provider class");
+ }
+ catch (IllegalArgumentException e)
+ {
+
assertTrue(e.getMessage().contains("com.not.a.real.AuthProviderClass"));
+ }
+ }
+
+ @Test
+ public void testMaskPassword()
+ {
+ assertEquals("aaa:*****@127.0.0.1:9042",
QueryReplayer.ParsedTargetHost.maskPassword("aaa:[email protected]:9042"));
+ assertEquals("127.0.0.1:9042",
QueryReplayer.ParsedTargetHost.maskPassword("127.0.0.1:9042"));
+ assertEquals("127.0.0.1",
QueryReplayer.ParsedTargetHost.maskPassword("127.0.0.1"));
+ assertEquals("user:*****@host:9042",
QueryReplayer.ParsedTargetHost.maskPassword("user:p@ssword@host:9042"));
+ assertEquals("cassandra:*****@127.0.0.1",
QueryReplayer.ParsedTargetHost.maskPassword("cassandra:p@ss@w@[email protected]"));
+ }
+
+ @Test
+ public void testResultHandlerKeepsTargetLabels() throws IOException
+ {
+ // fqltool compare passes result directories as labels, they must
reach the listener unchanged
+ List<String> targetHosts =
Lists.newArrayList("/mnt/a:b/user@host/res", "hostb");
+ List<List<String>> reportedHosts = new ArrayList<>();
+ MismatchListener listener = new MismatchListener()
+ {
+ public void mismatch(UUID mismatchUUID, List<String> targetHosts,
FQLQuery query, List<ResultHandler.ComparableRow> rows)
+ {
+ reportedHosts.add(targetHosts);
+ }
+
+ public void columnDefMismatch(UUID mismatchUUID, List<String>
targetHosts, FQLQuery query, List<ResultHandler.ComparableColumnDefinitions>
cds)
+ {
+ reportedHosts.add(targetHosts);
+ }
+ };
+ FQLQuery query = new FQLQuery.Single("aaa",
QueryOptions.DEFAULT.getProtocolVersion().asInt(), QueryOptions.DEFAULT, 1, 2,
3, "select * from xyz", Collections.emptyList());
+ try (ResultHandler rh = new ResultHandler(targetHosts, null, null,
listener))
+ {
+ rh.handleResults(query, Lists.newArrayList(createResultSet(10, 10,
false), createResultSet(5, 10, false)));
+ }
+ assertFalse(reportedHosts.isEmpty());
+ reportedHosts.forEach(hosts -> assertEquals(targetHosts, hosts));
+ }
+
+ @Test
+ public void testInvalidTruststorePathThrows()
+ {
+ try
+ {
+
ConnectionOptions.builder().withSsl(true).withTruststore("/path/does/not/exist.jks").withTruststorePassword("password").build();
+ throw new AssertionError("Expected RuntimeException for a bad
truststore path");
+ }
+ catch (RuntimeException e)
+ {
+ assertTrue(e.getMessage().contains("SSL"));
+ }
+ }
+
+ @Test
+ public void testImplicitSslEnableWithTruststoreOnly()
+ {
+ // no --ssl flag given, but a truststore path should still turn SSL on
rather than being ignored
+ try
+ {
+
ConnectionOptions.builder().withTruststore("/path/does/not/exist.jks").build();
+ throw new AssertionError("Expected RuntimeException since SSL
should be implicitly enabled");
+ }
+ catch (RuntimeException e)
+ {
+ assertTrue(e.getMessage().contains("SSL"));
+ }
+ }
+
+ @Test
+ public void testTruststorePasswordWithoutPathThrows()
+ {
+ try
+ {
+
ConnectionOptions.builder().withTruststorePassword("somepassword").build();
+ throw new AssertionError("Expected IllegalArgumentException when
truststore password is given without a path");
+ }
+ catch (IllegalArgumentException e)
+ {
+ assertTrue(e.getMessage().contains("--ssl-truststore-password
requires --ssl-truststore"));
+ }
+ }
+
+ @Test
+ public void testKeystorePasswordWithoutPathThrows()
+ {
+ try
+ {
+
ConnectionOptions.builder().withKeystorePassword("somepassword").build();
+ throw new AssertionError("Expected IllegalArgumentException when
keystore password is given without a path");
+ }
+ catch (IllegalArgumentException e)
+ {
+ assertTrue(e.getMessage().contains("--ssl-keystore-password
requires --ssl-keystore"));
+ }
+ }
+
+ public static class NoArgOnlyAuthProvider implements
com.datastax.driver.core.AuthProvider
+ {
+ public NoArgOnlyAuthProvider() { }
+
+ public com.datastax.driver.core.Authenticator
newAuthenticator(java.net.InetSocketAddress host, String authenticator)
+ {
+ throw new UnsupportedOperationException("not needed for this
test");
+ }
+ }
+
+ @Test
+ public void testAuthProviderWithoutCredentialsConstructorThrows()
+ {
+ ConnectionOptions connectionOptions = ConnectionOptions.builder()
+
.withAuthProviderClass(NoArgOnlyAuthProvider.class.getName())
+ .build();
+ try
+ {
+ new QueryReplayer(Collections.emptyIterator(),
+ Lists.newArrayList("aaa:[email protected]:9999"),
+ null,
+ new ArrayList<>(),
+ null,
+ connectionOptions);
+ throw new AssertionError("Expected RuntimeException when auth
provider lacks a (String,String) constructor");
+ }
+ catch (RuntimeException e)
+ {
+ assertTrue(e.getMessage().contains("does not support plain text
credentials"));
+ }
+ }
+
+ @Test
+ public void testOneWayTLSSucceeds()
+ {
+ ConnectionOptions connectionOptions = ConnectionOptions.builder()
+
.withTruststore("test/conf/cassandra_ssl_test.truststore")
+
.withTruststorePassword("cassandra")
+ .build();
+ assertTrue(connectionOptions.ssl());
+ assertNotNull(connectionOptions.sslOptions());
+ }
+
+ @Test
+ public void testInvalidStoreTypeThrows()
+ {
+ try
+ {
+ ConnectionOptions.builder()
+
.withTruststore("test/conf/cassandra_ssl_test.truststore")
+ .withTruststorePassword("cassandra")
+ .withStoreType("NOTATYPE")
+ .build();
+ throw new AssertionError("Expected RuntimeException for an unknown
store type");
+ }
+ catch (RuntimeException e)
+ {
+ assertTrue(e.getMessage().contains("SSL"));
+ assertTrue(e.getCause() instanceof KeyStoreException);
+ }
+ }
+
+ @Test
+ public void testStoreTypeWithoutStorePathThrows()
+ {
+ try
+ {
+ ConnectionOptions.builder().withStoreType("PKCS12").build();
+ throw new AssertionError("Expected IllegalArgumentException when
store type is given without a store path");
+ }
+ catch (IllegalArgumentException e)
+ {
+ assertTrue(e.getMessage().contains("--ssl-store-type requires
--ssl-truststore or --ssl-keystore"));
+ }
+ }
+
+ @Test
+ public void testPkcs12StoreTypeSucceeds() throws IOException,
GeneralSecurityException
+ {
+ ConnectionOptions connectionOptions = ConnectionOptions.builder()
+
.withTruststore(toPkcs12("test/conf/cassandra_ssl_test.truststore",
"cassandra"))
+
.withTruststorePassword("cassandra")
+
.withKeystore(toPkcs12("test/conf/cassandra_ssl_test.keystore", "cassandra"))
+
.withKeystorePassword("cassandra")
+
.withStoreType("PKCS12")
+ .build();
+ assertTrue(connectionOptions.ssl());
+ assertNotNull(connectionOptions.sslOptions());
+ }
+
+ private static String toPkcs12(String jksPath, String password) throws
IOException, GeneralSecurityException
+ {
+ KeyStore jks = KeyStore.getInstance("JKS");
+ try (FileInputStream in = new FileInputStream(jksPath))
+ {
+ jks.load(in, password.toCharArray());
+ }
+ KeyStore pkcs12 = KeyStore.getInstance("PKCS12");
+ pkcs12.load(null, password.toCharArray());
+ KeyStore.PasswordProtection protection = new
KeyStore.PasswordProtection(password.toCharArray());
+ for (String alias : Collections.list(jks.aliases()))
+ {
+ KeyStore.PasswordProtection entryProtection =
jks.isKeyEntry(alias) ? protection : null;
+ pkcs12.setEntry(alias, jks.getEntry(alias, entryProtection),
entryProtection);
+ }
+ File file = Files.createTempFile("fqltool", ".p12").toFile();
+ file.deleteOnExit();
+ try (FileOutputStream out = new FileOutputStream(file))
+ {
+ pkcs12.store(out, password.toCharArray());
+ }
+ return file.getPath();
+ }
+
+ @Test
+ public void testTwoWayTLSSucceeds()
+ {
+ ConnectionOptions connectionOptions = ConnectionOptions.builder()
+
.withTruststore("test/conf/cassandra_ssl_test.truststore")
+
.withTruststorePassword("cassandra")
+
.withKeystore("test/conf/cassandra_ssl_test.keystore")
+
.withKeystorePassword("cassandra")
+ .build();
+ assertTrue(connectionOptions.ssl());
+ assertNotNull(connectionOptions.sslOptions());
}
@Test(expected = RuntimeException.class)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]