You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by sm...@apache.org on 2024/02/13 11:18:14 UTC
(cassandra) branch trunk updated: Expose auth mode in system_views.clients, nodetool clientstats, metrics
This is an automated email from the ASF dual-hosted git repository.
smiklosovic 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 4120b8ce4f Expose auth mode in system_views.clients, nodetool clientstats, metrics
4120b8ce4f is described below
commit 4120b8ce4f1bc7bd7ce101e4e298fc2211a21fe0
Author: Andy Tolbert <68...@users.noreply.github.com>
AuthorDate: Wed Jan 31 11:06:59 2024 -0600
Expose auth mode in system_views.clients, nodetool clientstats, metrics
Adds 'authenticationMode' and 'metadata' fields to AuthenticatedUser to add context
about how the user was authenticated and updates system_views.clients,
nodetool clientstats (behind --verbose flag) to include this information.
Also adds new metrics to ClientMetrics to help operators identify which
authentication modes are being used.
patch by Andy Tolbert; reviewed by Francisco Guerrero, Stefan Miklosovic for CASSANDRA-19366
---
CHANGES.txt | 1 +
NEWS.txt | 4 +
.../pages/managing/operating/metrics.adoc | 81 ++++++-
.../cassandra/auth/AllowAllAuthenticator.java | 8 +
.../apache/cassandra/auth/AuthenticatedUser.java | 59 ++++-
.../org/apache/cassandra/auth/IAuthenticator.java | 100 ++++++++-
.../cassandra/auth/MutualTlsAuthenticator.java | 20 +-
...MutualTlsWithPasswordFallbackAuthenticator.java | 13 ++
.../cassandra/auth/PasswordAuthenticator.java | 16 +-
.../apache/cassandra/db/virtual/ClientsTable.java | 19 +-
.../apache/cassandra/metrics/ClientMetrics.java | 139 +++++++++++-
src/java/org/apache/cassandra/tools/NodeProbe.java | 2 +-
.../cassandra/tools/nodetool/ClientStats.java | 95 ++++----
.../cassandra/transport/ConnectedClient.java | 27 +++
.../org/apache/cassandra/transport/Server.java | 27 +++
.../cassandra/transport/ServerConnection.java | 11 +
.../cassandra/transport/messages/AuthUtil.java | 6 +-
.../test/AbstractEncryptionOptionsImpl.java | 10 +-
.../cassandra/distributed/test/AuthTest.java | 10 +-
.../test/InternodeEncryptionEnforcementTest.java | 29 +--
.../test/NativeTransportEncryptionOptionsTest.java | 11 +-
.../org/apache/cassandra/auth/AuthTestUtils.java | 15 ++
.../auth/MutualTlsInternodeAuthenticatorTest.java | 7 +-
.../config/EncryptionOptionsEqualityTest.java | 29 +--
test/unit/org/apache/cassandra/cql3/CQLTester.java | 95 ++++++--
.../cassandra/db/virtual/ClientsTableTest.java | 43 +++-
.../cassandra/metrics/ClientMetricsTest.java | 241 ++++++++++++++++++++
.../org/apache/cassandra/net/ConnectionTest.java | 9 +-
.../org/apache/cassandra/net/HandshakeTest.java | 13 +-
.../security/DefaultSslContextFactoryTest.java | 25 ++-
.../security/FileBasedSslContextFactoryTest.java | 17 +-
.../security/PEMBasedSslContextFactoryTest.java | 25 ++-
.../apache/cassandra/security/SSLFactoryTest.java | 31 ++-
.../service/NativeTransportServiceTest.java | 5 +-
.../cassandra/tools/nodetool/ClientStatsTest.java | 159 ++++++++++++--
.../cassandra/transport/ConnectionTrackerTest.java | 242 +++++++++++++++++++++
.../transport/EarlyAuthenticationTest.java | 26 +--
.../transport/SimpleClientSslContextFactory.java | 32 +++
.../apache/cassandra/transport/TlsTestUtils.java | 112 ++++++++++
39 files changed, 1564 insertions(+), 250 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index f7462fa059..f36529a350 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
5.1
+ * Expose auth mode in system_views.clients, nodetool clientstats, metrics (CASSANDRA-19366)
* Remove sealed_periods and last_sealed_period tables (CASSANDRA-19189)
* Improve setup and initialisation of LocalLog/LogSpec (CASSANDRA-19271)
* Refactor structure of caching metrics and expose auth cache metrics via JMX (CASSANDRA-17062)
diff --git a/NEWS.txt b/NEWS.txt
index dd534dd672..fe8ee12fb8 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -81,6 +81,10 @@ New features
- Whether bulk loading of SSTables is allowed.
- nodetool tpstats can display core pool size, max pool size and max tasks queued if --verbose / -v flag is specified.
system_views.thread_pools adds core_pool_size, max_pool_size and max_tasks_queued columns.
+ - Authentication mode is exposed in system_views.clients table, nodetool clientstats and ClientMetrics
+ to help operators identify which authentication modes are being used. nodetool clientstats introduces --verbose flag
+ behind which this information is visible.
+
Upgrading
---------
diff --git a/doc/modules/cassandra/pages/managing/operating/metrics.adoc b/doc/modules/cassandra/pages/managing/operating/metrics.adoc
index 2c709b9d16..c4392fea82 100644
--- a/doc/modules/cassandra/pages/managing/operating/metrics.adoc
+++ b/doc/modules/cassandra/pages/managing/operating/metrics.adoc
@@ -969,14 +969,87 @@ Reported name format:
[cols=",,",options="header",]
|===
|Name |Type |Description
-|connectedNativeClients |Gauge<Integer> |Number of clients connected to
+|AuthFailure |Meter | Rate of failed authentications
+
+|AuthSuccess |Meter | Rate of successful authentications
+
+|ClientsByProtocolVersion |Gauge<List<Map<String, Integer>> | List of
+all connections' protocol version and ip address
+
+|ConnectedNativeClients |Gauge<Integer> |Number of clients connected to
this nodes native protocol server
-|connections |Gauge<List<Map<String, String>> |List of all connections
+|ConnectedNativeClientsByUser |Gauge<Map<String, Integer>> |Number of
+connnective native clients by username
+
+|Connections |Gauge<List<Map<String, String>> |List of all connections
and their state information
-|connectedNativeClientsByUser |Gauge<Map<String, Int> |Number of
-connnective native clients by username
+|PausedConnections|Gauge<Integer>|Number of connections currently
+paused by rate limiter
+
+|ProtocolException|Meter|Rate of requests resulting in a protocol
+exception
+
+|UnknownException|Meter|Rate of requests resulting in an unknown
+exception
+
+|RequestDiscarded|Meter|Rate of requests discarded by rate limiter
+
+|RequestDispatched|Meter|Rate of requests dispatched (not discarded)
+
+|RequestsSizeByIpDistribution|Histogram|Histogram of distribution of
+requests coming from unique IPs
+
+|===
+
+== Client Encryption Metrics
+
+Metrics specific to Client encryption
+
+*Metric Name*::
+`org.apache.cassandra.metrics.Client.<Encrypted|Unencrypted>.<MetricName>`
+*JMX MBean*::
+`org.apache.cassandra.metrics:type=Client scope=<Encrypted|Unencrypted> name=<MetricName>`
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|ConnectedNativeClients |Gauge<Integer> |Number of clients connected in
+this way to this nodes native protocol server
+
+|===
+
+== Client Authentication Mode-Specific Metrics
+
+Metrics specific to connectivity for a given Authentication 'mode'.
+
+*Metric Name*::
+`org.apache.cassandra.metrics.Client.<Mode>.<MetricName>`
+*JMX MBean*::
+`org.apache.cassandra.metrics:type=Client scope=<Mode> name=<MetricName>`
+
+An authentication mode is a supported method of authentication. The following
+authentication modes exist for the given supported `IAuthenticators`:
+
+* `PasswordAuthenticator` - Password
+* `MutualTlsAuthenticator` - MutualTls
+* `MutualTlsWithPasswordFallbackAuthenticator` - MutualTls, Password
+* `AllowAllAuthenticator` - Unauthenticated
+
+A custom implementation of `IAuthenticator` may expose metrics on their own
+custom mode by implementing `IAuthenticator.getSupportedAuthenticationModes()`.
+
+[cols=",,",options="header",]
+|===
+|Name |Type |Description
+|ConnectedNativeClients |Gauge<Int> |Number of clients connected and
+authenticated to this nodes native protocol server using this mode
+
+|AuthFailure |Meter | Rate of failed authentications using this mode
+
+|AuthSuccess |Meter | Rate of successful authentications using this mode
+
|===
== Batch Metrics
diff --git a/src/java/org/apache/cassandra/auth/AllowAllAuthenticator.java b/src/java/org/apache/cassandra/auth/AllowAllAuthenticator.java
index 7b21dc60a9..fb0f81a865 100644
--- a/src/java/org/apache/cassandra/auth/AllowAllAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/AllowAllAuthenticator.java
@@ -29,6 +29,8 @@ public class AllowAllAuthenticator implements IAuthenticator
{
private static final SaslNegotiator AUTHENTICATOR_INSTANCE = new Negotiator();
+ private static final Set<AuthenticationMode> AUTHENTICATION_MODES = Collections.singleton(AuthenticationMode.UNAUTHENTICATED);
+
public boolean requireAuthentication()
{
return false;
@@ -52,6 +54,12 @@ public class AllowAllAuthenticator implements IAuthenticator
return AUTHENTICATOR_INSTANCE;
}
+ @Override
+ public Set<AuthenticationMode> getSupportedAuthenticationModes()
+ {
+ return AUTHENTICATION_MODES;
+ }
+
public AuthenticatedUser legacyAuthenticate(Map<String, String> credentialsData)
{
return AuthenticatedUser.ANONYMOUS_USER;
diff --git a/src/java/org/apache/cassandra/auth/AuthenticatedUser.java b/src/java/org/apache/cassandra/auth/AuthenticatedUser.java
index 6b31110e95..f8d8a9c790 100644
--- a/src/java/org/apache/cassandra/auth/AuthenticatedUser.java
+++ b/src/java/org/apache/cassandra/auth/AuthenticatedUser.java
@@ -18,12 +18,17 @@
package org.apache.cassandra.auth;
import java.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.Map;
import java.util.Set;
import com.google.common.base.Objects;
+import org.apache.cassandra.auth.IAuthenticator.AuthenticationMode;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.dht.Datacenters;
+import static org.apache.cassandra.auth.IAuthenticator.AuthenticationMode.UNAUTHENTICATED;
+
/**
* Returned from IAuthenticator#authenticate(), represents an authenticated user everywhere internally.
*
@@ -55,13 +60,39 @@ public class AuthenticatedUser
private final String name;
- // Primary Role of the logged in user
+ private final AuthenticationMode authenticationMode;
+
+ private final Map<String, Object> metadata;
+
+ // Primary Role of the logged-in user
private final RoleResource role;
public AuthenticatedUser(String name)
+ {
+ this(name, UNAUTHENTICATED);
+ }
+
+ public AuthenticatedUser(String name, AuthenticationMode authenticationMode)
+ {
+ this(name, authenticationMode, Collections.emptyMap());
+ }
+
+ /**
+ * Defines authenticated user context established within a client connection.
+ *
+ * @param name The user's role name
+ * @param authenticationMode How the user was authenticated
+ * @param metadata contextual metadata about how the user was authenticated. Note that this data is exposed
+ * through the <code>system_views.clients table</code>, <code>nodetool clientstats</code> and
+ * {@link org.apache.cassandra.metrics.ClientMetrics}-based JMX Beans. Implementors should
+ * take care not to store anything sensitive here.
+ */
+ public AuthenticatedUser(String name, AuthenticationMode authenticationMode, Map<String, Object> metadata)
{
this.name = name;
this.role = RoleResource.role(name);
+ this.authenticationMode = authenticationMode;
+ this.metadata = metadata;
}
public String getName()
@@ -74,6 +105,26 @@ public class AuthenticatedUser
return role;
}
+ /**
+ * @returns the mode of authentication used to authenticate this user
+ */
+ public AuthenticationMode getAuthenticationMode()
+ {
+ return authenticationMode;
+ }
+
+ /**
+ * @returns {@link IAuthenticator}-contextual metadata about how the user was authenticated.
+ * <p>
+ * Note that this data is exposed through the <code>system_views.clients table</code>,
+ * <code>nodetool clientstats</code> and {@link org.apache.cassandra.metrics.ClientMetrics}-based JMX Beans.
+ * Implementors should take care not to store anything sensitive here.
+ */
+ public Map<String, Object> getMetadata()
+ {
+ return metadata;
+ }
+
/**
* Checks the user's superuser status.
* Only a superuser is allowed to perform CREATE USER and DROP USER queries.
@@ -179,6 +230,12 @@ public class AuthenticatedUser
@Override
public int hashCode()
{
+ // Note: for reasons of maintaining the invariant that an object that equals maintains the same hashCode,
+ // we do not include mode and metadata in the hashCode calculation.
+ // This is particularly salient as there are cases where AuthenticatedUser is used as a key in
+ // Role/Permissions cache. In effect, we would like to treat all connections sharing the same name as the same
+ // user, where mode and metadata are just additional context about how the user authenticated that
+ // should not factor into 'equivalence' of users.
return Objects.hashCode(name);
}
}
diff --git a/src/java/org/apache/cassandra/auth/IAuthenticator.java b/src/java/org/apache/cassandra/auth/IAuthenticator.java
index 89c08243f2..03f48d534d 100644
--- a/src/java/org/apache/cassandra/auth/IAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/IAuthenticator.java
@@ -19,9 +19,13 @@ package org.apache.cassandra.auth;
import java.net.InetAddress;
import java.security.cert.Certificate;
+import java.util.Collections;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
+import javax.annotation.Nonnull;
+
import org.apache.cassandra.exceptions.AuthenticationException;
import org.apache.cassandra.exceptions.ConfigurationException;
@@ -53,7 +57,7 @@ public interface IAuthenticator
return false;
}
- /**
+ /**
* Set of resources that should be made inaccessible to users and only accessible internally.
*
* @return Keyspaces, column families that will be unmodifiable by users; other resources.
@@ -69,7 +73,7 @@ public interface IAuthenticator
/**
* Setup is called once upon system startup to initialize the IAuthenticator.
- *
+ * <p>
* For example, use this method to create any required keyspaces/column families.
*/
void setup();
@@ -78,6 +82,7 @@ public interface IAuthenticator
* Provide a SASL handler to perform authentication for an single connection. SASL
* is a stateful protocol, so a new instance must be used for each authentication
* attempt.
+ *
* @param clientAddress the IP address of the client whom we wish to authenticate, or null
* if an internal client (one not connected over the remote transport).
* @return org.apache.cassandra.auth.IAuthenticator.SaslNegotiator implementation
@@ -90,12 +95,13 @@ public interface IAuthenticator
* is a stateful protocol, so a new instance must be used for each authentication
* attempt. This method accepts certificates as well. Authentication strategies can
* override this method to gain access to client's certificate chain, if present.
+ *
* @param clientAddress the IP address of the client whom we wish to authenticate, or null
* if an internal client (one not connected over the remote transport).
- * @param certificates the peer's Certificate chain, if present.
- * It is expected that these will all be instances of {@link java.security.cert.X509Certificate},
- * but we pass them as the base {@link Certificate} in case future implementations leverage
- * other certificate types.
+ * @param certificates the peer's Certificate chain, if present.
+ * It is expected that these will all be instances of {@link java.security.cert.X509Certificate},
+ * but we pass them as the base {@link Certificate} in case future implementations leverage
+ * other certificate types.
* @return org.apache.cassandra.auth.IAuthenticator.SaslNegotiator implementation
* (see {@link org.apache.cassandra.auth.PasswordAuthenticator.PlainTextSaslAuthenticator})
*/
@@ -104,12 +110,22 @@ public interface IAuthenticator
return newSaslNegotiator(clientAddress);
}
+ /**
+ * @return The supported authentication 'modes' of this authenticator scheme.
+ * <p>
+ * This is currently only used for registering metrics tied to authentication by mode.
+ */
+ default Set<AuthenticationMode> getSupportedAuthenticationModes()
+ {
+ return Collections.emptySet();
+ }
+
/**
* A legacy method that is still used by JMX authentication.
- *
+ * <p>
* You should implement this for having JMX authentication through your
* authenticator.
- *
+ * <p>
* Should never return null - always throw AuthenticationException instead.
* Returning AuthenticatedUser.ANONYMOUS_USER is an option as well if authentication is not required.
*
@@ -129,7 +145,7 @@ public interface IAuthenticator
/**
* Evaluates the client response data and generates a byte[] response which may be a further challenge or purely
* informational in the case that the negotiation is completed on this round.
- *
+ * <p>
* This method is called each time a {@link org.apache.cassandra.transport.messages.AuthResponse} is received
* from a client. After it is called, {@link #isComplete()} is checked to determine whether the negotiation has
* finished. If so, an AuthenticatedUser is obtained by calling {@link #getAuthenticatedUser()} and that user
@@ -141,8 +157,7 @@ public interface IAuthenticator
*
* @param clientResponse The non-null (but possibly empty) response sent by the client
* @return The possibly null response to send to the client.
- * @throws AuthenticationException
- * see {@link javax.security.sasl.SaslServer#evaluateResponse(byte[])}
+ * @throws AuthenticationException see {@link javax.security.sasl.SaslServer#evaluateResponse(byte[])}
*/
public byte[] evaluateResponse(byte[] clientResponse) throws AuthenticationException;
@@ -179,5 +194,68 @@ public interface IAuthenticator
{
return true;
}
+
+ /**
+ * @return The assumed mode of authentication attempted using this negotiator, this will usually be some value
+ * of {@link AuthenticationMode#toString()}} unless an implementor provides their own custom authentication
+ * scheme.
+ */
+ default AuthenticationMode getAuthenticationMode()
+ {
+ return AuthenticationMode.UNAUTHENTICATED;
+ }
+ }
+
+ /**
+ * Known modes of authentication supported by Cassandra's provided {@link IAuthenticator} implementations.
+ */
+ abstract class AuthenticationMode
+ {
+ private final String displayName;
+
+ /**
+ * @param displayName How this mode should be displayed in tooling and JMX beans. Note that it is desirable
+ * for this name to not have spaces as it may not work well with tooling around JMX.
+ */
+ public AuthenticationMode(@Nonnull String displayName)
+ {
+ this.displayName = displayName;
+ }
+
+ /**
+ * User was not authenticated in any particular way.
+ */
+ public static final AuthenticationMode UNAUTHENTICATED = new AuthenticationMode("Unauthenticated") {};
+
+ /**
+ * User authenticated using a password.
+ */
+ public static final AuthenticationMode PASSWORD = new AuthenticationMode("Password") {};
+
+ /**
+ * User authenticated using a trusted identity in their client certificate.
+ */
+ public static final AuthenticationMode MTLS = new AuthenticationMode("MutualTls") {};
+
+ @Override
+ public String toString()
+ {
+ return displayName;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AuthenticationMode that = (AuthenticationMode) o;
+ return displayName.equals(that.displayName);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(displayName);
+ }
}
}
diff --git a/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java b/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java
index 8c379a6dbd..a7df4241e9 100644
--- a/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java
@@ -21,6 +21,7 @@ package org.apache.cassandra.auth;
import java.net.InetAddress;
import java.security.cert.Certificate;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -40,6 +41,7 @@ import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.utils.NoSpamLogger;
+import static org.apache.cassandra.auth.IAuthenticator.AuthenticationMode.MTLS;
import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED;
/*
@@ -69,6 +71,10 @@ public class MutualTlsAuthenticator implements IAuthenticator
private static final String CACHE_NAME = "IdentitiesCache";
private final IdentityCache identityCache = new IdentityCache();
private final MutualTlsCertificateValidator certificateValidator;
+ private static final Set<AuthenticationMode> AUTHENTICATION_MODES = Collections.singleton(MTLS);
+
+ // key for the 'identity' value in AuthenticatedUser metadata map.
+ static final String METADATA_IDENTITY_KEY = "identity";
public MutualTlsAuthenticator(Map<String, String> parameters)
{
@@ -133,6 +139,12 @@ public class MutualTlsAuthenticator implements IAuthenticator
return new CertificateNegotiator(certificates);
}
+ @Override
+ public Set<AuthenticationMode> getSupportedAuthenticationModes()
+ {
+ return AUTHENTICATION_MODES;
+ }
+
@Override
public AuthenticatedUser legacyAuthenticate(Map<String, String> credentials) throws AuthenticationException
{
@@ -196,7 +208,13 @@ public class MutualTlsAuthenticator implements IAuthenticator
nospamLogger.error(msg, identity);
throw new AuthenticationException(MessageFormatter.format(msg, identity).getMessage());
}
- return new AuthenticatedUser(role);
+ return new AuthenticatedUser(role, MTLS, Collections.singletonMap(METADATA_IDENTITY_KEY, identity));
+ }
+
+ @Override
+ public AuthenticationMode getAuthenticationMode()
+ {
+ return MTLS;
}
}
diff --git a/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java b/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java
index f10b7eb1a0..219353d76a 100644
--- a/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java
@@ -21,6 +21,9 @@ package org.apache.cassandra.auth;
import java.net.InetAddress;
import java.security.cert.Certificate;
import java.util.Map;
+import java.util.Set;
+
+import com.google.common.collect.Sets;
import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
@@ -34,9 +37,13 @@ import org.apache.cassandra.exceptions.ConfigurationException;
public class MutualTlsWithPasswordFallbackAuthenticator extends PasswordAuthenticator
{
private final MutualTlsAuthenticator mutualTlsAuthenticator;
+
+ private final Set<AuthenticationMode> AUTHENTICATION_MODES;
+
public MutualTlsWithPasswordFallbackAuthenticator(Map<String, String> parameters)
{
mutualTlsAuthenticator = new MutualTlsAuthenticator(parameters);
+ AUTHENTICATION_MODES = Sets.union(super.getSupportedAuthenticationModes(), mutualTlsAuthenticator.getSupportedAuthenticationModes());
}
@Override
@@ -52,6 +59,12 @@ public class MutualTlsWithPasswordFallbackAuthenticator extends PasswordAuthenti
return true;
}
+ @Override
+ public Set<AuthenticationMode> getSupportedAuthenticationModes()
+ {
+ return AUTHENTICATION_MODES;
+ }
+
@Override
public SaslNegotiator newSaslNegotiator(InetAddress clientAddress, Certificate[] certificates)
{
diff --git a/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java b/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
index b96aabc76a..77bc3e51b3 100644
--- a/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.auth;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -72,6 +73,7 @@ public class PasswordAuthenticator implements IAuthenticator, AuthCache.BulkLoad
// really this is a rolename now, but as it only matters for Thrift, we leave it for backwards compatibility
public static final String USERNAME_KEY = "username";
public static final String PASSWORD_KEY = "password";
+ private static final Set<AuthenticationMode> AUTHENTICATION_MODES = Collections.singleton(AuthenticationMode.PASSWORD);
static final byte NUL = 0;
private SelectStatement authenticateStatement;
@@ -164,7 +166,7 @@ public class PasswordAuthenticator implements IAuthenticator, AuthCache.BulkLoad
if (!checkpw(password, hash))
throw new AuthenticationException(String.format("Provided username %s and/or password are incorrect", username));
- return new AuthenticatedUser(username);
+ return new AuthenticatedUser(username, AuthenticationMode.PASSWORD);
}
private String queryHashedPassword(String username)
@@ -238,6 +240,12 @@ public class PasswordAuthenticator implements IAuthenticator, AuthCache.BulkLoad
return new PlainTextSaslAuthenticator();
}
+ @Override
+ public Set<AuthenticationMode> getSupportedAuthenticationModes()
+ {
+ return AUTHENTICATION_MODES;
+ }
+
private static SelectStatement prepare(String query)
{
return (SelectStatement) QueryProcessor.getStatement(query, ClientState.forInternalCalls());
@@ -269,6 +277,12 @@ public class PasswordAuthenticator implements IAuthenticator, AuthCache.BulkLoad
return authenticate(username, password);
}
+ @Override
+ public AuthenticationMode getAuthenticationMode()
+ {
+ return AuthenticationMode.PASSWORD;
+ }
+
/**
* SASL PLAIN mechanism specifies that credentials are encoded in a
* sequence of UTF-8 bytes, delimited by 0 (US-ASCII NUL).
diff --git a/src/java/org/apache/cassandra/db/virtual/ClientsTable.java b/src/java/org/apache/cassandra/db/virtual/ClientsTable.java
index 027bb9becd..8f99f3a771 100644
--- a/src/java/org/apache/cassandra/db/virtual/ClientsTable.java
+++ b/src/java/org/apache/cassandra/db/virtual/ClientsTable.java
@@ -18,8 +18,15 @@
package org.apache.cassandra.db.virtual;
import java.net.InetSocketAddress;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
-import org.apache.cassandra.db.marshal.*;
+import org.apache.cassandra.db.marshal.BooleanType;
+import org.apache.cassandra.db.marshal.InetAddressType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.marshal.MapType;
+import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.dht.LocalPartitioner;
import org.apache.cassandra.metrics.ClientMetrics;
import org.apache.cassandra.schema.TableMetadata;
@@ -41,6 +48,8 @@ final class ClientsTable extends AbstractVirtualTable
private static final String SSL_PROTOCOL = "ssl_protocol";
private static final String SSL_CIPHER_SUITE = "ssl_cipher_suite";
private static final String KEYSPACE_NAME = "keyspace_name";
+ private static final String AUTHENTICATION_MODE = "authentication_mode";
+ private static final String AUTHENTICATION_METADATA = "authentication_metadata";
ClientsTable(String keyspace)
{
@@ -62,6 +71,8 @@ final class ClientsTable extends AbstractVirtualTable
.addRegularColumn(SSL_PROTOCOL, UTF8Type.instance)
.addRegularColumn(SSL_CIPHER_SUITE, UTF8Type.instance)
.addRegularColumn(KEYSPACE_NAME, UTF8Type.instance)
+ .addRegularColumn(AUTHENTICATION_MODE, UTF8Type.instance)
+ .addRegularColumn(AUTHENTICATION_METADATA, MapType.getInstance(UTF8Type.instance, UTF8Type.instance, false))
.build());
}
@@ -86,7 +97,11 @@ final class ClientsTable extends AbstractVirtualTable
.column(SSL_ENABLED, client.sslEnabled())
.column(SSL_PROTOCOL, client.sslProtocol().orElse(null))
.column(SSL_CIPHER_SUITE, client.sslCipherSuite().orElse(null))
- .column(KEYSPACE_NAME, client.keyspace().orElse(null));
+ .column(KEYSPACE_NAME, client.keyspace().orElse(null))
+ .column(AUTHENTICATION_MODE, client.authenticationMode().toString())
+ .column(AUTHENTICATION_METADATA, client.authenticationMetadata()
+ .entrySet().stream()
+ .collect(Collectors.toMap(Entry::getKey, entry -> String.valueOf(entry.getValue()))));
}
return result;
diff --git a/src/java/org/apache/cassandra/metrics/ClientMetrics.java b/src/java/org/apache/cassandra/metrics/ClientMetrics.java
index 4e3eb4f8ec..f0d075bd0c 100644
--- a/src/java/org/apache/cassandra/metrics/ClientMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/ClientMetrics.java
@@ -18,14 +18,32 @@
*/
package org.apache.cassandra.metrics;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
+
+import com.google.common.annotations.VisibleForTesting;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Reservoir;
-import org.apache.cassandra.transport.*;
+import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.auth.IAuthenticator;
+import org.apache.cassandra.auth.IAuthenticator.AuthenticationMode;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.transport.ClientResourceLimits;
+import org.apache.cassandra.transport.ClientStat;
+import org.apache.cassandra.transport.ConnectedClient;
+import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.ServerConnection;
import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
@@ -38,10 +56,35 @@ public final class ClientMetrics
private volatile boolean initialized = false;
private Collection<Server> servers = Collections.emptyList();
- private Meter authSuccess;
- private Meter authFailure;
+ @VisibleForTesting
+ Meter authSuccess;
+
+ @VisibleForTesting
+ final Map<AuthenticationMode, Meter> authSuccessByMode = new HashMap<>();
+
+ @VisibleForTesting
+ Meter authFailure;
+
+ @VisibleForTesting
+ final Map<AuthenticationMode, Meter> authFailureByMode = new HashMap<>();
+
+ @VisibleForTesting
+ Gauge<Integer> connectedNativeClients;
+
+ @VisibleForTesting
+ Gauge<Integer> encryptedConnectedNativeClients;
+
+ @VisibleForTesting
+ Gauge<Integer> unencryptedConnectedNativeClients;
+
+ @VisibleForTesting
+ Gauge<Map<String, Integer>> connectedNativeClientsByUser;
+
+ @VisibleForTesting
+ final Map<AuthenticationMode, Gauge<Integer>> connectedNativeClientsByAuthMode = new HashMap<>();
+
private AtomicInteger pausedConnections;
-
+
@SuppressWarnings({ "unused", "FieldCanBeLocal" })
private Gauge<Integer> pausedConnectionsGauge;
@@ -51,18 +94,48 @@ public final class ClientMetrics
private Meter protocolException;
private Meter unknownException;
+ private static final String AUTH_SUCCESS = "AuthSuccess";
+
+ private static final String AUTH_FAILURE = "AuthFailure";
+
+ private static final String CONNECTED_NATIVE_CLIENTS = "ConnectedNativeClients";
+
private ClientMetrics()
{
}
+ /**
+ * @deprecated by {@link #markAuthSuccess(AuthenticationMode)}
+ */
+ @Deprecated(since="5.1", forRemoval = true)
public void markAuthSuccess()
+ {
+ markAuthSuccess(null);
+ }
+
+ public void markAuthSuccess(AuthenticationMode authenticationMode)
{
authSuccess.mark();
+ Meter meterByMode;
+ if (authenticationMode != null && (meterByMode = authSuccessByMode.get(authenticationMode)) != null)
+ meterByMode.mark();
}
+ /**
+ * @deprecated by {@link #markAuthFailure(AuthenticationMode)}
+ */
+ @Deprecated(since="5.1", forRemoval = true)
public void markAuthFailure()
+ {
+ markAuthFailure(null);
+ }
+
+ public void markAuthFailure(AuthenticationMode authenticationMode)
{
authFailure.mark();
+ Meter meterByMode;
+ if (authenticationMode != null && (meterByMode = authFailureByMode.get(authenticationMode)) != null)
+ meterByMode.mark();
}
public void pauseConnection() { pausedConnections.incrementAndGet(); }
@@ -99,8 +172,8 @@ public final class ClientMetrics
this.servers = servers;
// deprecated the lower-cased initial letter metric names in 4.0
- registerGauge("ConnectedNativeClients", "connectedNativeClients", this::countConnectedClients);
- registerGauge("ConnectedNativeClientsByUser", "connectedNativeClientsByUser", this::countConnectedClientsByUser);
+ connectedNativeClients = registerGauge(CONNECTED_NATIVE_CLIENTS, "connectedNativeClients", this::countConnectedClients);
+ connectedNativeClientsByUser = registerGauge("ConnectedNativeClientsByUser", "connectedNativeClientsByUser", this::countConnectedClientsByUser);
registerGauge("Connections", "connections", this::connectedClients);
registerGauge("ClientsByProtocolVersion", "clientsByProtocolVersion", this::recentClientStats);
registerGauge("RequestsSize", ClientResourceLimits::getCurrentGlobalUsage);
@@ -118,6 +191,27 @@ public final class ClientMetrics
authSuccess = registerMeter("AuthSuccess");
authFailure = registerMeter("AuthFailure");
+ // For each of SSL, non-SSL register a gauge:
+ encryptedConnectedNativeClients = registerGauge(new DefaultNameFactory("Client", "Encrypted"), CONNECTED_NATIVE_CLIENTS, () -> countConnectedClients((ServerConnection::isSSL)));
+ unencryptedConnectedNativeClients = registerGauge(new DefaultNameFactory("Client", "Unencrypted"), CONNECTED_NATIVE_CLIENTS, () -> countConnectedClients(((ServerConnection connection) -> !connection.isSSL())));
+
+ // for each supported authentication mode, register a meter for success and failures.
+ IAuthenticator authenticator = DatabaseDescriptor.getAuthenticator();
+ for (AuthenticationMode mode : authenticator.getSupportedAuthenticationModes())
+ {
+ MetricNameFactory factory = new DefaultNameFactory("Client", mode.toString());
+ authSuccessByMode.put(mode, registerMeter(factory, AUTH_SUCCESS));
+ authFailureByMode.put(mode, registerMeter(factory, AUTH_FAILURE));
+
+ Gauge<Integer> clients = registerGauge(factory, CONNECTED_NATIVE_CLIENTS, () -> countConnectedClients((ServerConnection connection) -> {
+ AuthenticatedUser user = connection.getClientState().getUser();
+ return Optional.ofNullable(user)
+ .map(u -> mode.equals(u.getAuthenticationMode()))
+ .orElse(false);
+ }));
+ connectedNativeClientsByAuthMode.put(mode, clients);
+ }
+
pausedConnections = new AtomicInteger();
pausedConnectionsGauge = registerGauge("PausedConnections", pausedConnections::get);
requestDiscarded = registerMeter("RequestDiscarded");
@@ -163,6 +257,16 @@ public final class ClientMetrics
return clients;
}
+ private int countConnectedClients(Predicate<ServerConnection> predicate)
+ {
+ int count = 0;
+
+ for (Server server : servers)
+ count += server.countConnectedClients(predicate);
+
+ return count;
+ }
+
private List<Map<String, String>> recentClientStats()
{
List<Map<String, String>> stats = new ArrayList<>();
@@ -178,17 +282,28 @@ public final class ClientMetrics
private <T> Gauge<T> registerGauge(String name, Gauge<T> gauge)
{
- return Metrics.register(factory.createMetricName(name), gauge);
+ return registerGauge(factory, name, gauge);
}
-
- private void registerGauge(String name, String deprecated, Gauge<?> gauge)
+
+ private <T> Gauge<T> registerGauge(MetricNameFactory metricNameFactory, String name, Gauge<T> gauge)
+ {
+ return Metrics.register(metricNameFactory.createMetricName(name), gauge);
+ }
+
+ private <T> Gauge<T> registerGauge(String name, String deprecated, Gauge<T> gauge)
{
- Gauge<?> registeredGauge = registerGauge(name, gauge);
+ Gauge<T> registeredGauge = registerGauge(name, gauge);
Metrics.registerMBean(registeredGauge, factory.createMetricName(deprecated).getMBeanName());
+ return registeredGauge;
}
private Meter registerMeter(String name)
{
- return Metrics.meter(factory.createMetricName(name));
+ return registerMeter(factory, name);
+ }
+
+ private Meter registerMeter(MetricNameFactory metricNameFactory, String name)
+ {
+ return Metrics.meter(metricNameFactory.createMetricName(name));
}
}
diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java
index 7b8fd1e096..180fc77766 100644
--- a/src/java/org/apache/cassandra/tools/NodeProbe.java
+++ b/src/java/org/apache/cassandra/tools/NodeProbe.java
@@ -2057,7 +2057,7 @@ public class NodeProbe implements AutoCloseable
case "connections": // List<Map<String,String>> - list of all native connections and their properties
case "connectedNativeClients": // number of connected native clients
case "connectedNativeClientsByUser": // number of native clients by username
- case "clientsByProtocolVersion": // number of native clients by username
+ case "clientsByProtocolVersion": // number of native clients by protocol version
return JMX.newMBeanProxy(mbeanServerConn,
new ObjectName("org.apache.cassandra.metrics:type=Client,name=" + metricName),
CassandraMetricsRegistry.JmxGaugeMBean.class).getValue();
diff --git a/src/java/org/apache/cassandra/tools/nodetool/ClientStats.java b/src/java/org/apache/cassandra/tools/nodetool/ClientStats.java
index 7ed9b9d443..fcc9d76629 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/ClientStats.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/ClientStats.java
@@ -23,7 +23,9 @@ import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.stream.Collectors;
+import com.google.common.collect.ImmutableList;
import io.airlift.airline.Command;
import io.airlift.airline.Option;
import org.apache.cassandra.tools.NodeProbe;
@@ -47,6 +49,9 @@ public class ClientStats extends NodeToolCmd
@Option(title = "list_connections_with_client_options", name = "--client-options", description = "Lists all connections and the client options")
private boolean clientOptions = false;
+ @Option(title = "verbose", name = "--verbose", description = "Lists all connections with additional details (client options, authenticator-specific metadata and more)")
+ private boolean verbose = false;
+
@Override
public void execute(NodeProbe probe)
{
@@ -86,55 +91,36 @@ public class ClientStats extends NodeToolCmd
return;
}
- if (listConnections)
+ // Note: for compatbility with existing implementation if someone passes --all (listConnections),
+ // --client-options, and --metadata all three will be printed.
+ List<Map<String, String>> clients = (List<Map<String, String>>) probe.getClientMetric("connections");
+ if (!clients.isEmpty() && (listConnections || clientOptions || verbose))
{
- List<Map<String, String>> clients = (List<Map<String, String>>) probe.getClientMetric("connections");
- if (!clients.isEmpty())
+ ImmutableList.Builder<String> tableHeaderBuilder = ImmutableList.<String>builder()
+ .add("Address", "SSL", "Cipher", "Protocol", "Version",
+ "User", "Keyspace", "Requests", "Driver-Name",
+ "Driver-Version");
+ ImmutableList.Builder<String> tableFieldsBuilder = ImmutableList.<String>builder()
+ .add(ConnectedClient.ADDRESS, ConnectedClient.SSL,
+ ConnectedClient.CIPHER, ConnectedClient.PROTOCOL,
+ ConnectedClient.VERSION, ConnectedClient.USER,
+ ConnectedClient.KEYSPACE, ConnectedClient.REQUESTS,
+ ConnectedClient.DRIVER_NAME, ConnectedClient.DRIVER_VERSION);
+ if (clientOptions || verbose)
{
- TableBuilder table = new TableBuilder();
- table.add("Address", "SSL", "Cipher", "Protocol", "Version", "User", "Keyspace", "Requests", "Driver-Name", "Driver-Version");
- for (Map<String, String> conn : clients)
- {
- table.add(conn.get(ConnectedClient.ADDRESS),
- conn.get(ConnectedClient.SSL),
- conn.get(ConnectedClient.CIPHER),
- conn.get(ConnectedClient.PROTOCOL),
- conn.get(ConnectedClient.VERSION),
- conn.get(ConnectedClient.USER),
- conn.get(ConnectedClient.KEYSPACE),
- conn.get(ConnectedClient.REQUESTS),
- conn.get(ConnectedClient.DRIVER_NAME),
- conn.get(ConnectedClient.DRIVER_VERSION));
- }
- table.printTo(out);
- out.println();
+ tableHeaderBuilder.add("Client-Options");
+ tableFieldsBuilder.add(ConnectedClient.CLIENT_OPTIONS);
}
- }
- if (clientOptions)
- {
- List<Map<String, String>> clients = (List<Map<String, String>>) probe.getClientMetric("connections");
- if (!clients.isEmpty())
+ if (verbose)
{
- TableBuilder table = new TableBuilder();
- table.add("Address", "SSL", "Cipher", "Protocol", "Version", "User", "Keyspace", "Requests", "Driver-Name", "Driver-Version", "Client-Options");
- for (Map<String, String> conn : clients)
- {
- table.add(conn.get(ConnectedClient.ADDRESS),
- conn.get(ConnectedClient.SSL),
- conn.get(ConnectedClient.CIPHER),
- conn.get(ConnectedClient.PROTOCOL),
- conn.get(ConnectedClient.VERSION),
- conn.get(ConnectedClient.USER),
- conn.get(ConnectedClient.KEYSPACE),
- conn.get(ConnectedClient.REQUESTS),
- conn.get(ConnectedClient.DRIVER_NAME),
- conn.get(ConnectedClient.DRIVER_VERSION),
- conn.get(ConnectedClient.CLIENT_OPTIONS));
- }
- table.printTo(out);
- out.println();
+ tableHeaderBuilder.add("Auth-Mode", "Auth-Metadata");
+ tableFieldsBuilder.add(ConnectedClient.AUTHENTICATION_MODE, ConnectedClient.AUTHENTICATION_METADATA);
}
+
+ List<String> tableHeader = tableHeaderBuilder.build();
+ List<String> tableFields = tableFieldsBuilder.build();
+ printTable(out, tableHeader, tableFields, clients);
}
Map<String, Integer> connectionsByUser = (Map<String, Integer>) probe.getClientMetric("connectedNativeClientsByUser");
@@ -149,4 +135,27 @@ public class ClientStats extends NodeToolCmd
}
table.printTo(out);
}
+
+ /**
+ * Convenience function to print a table with the given header and the resolved fields for each connection.
+ *
+ * @param out print stream to print to.
+ * @param headers headers for the table
+ * @param tableFields the fields from {@link ConnectedClient} to retrieve from each client connection.
+ * @param clients the clients to print, each client being a row inthe table.
+ */
+ private void printTable(PrintStream out, List<String> headers, List<String> tableFields, List<Map<String, String>> clients)
+ {
+ TableBuilder table = new TableBuilder();
+ table.add(headers);
+ for (Map<String, String> conn : clients)
+ {
+ List<String> connectionFieldValues = tableFields.stream()
+ .map(conn::get)
+ .collect(Collectors.toList());
+ table.add(connectionFieldValues);
+ }
+ table.printTo(out);
+ out.println();
+ }
}
diff --git a/src/java/org/apache/cassandra/transport/ConnectedClient.java b/src/java/org/apache/cassandra/transport/ConnectedClient.java
index a4af32fca1..b8624de983 100644
--- a/src/java/org/apache/cassandra/transport/ConnectedClient.java
+++ b/src/java/org/apache/cassandra/transport/ConnectedClient.java
@@ -27,8 +27,11 @@ import com.google.common.collect.ImmutableMap;
import io.netty.handler.ssl.SslHandler;
import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.service.ClientState;
+import static org.apache.cassandra.auth.IAuthenticator.AuthenticationMode.UNAUTHENTICATED;
+
public final class ConnectedClient
{
public static final String ADDRESS = "address";
@@ -42,6 +45,8 @@ public final class ConnectedClient
public static final String SSL = "ssl";
public static final String CIPHER = "cipher";
public static final String PROTOCOL = "protocol";
+ public static final String AUTHENTICATION_MODE = "authenticationMode";
+ public static final String AUTHENTICATION_METADATA = "authenticationMetadata";
private static final String UNDEFINED = "undefined";
@@ -124,6 +129,24 @@ public final class ConnectedClient
: Optional.empty();
}
+ public Map<String, Object> authenticationMetadata()
+ {
+ AuthenticatedUser user = state().getUser();
+
+ return null != user
+ ? user.getMetadata()
+ : Collections.emptyMap();
+ }
+
+ public IAuthenticator.AuthenticationMode authenticationMode()
+ {
+ AuthenticatedUser user = state().getUser();
+
+ return null != user
+ ? user.getAuthenticationMode()
+ : UNAUTHENTICATED;
+ }
+
private ClientState state()
{
return connection.getClientState();
@@ -150,6 +173,10 @@ public final class ConnectedClient
.put(SSL, Boolean.toString(sslEnabled()))
.put(CIPHER, sslCipherSuite().orElse(UNDEFINED))
.put(PROTOCOL, sslProtocol().orElse(UNDEFINED))
+ .put(AUTHENTICATION_MODE, authenticationMode().toString())
+ .put(AUTHENTICATION_METADATA, Joiner.on(", ")
+ .withKeyValueSeparator("=")
+ .join(authenticationMetadata()))
.build();
}
}
diff --git a/src/java/org/apache/cassandra/transport/Server.java b/src/java/org/apache/cassandra/transport/Server.java
index 5ec6d20187..02de68596c 100644
--- a/src/java/org/apache/cassandra/transport/Server.java
+++ b/src/java/org/apache/cassandra/transport/Server.java
@@ -23,6 +23,7 @@ import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -145,6 +146,14 @@ public class Server implements CassandraDaemon.Server
return connectionTracker.countConnectedClientsByUser();
}
+ /**
+ * @return A count of the number of clients matching the given predicate.
+ */
+ public int countConnectedClients(Predicate<ServerConnection> predicate)
+ {
+ return connectionTracker.countConnectedClients(predicate);
+ }
+
public List<ConnectedClient> getConnectedClients()
{
List<ConnectedClient> result = new ArrayList<>();
@@ -306,6 +315,24 @@ public class Server implements CassandraDaemon.Server
return allChannels.size() != 0 ? allChannels.size() - 1 : 0;
}
+ int countConnectedClients(Predicate<ServerConnection> predicate)
+ {
+ int count = 0;
+ for (Channel c : allChannels)
+ {
+ Connection connection = c.attr(Connection.attributeKey).get();
+ if (connection instanceof ServerConnection)
+ {
+ ServerConnection conn = (ServerConnection) connection;
+ if (predicate.test(conn))
+ {
+ count++;
+ }
+ }
+ }
+ return count;
+ }
+
Map<String, Integer> countConnectedClientsByUser()
{
Map<String, Integer> result = new HashMap<>();
diff --git a/src/java/org/apache/cassandra/transport/ServerConnection.java b/src/java/org/apache/cassandra/transport/ServerConnection.java
index dd6fa313ae..21f2e0b0e6 100644
--- a/src/java/org/apache/cassandra/transport/ServerConnection.java
+++ b/src/java/org/apache/cassandra/transport/ServerConnection.java
@@ -142,4 +142,15 @@ public class ServerConnection extends Connection
}
return certificates;
}
+
+ /**
+ * @return Whether this connection is SSL-encrypted.
+ */
+ public boolean isSSL()
+ {
+ // If an SslHandler is present on the pipeline, the connection is using ssl.
+ SslHandler sslHandler = (SslHandler) channel().pipeline()
+ .get("ssl");
+ return sslHandler != null;
+ }
}
diff --git a/src/java/org/apache/cassandra/transport/messages/AuthUtil.java b/src/java/org/apache/cassandra/transport/messages/AuthUtil.java
index 1009984eac..1c091c7278 100644
--- a/src/java/org/apache/cassandra/transport/messages/AuthUtil.java
+++ b/src/java/org/apache/cassandra/transport/messages/AuthUtil.java
@@ -58,6 +58,7 @@ public final class AuthUtil
static Response handleLogin(Connection connection, QueryState queryState, byte[] token,
BiFunction<Boolean, byte[], Response> messageToSendBasedOnNegotiation)
{
+ IAuthenticator.SaslNegotiator negotiator = ((ServerConnection) connection).getSaslNegotiator(queryState);
try
{
// client-side timeout can disconnect while sitting in auth executor queue so (client default 12s)
@@ -66,13 +67,12 @@ public final class AuthUtil
{
throw new AuthenticationException("Auth check after connection closed");
}
- IAuthenticator.SaslNegotiator negotiator = ((ServerConnection) connection).getSaslNegotiator(queryState);
byte[] challenge = negotiator.evaluateResponse(token);
if (negotiator.isComplete())
{
AuthenticatedUser user = negotiator.getAuthenticatedUser();
queryState.getClientState().login(user);
- ClientMetrics.instance.markAuthSuccess();
+ ClientMetrics.instance.markAuthSuccess(user.getAuthenticationMode());
AuthEvents.instance.notifyAuthSuccess(queryState);
// authentication is complete, complete the authentication flow.
return messageToSendBasedOnNegotiation.apply(true, challenge);
@@ -85,7 +85,7 @@ public final class AuthUtil
}
catch (AuthenticationException e)
{
- ClientMetrics.instance.markAuthFailure();
+ ClientMetrics.instance.markAuthFailure(negotiator.getAuthenticationMode());
AuthEvents.instance.notifyAuthFailure(queryState, e);
return ErrorMessage.fromException(e);
}
diff --git a/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java b/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java
index 5f119c6bdf..51af668c29 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java
@@ -28,6 +28,8 @@ import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;
import com.google.common.collect.ImmutableMap;
+
+import org.apache.cassandra.transport.TlsTestUtils;
import org.apache.cassandra.utils.concurrent.Condition;
import org.junit.Assert;
import org.slf4j.Logger;
@@ -62,10 +64,10 @@ import static org.apache.cassandra.utils.concurrent.Condition.newOneTimeConditio
public class AbstractEncryptionOptionsImpl extends TestBaseImpl
{
final Logger logger = LoggerFactory.getLogger(EncryptionOptions.class);
- final static String validKeyStorePath = "test/conf/cassandra_ssl_test.keystore";
- final static String validKeyStorePassword = "cassandra";
- final static String validTrustStorePath = "test/conf/cassandra_ssl_test.truststore";
- final static String validTrustStorePassword = "cassandra";
+ final static String validKeyStorePath = TlsTestUtils.SERVER_KEYSTORE_PATH;
+ final static String validKeyStorePassword = TlsTestUtils.SERVER_KEYSTORE_PASSWORD;
+ final static String validTrustStorePath = TlsTestUtils.SERVER_TRUSTSTORE_PATH;
+ final static String validTrustStorePassword = TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD;
// Base configuration map for a valid keystore that can be opened
final static Map<String,Object> validKeystore = ImmutableMap.of("keystore", validKeyStorePath,
diff --git a/test/distributed/org/apache/cassandra/distributed/test/AuthTest.java b/test/distributed/org/apache/cassandra/distributed/test/AuthTest.java
index 8f67a6ac79..f424691eac 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/AuthTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/AuthTest.java
@@ -27,7 +27,6 @@ import org.junit.Test;
import com.datastax.driver.core.PlainTextAuthProvider;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.policies.DCAwareRoundRobinPolicy;
-import org.apache.cassandra.auth.CassandraRoleManager;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.ConsistencyLevel;
import org.apache.cassandra.distributed.api.ICoordinator;
@@ -43,6 +42,7 @@ import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
import static org.apache.cassandra.distributed.api.Feature.NETWORK;
+import static org.apache.cassandra.distributed.util.Auth.waitForExistingRoles;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -161,14 +161,6 @@ public class AuthTest extends TestBaseImpl
return cluster.bootstrap(config);
}
- private void waitForExistingRoles(IInvokableInstance instance)
- {
- await().pollDelay(1, SECONDS)
- .pollInterval(1, SECONDS)
- .atMost(30, SECONDS)
- .until(() -> instance.callOnInstance(CassandraRoleManager::hasExistingRoles));
- }
-
private long getPasswordWritetime(ICoordinator coordinator)
{
return (Long) coordinator.execute("SELECT WRITETIME (salted_hash) from system_auth.roles where role = 'cassandra'",
diff --git a/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionEnforcementTest.java b/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionEnforcementTest.java
index a37f092a74..142a95b362 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionEnforcementTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionEnforcementTest.java
@@ -55,6 +55,7 @@ import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.OutboundConnections;
import org.apache.cassandra.net.PingRequest;
import org.apache.cassandra.net.Verb;
+import org.apache.cassandra.transport.TlsTestUtils;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.Pair;
import org.awaitility.Awaitility;
@@ -199,10 +200,10 @@ public final class InternodeEncryptionEnforcementTest extends TestBaseImpl
encryption.put("internode_encryption", "none");
if (c.num() == 1)
{
- encryption.put("keystore", "test/conf/cassandra_ssl_test.keystore");
- encryption.put("keystore_password", "cassandra");
- encryption.put("truststore", "test/conf/cassandra_ssl_test.truststore");
- encryption.put("truststore_password", "cassandra");
+ encryption.put("keystore", TlsTestUtils.SERVER_KEYSTORE_PATH);
+ encryption.put("keystore_password", TlsTestUtils.SERVER_KEYSTORE_PASSWORD);
+ encryption.put("truststore", TlsTestUtils.SERVER_TRUSTSTORE_PATH);
+ encryption.put("truststore_password", TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD);
encryption.put("internode_encryption", "all");
}
c.set("server_encryption_options", encryption);
@@ -276,10 +277,10 @@ public final class InternodeEncryptionEnforcementTest extends TestBaseImpl
c.with(Feature.NETWORK);
c.with(Feature.NATIVE_PROTOCOL);
- HashMap<String, Object> encryption = new HashMap<>(); encryption.put("keystore", "test/conf/cassandra_ssl_test.keystore");
- encryption.put("keystore_password", "cassandra");
- encryption.put("truststore", "test/conf/cassandra_ssl_test.truststore");
- encryption.put("truststore_password", "cassandra");
+ HashMap<String, Object> encryption = new HashMap<>(); encryption.put("keystore", TlsTestUtils.SERVER_KEYSTORE_PATH);
+ encryption.put("keystore_password", TlsTestUtils.SERVER_KEYSTORE_PASSWORD);
+ encryption.put("truststore", TlsTestUtils.SERVER_TRUSTSTORE_PATH);
+ encryption.put("truststore_password", TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD);
encryption.put("internode_encryption", "dc");
c.set("server_encryption_options", encryption);
})
@@ -395,10 +396,10 @@ public final class InternodeEncryptionEnforcementTest extends TestBaseImpl
c.with(Feature.NATIVE_PROTOCOL);
HashMap<String, Object> encryption = new HashMap<>();
- encryption.put("keystore", "test/conf/cassandra_ssl_test.keystore");
- encryption.put("keystore_password", "cassandra");
- encryption.put("truststore", "test/conf/cassandra_ssl_test.truststore");
- encryption.put("truststore_password", "cassandra");
+ encryption.put("keystore", TlsTestUtils.SERVER_KEYSTORE_PATH);
+ encryption.put("keystore_password", TlsTestUtils.SERVER_KEYSTORE_PASSWORD);
+ encryption.put("truststore", TlsTestUtils.SERVER_TRUSTSTORE_PATH);
+ encryption.put("truststore_password", TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD);
encryption.put("internode_encryption", "all");
encryption.put("require_client_auth", "true");
c.set("server_encryption_options", encryption);
@@ -430,8 +431,8 @@ public final class InternodeEncryptionEnforcementTest extends TestBaseImpl
// Check if the presented certificates during internode authentication are the ones in the keystores
// configured in the cassandra.yaml configuration.
KeyStore keyStore = KeyStore.getInstance("JKS");
- char[] keyStorePassword = "cassandra".toCharArray();
- InputStream keyStoreData = new FileInputStream("test/conf/cassandra_ssl_test.keystore");
+ char[] keyStorePassword = TlsTestUtils.SERVER_KEYSTORE_PASSWORD.toCharArray();
+ InputStream keyStoreData = new FileInputStream(TlsTestUtils.SERVER_KEYSTORE_PATH);
keyStore.load(keyStoreData, keyStorePassword);
return certificates != null && certificates.length != 0 && keyStore.getCertificate("cassandra_ssl_test").equals(certificates[0]);
}
diff --git a/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java b/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java
index 86d4e8fa26..dfb3a6656d 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java
@@ -40,6 +40,7 @@ import com.datastax.shaded.netty.handler.ssl.SslContext;
import com.datastax.shaded.netty.handler.ssl.SslContextBuilder;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.Feature;
+import org.apache.cassandra.transport.TlsTestUtils;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -403,10 +404,10 @@ public class NativeTransportEncryptionOptionsTest extends AbstractEncryptionOpti
private SSLOptions sslOptions(boolean withKeyStore) throws Exception
{
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
- sslContextBuilder.trustManager(createTrustManagerFactory("test/conf/cassandra_ssl_test.truststore", "cassandra"));
+ sslContextBuilder.trustManager(createTrustManagerFactory(TlsTestUtils.SERVER_TRUSTSTORE_PATH, TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD));
if (withKeyStore)
{
- sslContextBuilder.keyManager(createKeyManagerFactory("test/conf/cassandra_ssl_test_outbound.keystore", "cassandra"));
+ sslContextBuilder.keyManager(createKeyManagerFactory(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PATH, TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PASSWORD));
}
SslContext sslContext = sslContextBuilder.build();
@@ -428,11 +429,11 @@ public class NativeTransportEncryptionOptionsTest extends AbstractEncryptionOpti
InetAddress address = cluster.get(1).config().broadcastAddress().getAddress();
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
if (ipInSAN)
- sslContextBuilder.keyManager(createKeyManagerFactory("test/conf/cassandra_ssl_test_endpoint_verify.keystore", "cassandra"));
+ sslContextBuilder.keyManager(createKeyManagerFactory(TlsTestUtils.SERVER_KEYSTORE_ENDPOINT_VERIFY_PATH, TlsTestUtils.SERVER_KEYSTORE_ENDPOINT_VERIFY_PASSWORD));
else
- sslContextBuilder.keyManager(createKeyManagerFactory("test/conf/cassandra_ssl_test_outbound.keystore", "cassandra"));
+ sslContextBuilder.keyManager(createKeyManagerFactory(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PATH, TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PASSWORD));
- SslContext sslContext = sslContextBuilder.trustManager(createTrustManagerFactory("test/conf/cassandra_ssl_test.truststore", "cassandra"))
+ SslContext sslContext = sslContextBuilder.trustManager(createTrustManagerFactory(TlsTestUtils.SERVER_TRUSTSTORE_PATH, TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD))
.build();
final SSLOptions sslOptions = socketChannel -> sslContext.newHandler(socketChannel.alloc());
com.datastax.driver.core.Cluster driverCluster = com.datastax.driver.core.Cluster.builder()
diff --git a/test/unit/org/apache/cassandra/auth/AuthTestUtils.java b/test/unit/org/apache/cassandra/auth/AuthTestUtils.java
index d2e40bd956..01d13bfdd3 100644
--- a/test/unit/org/apache/cassandra/auth/AuthTestUtils.java
+++ b/test/unit/org/apache/cassandra/auth/AuthTestUtils.java
@@ -59,8 +59,11 @@ import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.transport.messages.ResultMessage;
import static java.lang.String.format;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.cassandra.auth.AuthKeyspace.CIDR_GROUPS;
import static org.apache.cassandra.schema.SchemaConstants.AUTH_KEYSPACE_NAME;
+import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertNotNull;
@@ -420,4 +423,16 @@ public class AuthTestUtils
System.arraycopy(passwordBytes, 0, token, usernameBytes.length + 2, passwordBytes.length);
return token;
}
+
+ /**
+ * Block on waiting for existence of the initial superuser roles. This should be used by all tests that
+ * rely on existence of these users.
+ */
+ public static void waitForExistingRoles()
+ {
+ await().pollDelay(0, MILLISECONDS)
+ .pollInterval(250, MILLISECONDS)
+ .atMost(10, SECONDS)
+ .until(CassandraRoleManager::hasExistingRoles);
+ }
}
diff --git a/test/unit/org/apache/cassandra/auth/MutualTlsInternodeAuthenticatorTest.java b/test/unit/org/apache/cassandra/auth/MutualTlsInternodeAuthenticatorTest.java
index d9fc3d0a3c..8fdd23a444 100644
--- a/test/unit/org/apache/cassandra/auth/MutualTlsInternodeAuthenticatorTest.java
+++ b/test/unit/org/apache/cassandra/auth/MutualTlsInternodeAuthenticatorTest.java
@@ -41,6 +41,7 @@ import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.transport.TlsTestUtils;
import static org.apache.cassandra.auth.AuthTestUtils.loadCertificateChain;
import static org.apache.cassandra.auth.IInternodeAuthenticator.InternodeConnectionDirection.INBOUND;
@@ -163,10 +164,10 @@ public class MutualTlsInternodeAuthenticatorTest
public void testNoIdentitiesInKeystore()
{
Config config = DatabaseDescriptor.getRawConfig();
- config.server_encryption_options = config.server_encryption_options.withOutboundKeystore("test/conf/cassandra_ssl_test.keystore")
- .withOutboundKeystorePassword("cassandra");
+ config.server_encryption_options = config.server_encryption_options.withOutboundKeystore(TlsTestUtils.SERVER_KEYSTORE_PATH)
+ .withOutboundKeystorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD);
expectedException.expect(ConfigurationException.class);
- expectedException.expectMessage("No identity was extracted from the outbound keystore 'test/conf/cassandra_ssl_test.keystore'");
+ expectedException.expectMessage(String.format("No identity was extracted from the outbound keystore '%s'", TlsTestUtils.SERVER_KEYSTORE_PATH));
new MutualTlsInternodeAuthenticator(getParams());
}
diff --git a/test/unit/org/apache/cassandra/config/EncryptionOptionsEqualityTest.java b/test/unit/org/apache/cassandra/config/EncryptionOptionsEqualityTest.java
index 7c86069a80..317c8ec626 100644
--- a/test/unit/org/apache/cassandra/config/EncryptionOptionsEqualityTest.java
+++ b/test/unit/org/apache/cassandra/config/EncryptionOptionsEqualityTest.java
@@ -25,6 +25,7 @@ import org.junit.Test;
import org.apache.cassandra.security.DefaultSslContextFactory;
import org.apache.cassandra.security.DummySslContextFactoryImpl;
+import org.apache.cassandra.transport.TlsTestUtils;
import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.NOT_REQUIRED;
import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED;
@@ -41,12 +42,12 @@ public class EncryptionOptionsEqualityTest
{
return new EncryptionOptions.ServerEncryptionOptions()
.withStoreType("JKS")
- .withKeyStore("test/conf/cassandra.keystore")
- .withKeyStorePassword("cassandra")
- .withTrustStore("test/conf/cassandra_ssl_test.truststore")
- .withTrustStorePassword("cassandra")
- .withOutboundKeystore("test/conf/cassandra_outbound.keystore")
- .withOutboundKeystorePassword("cassandra")
+ .withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH)
+ .withKeyStorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
+ .withTrustStore(TlsTestUtils.SERVER_TRUSTSTORE_PATH)
+ .withTrustStorePassword(TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD)
+ .withOutboundKeystore(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PATH)
+ .withOutboundKeystorePassword(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PASSWORD)
.withProtocol("TLSv1.1")
.withRequireClientAuth(REQUIRED)
.withRequireEndpointVerification(false);
@@ -57,10 +58,10 @@ public class EncryptionOptionsEqualityTest
EncryptionOptions encryptionOptions1 =
new EncryptionOptions()
.withStoreType("JKS")
- .withKeyStore("test/conf/cassandra.keystore")
- .withKeyStorePassword("cassandra")
- .withTrustStore("test/conf/cassandra_ssl_test.truststore")
- .withTrustStorePassword("cassandra")
+ .withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH)
+ .withKeyStorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
+ .withTrustStore(TlsTestUtils.SERVER_TRUSTSTORE_PATH)
+ .withTrustStorePassword(TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD)
.withProtocol("TLSv1.1")
.withRequireClientAuth(REQUIRED)
.withRequireEndpointVerification(false);
@@ -68,10 +69,10 @@ public class EncryptionOptionsEqualityTest
EncryptionOptions encryptionOptions2 =
new EncryptionOptions()
.withStoreType("JKS")
- .withKeyStore("test/conf/cassandra.keystore")
- .withKeyStorePassword("cassandra")
- .withTrustStore("test/conf/cassandra_ssl_test.truststore")
- .withTrustStorePassword("cassandra")
+ .withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH)
+ .withKeyStorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
+ .withTrustStore(TlsTestUtils.SERVER_TRUSTSTORE_PATH)
+ .withTrustStorePassword(TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD)
.withProtocol("TLSv1.1")
.withRequireClientAuth(REQUIRED)
.withRequireEndpointVerification(false);
diff --git a/test/unit/org/apache/cassandra/cql3/CQLTester.java b/test/unit/org/apache/cassandra/cql3/CQLTester.java
index de68125f12..5f996e49af 100644
--- a/test/unit/org/apache/cassandra/cql3/CQLTester.java
+++ b/test/unit/org/apache/cassandra/cql3/CQLTester.java
@@ -58,6 +58,7 @@ import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnectorServer;
+import javax.net.ssl.SSLException;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
@@ -165,6 +166,7 @@ import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.transport.Server;
import org.apache.cassandra.transport.SimpleClient;
+import org.apache.cassandra.transport.TlsTestUtils;
import org.apache.cassandra.transport.messages.ResultMessage;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.FBUtilities;
@@ -229,8 +231,8 @@ public abstract class CQLTester
protected static int nativePort;
protected static final InetAddress nativeAddr;
protected static final Set<InetAddressAndPort> remoteAddrs = new HashSet<>();
- private static final Map<Pair<User, ProtocolVersion>, Cluster> clusters = new HashMap<>();
- private static final Map<Pair<User, ProtocolVersion>, Session> sessions = new HashMap<>();
+ private static final Map<ClusterSettings, Cluster> clusters = new HashMap<>();
+ private static final Map<ClusterSettings, Session> sessions = new HashMap<>();
private static Consumer<Cluster.Builder> clusterBuilderConfigurator;
@@ -283,6 +285,10 @@ public abstract class CQLTester
private User user;
+ private boolean useEncryption = false;
+
+ private boolean useClientCert = false;
+
// We don't use USE_PREPARED_VALUES in the code below so some test can foce value preparation (if the result
// is not expected to be the same without preparation)
private boolean usePrepared = USE_PREPARED_VALUES;
@@ -554,6 +560,22 @@ public abstract class CQLTester
AuthCacheService.initializeAndRegisterCaches();
}
+ /**
+ * @param useEncryption Whether created clients should use encryption.
+ */
+ public void shouldUseEncryption(boolean useEncryption)
+ {
+ this.useEncryption = useEncryption;
+ }
+
+ /**
+ * @param useClientCert Whether created clients should provide a client cert if encryption is enabled.
+ */
+ public void shouldUseClientCertificate(boolean useClientCert)
+ {
+ this.useClientCert = useClientCert;
+ }
+
/**
* Configures the server to require client encryption for CQL. Useful for tests which exercise TLS specific
* behavior.
@@ -562,19 +584,18 @@ public abstract class CQLTester
* with {@link Server.Builder#withTlsEncryptionPolicy(EncryptionOptions.TlsEncryptionPolicy)} using
* {@link org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy#ENCRYPTED}.
*/
- protected static void requireNativeProtocolClientEncryption()
+ public static void requireNativeProtocolClientEncryption()
{
- DatabaseDescriptor.updateNativeProtocolEncryptionOptions((encryptionOptions ->
- encryptionOptions.withEnabled(true)
- .withKeyStore("test/conf/cassandra_ssl_test.keystore")
- .withKeyStorePassword("cassandra")
- .withTrustStore("test/conf/cassandra_ssl_test.truststore")
- .withTrustStorePassword("cassandra")
- .withRequireEndpointVerification(false)
- .withRequireClientAuth(EncryptionOptions.ClientAuth.OPTIONAL)));
+ DatabaseDescriptor.updateNativeProtocolEncryptionOptions((encryptionOptions) ->
+ encryptionOptions.withEnabled(true)
+ .withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH)
+ .withKeyStorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
+ .withTrustStore(TlsTestUtils.SERVER_TRUSTSTORE_PATH)
+ .withTrustStorePassword(TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD)
+ .withRequireEndpointVerification(false)
+ .withRequireClientAuth(EncryptionOptions.ClientAuth.OPTIONAL));
}
-
/**
* Initialize Native Transport for test that need it.
*/
@@ -643,7 +664,7 @@ public abstract class CQLTester
server.start();
}
- private static Cluster initClientCluster(User user, ProtocolVersion version)
+ private static Cluster initClientCluster(User user, ProtocolVersion version, boolean useEncryption, boolean useClientCert)
{
SocketOptions socketOptions =
new SocketOptions().setConnectTimeoutMillis(TEST_DRIVER_CONNECTION_TIMEOUT_MS.getInt()) // default is 5000
@@ -662,6 +683,19 @@ public abstract class CQLTester
if (user != null)
builder.withCredentials(user.username, user.password);
+ if (useEncryption)
+ {
+ try
+ {
+ builder.withSSL(TlsTestUtils.getSSLOptions(useClientCert));
+ }
+ catch (SSLException e)
+ {
+ // generally this should work.
+ throw new RuntimeException(e);
+ }
+ }
+
if (version.isBeta())
builder = builder.allowBetaProtocolVersion();
else
@@ -1540,12 +1574,12 @@ public abstract class CQLTester
private Session getSession(ProtocolVersion protocolVersion)
{
Cluster cluster = getCluster(protocolVersion);
- return sessions.computeIfAbsent(Pair.create(user, protocolVersion), userProto -> cluster.connect());
+ return sessions.computeIfAbsent(new ClusterSettings(user, protocolVersion, useEncryption, useClientCert), settings -> cluster.connect());
}
private Cluster getCluster(ProtocolVersion protocolVersion)
{
- return clusters.computeIfAbsent(Pair.create(user, protocolVersion), userProto -> initClientCluster(userProto.left, userProto.right));
+ return clusters.computeIfAbsent(new ClusterSettings(user, protocolVersion, useEncryption, useClientCert), settings -> initClientCluster(user, protocolVersion, useEncryption, useClientCert));
}
protected SimpleClient newSimpleClient(ProtocolVersion version) throws IOException
@@ -2868,4 +2902,35 @@ public abstract class CQLTester
fs.clearListeners();
}
}
+
+ private static class ClusterSettings
+ {
+ private final User user;
+ private final ProtocolVersion protocolVersion;
+ private final boolean shouldUseEncryption;
+ private final boolean shouldUseCertificate;
+
+ ClusterSettings(User user, ProtocolVersion protocolVersion, boolean shouldUseEncryption, boolean shouldUseCertificate)
+ {
+ this.user = user;
+ this.protocolVersion = protocolVersion;
+ this.shouldUseEncryption = shouldUseEncryption;
+ this.shouldUseCertificate = shouldUseCertificate;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ClusterSettings that = (ClusterSettings) o;
+ return shouldUseEncryption == that.shouldUseEncryption && shouldUseCertificate == that.shouldUseCertificate && java.util.Objects.equals(user, that.user) && protocolVersion == that.protocolVersion;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return java.util.Objects.hash(user, protocolVersion, shouldUseEncryption, shouldUseCertificate);
+ }
+ }
}
diff --git a/test/unit/org/apache/cassandra/db/virtual/ClientsTableTest.java b/test/unit/org/apache/cassandra/db/virtual/ClientsTableTest.java
index 862a282a23..7b3c243ffd 100644
--- a/test/unit/org/apache/cassandra/db/virtual/ClientsTableTest.java
+++ b/test/unit/org/apache/cassandra/db/virtual/ClientsTableTest.java
@@ -19,37 +19,57 @@
package org.apache.cassandra.db.virtual;
import java.net.InetAddress;
+import java.util.Collections;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import org.junit.Assert;
-import org.junit.Before;
+import org.junit.BeforeClass;
import org.junit.Test;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
+import org.apache.cassandra.auth.AuthTestUtils;
+import org.apache.cassandra.auth.IAuthenticator;
+import org.apache.cassandra.auth.MutualTlsAuthenticator;
+import org.apache.cassandra.auth.SpiffeCertificateValidator;
+import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.transport.TlsTestUtils;
import org.assertj.core.api.Assertions;
+
+import static org.apache.cassandra.auth.AuthTestUtils.waitForExistingRoles;
import static org.assertj.core.api.Assertions.assertThat;
public class ClientsTableTest extends CQLTester
{
private static final String KS_NAME = "vts";
- private ClientsTable table;
-
- @Before
- public void config()
+ @BeforeClass
+ public static void config()
{
- table = new ClientsTable(KS_NAME);
+ // Enable TLS and require native protocol encryption
+ requireNativeProtocolClientEncryption();
+ requireNetwork(server -> server.withTlsEncryptionPolicy(EncryptionOptions.TlsEncryptionPolicy.ENCRYPTED), cluster -> {});
+
+ IAuthenticator authenticator = new MutualTlsAuthenticator( ImmutableMap.of("validator_class_name", SpiffeCertificateValidator.class.getSimpleName()));
+ requireAuthentication(authenticator);
+
+ ClientsTable table = new ClientsTable(KS_NAME);
VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME, ImmutableList.of(table)));
+
+ waitForExistingRoles();
+
+ AuthTestUtils.addIdentityToRole(TlsTestUtils.CLIENT_SPIFFE_IDENTITY, "cassandra");
}
@Test
- public void testSelectAll() throws Throwable
+ public void testSelectAll()
{
+ shouldUseEncryption(true);
+ shouldUseClientCertificate(true);
ResultSet result = executeNet("SELECT * FROM vts.clients");
-
for (Row r : result)
{
Assert.assertEquals(InetAddress.getLoopbackAddress(), r.getInet("address"));
@@ -62,6 +82,13 @@ public class ClientsTableTest extends CQLTester
Assertions.assertThat(r.getMap("client_options", String.class, String.class))
.hasEntrySatisfying("DRIVER_VERSION", value -> assertThat(value.contains(r.getString("driver_name"))))
.hasEntrySatisfying("DRIVER_VERSION", value -> assertThat(value.contains(r.getString("driver_version"))));
+ Assert.assertTrue(r.getBool("ssl_enabled"));
+ Assert.assertTrue(r.getString("ssl_protocol").startsWith("TLS"));
+ Assert.assertNotNull(r.getString("ssl_cipher_suite"));
+ Assert.assertEquals("cassandra", r.getString("username"));
+ Assert.assertEquals("MutualTls", r.getString("authentication_mode"));
+ Assert.assertEquals(Collections.singletonMap("identity", TlsTestUtils.CLIENT_SPIFFE_IDENTITY),
+ r.getMap("authentication_metadata", String.class, String.class));
}
}
}
diff --git a/test/unit/org/apache/cassandra/metrics/ClientMetricsTest.java b/test/unit/org/apache/cassandra/metrics/ClientMetricsTest.java
new file mode 100644
index 0000000000..0f1cd26f50
--- /dev/null
+++ b/test/unit/org/apache/cassandra/metrics/ClientMetricsTest.java
@@ -0,0 +1,241 @@
+/*
+ * 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.metrics;
+
+import javax.net.ssl.SSLException;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Meter;
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.HostDistance;
+import com.datastax.driver.core.PoolingOptions;
+import com.datastax.driver.core.Session;
+import org.apache.cassandra.ServerTestUtils;
+import org.apache.cassandra.auth.IAuthenticator.AuthenticationMode;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.OverrideConfigurationLoader;
+import org.apache.cassandra.service.EmbeddedCassandraService;
+import org.apache.cassandra.transport.TlsTestUtils;
+
+import static org.apache.cassandra.auth.AuthTestUtils.waitForExistingRoles;
+import static org.apache.cassandra.config.CassandraRelevantProperties.SUPERUSER_SETUP_DELAY_MS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+public class ClientMetricsTest
+{
+ private static EmbeddedCassandraService cassandra;
+
+ private static final ClientMetrics clientMetrics = ClientMetrics.instance;
+
+ private static final String USER1 = "user1";
+ private static final String USER1PW = "user1pw";
+
+ @BeforeClass
+ public static void setUp() throws Throwable
+ {
+ OverrideConfigurationLoader.override(TlsTestUtils::configureWithMutualTlsWithPasswordFallbackAuthenticator);
+
+ SUPERUSER_SETUP_DELAY_MS.setLong(0);
+
+ cassandra = ServerTestUtils.startEmbeddedCassandraService();
+
+ waitForExistingRoles();
+
+ // Allow client to connect as cassandra using an mTLS identity.
+ try (Cluster cluster = clusterBuilder()
+ .withCredentials("cassandra", "cassandra")
+ .build())
+ {
+ try (Session session = cluster.connect())
+ {
+ session.execute(String.format("CREATE ROLE %s WITH password = '%s' AND LOGIN = true", USER1, USER1PW));
+ session.execute(String.format("ADD IDENTITY '%s' TO ROLE '%s'", TlsTestUtils.CLIENT_SPIFFE_IDENTITY, USER1));
+ }
+ }
+ }
+
+ public static void tearDown()
+ {
+ if (cassandra != null)
+ {
+ cassandra.stop();
+ }
+ }
+
+ private static Cluster.Builder clusterBuilder()
+ {
+ return Cluster.builder().addContactPoint("127.0.0.1").withPort(DatabaseDescriptor.getNativeTransportPort());
+ }
+
+ /**
+ * Validates the behavior of metrics tied to connectivity and authentication by doing the following:
+ *
+ * <ol>
+ * <li>Attempt to connect with password authentication and ensure ConnectedNativeClients and AuthSuccess metrics
+ * are incremented, in addition to the Password specific metrics for these.</li>
+ * <li>Attempt to connect with mtls authentication and ensure ConnectedNativeClients and AuthSuccess metrics
+ * are incremented, in addition to the MTLS specific metrics.</li>
+ * <li>Attempt to connect with mtls authentication using a non-permitted identity and ensure AuthFailure
+ * metrics are incremented.</li>
+ * </ol>
+ * @throws SSLException
+ */
+ @Test
+ public void testConnectedClientsAndAuthMetrics() throws SSLException
+ {
+ // Should be no connections at this time.
+ assertEquals(0, clientMetrics.connectedNativeClients.getValue().intValue());
+ assertEquals(0, clientMetrics.encryptedConnectedNativeClients.getValue().intValue());
+ assertEquals(0, clientMetrics.unencryptedConnectedNativeClients.getValue().intValue());
+
+ long initialAuthSuccess = clientMetrics.authSuccess.getCount();
+ long initialAuthFailure = clientMetrics.authFailure.getCount();
+
+ // For each authentication mode, we should have connectedNativeClientsByAuthMode, authSuccessByMode, and authFailureByMode entries.
+ Gauge<Integer> passwordConnections = clientMetrics.connectedNativeClientsByAuthMode.get(AuthenticationMode.PASSWORD);
+ assertNotNull(passwordConnections);
+
+ Meter authSuccessPassword = clientMetrics.authSuccessByMode.get(AuthenticationMode.PASSWORD);
+ assertNotNull(authSuccessPassword);
+ long initialAuthSuccessPassword = authSuccessPassword.getCount();
+
+ Meter authFailurePassword = clientMetrics.authFailureByMode.get(AuthenticationMode.PASSWORD);
+ assertNotNull(authFailurePassword);
+ long initialAuthFailurePassword = authFailurePassword.getCount();
+
+ Gauge<Integer> mtlsConnections = clientMetrics.connectedNativeClientsByAuthMode.get(AuthenticationMode.MTLS);
+ assertNotNull(mtlsConnections);
+
+ Meter authSuccessMtls = clientMetrics.authSuccessByMode.get(AuthenticationMode.MTLS);
+ assertNotNull(authSuccessMtls);
+ long initialAuthSuccessMtls = authSuccessMtls.getCount();
+
+ Meter authFailureMtls = clientMetrics.authFailureByMode.get(AuthenticationMode.MTLS);
+ assertNotNull(authFailureMtls);
+ long initialAuthFailureMtls = authFailureMtls.getCount();
+
+ try (Cluster cluster = clusterBuilder().withCredentials("cassandra", "cassandra").build();
+ Session session = cluster.connect())
+ {
+ // Connected native clients should be 2 (1 for core connection, 1 for control connection)
+ assertEquals(2, clientMetrics.connectedNativeClients.getValue().intValue());
+ assertEquals(2, clientMetrics.unencryptedConnectedNativeClients.getValue().intValue());
+ assertEquals(0, clientMetrics.encryptedConnectedNativeClients.getValue().intValue());
+
+ // Should be two connections on the "cassandra" user.
+ Integer cassandraConnections = clientMetrics.connectedNativeClientsByUser.getValue().get("cassandra");
+ assertNotNull(cassandraConnections);
+ assertEquals(2, cassandraConnections.intValue());
+
+ // Should be two password-authed connections.
+ assertEquals(2, passwordConnections.getValue().intValue());
+
+ // auth success should increment by 2.
+ assertEquals(initialAuthSuccess+2, clientMetrics.authSuccess.getCount());
+ assertEquals(initialAuthFailure, clientMetrics.authFailure.getCount());
+
+ // should increment by 2 for password mode specific metric as well.
+ assertEquals(initialAuthSuccessPassword+2L, authSuccessPassword.getCount());
+ assertEquals(initialAuthFailurePassword, authFailurePassword.getCount());
+
+ // Should be no mtls connections.
+ assertEquals(0, mtlsConnections.getValue().intValue());
+
+ // Should be no mtls attempts.
+ assertEquals(initialAuthSuccessMtls, authSuccessMtls.getCount());
+ // No auth failure for mtls
+ assertEquals(initialAuthFailureMtls, authFailureMtls.getCount());
+
+ Cluster.Builder user1ClusterBuilder = clusterBuilder()
+ .withSSL(TlsTestUtils.getSSLOptions(true))
+ .withPoolingOptions(new PoolingOptions()
+ .setConnectionsPerHost(HostDistance.LOCAL, 4, 4));
+ // Authenticate with 'user1' over mtls with a connection pool of size 4
+ try (Cluster user1Cluster = user1ClusterBuilder.build();
+ Session user1Session = user1Cluster.connect())
+ {
+ // Connected native clients should increment by 5 for new cluster (4 core + 1 control)
+ assertEquals(7, clientMetrics.connectedNativeClients.getValue().intValue());
+ assertEquals(2, clientMetrics.unencryptedConnectedNativeClients.getValue().intValue());
+ assertEquals(5, clientMetrics.encryptedConnectedNativeClients.getValue().intValue());
+
+ // auth success should increment by 5 (accounting for the 2 initial password connections).
+ assertEquals(initialAuthSuccess + 2 + 5, clientMetrics.authSuccess.getCount());
+
+ // Should still be two connections on the "cassandra" user.
+ cassandraConnections = clientMetrics.connectedNativeClientsByUser.getValue().get("cassandra");
+ assertNotNull(cassandraConnections);
+ assertEquals(2, cassandraConnections.intValue());
+
+ // User1 should have 5 connections.
+ Integer user1Connections = clientMetrics.connectedNativeClientsByUser.getValue().get(USER1);
+ assertNotNull(user1Connections);
+ assertEquals(5, user1Connections.intValue());
+
+ // Expect 5 mtls connections, password connections should remain unchanged.
+ assertEquals(5, mtlsConnections.getValue().intValue());
+ assertEquals(2, passwordConnections.getValue().intValue());
+ assertEquals(initialAuthSuccessMtls + 5L, authSuccessMtls.getCount());
+ assertEquals(initialAuthFailureMtls, authFailureMtls.getCount());
+ }
+
+ // after user1's session is closed, metrics should drop to what they were before.
+ assertEquals(2, clientMetrics.connectedNativeClients.getValue().intValue());
+ assertEquals(2, clientMetrics.unencryptedConnectedNativeClients.getValue().intValue());
+ assertEquals(0, clientMetrics.encryptedConnectedNativeClients.getValue().intValue());
+
+ // user1 should not be present in map
+ Integer user1Connections = clientMetrics.connectedNativeClientsByUser.getValue().get(USER1);
+ assertNull(user1Connections);
+ // No mTLS connections should be present.
+ assertEquals(0, mtlsConnections.getValue().intValue());
+
+ // Drop the identity used for mTLS to create an auth failure scenario for mTLS.
+ session.execute(String.format("DROP IDENTITY '%s'", TlsTestUtils.CLIENT_SPIFFE_IDENTITY));
+
+ // Attempt to connect with mTLS again, which should fail.
+ try (Cluster user1Cluster = user1ClusterBuilder.build())
+ {
+ user1Cluster.connect();
+ } catch (Exception e)
+ {
+ // expected.
+ }
+ // No mTLS connections should be present.
+ assertEquals(0, mtlsConnections.getValue().intValue());
+ // No new successful mTLS attempts.
+ assertEquals(initialAuthSuccessMtls + 5L, authSuccessMtls.getCount());
+ assertEquals(initialAuthSuccess + 2 + 5, clientMetrics.authSuccess.getCount());
+ // 1 failed mTLS attempt (control connection cannot establish)
+ assertEquals(initialAuthFailureMtls + 1L, authFailureMtls.getCount());
+ assertEquals(initialAuthFailure+1, clientMetrics.authFailure.getCount());
+ }
+
+ // Should be no connections as all clusters are closed at this point.
+ assertEquals(0, clientMetrics.connectedNativeClients.getValue().intValue());
+ assertEquals(0, clientMetrics.unencryptedConnectedNativeClients.getValue().intValue());
+ assertEquals(0, clientMetrics.encryptedConnectedNativeClients.getValue().intValue());
+ assertEquals(0, passwordConnections.getValue().intValue());
+ }
+}
diff --git a/test/unit/org/apache/cassandra/net/ConnectionTest.java b/test/unit/org/apache/cassandra/net/ConnectionTest.java
index 1f8c6f612d..d0824ac4f8 100644
--- a/test/unit/org/apache/cassandra/net/ConnectionTest.java
+++ b/test/unit/org/apache/cassandra/net/ConnectionTest.java
@@ -66,6 +66,7 @@ import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.transport.TlsTestUtils;
import org.apache.cassandra.utils.FBUtilities;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -179,10 +180,10 @@ public class ConnectionTest
.withLegacySslStoragePort(true)
.withOptional(true)
.withInternodeEncryption(EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.all)
- .withKeyStore("test/conf/cassandra_ssl_test.keystore")
- .withKeyStorePassword("cassandra")
- .withTrustStore("test/conf/cassandra_ssl_test.truststore")
- .withTrustStorePassword("cassandra")
+ .withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH)
+ .withKeyStorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
+ .withTrustStore(TlsTestUtils.SERVER_TRUSTSTORE_PATH)
+ .withTrustStorePassword(TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD)
.withRequireClientAuth(NOT_REQUIRED)
.withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA");
diff --git a/test/unit/org/apache/cassandra/net/HandshakeTest.java b/test/unit/org/apache/cassandra/net/HandshakeTest.java
index ec7e26b322..99e33eea90 100644
--- a/test/unit/org/apache/cassandra/net/HandshakeTest.java
+++ b/test/unit/org/apache/cassandra/net/HandshakeTest.java
@@ -46,6 +46,7 @@ import org.apache.cassandra.gms.GossipDigestSyn;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.OutboundConnectionInitiator.Result.MessagingSuccess;
import org.apache.cassandra.security.DefaultSslContextFactory;
+import org.apache.cassandra.transport.TlsTestUtils;
import org.apache.cassandra.utils.concurrent.AsyncPromise;
import static org.apache.cassandra.net.ConnectionType.SMALL_MESSAGES;
@@ -276,12 +277,12 @@ public class HandshakeTest
private ServerEncryptionOptions getServerEncryptionOptions(SslFallbackConnectionType sslConnectionType, boolean optional)
{
ServerEncryptionOptions serverEncryptionOptions = new ServerEncryptionOptions().withOptional(optional)
- .withKeyStore("test/conf/cassandra_ssl_test.keystore")
- .withKeyStorePassword("cassandra")
- .withOutboundKeystore("test/conf/cassandra_ssl_test_outbound.keystore")
- .withOutboundKeystorePassword("cassandra")
- .withTrustStore("test/conf/cassandra_ssl_test.truststore")
- .withTrustStorePassword("cassandra")
+ .withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH)
+ .withKeyStorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
+ .withOutboundKeystore(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PATH)
+ .withOutboundKeystorePassword(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PASSWORD)
+ .withTrustStore(TlsTestUtils.SERVER_TRUSTSTORE_PATH)
+ .withTrustStorePassword(TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD)
.withSslContextFactory((new ParameterizedClass(DefaultSslContextFactory.class.getName(),
new HashMap<>())));
if (sslConnectionType == SslFallbackConnectionType.MTLS)
diff --git a/test/unit/org/apache/cassandra/security/DefaultSslContextFactoryTest.java b/test/unit/org/apache/cassandra/security/DefaultSslContextFactoryTest.java
index 1b7db326e1..fa3eb7c845 100644
--- a/test/unit/org/apache/cassandra/security/DefaultSslContextFactoryTest.java
+++ b/test/unit/org/apache/cassandra/security/DefaultSslContextFactoryTest.java
@@ -34,6 +34,7 @@ import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslProvider;
import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.distributed.shared.WithProperties;
+import org.apache.cassandra.transport.TlsTestUtils;
import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_TCACTIVE_OPENSSL;
@@ -47,33 +48,33 @@ public class DefaultSslContextFactoryTest
@Before
public void setup()
{
- commonConfig.put("truststore", "test/conf/cassandra_ssl_test.truststore");
- commonConfig.put("truststore_password", "cassandra");
+ commonConfig.put("truststore", TlsTestUtils.SERVER_TRUSTSTORE_PATH);
+ commonConfig.put("truststore_password", TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD);
commonConfig.put("require_client_auth", "false");
commonConfig.put("cipher_suites", Arrays.asList("TLS_RSA_WITH_AES_128_CBC_SHA"));
}
private void addKeystoreOptions(Map<String,Object> config)
{
- config.put("keystore", "test/conf/cassandra_ssl_test.keystore");
- config.put("keystore_password", "cassandra");
+ config.put("keystore", TlsTestUtils.SERVER_KEYSTORE_PATH);
+ config.put("keystore_password", TlsTestUtils.SERVER_KEYSTORE_PASSWORD);
}
private void addOutboundKeystoreOptions(Map<String, Object> config)
{
- config.put("outbound_keystore", "test/conf/cassandra_ssl_test_outbound.keystore");
- config.put("outbound_keystore_password", "cassandra");
+ config.put("outbound_keystore", TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PATH);
+ config.put("outbound_keystore_password", TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PASSWORD);
}
@Test
public void getSslContextOpenSSL() throws IOException
{
- EncryptionOptions.ServerEncryptionOptions options = new EncryptionOptions.ServerEncryptionOptions().withTrustStore("test/conf/cassandra_ssl_test.truststore")
- .withTrustStorePassword("cassandra")
- .withKeyStore("test/conf/cassandra_ssl_test.keystore")
- .withKeyStorePassword("cassandra")
- .withOutboundKeystore("test/conf/cassandra_ssl_test_outbound.keystore")
- .withOutboundKeystorePassword("cassandra")
+ EncryptionOptions.ServerEncryptionOptions options = new EncryptionOptions.ServerEncryptionOptions().withTrustStore(TlsTestUtils.SERVER_TRUSTSTORE_PATH)
+ .withTrustStorePassword(TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD)
+ .withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH)
+ .withKeyStorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
+ .withOutboundKeystore(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PATH)
+ .withOutboundKeystorePassword(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PASSWORD)
.withRequireClientAuth(NOT_REQUIRED)
.withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA");
SslContext sslContext = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test");
diff --git a/test/unit/org/apache/cassandra/security/FileBasedSslContextFactoryTest.java b/test/unit/org/apache/cassandra/security/FileBasedSslContextFactoryTest.java
index 73fe80b304..2916f0d460 100644
--- a/test/unit/org/apache/cassandra/security/FileBasedSslContextFactoryTest.java
+++ b/test/unit/org/apache/cassandra/security/FileBasedSslContextFactoryTest.java
@@ -27,12 +27,11 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.config.ParameterizedClass;
import org.apache.cassandra.distributed.shared.WithProperties;
+import org.apache.cassandra.transport.TlsTestUtils;
import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CONFIG;
@@ -40,8 +39,6 @@ import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.NOT_REQUI
public class FileBasedSslContextFactoryTest
{
- private static final Logger logger = LoggerFactory.getLogger(FileBasedSslContextFactoryTest.class);
-
private EncryptionOptions.ServerEncryptionOptions encryptionOptions;
static WithProperties properties;
@@ -65,14 +62,14 @@ public class FileBasedSslContextFactoryTest
encryptionOptions = new EncryptionOptions.ServerEncryptionOptions()
.withSslContextFactory(new ParameterizedClass(TestFileBasedSSLContextFactory.class.getName(),
new HashMap<>()))
- .withTrustStore("test/conf/cassandra_ssl_test.truststore")
- .withTrustStorePassword("cassandra")
+ .withTrustStore(TlsTestUtils.SERVER_TRUSTSTORE_PATH)
+ .withTrustStorePassword(TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD)
.withRequireClientAuth(NOT_REQUIRED)
.withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA")
- .withKeyStore("test/conf/cassandra_ssl_test.keystore")
- .withKeyStorePassword("cassandra")
- .withOutboundKeystore("test/conf/cassandra_ssl_test_outbound.keystore")
- .withOutboundKeystorePassword("cassandra");
+ .withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH)
+ .withKeyStorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
+ .withOutboundKeystore(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PATH)
+ .withOutboundKeystorePassword(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PASSWORD);
}
@Test
diff --git a/test/unit/org/apache/cassandra/security/PEMBasedSslContextFactoryTest.java b/test/unit/org/apache/cassandra/security/PEMBasedSslContextFactoryTest.java
index ca319de5fd..9781e9d08a 100644
--- a/test/unit/org/apache/cassandra/security/PEMBasedSslContextFactoryTest.java
+++ b/test/unit/org/apache/cassandra/security/PEMBasedSslContextFactoryTest.java
@@ -36,6 +36,7 @@ import io.netty.handler.ssl.SslProvider;
import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.config.ParameterizedClass;
import org.apache.cassandra.distributed.shared.WithProperties;
+import org.apache.cassandra.transport.TlsTestUtils;
import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_TCACTIVE_OPENSSL;
import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.NOT_REQUIRED;
@@ -195,18 +196,18 @@ public class PEMBasedSslContextFactoryTest
private void addFileBaseTrustStoreOptions(Map<String, Object> config)
{
- config.put("truststore", "test/conf/cassandra_ssl_test.truststore.pem");
+ config.put("truststore", TlsTestUtils.SERVER_TRUSTSTORE_PEM_PATH);
}
private void addFileBaseKeyStoreOptions(Map<String, Object> config)
{
- config.put("keystore", "test/conf/cassandra_ssl_test.keystore.pem");
- config.put("keystore_password", "cassandra");
+ config.put("keystore", TlsTestUtils.SERVER_KEYSTORE_PATH_PEM);
+ config.put("keystore_password", TlsTestUtils.SERVER_KEYSTORE_PASSWORD);
}
private void addFileBaseUnencryptedKeyStoreOptions(Map<String, Object> config)
{
- config.put("keystore", "test/conf/cassandra_ssl_test.unencrypted_keystore.pem");
+ config.put("keystore", TlsTestUtils.SERVER_KEYSTORE_PATH_UNENCRYPTED_PEM);
}
@Test
@@ -214,9 +215,9 @@ public class PEMBasedSslContextFactoryTest
{
ParameterizedClass sslContextFactory = new ParameterizedClass(PEMBasedSslContextFactory.class.getSimpleName()
, new HashMap<>());
- EncryptionOptions options = new EncryptionOptions().withTrustStore("test/conf/cassandra_ssl_test.truststore.pem")
- .withKeyStore("test/conf/cassandra_ssl_test.keystore.pem")
- .withKeyStorePassword("cassandra")
+ EncryptionOptions options = new EncryptionOptions().withTrustStore(TlsTestUtils.SERVER_TRUSTSTORE_PEM_PATH)
+ .withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH_PEM)
+ .withKeyStorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
.withRequireClientAuth(NOT_REQUIRED)
.withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA")
.withSslContextFactory(sslContextFactory);
@@ -233,11 +234,11 @@ public class PEMBasedSslContextFactoryTest
{
ParameterizedClass sslContextFactory = new ParameterizedClass(PEMBasedSslContextFactory.class.getSimpleName()
, new HashMap<>());
- EncryptionOptions.ServerEncryptionOptions options = new EncryptionOptions.ServerEncryptionOptions().withTrustStore("test/conf/cassandra_ssl_test.truststore.pem")
- .withKeyStore("test/conf/cassandra_ssl_test.keystore.pem")
- .withKeyStorePassword("cassandra")
- .withOutboundKeystore("test/conf/cassandra_ssl_test.keystore.pem")
- .withOutboundKeystorePassword("cassandra")
+ EncryptionOptions.ServerEncryptionOptions options = new EncryptionOptions.ServerEncryptionOptions().withTrustStore(TlsTestUtils.SERVER_TRUSTSTORE_PEM_PATH)
+ .withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH_PEM)
+ .withKeyStorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
+ .withOutboundKeystore(TlsTestUtils.SERVER_KEYSTORE_PATH_PEM)
+ .withOutboundKeystorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
.withRequireClientAuth(NOT_REQUIRED)
.withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA")
.withSslContextFactory(sslContextFactory);
diff --git a/test/unit/org/apache/cassandra/security/SSLFactoryTest.java b/test/unit/org/apache/cassandra/security/SSLFactoryTest.java
index a7c14dfcfb..ba46588686 100644
--- a/test/unit/org/apache/cassandra/security/SSLFactoryTest.java
+++ b/test/unit/org/apache/cassandra/security/SSLFactoryTest.java
@@ -37,8 +37,6 @@ import org.apache.commons.io.FileUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import io.netty.handler.ssl.OpenSslClientContext;
import io.netty.handler.ssl.OpenSslServerContext;
@@ -50,6 +48,7 @@ import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions;
import org.apache.cassandra.config.ParameterizedClass;
import org.apache.cassandra.io.util.File;
+import org.apache.cassandra.transport.TlsTestUtils;
import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.NOT_REQUIRED;
import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED;
@@ -58,8 +57,6 @@ import static org.junit.Assert.assertNotNull;
public class SSLFactoryTest
{
- private static final Logger logger = LoggerFactory.getLogger(SSLFactoryTest.class);
-
static final SelfSignedCertificate ssc;
static
{
@@ -81,8 +78,8 @@ public class SSLFactoryTest
{
SSLFactory.clearSslContextCache();
encryptionOptions = new ServerEncryptionOptions()
- .withTrustStore("test/conf/cassandra_ssl_test.truststore")
- .withTrustStorePassword("cassandra")
+ .withTrustStore(TlsTestUtils.SERVER_TRUSTSTORE_PATH)
+ .withTrustStorePassword(TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD)
.withRequireClientAuth(NOT_REQUIRED)
.withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA")
.withSslContextFactory(new ParameterizedClass(TestFileBasedSSLContextFactory.class.getName(),
@@ -91,10 +88,10 @@ public class SSLFactoryTest
private ServerEncryptionOptions addKeystoreOptions(ServerEncryptionOptions options)
{
- return options.withKeyStore("test/conf/cassandra_ssl_test.keystore")
- .withKeyStorePassword("cassandra")
- .withOutboundKeystore("test/conf/cassandra_ssl_test_outbound.keystore")
- .withOutboundKeystorePassword("cassandra");
+ return options.withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH)
+ .withKeyStorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
+ .withOutboundKeystore(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PATH)
+ .withOutboundKeystorePassword(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PASSWORD);
}
private ServerEncryptionOptions addPEMKeystoreOptions(ServerEncryptionOptions options)
@@ -102,11 +99,11 @@ public class SSLFactoryTest
ParameterizedClass sslContextFactoryClass = new ParameterizedClass("org.apache.cassandra.security.PEMBasedSslContextFactory",
new HashMap<>());
return options.withSslContextFactory(sslContextFactoryClass)
- .withKeyStore("test/conf/cassandra_ssl_test.keystore.pem")
- .withKeyStorePassword("cassandra")
- .withOutboundKeystore("test/conf/cassandra_ssl_test.keystore.pem")
- .withOutboundKeystorePassword("cassandra")
- .withTrustStore("test/conf/cassandra_ssl_test.truststore.pem");
+ .withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH_PEM)
+ .withKeyStorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
+ .withOutboundKeystore(TlsTestUtils.SERVER_KEYSTORE_PATH_PEM)
+ .withOutboundKeystorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
+ .withTrustStore(TlsTestUtils.SERVER_TRUSTSTORE_PEM_PATH);
}
@Test
@@ -158,7 +155,7 @@ public class SSLFactoryTest
// Verify if right certificate is loaded into SslContext
final Certificate loadedCertificate = getCertificateLoadedInSslContext(context.sessionContext());
- final Certificate certificate = getCertificates("test/conf/cassandra_ssl_test.keystore", "cassandra");
+ final Certificate certificate = getCertificates(TlsTestUtils.SERVER_KEYSTORE_PATH, TlsTestUtils.SERVER_KEYSTORE_PASSWORD);
assertEquals(loadedCertificate, certificate);
}
@@ -175,7 +172,7 @@ public class SSLFactoryTest
// Verify if right certificate is loaded into SslContext
final Certificate loadedCertificate = getCertificateLoadedInSslContext(context.sessionContext());
- final Certificate certificate = getCertificates("test/conf/cassandra_ssl_test_outbound.keystore", "cassandra");
+ final Certificate certificate = getCertificates(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PATH, TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PASSWORD);
assertEquals(loadedCertificate, certificate);
}
diff --git a/test/unit/org/apache/cassandra/service/NativeTransportServiceTest.java b/test/unit/org/apache/cassandra/service/NativeTransportServiceTest.java
index d5f825fc62..2f45748739 100644
--- a/test/unit/org/apache/cassandra/service/NativeTransportServiceTest.java
+++ b/test/unit/org/apache/cassandra/service/NativeTransportServiceTest.java
@@ -31,6 +31,7 @@ import org.junit.Test;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.transport.TlsTestUtils;
import org.apache.cassandra.utils.Pair;
import static org.junit.Assert.assertEquals;
@@ -168,7 +169,7 @@ public class NativeTransportServiceTest
DatabaseDescriptor.updateNativeProtocolEncryptionOptions(
options -> options.withEnabled(true)
.withOptional(true)
- .withKeyStore("test/conf/cassandra_ssl_test.keystore"));
+ .withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH));
DatabaseDescriptor.setNativeTransportPortSSL(8432);
withService((NativeTransportService service) ->
@@ -224,7 +225,7 @@ public class NativeTransportServiceTest
DatabaseDescriptor.updateNativeProtocolEncryptionOptions(
options -> options.withEnabled(true)
.withOptional(false)
- .withKeyStore("test/conf/cassandra_ssl_test.keystore"));
+ .withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH));
DatabaseDescriptor.setNativeTransportPortSSL(8432);
withService((NativeTransportService service) ->
diff --git a/test/unit/org/apache/cassandra/tools/nodetool/ClientStatsTest.java b/test/unit/org/apache/cassandra/tools/nodetool/ClientStatsTest.java
index d5b01730ef..2189a0e963 100644
--- a/test/unit/org/apache/cassandra/tools/nodetool/ClientStatsTest.java
+++ b/test/unit/org/apache/cassandra/tools/nodetool/ClientStatsTest.java
@@ -32,22 +32,34 @@ import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.datastax.driver.core.Cluster;
-import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Session;
import org.apache.cassandra.ServerTestUtils;
import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.OverrideConfigurationLoader;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.service.EmbeddedCassandraService;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.tools.ToolRunner;
+import org.apache.cassandra.transport.TlsTestUtils;
import org.assertj.core.groups.Tuple;
+import static org.apache.cassandra.auth.AuthTestUtils.waitForExistingRoles;
import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_JMX_LOCAL_PORT;
+import static org.apache.cassandra.config.CassandraRelevantProperties.SUPERUSER_SETUP_DELAY_MS;
import static org.assertj.core.api.Assertions.assertThat;
public class ClientStatsTest
{
private static Cluster cluster;
+
+ private static Cluster tlsCluster;
+
+ private static Session tlsSession;
+
+ private static Cluster mtlsCluster;
+
+ private static Session mtlsSession;
+
private Session session;
private static EmbeddedCassandraService cassandra;
@@ -55,19 +67,54 @@ public class ClientStatsTest
@BeforeClass
public static void setup() throws Throwable
{
+ OverrideConfigurationLoader.override(TlsTestUtils::configureWithMutualTlsWithPasswordFallbackAuthenticator);
+
+ SUPERUSER_SETUP_DELAY_MS.setLong(0);
// Since we run EmbeddedCassandraServer, we need to manually associate JMX address; otherwise it won't start
int jmxPort = CQLTester.getAutomaticallyAllocatedPort(InetAddress.getLoopbackAddress());
CASSANDRA_JMX_LOCAL_PORT.setInt(jmxPort);
cassandra = ServerTestUtils.startEmbeddedCassandraService();
- cluster = Cluster.builder().addContactPoint("127.0.0.1").withPort(DatabaseDescriptor.getNativeTransportPort()).build();
+
+ waitForExistingRoles();
+
+ cluster = clusterBuilder()
+ .withCredentials("cassandra", "cassandra")
+ .build();
+
+ // Allow client to connect as cassandra using an mTLS identity.
+ try(Session session = cluster.connect())
+ {
+ session.execute(String.format("ADD IDENTITY '%s' TO ROLE 'cassandra'", TlsTestUtils.CLIENT_SPIFFE_IDENTITY));
+ }
+
+ // Configure a TLS-based cluster with password authentication.
+ tlsCluster = clusterBuilder()
+ .withSSL(TlsTestUtils.getSSLOptions(false))
+ .withCredentials("cassandra", "cassandra")
+ .build();
+
+ // Configure a TLS-based cluster with certificate (mtls) authentication.
+ mtlsCluster = clusterBuilder()
+ .withSSL(TlsTestUtils.getSSLOptions(true))
+ .build();
+ }
+
+ private static Cluster.Builder clusterBuilder()
+ {
+ return Cluster.builder().addContactPoint("127.0.0.1").withPort(DatabaseDescriptor.getNativeTransportPort());
}
@Before
public void config() throws Throwable
{
+
session = cluster.connect();
- ResultSet result = session.execute("select release_version from system.local");
+ session.execute("select release_version from system.local");
+
+ tlsSession = tlsCluster.connect();
+ // connect with system keyspace to assert it's present in output.
+ mtlsSession = mtlsCluster.connect("system");
}
@After
@@ -75,6 +122,10 @@ public class ClientStatsTest
{
if (session != null)
session.close();
+ if (tlsSession != null)
+ tlsSession.close();
+ if (mtlsSession != null)
+ mtlsSession.close();
}
@AfterClass
@@ -82,6 +133,10 @@ public class ClientStatsTest
{
if (cluster != null)
cluster.close();
+ if (tlsCluster != null)
+ tlsCluster.close();
+ if (mtlsCluster != null)
+ mtlsCluster.close();
if (cassandra != null)
cassandra.stop();
}
@@ -100,7 +155,7 @@ public class ClientStatsTest
" [(-pp | --print-port)] [(-pw <password> | --password <password>)]\n" +
" [(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]\n" +
" [(-u <username> | --username <username>)] clientstats [--all]\n" +
- " [--by-protocol] [--clear-history] [--client-options]\n" +
+ " [--by-protocol] [--clear-history] [--client-options] [--verbose]\n" +
"\n" +
"OPTIONS\n" +
" --all\n" +
@@ -111,9 +166,9 @@ public class ClientStatsTest
"\n" +
" --clear-history\n" +
" Clear the history of connected clients\n" +
- "\n" +
- " --client-options\n" +
- " Lists all connections and the client options\n" +
+ "\n" +
+ " --client-options\n" +
+ " Lists all connections and the client options\n" +
"\n" +
" -h <host>, --host <host>\n" +
" Node hostname or ip address\n" +
@@ -133,6 +188,10 @@ public class ClientStatsTest
" -u <username>, --username <username>\n" +
" Remote jmx agent username\n" +
"\n" +
+ " --verbose\n" +
+ " Lists all connections with additional details (client options,\n" +
+ " authenticator-specific metadata and more)\n" +
+ "\n" +
"\n";
assertThat(tool.getStdout()).isEqualTo(help);
}
@@ -143,9 +202,7 @@ public class ClientStatsTest
ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("clientstats");
tool.assertOnCleanExit();
String stdout = tool.getStdout();
- assertThat(stdout).contains("Total connected clients: 2");
- assertThat(stdout).contains("User Connections");
- assertThat(stdout).contains("anonymous 2");
+ assertClientCount(stdout);
}
@Test
@@ -165,27 +222,85 @@ public class ClientStatsTest
ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("clientstats", "--all");
tool.assertOnCleanExit();
String stdout = tool.getStdout();
+ /**
+ * Example expected output:
+ * Address SSL Cipher Protocol Version User Keyspace Requests Driver-Name Driver-Version
+ * /127.0.0.1:52549 true TLS_AES_256_GCM_SHA384 TLSv1.3 5 cassandra 17 DataStax Java Driver 3.11.5
+ * /127.0.0.1:52550 true TLS_AES_256_GCM_SHA384 TLSv1.3 5 cassandra 3 DataStax Java Driver 3.11.5
+ * /127.0.0.1:52551 true TLS_AES_256_GCM_SHA384 TLSv1.3 5 cassandra 16 DataStax Java Driver 3.11.5
+ * /127.0.0.1:52552 true TLS_AES_256_GCM_SHA384 TLSv1.3 5 cassandra system 3 DataStax Java Driver 3.11.5
+ * /127.0.0.1:52546 false undefined undefined 5 cassandra 17 DataStax Java Driver 3.11.5
+ * /127.0.0.1:52548 false undefined undefined 5 cassandra 4 DataStax Java Driver 3.11.5
+ */
assertThat(stdout).containsPattern("Address +SSL +Cipher +Protocol +Version +User +Keyspace +Requests +Driver-Name +Driver-Version");
- assertThat(stdout).containsPattern("/127.0.0.1:[0-9]+ false undefined undefined [0-9]+ +anonymous +[0-9]+ +DataStax Java Driver 3.11.5");
- assertThat(stdout).contains("Total connected clients: 2");
- assertThat(stdout).contains("User Connections");
- assertThat(stdout).contains("anonymous 2");
+ // Unencrypted password-based client.
+ assertThat(stdout).containsPattern("/127.0.0.1:[0-9]+ false +undefined +undefined +[0-9]+ +cassandra +[0-9]+ +DataStax Java Driver 3.11.5");
+ // TLS-encrypted password-based client.
+ assertThat(stdout).containsPattern("/127.0.0.1:[0-9]+ true +TLS\\S+ +TLS\\S+ +[0-9]+ +cassandra +[0-9]+ +DataStax Java Driver 3.11.5");
+ // MTLS-based client.
+ assertThat(stdout).containsPattern("/127.0.0.1:[0-9]+ true +TLS\\S+ +TLS\\S+ +[0-9]+ +cassandra +[0-9]+ +DataStax Java Driver 3.11.5");
+ // MTLS-based client with 'system' keyspace set on connection.
+ assertThat(stdout).containsPattern("/127.0.0.1:[0-9]+ true +TLS\\S+ +TLS\\S+ +[0-9]+ +cassandra +system +[0-9]+ +DataStax Java Driver 3.11.5");
+
+ assertClientCount(stdout);
}
@Test
public void testClientStatsClientOptions()
{
+ // given 'clientstats --metadata' invoked, we expect 'Client-Options' to be present.
ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("clientstats", "--client-options");
tool.assertOnCleanExit();
String stdout = tool.getStdout();
+
+ /*
+ * Example expected output:
+ * Address SSL Cipher Protocol Version User Keyspace Requests Driver-Name Driver-Version Client-Options
+ * /127.0.0.1:51047 true TLS_AES_256_GCM_SHA384 TLSv1.3 5 cassandra 17 DataStax Java Driver 3.11.5 DRIVER_VERSION=3.11.5, DRIVER_NAME=DataStax Java Driver, CQL_VERSION=3.0.0
+ * /127.0.0.1:51048 true TLS_AES_256_GCM_SHA384 TLSv1.3 5 cassandra 3 DataStax Java Driver 3.11.5 DRIVER_VERSION=3.11.5, DRIVER_NAME=DataStax Java Driver, CQL_VERSION=3.0.0
+ * /127.0.0.1:51046 false undefined undefined 5 cassandra 4 DataStax Java Driver 3.11.5 DRIVER_VERSION=3.11.5, DRIVER_NAME=DataStax Java Driver, CQL_VERSION=3.0.0
+ * /127.0.0.1:51044 false undefined undefined 5 cassandra 17 DataStax Java Driver 3.11.5 DRIVER_VERSION=3.11.5, DRIVER_NAME=DataStax Java Driver, CQL_VERSION=3.0.0
+ * /127.0.0.1:51049 true TLS_AES_256_GCM_SHA384 TLSv1.3 5 cassandra 16 DataStax Java Driver 3.11.5 DRIVER_VERSION=3.11.5, DRIVER_NAME=DataStax Java Driver, CQL_VERSION=3.0.0
+ * /127.0.0.1:51050 true TLS_AES_256_GCM_SHA384 TLSv1.3 5 cassandra system 3 DataStax Java Driver 3.11.5 DRIVER_VERSION=3.11.5, DRIVER_NAME=DataStax Java Driver, CQL_VERSION=3.0.0
+ */
assertThat(stdout).containsPattern("Address +SSL +Cipher +Protocol +Version +User +Keyspace +Requests +Driver-Name +Driver-Version +Client-Options");
- assertThat(stdout).containsPattern("/127.0.0.1:[0-9]+ false undefined undefined [0-9]+ +anonymous +[0-9]+ +DataStax Java Driver 3.11.5");
+ assertThat(stdout).containsPattern("/127.0.0.1:[0-9]+ false+ undefined +undefined +[0-9]+ +cassandra +[0-9]+ +DataStax Java Driver 3.11.5");
assertThat(stdout).containsPattern("DRIVER_NAME=DataStax Java Driver");
assertThat(stdout).containsPattern("DRIVER_VERSION=3.11.5");
assertThat(stdout).containsPattern("CQL_VERSION=3.0.0");
- assertThat(stdout).contains("Total connected clients: 2");
- assertThat(stdout).contains("User Connections");
- assertThat(stdout).contains("anonymous 2");
+
+ assertClientCount(stdout);
+ }
+
+ @Test
+ public void testClientStatsClientVerbose()
+ {
+ // given 'clientstats --verbose' invoked, we expect 'Client-Options', 'Auth-Mode', 'Auth-Metadata', and 'Client-Options' columns to be present.
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("clientstats", "--verbose");
+ tool.assertOnCleanExit();
+ String stdout = tool.getStdout();
+ /*
+ * Example expected output:
+ * Address SSL Cipher Protocol Version User Keyspace Requests Driver-Name Driver-Version Client-Options Auth-Mode Auth-Metadata
+ * /127.0.0.1:57141 false undefined undefined 5 cassandra 17 DataStax Java Driver 3.11.5 DRIVER_VERSION=3.11.5, DRIVER_NAME=DataStax Java Driver, CQL_VERSION=3.0.0 Password
+ * /127.0.0.1:57165 true TLS_AES_256_GCM_SHA384 TLSv1.3 5 cassandra system 3 DataStax Java Driver 3.11.5 DRIVER_VERSION=3.11.5, DRIVER_NAME=DataStax Java Driver, CQL_VERSION=3.0.0 MutualTls identity=spiffe://test.cassandra.apache.org/unitTest/mtls
+ * /127.0.0.1:57164 true TLS_AES_256_GCM_SHA384 TLSv1.3 5 cassandra 3 DataStax Java Driver 3.11.5 DRIVER_VERSION=3.11.5, DRIVER_NAME=DataStax Java Driver, CQL_VERSION=3.0.0 Password
+ * /127.0.0.1:57144 true TLS_AES_256_GCM_SHA384 TLSv1.3 5 cassandra 17 DataStax Java Driver 3.11.5 DRIVER_VERSION=3.11.5, DRIVER_NAME=DataStax Java Driver, CQL_VERSION=3.0.0 Password
+ * /127.0.0.1:57146 true TLS_AES_256_GCM_SHA384 TLSv1.3 5 cassandra 16 DataStax Java Driver 3.11.5 DRIVER_VERSION=3.11.5, DRIVER_NAME=DataStax Java Driver, CQL_VERSION=3.0.0 MutualTls identity=spiffe://test.cassandra.apache.org/unitTest/mtls
+ * /127.0.0.1:57163 false undefined undefined 5 cassandra 4 DataStax Java Driver 3.11.5 DRIVER_VERSION=3.11.5, DRIVER_NAME=DataStax Java Driver, CQL_VERSION=3.0.0 Password
+ */
+ // Header
+ assertThat(stdout).containsPattern("Address +SSL +Cipher +Protocol +Version +User +Keyspace +Requests +Driver-Name +Driver-Version +Client-Options +Auth-Mode +Auth-Metadata");
+ // Unencrypted password-based client. Expect 'DRIVER_VERSION' to appear before Password.
+ assertThat(stdout).containsPattern("/127.0.0.1:[0-9]+ false +undefined +undefined +[0-9]+ +cassandra +[0-9]+ +DataStax Java Driver 3.11.5 +.*DRIVER_VERSION.* +Password");
+ // TLS-encrypted password-based client.
+ assertThat(stdout).containsPattern("/127.0.0.1:[0-9]+ true +TLS\\S+ +TLS\\S+ +[0-9]+ +cassandra +[0-9]+ +DataStax Java Driver 3.11.5 +.*DRIVER_VERSION.* +Password");
+ // MTLS-based client.
+ assertThat(stdout).containsPattern("/127.0.0.1:[0-9]+ true +TLS\\S+ +TLS\\S+ +[0-9]+ +cassandra +[0-9]+ +DataStax Java Driver 3.11.5 +.*DRIVER_VERSION.* +MutualTls +identity=" + TlsTestUtils.CLIENT_SPIFFE_IDENTITY);
+ // MTLS-based client with 'system' keyspace set on connection.
+ assertThat(stdout).containsPattern("/127.0.0.1:[0-9]+ true +TLS\\S+ +TLS\\S+ +[0-9]+ +cassandra +system +[0-9]+ +DataStax Java Driver 3.11.5 +.*DRIVER_VERSION.* +MutualTls +identity=" + TlsTestUtils.CLIENT_SPIFFE_IDENTITY);
+
+ assertClientCount(stdout);
}
@Test
@@ -205,4 +320,12 @@ public class ClientStatsTest
.extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel)
.contains(Tuple.tuple("Cleared connection history", Level.INFO));
}
+
+ public void assertClientCount(String stdout)
+ {
+ // Expect two connections for each client (1 control connection, 1 core pool connection)
+ assertThat(stdout).contains("Total connected clients: 6");
+ assertThat(stdout).contains("User Connections");
+ assertThat(stdout).contains("cassandra 6");
+ }
}
diff --git a/test/unit/org/apache/cassandra/transport/ConnectionTrackerTest.java b/test/unit/org/apache/cassandra/transport/ConnectionTrackerTest.java
new file mode 100644
index 0000000000..ec3e43efcc
--- /dev/null
+++ b/test/unit/org/apache/cassandra/transport/ConnectionTrackerTest.java
@@ -0,0 +1,242 @@
+/*
+ * 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.transport;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelId;
+import io.netty.channel.embedded.EmbeddedChannel;
+import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.transport.Server.ConnectionTracker;
+import org.mockito.Mockito;
+
+import static org.junit.Assert.assertEquals;
+
+public class ConnectionTrackerTest
+{
+
+ private static final Predicate<ServerConnection> PREDICATE_TRUE = (__) -> true;
+
+ @BeforeClass
+ public static void beforeClass()
+ {
+ // perform daemon initialization to intialize core components (like authenticator) that ConnectionTracker
+ // depends on.
+ DatabaseDescriptor.daemonInitialization();
+ }
+
+ private void registerConnections(ConnectionTracker tracker, int count)
+ {
+ registerConnections(tracker, count, (addr) -> null);
+ }
+
+ private void registerConnections(ConnectionTracker tracker, int count, Function<InetSocketAddress, AuthenticatedUser> authenticatedUserGenerator)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ InetSocketAddress socketAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(), 9040 + i);
+
+ Channel c = new EmbeddedChannelWithSocketAddress(socketAddress);
+ AuthenticatedUser user = authenticatedUserGenerator.apply(socketAddress);
+ ServerConnection connection = createAuthenticatedConnection(c, user);
+ c.attr(Connection.attributeKey).set(connection);
+
+ tracker.addConnection(c, connection);
+ }
+ }
+
+ @Test
+ public void testCountConnectedClients()
+ {
+ final String ODD_USER = "odd";
+ final String EVEN_USER = "even";
+ ConnectionTracker connectionTracker = new ConnectionTracker();
+ registerConnections(connectionTracker, 6, (addr) -> {
+ // if port divisible by 10, return 0, this will be treated as an anonymous user.
+ if (addr.getPort() % 10 == 0)
+ {
+ return null;
+ }
+ return new AuthenticatedUser(addr.getPort() % 2 == 0 ? EVEN_USER : ODD_USER);
+ });
+
+ // Validate that countConnectedClients by predicate returns the proper count.
+
+ // Should be 6 total connections when all matching this predicate
+ assertEquals(6, connectionTracker.countConnectedClients(PREDICATE_TRUE));
+
+ // Three connections should have a port > 9042.
+ assertEquals(3, connectionTracker.countConnectedClients((conn) -> {
+ int port = conn.getClientState().getRemoteAddress().getPort();
+ return port > 9042;
+ }));
+
+ // Zero connections using ssl.
+ assertEquals(0, connectionTracker.countConnectedClients(ServerConnection::isSSL));
+
+ // Verify countConnectedClientsByUser appropriately counts by user.
+
+ // Expect 2 users authenticated as 'even' {2, 4}, 3 as 'odd' {1, 3, 5}, one anonymous {0}.
+ assertEquals(ImmutableMap.of(EVEN_USER, 2, ODD_USER, 3, "anonymous", 1), connectionTracker.countConnectedClientsByUser());
+ }
+
+ @Test
+ public void testShouldNotCountChannelsMissingConnectionAttribute()
+ {
+ ConnectionTracker connectionTracker = new ConnectionTracker();
+ registerConnections(connectionTracker, 10);
+
+ // Given a ServerConnection registered that lacks a 'Connection.attributeKey' attribute, it should
+ // be skipped over when counting client connections.
+ InetSocketAddress socketAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(), 9042);
+ Channel c = new EmbeddedChannelWithSocketAddress(socketAddress);
+ Connection connection = new ServerConnection(c, ProtocolVersion.V5, (c1, c2) -> {
+ });
+ // intentionally omit setting Connection attribute.
+ connectionTracker.addConnection(c, connection);
+
+ // Expect the 10 existing connections, omitting the one added without a connection attribute.
+ assertEquals(10, connectionTracker.countConnectedClients(PREDICATE_TRUE));
+ assertEquals(Collections.singletonMap("anonymous", 10), connectionTracker.countConnectedClientsByUser());
+ }
+
+ @Test
+ public void testShouldNotCountNonServerConnection()
+ {
+ ConnectionTracker connectionTracker = new ConnectionTracker();
+ registerConnections(connectionTracker, 10);
+
+ // Given a Connection registered that is not a 'ServerConnection' it should be skipped over when counting client
+ // connections.
+ InetSocketAddress socketAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(), 9042);
+ Channel c = new EmbeddedChannelWithSocketAddress(socketAddress);
+ Connection connection = new Connection(c, ProtocolVersion.V5, (c1, c2) -> {
+ });
+ c.attr(Connection.attributeKey).set(connection);
+ connectionTracker.addConnection(c, connection);
+
+ // Expect the 10 existing connections, omitting the one that isn't a ServerConnection.
+ assertEquals(10, connectionTracker.countConnectedClients(PREDICATE_TRUE));
+ assertEquals(Collections.singletonMap("anonymous", 10), connectionTracker.countConnectedClientsByUser());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testShouldThrowThrowable()
+ {
+ ConnectionTracker connectionTracker = new ConnectionTracker();
+ registerConnections(connectionTracker, 10);
+
+ // Mock a ServerConnection such that when you attempt to access its connection attribute, an unhandled throwable
+ // is raised.
+ InetSocketAddress socketAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(), 9042);
+ Channel c = new EmbeddedChannelWithSocketAddress(socketAddress);
+ Channel spyChannel = Mockito.spy(c);
+ Connection connection = new ServerConnection(c, ProtocolVersion.V5, (c1, c2) -> {
+ });
+ c.attr(Connection.attributeKey).set(connection);
+ Mockito.when(spyChannel.attr(Connection.attributeKey)).thenThrow(new IllegalArgumentException("Unexpected behavior"));
+ connectionTracker.addConnection(spyChannel, connection);
+
+ // When calling countConnectedClients, we expect that expection to be propagated as we can't get a correct calculation.
+ connectionTracker.countConnectedClients(PREDICATE_TRUE);
+ }
+
+ /**
+ * @return a {@link ServerConnection} whose {@link ClientState} has the given {@link AuthenticatedUser}
+ */
+ private ServerConnection createAuthenticatedConnection(Channel channel, AuthenticatedUser user)
+ {
+ ServerConnection connection = new ServerConnection(channel, ProtocolVersion.V5, (c1, c2) -> {
+ });
+
+ if (user == null)
+ {
+ return connection;
+ }
+
+ ClientState state = connection.getClientState();
+
+ ClientState spyState = Mockito.spy(state);
+ Mockito.when(spyState.getUser()).thenReturn(user);
+
+ ServerConnection spyConnection = Mockito.spy(connection);
+ Mockito.when(spyConnection.getClientState()).thenReturn(spyState);
+
+ return spyConnection;
+ }
+
+ /**
+ * An embedded channel that uses the given {@link SocketAddress}'s string as its channel id, and {@link #remoteAddress()}
+ * returns the provided address.
+ *
+ * This is needed because {@link ServerConnection} expects a {@link Channel} with a {@link SocketAddress}
+ */
+ private static class EmbeddedChannelWithSocketAddress extends EmbeddedChannel
+ {
+ private final SocketAddress socketAddress;
+
+ EmbeddedChannelWithSocketAddress(SocketAddress socketAddress)
+ {
+ super(createChannelId(socketAddress));
+ this.socketAddress = socketAddress;
+ }
+
+ private static ChannelId createChannelId(SocketAddress socketAddress)
+ {
+ return new ChannelId()
+ {
+ @Override
+ public String asShortText()
+ {
+ return socketAddress.toString();
+ }
+
+ @Override
+ public String asLongText()
+ {
+ return asShortText();
+ }
+
+ @Override
+ public int compareTo(ChannelId o)
+ {
+ return this.asShortText().compareTo(this.asShortText());
+ }
+ };
+ }
+
+ @Override
+ public SocketAddress remoteAddress()
+ {
+ return this.socketAddress;
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/transport/EarlyAuthenticationTest.java b/test/unit/org/apache/cassandra/transport/EarlyAuthenticationTest.java
index 113865482a..ed98140b9b 100644
--- a/test/unit/org/apache/cassandra/transport/EarlyAuthenticationTest.java
+++ b/test/unit/org/apache/cassandra/transport/EarlyAuthenticationTest.java
@@ -60,9 +60,6 @@ public class EarlyAuthenticationTest extends CQLTester
static final Map<String, String> authenticatorParams = ImmutableMap.of("validator_class_name", SpiffeCertificateValidator.class.getSimpleName());
- // identity present in the client cert being used.
- static final String spiffeIdentity = "spiffe://test.cassandra.apache.org/unitTest/mtls";
-
@BeforeClass
public static void setup()
{
@@ -89,25 +86,18 @@ public class EarlyAuthenticationTest extends CQLTester
private EncryptionOptions clientEncryptionOptions(boolean presentClientCertificate)
{
- // To regenerate:
- // 1. generate keystore
- // keytool -genkeypair -keystore test/conf/cassandra_ssl_test_spiffe.keystore -validity 100000 -keyalg RSA -dname "CN=Apache Cassandra, OU=ssl_test, O=Unknown, L=Unknown, ST=Unknown, C=Unknown" -keypass cassandra -storepass cassandra -alias spiffecert -ext SAN=URI:spiffe://test.cassandra.apache.org/unitTest/mtls -storetype jks
- // 2. export cert
- // keytool -export -alias spiffecert -file spiffecert.cer -keystore test/conf/cassandra_ssl_test_spiffe.keystore
- // 3. import cert into truststore
- // keytool -import -v -trustcacerts -alias spiffecert -file spiffecert.cer -keystore test/conf/cassandra_ssl_test.truststore
EncryptionOptions encryptionOptions = new EncryptionOptions()
.withEnabled(true)
.withRequireClientAuth(EncryptionOptions.ClientAuth.OPTIONAL)
- .withTrustStore("test/conf/cassandra_ssl_test.truststore")
- .withTrustStorePassword("cassandra")
+ .withTrustStore(TlsTestUtils.CLIENT_TRUSTSTORE_PATH)
+ .withTrustStorePassword(TlsTestUtils.CLIENT_TRUSTSTORE_PASSWORD)
.withSslContextFactory(new ParameterizedClass(SimpleClientSslContextFactory.class.getName()));
if (presentClientCertificate)
{
- encryptionOptions = encryptionOptions.withKeyStore("test/conf/cassandra_ssl_test_spiffe.keystore")
+ encryptionOptions = encryptionOptions.withKeyStore(TlsTestUtils.CLIENT_SPIFFE_KEYSTORE_PATH)
.withStoreType("JKS")
- .withKeyStorePassword("cassandra");
+ .withKeyStorePassword(TlsTestUtils.CLIENT_SPIFFE_KEYSTORE_PASSWORD);
}
return new EncryptionOptions(encryptionOptions);
@@ -117,7 +107,7 @@ public class EarlyAuthenticationTest extends CQLTester
public void testEarlyAuthSuccess()
{
// given server is configured with a Mutual TLS Authenticator and the identity in the client's keystore is bound to cassandra.
- addIdentityToRole(spiffeIdentity, "cassandra");
+ addIdentityToRole(TlsTestUtils.CLIENT_SPIFFE_IDENTITY, "cassandra");
// when connecting, we expect to get a 'READY' message after sending a 'STARTUP' as MutualTlsAuthenticator
// supports early authentication and the client presented a cert with an identity that was bound to a role.
@@ -135,8 +125,8 @@ public class EarlyAuthenticationTest extends CQLTester
// given server is configured with a Mutual TLS Authenticator and the identity in the client's keystore is bound
// to a role that is not permitted to log in.
// when connecting, we expect an 'ERROR' message.
- addIdentityToRole(spiffeIdentity, "readonly_user");
- testStartupResponse(true, expectAuthenticationError(String.format("readonly_user is not permitted to log in", spiffeIdentity)));
+ addIdentityToRole(TlsTestUtils.CLIENT_SPIFFE_IDENTITY, "readonly_user");
+ testStartupResponse(true, expectAuthenticationError("readonly_user is not permitted to log in"));
}
@Test
@@ -144,7 +134,7 @@ public class EarlyAuthenticationTest extends CQLTester
{
// given server is configured with a Mutual TLS Authenticator, but no identities are bound to roles.
// when connecting, we expect an 'ERROR' message.
- testStartupResponse(true, expectAuthenticationError(String.format("Certificate identity '%s' not authorized", spiffeIdentity)));
+ testStartupResponse(true, expectAuthenticationError(String.format("Certificate identity '%s' not authorized", TlsTestUtils.CLIENT_SPIFFE_IDENTITY)));
}
@Test
diff --git a/test/unit/org/apache/cassandra/transport/SimpleClientSslContextFactory.java b/test/unit/org/apache/cassandra/transport/SimpleClientSslContextFactory.java
index 709cc50f70..1a6871716a 100644
--- a/test/unit/org/apache/cassandra/transport/SimpleClientSslContextFactory.java
+++ b/test/unit/org/apache/cassandra/transport/SimpleClientSslContextFactory.java
@@ -19,7 +19,10 @@
package org.apache.cassandra.transport;
import java.util.Map;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
+import javax.net.ssl.TrustManager;
import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.SslContext;
@@ -27,6 +30,8 @@ import io.netty.handler.ssl.SslContextBuilder;
import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.security.FileBasedSslContextFactory;
+import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.NOT_REQUIRED;
+
/**
* A custom implementation of {@link FileBasedSslContextFactory} to be used by tests utilizing {@link SimpleClient}.
* <p>
@@ -43,6 +48,33 @@ public class SimpleClientSslContextFactory extends FileBasedSslContextFactory
super(parameters);
}
+ @Override
+ public SSLContext createJSSESslContext(EncryptionOptions.ClientAuth clientAuth) throws SSLException
+ {
+ TrustManager[] trustManagers = null;
+ if (clientAuth != NOT_REQUIRED)
+ trustManagers = buildTrustManagerFactory().getTrustManagers();
+
+ KeyManagerFactory kmf = null;
+
+ // only provide a client certificate if keystore is present.
+ if (hasOutboundKeystore())
+ {
+ kmf = buildKeyManagerFactory();
+ }
+
+ try
+ {
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ ctx.init(kmf != null ? kmf.getKeyManagers() : null, trustManagers, null);
+ return ctx;
+ }
+ catch (Exception e)
+ {
+ throw new SSLException("Error creating/initializing the SSL Context", e);
+ }
+ }
+
@Override
public SslContext createNettySslContext(EncryptionOptions.ClientAuth clientAuth, SocketType socketType,
CipherSuiteFilter cipherFilter) throws SSLException
diff --git a/test/unit/org/apache/cassandra/transport/TlsTestUtils.java b/test/unit/org/apache/cassandra/transport/TlsTestUtils.java
new file mode 100644
index 0000000000..8939937c98
--- /dev/null
+++ b/test/unit/org/apache/cassandra/transport/TlsTestUtils.java
@@ -0,0 +1,112 @@
+/*
+ * 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.transport;
+
+import java.util.Collections;
+import java.util.Map;
+import javax.net.ssl.SSLException;
+
+import com.google.common.collect.ImmutableMap;
+
+import com.datastax.driver.core.RemoteEndpointAwareJdkSSLOptions;
+import com.datastax.driver.core.SSLOptions;
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.EncryptionOptions;
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.security.ISslContextFactory;
+
+public class TlsTestUtils
+{
+ public static String SERVER_KEYSTORE_PATH = "test/conf/cassandra_ssl_test.keystore";
+ public static String SERVER_KEYSTORE_PATH_PEM = "test/conf/cassandra_ssl_test.keystore.pem";
+ public static String SERVER_KEYSTORE_PATH_UNENCRYPTED_PEM = "test/conf/cassandra_ssl_test.unencrypted_keystore.pem";
+ public static String SERVER_KEYSTORE_PASSWORD = "cassandra";
+
+ public static String SERVER_KEYSTORE_ENDPOINT_VERIFY_PATH = "test/conf/cassandra_ssl_test_endpoint_verify.keystore";
+ public static String SERVER_KEYSTORE_ENDPOINT_VERIFY_PASSWORD = "cassandra";
+
+ public static String SERVER_OUTBOUND_KEYSTORE_PATH = "test/conf/cassandra_ssl_test_outbound.keystore";
+ public static String SERVER_OUTBOUND_KEYSTORE_PASSWORD = "cassandra";
+
+ public static String SERVER_TRUSTSTORE_PATH = "test/conf/cassandra_ssl_test.truststore";
+ public static String SERVER_TRUSTSTORE_PEM_PATH = "test/conf/cassandra_ssl_test.truststore.pem";
+ public static String SERVER_TRUSTSTORE_PASSWORD = "cassandra";
+
+ // To regenerate:
+ // 1. generate keystore
+ // keytool -genkeypair -keystore test/conf/cassandra_ssl_test_spiffe.keystore -validity 100000 -keyalg RSA -dname "CN=Apache Cassandra, OU=ssl_test, O=Unknown, L=Unknown, ST=Unknown, C=Unknown" -keypass cassandra -storepass cassandra -alias spiffecert -ext SAN=URI:spiffe://test.cassandra.apache.org/unitTest/mtls -storetype jks
+ // 2. export cert
+ // keytool -export -alias spiffecert -file spiffecert.cer -keystore test/conf/cassandra_ssl_test_spiffe.keystore
+ // 3. import cert into truststore
+ // keytool -import -v -trustcacerts -alias spiffecert -file spiffecert.cer -keystore test/conf/cassandra_ssl_test.truststore
+ public static String CLIENT_SPIFFE_KEYSTORE_PATH = "test/conf/cassandra_ssl_test_spiffe.keystore";
+ public static String CLIENT_SPIFFE_KEYSTORE_PASSWORD = "cassandra";
+ public static String CLIENT_SPIFFE_IDENTITY = "spiffe://test.cassandra.apache.org/unitTest/mtls";
+
+ public static String CLIENT_TRUSTSTORE_PATH = "test/conf/cassandra_ssl_test.truststore";
+ public static String CLIENT_TRUSTSTORE_PASSWORD = "cassandra";
+
+ public static EncryptionOptions getClientEncryptionOptions()
+ {
+ return new EncryptionOptions(new EncryptionOptions()
+ .withEnabled(true)
+ .withRequireClientAuth(EncryptionOptions.ClientAuth.OPTIONAL)
+ .withOptional(true)
+ .withKeyStore(SERVER_KEYSTORE_PATH)
+ .withKeyStorePassword(SERVER_KEYSTORE_PASSWORD)
+ .withTrustStore(SERVER_TRUSTSTORE_PATH)
+ .withTrustStorePassword(SERVER_TRUSTSTORE_PASSWORD)
+ .withRequireEndpointVerification(false));
+ }
+
+ public static void configureWithMutualTlsWithPasswordFallbackAuthenticator(Config config)
+ {
+ // Configure an authenticator that supports multiple authentication mechanisms.
+ Map<String, String> parameters = Collections.singletonMap("validator_class_name", "org.apache.cassandra.auth.SpiffeCertificateValidator");
+ config.authenticator = new ParameterizedClass("MutualTlsWithPasswordFallbackAuthenticator", parameters);
+ // Configure client encryption such that we can optionally connect with SSL.
+ config.client_encryption_options = TlsTestUtils.getClientEncryptionOptions();
+ config.role_manager = "CassandraRoleManager";
+ config.authorizer = "CassandraAuthorizer";
+ }
+
+ public static ISslContextFactory getClientSslContextFactory(boolean provideClientCert)
+ {
+ ImmutableMap.Builder<String, Object> params = ImmutableMap.<String, Object>builder()
+ .put("truststore", CLIENT_TRUSTSTORE_PATH)
+ .put("truststore_password", CLIENT_TRUSTSTORE_PASSWORD);
+
+ if (provideClientCert)
+ {
+ params.put("keystore", CLIENT_SPIFFE_KEYSTORE_PATH)
+ .put("keystore_password", CLIENT_SPIFFE_KEYSTORE_PASSWORD);
+ }
+
+ return new SimpleClientSslContextFactory(params.build());
+ }
+
+ public static SSLOptions getSSLOptions(boolean provideClientCert) throws SSLException
+ {
+ return RemoteEndpointAwareJdkSSLOptions.builder()
+ .withSSLContext(getClientSslContextFactory(provideClientCert)
+ .createJSSESslContext(EncryptionOptions.ClientAuth.OPTIONAL))
+ .build();
+ }
+
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org