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]

Reply via email to