This is an automated email from the ASF dual-hosted git repository.
djoshi 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 f078c02cb5 Adding Mutual TLS authenticators for client & internode
connections
f078c02cb5 is described below
commit f078c02cb58bddd735490b07548f7352f0eb09aa
Author: jkonisa <[email protected]>
AuthorDate: Mon May 22 22:15:22 2023 -0700
Adding Mutual TLS authenticators for client & internode connections
Patch by Jyothsna Konisa & Dinesh Joshi; reviewed by Yifan Cai, Jon
Meredith,
Yuki Morishita & Dinesh Joshi for CASSANDRA-18554
Co-Authored-By: Dinesh Joshi <[email protected]>
---
conf/cassandra.yaml | 22 +-
.../pages/developing/cql/cql_singlefile.adoc | 51 +++++
.../pages/getting-started/mtlsauthenticators.adoc | 133 ++++++++++++
src/antlr/Lexer.g | 1 +
src/antlr/Parser.g | 31 +++
.../apache/cassandra/audit/AuditLogEntryType.java | 2 +
src/java/org/apache/cassandra/auth/AuthConfig.java | 20 +-
.../org/apache/cassandra/auth/AuthKeyspace.java | 14 +-
.../cassandra/auth/CassandraRoleManager.java | 114 +++++++++++
.../cassandra/auth/IInternodeAuthenticator.java | 2 +-
.../org/apache/cassandra/auth/IRoleManager.java | 56 +++++
.../cassandra/auth/MutualTlsAuthenticator.java | 208 +++++++++++++++++++
.../auth/MutualTlsCertificateValidator.java | 63 ++++++
.../auth/MutualTlsInternodeAuthenticator.java | 226 +++++++++++++++++++++
...MutualTlsWithPasswordFallbackAuthenticator.java | 53 +++++
.../cassandra/auth/PasswordAuthenticator.java | 3 +-
.../cassandra/auth/SpiffeCertificateValidator.java | 94 +++++++++
src/java/org/apache/cassandra/config/Config.java | 4 +-
.../cassandra/config/DatabaseDescriptor.java | 3 +-
.../cassandra/config/ParameterizedClass.java | 49 +++++
.../cql3/statements/AddIdentityStatement.java | 86 ++++++++
.../cql3/statements/DropIdentityStatement.java | 79 +++++++
.../apache/cassandra/service/StorageService.java | 2 +-
.../cassandra-mtls-backward-compatibility.yaml | 76 +++++++
test/conf/cassandra-mtls.yaml | 89 ++++++++
test/conf/cassandra_ssl_test.truststore | Bin 5295 -> 7638 bytes
test/conf/cassandra_ssl_test_outbound.keystore | Bin 2286 -> 2375 bytes
test/resources/auth/SampleInvalidCertificate.pem | 21 ++
.../resources/auth/SampleMtlsClientCertificate.pem | 29 +++
.../SampleUnauthorizedMtlsClientCertificate.pem | 29 +++
.../cassandra/audit/AuditLoggerAuthTest.java | 2 +-
.../org/apache/cassandra/auth/AuthConfigTest.java | 100 +++++++++
.../org/apache/cassandra/auth/AuthTestUtils.java | 46 +++++
.../cassandra/auth/MutualTlsAuthenticatorTest.java | 181 +++++++++++++++++
.../auth/MutualTlsInternodeAuthenticatorTest.java | 188 +++++++++++++++++
...alTlsWithPasswordFallbackAuthenticatorTest.java | 93 +++++++++
.../auth/SpiffeCertificateValidatorTest.java | 58 ++++++
.../cassandra/config/ConfigCompatabilityTest.java | 17 +-
.../config/YamlConfigurationLoaderTest.java | 39 +++-
.../cql3/statements/AddIdentityStatementTest.java | 198 ++++++++++++++++++
.../cql3/statements/DropIdentityStatementTest.java | 157 ++++++++++++++
.../cassandra/transport/CQLUserAuditTest.java | 2 +-
42 files changed, 2618 insertions(+), 23 deletions(-)
diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index 6414fab956..8783f53252 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -150,7 +150,14 @@ batchlog_replay_throttle: 1024KiB
# users. It keeps usernames and hashed passwords in system_auth.roles table.
# Please increase system_auth keyspace replication factor if you use this
authenticator.
# If using PasswordAuthenticator, CassandraRoleManager must also be used
(see below)
-authenticator: AllowAllAuthenticator
+authenticator:
+ class_name : org.apache.cassandra.auth.AllowAllAuthenticator
+# MutualTlsAuthenticator can be configured using the following configuration.
One can add their own validator
+# which implements MutualTlsCertificateValidator class and provide logic for
extracting identity out of certificates
+# and validating certificates.
+# class_name : org.apache.cassandra.auth.MutualTlsAuthenticator
+# parameters :
+# validator_class_name: org.apache.cassandra.auth.SpiffeCertificateValidator
# Authorization backend, implementing IAuthorizer; used to limit
access/provide permissions
# Out of the box, Cassandra provides
org.apache.cassandra.auth.{AllowAllAuthorizer,
@@ -820,8 +827,17 @@ listen_address: localhost
# Internode authentication backend, implementing IInternodeAuthenticator;
# used to allow/disallow connections from peer nodes.
-# internode_authenticator:
org.apache.cassandra.auth.AllowAllInternodeAuthenticator
-
+#internode_authenticator:
+# class_name : org.apache.cassandra.auth.AllowAllInternodeAuthenticator
+# parameters :
+# MutualTlsInternodeAuthenticator can be configured using the following
configuration.One can add their own validator
+# which implements MutualTlsCertificateValidator class and provide logic for
extracting identity out of certificates
+# and validating certificates.
+# class_name : org.apache.cassandra.auth.MutualTlsInternodeAuthenticator
+# parameters :
+# validator_class_name: org.apache.cassandra.auth.SpiffeCertificateValidator
+# trusted_peer_identities: "spiffe1,spiffe2"
+# node_identity: "spiffe1"
# Whether to start the native transport server.
# The address on which the native transport is bound is defined by rpc_address.
start_native_transport: true
diff --git a/doc/modules/cassandra/pages/developing/cql/cql_singlefile.adoc
b/doc/modules/cassandra/pages/developing/cql/cql_singlefile.adoc
index 64e71b051c..286a9923c2 100644
--- a/doc/modules/cassandra/pages/developing/cql/cql_singlefile.adoc
+++ b/doc/modules/cassandra/pages/developing/cql/cql_singlefile.adoc
@@ -2246,6 +2246,57 @@ LIST ROLES;
but only roles with the `LOGIN` privilege are included in the output.
+[[databaseIdentity]]
+=== Database Identities
+
+[[AddIdentityStmt]]
+==== ADD IDENTITY
+
+_Syntax:_
+
+bc(syntax).. +
+::= ADD IDENTITY ( IF NOT EXISTS )? TO ROLE ?
+
+_Sample:_
+
+bc(sample). +
+ADD IDENTITY 'id1' TO ROLE 'role1';
+
+Only a user with privileges to add roles can add identities
+
+Role names & Identity names should be quoted if they contain non-alphanumeric
characters.
+
+[[addIdentityConditional]]
+===== Adding an identity conditionally
+
+Attempting to add an existing identity results in an invalid query
+condition unless the `IF NOT EXISTS` option is used. If the option is
+used and the identity exists, the statement is a no-op.
+
+bc(sample). +
+ADD IDENTITY IF NOT EXISTS 'id1' TO ROLE 'role1';
+
+[[dropIdentityStmt]]
+==== DROP IDENTITY
+
+_Syntax:_
+
+bc(syntax).. +
+::= DROP IDENTITY ( IF EXISTS )? +
+p.
+
+_Sample:_
+
+bc(sample). +
+DROP IDENTITY 'testIdentity'; +
+DROP IDENTITY IF EXISTS 'testIdentity';
+
+Only a user with privileges to drop roles can remove identities
+
+Attempting to drop an Identity which does not exist results in an invalid
+query condition unless the `IF EXISTS` option is used. If the option is
+used and the identity does not exist the statement is a no-op.
+
[[dataControl]]
=== Data Control
diff --git
a/doc/modules/cassandra/pages/getting-started/mtlsauthenticators.adoc
b/doc/modules/cassandra/pages/getting-started/mtlsauthenticators.adoc
new file mode 100644
index 0000000000..e3cd6ef79f
--- /dev/null
+++ b/doc/modules/cassandra/pages/getting-started/mtlsauthenticators.adoc
@@ -0,0 +1,133 @@
+= Getting started with mTLS authenticators
+
+When a certificate based authentication protocol like TLS is used for client
and
+Internode connections, `MutualTlsAuthenticator` &
`MutualTlsInternodeAuthenticator`
+can be used for the authentication by leveraging the client certificates from
the
+SSL handshake.
+
+After SSL handshake, identity from the client certificates is extracted and
only
+authorized users will be granted access.
+
+== What is an Identity
+
+Operators can define their own identity for certificates by extracting some
fields or
+information from the certificates. Implementing the interface
`MutualTlsCertificateValidator`
+supports validating & extracting identities from the certificates that can be
used by
+`MutualTlsAuthenticator` and `MutualTlsInternodeAuthenticator` to customize
for the
+certificate conventions used in the deployment environment.
+
+There is a default implementation of `MutualTlsCertificateValidator` with
+https://spiffe.io/docs/latest/spiffe-about/spiffe-concepts/[SPIFFE] as the
identity
+of the certificates.This requires spiffe to be present in the SAN of the
certificate.
+
+Instead of using `SPIFFE` based validator, a custom `CN` based validator that
implements `MutualTlsCertificateValidator`
+could be configured by the operator if required.
+
+== Configuring mTLS authenticator for client connections
+
+Note that the following steps uses SPIFFE identity as an example, If you are
using
+a custom validator, use appropriate identity in place of
`spiffe://testdomain.com/testIdentifier/testValue`.
+
+*STEP 1: Add authorized users to system_auth.identity_to_roles table*
+
+Note that only users with permissions to create/modify roles can add/remove
identities.
+Client certificates with the identities in this table will be trusted by C*.
+[source, plaintext]
+----
+ADD IDENTITY 'spiffe://testdomain.com/testIdentifier/testValue' TO ROLE
'read_only_user'
+----
+
+*STEP 2: Configure Cassandra.yaml with right properties*
+
+`client_encryption_options` configuration for mTLS connections
+[source, plaintext]
+----
+client_encryption_options:
+ enabled: true
+ optional: false
+ keystore: conf/.keystore
+ keystore_password: cassandra
+ truststore: conf/.truststore
+ truststore_password: cassandra
+ require_client_auth: true // to enable mTLS
+----
+Configure mTLS authenticator and the validator for client connections . If you
are
+implementing a custom validator, use that instead of Spiffe validator
+[source, plaintext]
+----
+authenticator:
+ class_name : org.apache.cassandra.auth.MutualTlsAuthenticator
+ parameters :
+ validator_class_name: org.apache.cassandra.auth.SpiffeCertificateValidator
+----
+
+*STEP 3: Bounce the cluster*
+
+After the bounce, C* will accept mTLS connections from the clients and if their
+identity is present in the `identity_to_roles` table, access will be granted.
+
+== Configuring mTLS authenticator for Internode connections
+
+Internode authenticator trusts certificates whose identities are present in
+`internode_authenticator.parameters.trusted_peer_identities` if configured.
+
+Otherwise, it trusts connections which have the same identity as the node.
+When a node is making an outbound connection to another node, it uses the
+certificate configured in `server_encryption_options.outbound_keystore`.
+During the start of the node, identity is extracted from the outbound keystore
and
+connections from other nodes who have the same identity will be trusted if
+`trusted_peer_identities` is not configured.
+
+For example, if a node has `testIdentity` embedded in the certificate in
+outbound keystore, It trusts connections from other nodes when their
certificates
+have `testIdentity` embedded in them.
+
+There is an optional configuration `node_identity` that can be used to verify
identity
+extracted from the keystore to avoid any configuration errors.
+
+*STEP 1: Configure server_encryption_options in cassandra.yaml*
+
+[source, plaintext]
+----
+server_encryption_options:
+ internode_encryption: all
+ optional: true
+ keystore: conf/.keystore
+ keystore_password: cassandra
+ outbound_keystore: conf/.outbound_keystore
+ outbound_keystore_password: cassandra
+ require_client_auth: true // for enabling mTLS
+ truststore: conf/.truststore
+ truststore_password: cassandra
+----
+
+*STEP 2: Configure Internode Authenticator and Validator*
+
+Configure mTLS Internode authenticator and validator. If you are
+implementing a custom validator, use that instead of Spiffe validator
+[source, plaintext]
+----
+internode_authenticator:
+ class_name : org.apache.cassandra.auth.MutualTlsInternodeAuthenticator
+ parameters :
+ validator_class_name: org.apache.cassandra.auth.SpiffeCertificateValidator
+ trusted_peer_identities : "spiffe1,spiffe2"
+----
+
+*STEP 3: Bounce the cluster*
+Once all nodes in the cluster are restarted, all internode communications will
be authenticated by mTLS.
+
+== Migration from existing password based authentication
+* For client connections, since the migration will not happen overnight,
+the operators can run cassandra in optional mTLS mode and use
+`MutualTlsWithPasswordFallbackAuthenticator` which will accept both mTLS &
password
+based connections, based on the type of connection client is making. These
settings
+can be configured in `cassandra.yaml`. Once all the clients migrate to using
mTLS,
+turn off optional mode and set the authenticator to be
`MutualTlsAuthenticator`. From
+that point only mTLS client connections will be accepted.
+
+* For Internode connections, while doing rolling upgrades from non-mTLS based
configuration
+to mTLS based configuration, set `server_encryption_options.optional:true` for
the new nodes to
+be able to connect to old nodes which are still using non-mTLS based
configuration during upgrade.
+After this, change the internode authenticator to be
`MutualTlsInternodeAuthenticator` and turn off the optional
+mode by setting `server_encryption_options.optional:false`.
\ No newline at end of file
diff --git a/src/antlr/Lexer.g b/src/antlr/Lexer.g
index a4f8ea715f..c75523ed74 100644
--- a/src/antlr/Lexer.g
+++ b/src/antlr/Lexer.g
@@ -156,6 +156,7 @@ K_NOLOGIN: N O L O G I N;
K_OPTIONS: O P T I O N S;
K_ACCESS: A C C E S S;
K_DATACENTERS: D A T A C E N T E R S;
+K_IDENTITY: I D E N T I T Y;
K_CLUSTERING: C L U S T E R I N G;
K_ASCII: A S C I I;
diff --git a/src/antlr/Parser.g b/src/antlr/Parser.g
index 65ed92a397..0d2150481d 100644
--- a/src/antlr/Parser.g
+++ b/src/antlr/Parser.g
@@ -247,6 +247,8 @@ cqlStatement returns [CQLStatement.Raw stmt]
| st39=dropMaterializedViewStatement { $stmt = st39; }
| st40=alterMaterializedViewStatement { $stmt = st40; }
| st41=describeStatement { $stmt = st41; }
+ | st42=addIdentityStatement { $stmt = st42; }
+ | st43=dropIdentityStatement { $stmt = st43; }
;
/*
@@ -1237,6 +1239,28 @@ dropUserStatement returns [DropRoleStatement stmt]
}
: K_DROP K_USER (K_IF K_EXISTS { ifExists = true; })? u=username {
name.setName($u.text, true); $stmt = new DropRoleStatement(name, ifExists); }
;
+/**
+ * ADD IDENTITY [IF NOT EXISTS] <identity> TO ROLE <role>
+ */
+addIdentityStatement returns [AddIdentityStatement stmt]
+ @init {
+ String identity = null;
+ String role = null;
+ boolean ifNotExists = false;
+ }
+ : K_ADD K_IDENTITY (K_IF K_NOT K_EXISTS { ifNotExists = true; })?
u=identity { identity= $u.text; } K_TO K_ROLE r=identity { role=$r.text; $stmt
= new AddIdentityStatement(identity, role, ifNotExists); }
+ ;
+
+/**
+ * DROP IDENTITY [IF EXISTS] <identity>
+ */
+ dropIdentityStatement returns [DropIdentityStatement stmt]
+ @init {
+ boolean ifExists = false;
+ String identity = null;
+ }
+ : K_DROP K_IDENTITY (K_IF K_EXISTS { ifExists = true; })? u=identity {
identity= $u.text; $stmt = new DropIdentityStatement(identity, ifExists);}
+ ;
/**
* LIST USERS
@@ -1878,6 +1902,12 @@ username
| QUOTED_NAME { addRecognitionError("Quoted strings are are not supported
for user names and USER is deprecated, please use ROLE");}
;
+identity
+ : IDENT
+ | STRING_LITERAL
+ | QUOTED_NAME { addRecognitionError("Quoted strings are are not supported
for identity");}
+ ;
+
mbean
: STRING_LITERAL
;
@@ -1923,6 +1953,7 @@ basic_unreserved_keyword returns [String str]
| K_USERS
| K_ROLE
| K_ROLES
+ | K_IDENTITY
| K_SUPERUSER
| K_NOSUPERUSER
| K_LOGIN
diff --git a/src/java/org/apache/cassandra/audit/AuditLogEntryType.java
b/src/java/org/apache/cassandra/audit/AuditLogEntryType.java
index ccf0169ff8..1055f875e0 100644
--- a/src/java/org/apache/cassandra/audit/AuditLogEntryType.java
+++ b/src/java/org/apache/cassandra/audit/AuditLogEntryType.java
@@ -58,6 +58,8 @@ public enum AuditLogEntryType
LIST_PERMISSIONS(AuditLogEntryCategory.DCL),
ALTER_TYPE(AuditLogEntryCategory.DDL),
CREATE_ROLE(AuditLogEntryCategory.DCL),
+ CREATE_IDENTITY(AuditLogEntryCategory.DCL),
+ DROP_IDENTITY(AuditLogEntryCategory.DCL),
USE_KEYSPACE(AuditLogEntryCategory.OTHER),
DESCRIBE(AuditLogEntryCategory.OTHER),
diff --git a/src/java/org/apache/cassandra/auth/AuthConfig.java
b/src/java/org/apache/cassandra/auth/AuthConfig.java
index 9c5fceb6dc..5cecd6f130 100644
--- a/src/java/org/apache/cassandra/auth/AuthConfig.java
+++ b/src/java/org/apache/cassandra/auth/AuthConfig.java
@@ -18,11 +18,14 @@
package org.apache.cassandra.auth;
+import java.util.Arrays;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.ParameterizedClass;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.utils.FBUtilities;
@@ -50,12 +53,15 @@ public final class AuthConfig
/* Authentication, authorization and role management backend,
implementing IAuthenticator, IAuthorizer & IRoleMapper*/
if (conf.authenticator != null)
- authenticator = FBUtilities.newAuthenticator(conf.authenticator);
+ {
+ authenticator = ParameterizedClass.newInstance(conf.authenticator,
+ Arrays.asList("",
AuthConfig.class.getPackage().getName()));
+ }
// the configuration options regarding credentials caching are only
guaranteed to
// work with PasswordAuthenticator, so log a message if some other
authenticator
// is in use and non-default values are detected
- if (!(authenticator instanceof PasswordAuthenticator)
+ if (!(authenticator instanceof PasswordAuthenticator || authenticator
instanceof MutualTlsAuthenticator)
&& (conf.credentials_update_interval != null
|| conf.credentials_validity.toMilliseconds() != 2000
|| conf.credentials_cache_max_entries != 1000))
@@ -75,7 +81,7 @@ public final class AuthConfig
authorizer = FBUtilities.newAuthorizer(conf.authorizer);
if (!authenticator.requireAuthentication() &&
authorizer.requireAuthorization())
- throw new ConfigurationException(conf.authenticator + " can't be
used with " + conf.authorizer, false);
+ throw new ConfigurationException(conf.authenticator.class_name + "
can't be used with " + conf.authorizer, false);
DatabaseDescriptor.setAuthorizer(authorizer);
@@ -95,14 +101,18 @@ public final class AuthConfig
// authenticator
if (conf.internode_authenticator != null)
-
DatabaseDescriptor.setInternodeAuthenticator(FBUtilities.construct(conf.internode_authenticator,
"internode_authenticator"));
+ {
+
DatabaseDescriptor.setInternodeAuthenticator(ParameterizedClass.newInstance(conf.internode_authenticator,
+
Arrays.asList("", AuthConfig.class.getPackage().getName())));
+ }
+
// network authorizer
INetworkAuthorizer networkAuthorizer =
FBUtilities.newNetworkAuthorizer(conf.network_authorizer);
DatabaseDescriptor.setNetworkAuthorizer(networkAuthorizer);
if (networkAuthorizer.requireAuthorization() &&
!authenticator.requireAuthentication())
{
- throw new ConfigurationException(conf.network_authorizer + " can't
be used with " + conf.authenticator, false);
+ throw new ConfigurationException(conf.network_authorizer + " can't
be used with " + conf.authenticator.class_name, false);
}
// Validate at last to have authenticator, authorizer, role-manager
and internode-auth setup
diff --git a/src/java/org/apache/cassandra/auth/AuthKeyspace.java
b/src/java/org/apache/cassandra/auth/AuthKeyspace.java
index 75d9871d03..b1616e07fa 100644
--- a/src/java/org/apache/cassandra/auth/AuthKeyspace.java
+++ b/src/java/org/apache/cassandra/auth/AuthKeyspace.java
@@ -58,7 +58,8 @@ public final class AuthKeyspace
public static final String ROLE_PERMISSIONS = "role_permissions";
public static final String RESOURCE_ROLE_INDEX =
"resource_role_permissons_index";
public static final String NETWORK_PERMISSIONS = "network_permissions";
- public static final Set<String> TABLE_NAMES = ImmutableSet.of(ROLES,
ROLE_MEMBERS, ROLE_PERMISSIONS, RESOURCE_ROLE_INDEX, NETWORK_PERMISSIONS);
+ public static final String IDENTITY_TO_ROLES = "identity_to_role";
+ public static final Set<String> TABLE_NAMES = ImmutableSet.of(ROLES,
ROLE_MEMBERS, ROLE_PERMISSIONS, RESOURCE_ROLE_INDEX, NETWORK_PERMISSIONS,
IDENTITY_TO_ROLES);
public static final long SUPERUSER_SETUP_DELAY =
SUPERUSER_SETUP_DELAY_MS.getLong();
@@ -73,6 +74,15 @@ public final class AuthKeyspace
+ "member_of set<text>,"
+ "PRIMARY KEY(role))");
+ private static final TableMetadata IdentityToRoles =
+ parse(IDENTITY_TO_ROLES,
+ "mtls authorized identities lookup table",
+ "CREATE TABLE %s ("
+ + "identity text," // opaque identity string for use by role
authenticators
+ + "role text,"
+ + "PRIMARY KEY(identity))"
+ );
+
private static final TableMetadata RoleMembers =
parse(ROLE_MEMBERS,
"role memberships lookup table",
@@ -119,6 +129,6 @@ public final class AuthKeyspace
{
return KeyspaceMetadata.create(SchemaConstants.AUTH_KEYSPACE_NAME,
KeyspaceParams.simple(Math.max(DEFAULT_RF,
DatabaseDescriptor.getDefaultKeyspaceRF())),
- Tables.of(Roles, RoleMembers,
RolePermissions, ResourceRoleIndex, NetworkPermissions));
+ Tables.of(Roles, RoleMembers,
RolePermissions, ResourceRoleIndex, NetworkPermissions, IdentityToRoles));
}
}
diff --git a/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
b/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
index 37bda4eac4..3221c85184 100644
--- a/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
+++ b/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
@@ -17,6 +17,7 @@
*/
package org.apache.cassandra.auth;
+import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
@@ -46,6 +47,7 @@ import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.transport.messages.ResultMessage;
import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.NoSpamLogger;
import org.mindrot.jbcrypt.BCrypt;
import static
org.apache.cassandra.config.CassandraRelevantProperties.AUTH_BCRYPT_GENSALT_LOG2_ROUNDS;
@@ -80,6 +82,7 @@ import static
org.apache.cassandra.utils.Clock.Global.nanoTime;
public class CassandraRoleManager implements IRoleManager
{
private static final Logger logger =
LoggerFactory.getLogger(CassandraRoleManager.class);
+ private static final NoSpamLogger nospamLogger =
NoSpamLogger.getLogger(logger, 1L, TimeUnit.MINUTES);
public static final String DEFAULT_SUPERUSER_NAME = "cassandra";
public static final String DEFAULT_SUPERUSER_PASSWORD = "cassandra";
@@ -125,6 +128,7 @@ public class CassandraRoleManager implements IRoleManager
}
private SelectStatement loadRoleStatement;
+ private SelectStatement loadIdentityStatement;
private final Set<Option> supportedOptions;
private final Set<Option> alterableOptions;
@@ -143,12 +147,72 @@ public class CassandraRoleManager implements IRoleManager
public void setup()
{
loadRoleStatement();
+ loadIdentityStatement();
scheduleSetupTask(() -> {
setupDefaultRole();
return null;
});
}
+ @Override
+ public String roleForIdentity(String identity)
+ {
+ QueryOptions options =
QueryOptions.forInternalCalls(CassandraAuthorizer.authReadConsistencyLevel(),
+
Collections.singletonList(byteBuf(identity)));
+ ResultMessage.Rows rows = select(loadIdentityStatement, options);
+ if (rows.result.isEmpty())
+ {
+ nospamLogger.warn("No such identity {} in the identity_to_roles
table", identity);
+ return null;
+ }
+ return UntypedResultSet.create(rows.result).one().getString("role");
+ }
+
+ @Override
+ public Map<String, String> authorizedIdentities()
+ {
+ Map<String, String> validIdentities = new HashMap<>();
+ String query = String.format("SELECT identity, role from %s.%s",
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.IDENTITY_TO_ROLES);
+ UntypedResultSet rows = process(query,
CassandraAuthorizer.authReadConsistencyLevel());
+ rows.forEach(row -> validIdentities.put(row.getString("identity"),
row.getString("role")));
+ return validIdentities;
+ }
+
+ @Override
+ public void addIdentity(String identity, String role)
+ {
+ if (isExistingIdentity(identity))
+ {
+ throw new IllegalStateException("Identity is already associated
with another role, cannot associate it with role " + role);
+ }
+
+ String query = String.format("INSERT INTO %s.%s (identity, role)
VALUES (?, ?)",
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.IDENTITY_TO_ROLES);
+ process(query, CassandraAuthorizer.authWriteConsistencyLevel(),
byteBuf(identity), byteBuf(role));
+ }
+
+ @Override
+ public boolean isExistingIdentity(String identity)
+ {
+ String query = String.format("SELECT identity from %s.%s where
identity=?",
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.IDENTITY_TO_ROLES);
+ UntypedResultSet rows = process(query,
CassandraAuthorizer.authReadConsistencyLevel(), byteBuf(identity));
+ return !rows.isEmpty();
+ }
+
+ @Override
+ public void dropIdentity(String identity)
+ {
+ String query = String.format("DELETE FROM %s.%s WHERE identity = ?",
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.IDENTITY_TO_ROLES);
+ process(query, CassandraAuthorizer.authWriteConsistencyLevel(),
byteBuf(identity));
+ }
+
protected final void loadRoleStatement()
{
loadRoleStatement = (SelectStatement) prepare("SELECT * from %s.%s
WHERE role = ?",
@@ -156,6 +220,14 @@ public class CassandraRoleManager implements IRoleManager
AuthKeyspace.ROLES);
}
+
+ protected void loadIdentityStatement()
+ {
+ loadIdentityStatement = (SelectStatement) prepare("SELECT role from
%s.%s where identity=?",
+
SchemaConstants.AUTH_KEYSPACE_NAME,
+
AuthKeyspace.IDENTITY_TO_ROLES);
+ }
+
public Set<Option> supportedOptions()
{
return supportedOptions;
@@ -169,6 +241,11 @@ public class CassandraRoleManager implements IRoleManager
public void createRole(AuthenticatedUser performer, RoleResource role,
RoleOptions options)
throws RequestValidationException, RequestExecutionException
{
+ List<String> identitiesOfRole = identitiesForRole(role.getRoleName());
+ if (!identitiesOfRole.isEmpty())
+ {
+ throw new IllegalStateException(String.format("Cannot create a
role '%s' when identities already exists for it", role.getRoleName()));
+ }
String insertCql = options.getPassword().isPresent() ||
options.getHashedPassword().isPresent()
? String.format("INSERT INTO %s.%s (role,
is_superuser, can_login, salted_hash) VALUES ('%s', %s, %s, '%s')",
SchemaConstants.AUTH_KEYSPACE_NAME,
@@ -194,6 +271,7 @@ public class CassandraRoleManager implements IRoleManager
escape(role.getRoleName())),
consistencyForRoleWrite(role.getRoleName()));
removeAllMembers(role.getRoleName());
+ removeAllIdentitiesOfRole(role.getRoleName());
}
public void alterRole(AuthenticatedUser performer, RoleResource role,
RoleOptions options)
@@ -470,6 +548,31 @@ public class CassandraRoleManager implements IRoleManager
consistencyForRoleWrite(grantee));
}
+ private List<String> identitiesForRole(String role)
+ {
+ // Get all identities associated with a given role
+ String query = String.format("SELECT identity FROM %s.%s WHERE role =
? ALLOW FILTERING",
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.IDENTITY_TO_ROLES);
+ UntypedResultSet rows = process(query, consistencyForRoleRead(role),
byteBuf(role));
+ List<String> identities = new ArrayList<>();
+ rows.forEach(row -> identities.add(row.getString("identity")));
+ return identities;
+ }
+
+ private void removeAllIdentitiesOfRole(String role)
+ {
+ List<String> identities = identitiesForRole(role);
+ String query = String.format("DELETE FROM %s.%s WHERE identity = ?",
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.IDENTITY_TO_ROLES);
+ // Remove all the identities associated with the role from the table
+ for (String identity : identities)
+ {
+ process(query, consistencyForRoleWrite(role), byteBuf(identity));
+ }
+ }
+
/*
* Clear the membership list of the given role
*/
@@ -534,6 +637,11 @@ public class CassandraRoleManager implements IRoleManager
return StringUtils.replace(name, "'", "''");
}
+ private static ByteBuffer byteBuf(String str)
+ {
+ return UTF8Type.instance.decompose(str);
+ }
+
/** Allows selective overriding of the consistency level for specific
roles. */
protected static ConsistencyLevel consistencyForRoleWrite(String role)
{
@@ -561,6 +669,12 @@ public class CassandraRoleManager implements IRoleManager
return QueryProcessor.process(query, consistencyLevel);
}
+ UntypedResultSet process(String query, ConsistencyLevel consistencyLevel,
ByteBuffer... values)
+ throws RequestValidationException, RequestExecutionException
+ {
+ return QueryProcessor.process(query, consistencyLevel,
Arrays.asList(values));
+ }
+
@VisibleForTesting
ResultMessage.Rows select(SelectStatement statement, QueryOptions options)
{
diff --git a/src/java/org/apache/cassandra/auth/IInternodeAuthenticator.java
b/src/java/org/apache/cassandra/auth/IInternodeAuthenticator.java
index e5038c0944..d59320abf7 100644
--- a/src/java/org/apache/cassandra/auth/IInternodeAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/IInternodeAuthenticator.java
@@ -74,7 +74,7 @@ public interface IInternodeAuthenticator
/**
* Setup is called once upon system startup to initialize the
IAuthenticator.
*
- * For example, use this method to create any required keyspaces/column
families.
+ * For example, use this method to do any required initialization of the
class.
*/
default void setupInternode()
{
diff --git a/src/java/org/apache/cassandra/auth/IRoleManager.java
b/src/java/org/apache/cassandra/auth/IRoleManager.java
index 688d5bb6c4..460bf1a16f 100644
--- a/src/java/org/apache/cassandra/auth/IRoleManager.java
+++ b/src/java/org/apache/cassandra/auth/IRoleManager.java
@@ -17,6 +17,7 @@
*/
package org.apache.cassandra.auth;
+import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -226,4 +227,59 @@ public interface IRoleManager extends
AuthCache.BulkLoader<RoleResource, Set<Rol
* For example, use this method to create any required keyspaces/column
families.
*/
void setup();
+
+ /**
+ * Each valid identity is associated with a role in the identity_to_role
table, this method returns role
+ * of a given identity
+ *
+ * @param identity identity whose role to be retrieved
+ * @return role of the given identity
+ */
+ default String roleForIdentity(String identity)
+ {
+ return null;
+ }
+
+ /**
+ * Returns all the authorized identities from the identity_to_role table
+ *
+ * @return Map of identity -> roles
+ */
+ default Map<String, String> authorizedIdentities()
+ {
+ return Collections.emptyMap();
+ }
+
+ /**
+ * Adds a row (identity, role) to the identity_to_role table
+ *
+ * @param identity identity to be added
+ * @param role role that is associated with the identity
+ */
+ default void addIdentity(String identity, String role)
+ {
+ }
+
+ /**
+ * Returns if an identity exists in the identity_to_role
+ *
+ * @param identity identity whose existence to verify
+ * @return
+ */
+ default boolean isExistingIdentity(String identity)
+ {
+ return false;
+ }
+
+ /**
+ * Called on the execution of DROP IDENTITY statement for removing a given
identity from the identity_role table.
+ * This implies we want to revoke the access for the given identity.
+ *
+ * @param identity identity that has to be removed from the table
+ */
+ default void dropIdentity(String identity)
+ {
+
+ }
+
}
diff --git a/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java
b/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java
new file mode 100644
index 0000000000..c327a4bd21
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java
@@ -0,0 +1,208 @@
+/*
+ * 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.auth;
+
+import java.net.InetAddress;
+import java.security.cert.Certificate;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.helpers.MessageFormatter;
+
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.exceptions.AuthenticationException;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.schema.SchemaConstants;
+import org.apache.cassandra.utils.NoSpamLogger;
+
+/*
+ * Performs mTLS authentication for client connections by extracting
identities from client certificate
+ * and verifying them against the authorized identities in IdentityCache.
IdentityCache is a loading cache that
+ * refreshes values on timely basis.
+ *
+ * During a client connection, after SSL handshake, identity of certificate is
extracted using the certificate validator
+ * and is verified whether the value exists in the cache or not. If it exists
access is granted, otherwise, the connection
+ * is rejected.
+ *
+ * Authenticator & Certificate validator can be configured using
cassandra.yaml, one can write their own mTLS certificate
+ * validator and configure it in cassandra.yaml.Below is an example on how to
configure validator.
+ * note that this example uses SPIFFE based validator, It could be any other
validator with any defined identifier format.
+ *
+ * Example:
+ * authenticator:
+ * class_name : org.apache.cassandra.auth.MutualTlsAuthenticator
+ * parameters :
+ * validator_class_name:
org.apache.cassandra.auth.SpiffeCertificateValidator
+ */
+public class MutualTlsAuthenticator implements IAuthenticator
+{
+ private static final Logger logger =
LoggerFactory.getLogger(MutualTlsAuthenticator.class);
+ private static final NoSpamLogger nospamLogger =
NoSpamLogger.getLogger(logger, 1L, TimeUnit.MINUTES);
+ private static final String VALIDATOR_CLASS_NAME = "validator_class_name";
+ private static final String CACHE_NAME = "IdentitiesCache";
+ private final IdentityCache identityCache = new IdentityCache();
+ private final MutualTlsCertificateValidator certificateValidator;
+
+ public MutualTlsAuthenticator(Map<String, String> parameters)
+ {
+ final String certificateValidatorClassName =
parameters.get(VALIDATOR_CLASS_NAME);
+ if (StringUtils.isEmpty(certificateValidatorClassName))
+ {
+ String message ="authenticator.parameters.validator_class_name is
not set";
+ logger.error(message);
+ throw new ConfigurationException(message);
+ }
+ certificateValidator = ParameterizedClass.newInstance(new
ParameterizedClass(certificateValidatorClassName),
+
Arrays.asList("", AuthConfig.class.getPackage().getName()));
+ checkMtlsConfigurationIsValid(DatabaseDescriptor.getRawConfig());
+ AuthCacheService.instance.register(identityCache);
+ }
+
+ @Override
+ public boolean requireAuthentication()
+ {
+ return true;
+ }
+
+ @Override
+ public Set<? extends IResource> protectedResources()
+ {
+ return
ImmutableSet.of(DataResource.table(SchemaConstants.AUTH_KEYSPACE_NAME,
AuthKeyspace.ROLES));
+ }
+
+ @Override
+ public void validateConfiguration() throws ConfigurationException
+ {
+
+ }
+
+ @Override
+ public void setup()
+ {
+ identityCache.warm();
+ }
+
+ @Override
+ public SaslNegotiator newSaslNegotiator(InetAddress clientAddress)
+ {
+ return null;
+ }
+
+ @Override
+ public SaslNegotiator newSaslNegotiator(InetAddress clientAddress,
Certificate[] certificates)
+ {
+ return new CertificateNegotiator(certificates);
+ }
+
+ @Override
+ public AuthenticatedUser legacyAuthenticate(Map<String, String>
credentials) throws AuthenticationException
+ {
+ throw new AuthenticationException("mTLS authentication is not
supported for CassandraLoginModule");
+ }
+
+ @VisibleForTesting
+ class CertificateNegotiator implements SaslNegotiator
+ {
+ private final Certificate[] clientCertificateChain;
+
+ private CertificateNegotiator(final Certificate[]
clientCertificateChain)
+ {
+ this.clientCertificateChain = clientCertificateChain;
+ }
+
+ @Override
+ public byte[] evaluateResponse(byte[] clientResponse) throws
AuthenticationException
+ {
+ return null;
+ }
+
+ @Override
+ public boolean isComplete()
+ {
+ return true;
+ }
+
+ @Override
+ public AuthenticatedUser getAuthenticatedUser() throws
AuthenticationException
+ {
+ if
(!certificateValidator.isValidCertificate(clientCertificateChain))
+ {
+ String message = "Invalid or not supported certificate";
+ nospamLogger.error(message);
+ throw new AuthenticationException(message);
+ }
+
+ final String identity =
certificateValidator.identity(clientCertificateChain);
+ if (StringUtils.isEmpty(identity))
+ {
+ String msg = "Unable to extract client identity from
certificate for authentication";
+ nospamLogger.error(msg);
+ throw new AuthenticationException(msg);
+ }
+ String role = identityCache.get(identity);
+ if (role == null)
+ {
+ String msg = "Certificate identity '{}' not authorized";
+ nospamLogger.error(msg, identity);
+ throw new AuthenticationException(MessageFormatter.format(msg,
identity).getMessage());
+ }
+ return new AuthenticatedUser(role);
+ }
+ }
+
+ private void checkMtlsConfigurationIsValid(Config config)
+ {
+ if (!config.client_encryption_options.getEnabled() ||
!config.client_encryption_options.require_client_auth)
+ {
+ String msg = "MutualTlsAuthenticator requires
client_encryption_options.enabled to be true" +
+ " & client_encryption_options.require_client_auth to
be true";
+ logger.error(msg);
+ throw new ConfigurationException(msg);
+ }
+ }
+
+ static class IdentityCache extends AuthCache<String, String>
+ {
+ IdentityCache()
+ {
+ super(CACHE_NAME,
+ DatabaseDescriptor::setCredentialsValidity,
+ DatabaseDescriptor::getCredentialsValidity,
+ DatabaseDescriptor::setCredentialsUpdateInterval,
+ DatabaseDescriptor::getCredentialsUpdateInterval,
+ DatabaseDescriptor::setCredentialsCacheMaxEntries,
+ DatabaseDescriptor::getCredentialsCacheMaxEntries,
+ DatabaseDescriptor::setCredentialsCacheActiveUpdate,
+ DatabaseDescriptor::getCredentialsCacheActiveUpdate,
+ identity ->
DatabaseDescriptor.getRoleManager().roleForIdentity(identity),
+ () ->
DatabaseDescriptor.getRoleManager().authorizedIdentities(),
+ () -> true,
+ (k, v) -> v == null);
+ }
+ }
+}
diff --git
a/src/java/org/apache/cassandra/auth/MutualTlsCertificateValidator.java
b/src/java/org/apache/cassandra/auth/MutualTlsCertificateValidator.java
new file mode 100644
index 0000000000..e9735f48bf
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/MutualTlsCertificateValidator.java
@@ -0,0 +1,63 @@
+/*
+ * 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.auth;
+
+import java.security.cert.Certificate;
+
+import org.apache.cassandra.exceptions.AuthenticationException;
+
+/**
+ * Interface for certificate validation and authorization for mTLS
authenticators.
+ *
+ * This interface can be implemented to provide logic for extracting custom
identities from client certificates
+ * to uniquely identify the certificates. It can also be used to provide
custom authorization logic to authenticate
+ * clients using client certificates during mTLS connections.
+ */
+public interface MutualTlsCertificateValidator
+{
+ /**
+ * Perform any checks that are to be performed on the certificate before
making authorization check to grant the
+ * access to the client during mTLS connection.
+ *
+ * For example
+ * - Verifying CA information
+ * - Checking CN information
+ * - Validating Issuer information
+ * - Checking organization information etc
+ *
+ * @param clientCertificateChain client certificate chain
+ * @return returns if the certificate is valid or not
+ */
+ boolean isValidCertificate(Certificate[] clientCertificateChain);
+
+ /**
+ * This method should provide logic to extract identity out of a
certificate to perform mTLS authentication.
+ *
+ * An example of identity could be the following
+ * - an identifier in SAN of the certificate like SPIFFE
+ * - CN of the certificate
+ * - any other fields in the certificate can be combined and be used as
identifier of the certificate
+ *
+ * @param clientCertificateChain client certificate chain
+ * @return identifier extracted from certificate
+ * @throws AuthenticationException when identity cannot be extracted
+ */
+ String identity(Certificate[] clientCertificateChain) throws
AuthenticationException;
+
+}
diff --git
a/src/java/org/apache/cassandra/auth/MutualTlsInternodeAuthenticator.java
b/src/java/org/apache/cassandra/auth/MutualTlsInternodeAuthenticator.java
new file mode 100644
index 0000000000..3c466df5fa
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/MutualTlsInternodeAuthenticator.java
@@ -0,0 +1,226 @@
+/*
+ * 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.auth;
+
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.nio.file.Files;
+import java.nio.file.Paths; // checkstyle: permit this import
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.EncryptionOptions;
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.exceptions.AuthenticationException;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.utils.NoSpamLogger;
+
+/*
+ * Performs mTLS authentication for internode connections by extracting
identities from the certificates of incoming
+ * connection and verifying them against a list of authorized peers.
Authorized peers can be configured in
+ * trusted_peer_identities in cassandra yaml, otherwise authenticator trusts
connections from peers which has the same
+ * identity as the one that the node uses for making outbound connections.
+ *
+ * Optionally cassandra can validate the identity extracted from outbound
keystore with node_identity that is configured
+ * in cassandra.yaml to avoid any configuration errors.
+ *
+ * Authenticator & Certificate validator can be configured using
cassandra.yaml, operators can write their own mTLS
+ * certificate validator and configure it in cassandra.yaml.Below is an
example on how to configure validator.
+ * Note that this example uses SPIFFE based validator, it could be any other
validator with any defined identifier format.
+ *
+ * internode_authenticator:
+ * class_name : org.apache.cassandra.auth.AllowAllInternodeAuthenticator
+ * parameters :
+ * validator_class_name:
org.apache.cassandra.auth.SpiffeCertificateValidator
+ * trusted_peer_identities: "spiffe1,spiffe2"
+ * node_identity: "spiffe1"
+ */
+public class MutualTlsInternodeAuthenticator implements IInternodeAuthenticator
+{
+ private static final String VALIDATOR_CLASS_NAME = "validator_class_name";
+ private static final String TRUSTED_PEER_IDENTITIES =
"trusted_peer_identities";
+ private static final String NODE_IDENTITY = "node_identity";
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+ private final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger,
30L, TimeUnit.SECONDS);
+ private final MutualTlsCertificateValidator certificateValidator;
+ private final List<String> trustedIdentities;
+
+ public MutualTlsInternodeAuthenticator(Map<String, String> parameters)
+ {
+ String certificateValidatorClassName =
parameters.get(VALIDATOR_CLASS_NAME);
+ if (StringUtils.isEmpty(certificateValidatorClassName))
+ {
+ String message =
"internode_authenticator.parameters.validator_class_name is not set";
+ logger.error(message);
+ throw new ConfigurationException(message);
+ }
+
+ certificateValidator = ParameterizedClass.newInstance(new
ParameterizedClass(certificateValidatorClassName),
+
Arrays.asList("", AuthConfig.class.getPackage().getName()));
+ Config config = DatabaseDescriptor.getRawConfig();
+ checkInternodeMtlsConfigurationIsValid(config);
+
+ if (parameters.containsKey(TRUSTED_PEER_IDENTITIES))
+ {
+ // If trusted_peer_identities identities is configured in
cassandra.yaml trust only those identities
+ trustedIdentities =
Arrays.stream(parameters.get(TRUSTED_PEER_IDENTITIES).split(","))
+ .collect(Collectors.toList());
+ }
+ else
+ {
+ // Otherwise, trust the identities extracted from outbound
keystore which is the identity that the node uses
+ // for making outbound connections.
+ trustedIdentities =
getIdentitiesFromKeyStore(config.server_encryption_options.outbound_keystore,
+
config.server_encryption_options.outbound_keystore_password,
+
config.server_encryption_options.store_type);
+ // optionally, if node_identity is configured in the yaml,
validate the identity extracted from outbound
+ // keystore to avoid any configuration errors
+ if(parameters.containsKey(NODE_IDENTITY))
+ {
+ String nodeIdentity = parameters.get(NODE_IDENTITY);
+ if(!trustedIdentities.contains(nodeIdentity))
+ {
+ throw new ConfigurationException("Configured node identity
is not matching identity extracted" +
+ "from the keystore");
+ }
+
trustedIdentities.retainAll(Collections.singleton(nodeIdentity));
+ }
+ }
+
+ if (!trustedIdentities.isEmpty())
+ {
+ logger.info("Initializing internode authenticator with identities
{}", trustedIdentities);
+ }
+ else
+ {
+ String message = String.format("No identity was extracted from the
outbound keystore '%s'", config.server_encryption_options.outbound_keystore);
+ logger.info(message);
+ throw new ConfigurationException(message);
+ }
+ }
+
+ @Override
+ public boolean authenticate(InetAddress remoteAddress, int remotePort)
+ {
+ throw new UnsupportedOperationException("mTLS Authenticator only
supports certificate based authenticate method");
+ }
+
+ @Override
+ public boolean authenticate(InetAddress remoteAddress, int remotePort,
Certificate[] certificates, InternodeConnectionDirection connectionType)
+ {
+ return authenticateInternodeWithMtls(remoteAddress, remotePort,
certificates, connectionType);
+ }
+
+
+ @Override
+ public void validateConfiguration() throws ConfigurationException
+ {
+
+ }
+
+ protected boolean authenticateInternodeWithMtls(InetAddress remoteAddress,
int remotePort, Certificate[] certificates,
+
IInternodeAuthenticator.InternodeConnectionDirection connectionType)
+ {
+ if (connectionType ==
IInternodeAuthenticator.InternodeConnectionDirection.INBOUND)
+ {
+ String identity = certificateValidator.identity(certificates);
+ if (!certificateValidator.isValidCertificate(certificates))
+ {
+ noSpamLogger.error("Not a valid certificate from {}:{} with
identity '{}'", remoteAddress, remotePort, identity);
+ return false;
+ }
+
+ if(!trustedIdentities.contains(identity))
+ {
+ noSpamLogger.error("Unable to authenticate user {}", identity);
+ return false;
+ }
+ return true;
+ }
+ // Outbound connections don't need to be authenticated again in
certificate based connections. SSL handshake
+ // makes sure that we are talking to valid server by checking root
certificates of the server in the
+ // truststore of the client.
+ return true;
+ }
+
+ @VisibleForTesting
+ List<String> getIdentitiesFromKeyStore(final String outboundKeyStorePath,
+ final String
outboundKeyStorePassword,
+ final String storeType)
+ {
+ final List<String> allUsers = new ArrayList<>();
+ try (InputStream ksf =
Files.newInputStream(Paths.get(outboundKeyStorePath)))
+ {
+ final KeyStore ks = KeyStore.getInstance(storeType);
+ ks.load(ksf, outboundKeyStorePassword.toCharArray());
+ Enumeration<String> enumeration = ks.aliases();
+ while (enumeration.hasMoreElements())
+ {
+ String alias = enumeration.nextElement();
+ Certificate[] chain = ks.getCertificateChain(alias);
+ if (chain == null)
+ {
+ logger.warn("Full chain/private key is not present in the
keystore for certificate {}", alias);
+ continue;
+ }
+ try
+ {
+ allUsers.add(certificateValidator.identity(chain));
+ }
+ catch (AuthenticationException e)
+ {
+ // When identity cannot be extracted, this exception is
thrown
+ // Ignore it, since only few certificates might contain
identity
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ logger.error("Failed to get identities from outbound_keystore {}",
outboundKeyStorePath, e);
+ }
+ return allUsers;
+ }
+
+ private void checkInternodeMtlsConfigurationIsValid(Config config)
+ {
+ if (config.server_encryption_options.internode_encryption ==
EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.none
+ || !config.server_encryption_options.require_client_auth)
+ {
+ String msg = "MutualTlsInternodeAuthenticator requires
server_encryption_options.internode_encryption to be enabled" +
+ " & server_encryption_options.require_client_auth to
be true";
+ logger.error(msg);
+ throw new ConfigurationException(msg);
+ }
+ }
+}
diff --git
a/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java
b/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java
new file mode 100644
index 0000000000..eba0ea54c0
--- /dev/null
+++
b/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java
@@ -0,0 +1,53 @@
+/*
+ * 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.auth;
+
+import java.net.InetAddress;
+import java.security.cert.Certificate;
+import java.util.Map;
+
+/**
+ * This authenticator can be used in optional mTLS mode, If the client doesn't
make an mTLS connection
+ * this fallbacks to password authentication.
+ */
+public class MutualTlsWithPasswordFallbackAuthenticator extends
PasswordAuthenticator
+{
+ private final MutualTlsAuthenticator mutualTlsAuthenticator;
+ public MutualTlsWithPasswordFallbackAuthenticator(Map<String, String>
parameters)
+ {
+ mutualTlsAuthenticator = new MutualTlsAuthenticator(parameters);
+ }
+
+ @Override
+ public void setup()
+ {
+ super.setup();
+ mutualTlsAuthenticator.setup();
+ }
+
+ @Override
+ public SaslNegotiator newSaslNegotiator(InetAddress clientAddress,
Certificate[] certificates)
+ {
+ if (certificates == null || certificates.length == 0)
+ {
+ return newSaslNegotiator(clientAddress);
+ }
+ return mutualTlsAuthenticator.newSaslNegotiator(clientAddress,
certificates);
+ }
+}
diff --git a/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
b/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
index 0ce96d80cf..e3046f6fc3 100644
--- a/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
@@ -243,7 +243,8 @@ public class PasswordAuthenticator implements
IAuthenticator, AuthCache.BulkLoad
return (SelectStatement) QueryProcessor.getStatement(query,
ClientState.forInternalCalls());
}
- private class PlainTextSaslAuthenticator implements SaslNegotiator
+ @VisibleForTesting
+ class PlainTextSaslAuthenticator implements SaslNegotiator
{
private boolean complete = false;
private String username;
diff --git a/src/java/org/apache/cassandra/auth/SpiffeCertificateValidator.java
b/src/java/org/apache/cassandra/auth/SpiffeCertificateValidator.java
new file mode 100644
index 0000000000..9260ce38a4
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/SpiffeCertificateValidator.java
@@ -0,0 +1,94 @@
+/*
+ * 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.auth;
+
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.cassandra.exceptions.AuthenticationException;
+
+/**
+ * This class assumes that the identity of a certificate is SPIFFE which is a
URI that is present as part of the SAN
+ * of the client certificate. It has logic to extract identity (Spiffe) out of
a certificate & knows how to validate
+ * the client certificates.
+ * <p>
+ *
+ * <p>
+ * Example:
+ * internode_authenticator:
+ * class_name : org.apache.cassandra.auth.MutualTlsAuthenticator
+ * parameters :
+ * validator_class_name: org.apache.cassandra.auth.SpiffeCertificateValidator
+ * authenticator:
+ * class_name : org.apache.cassandra.auth.MutualTlsInternodeAuthenticator
+ * parameters :
+ * validator_class_name: org.apache.cassandra.auth.SpiffeCertificateValidator
+ */
+public class SpiffeCertificateValidator implements
MutualTlsCertificateValidator
+{
+ @Override
+ public boolean isValidCertificate(Certificate[] clientCertificateChain)
+ {
+ return true;
+ }
+
+ @Override
+ public String identity(Certificate[] clientCertificateChain) throws
AuthenticationException
+ {
+ // returns spiffe
+ try
+ {
+ return getSANSpiffe(clientCertificateChain);
+ }
+ catch (CertificateException e)
+ {
+ throw new AuthenticationException(e.getMessage(), e);
+ }
+ }
+
+ private static String getSANSpiffe(final Certificate[] clientCertificates)
throws CertificateException
+ {
+ int URI_TYPE = 6;
+ X509Certificate[] castedCerts = castCertsToX509(clientCertificates);
+ Collection<List<?>> subjectAltNames =
castedCerts[0].getSubjectAlternativeNames();
+
+ if (subjectAltNames != null)
+ {
+ for (List<?> item : subjectAltNames)
+ {
+ Integer type = (Integer) item.get(0);
+ String spiffe = (String) item.get(1);
+ if (type == URI_TYPE && spiffe.startsWith("spiffe://"))
+ { // Spiffe is a URI
+ return spiffe;
+ }
+ }
+ }
+ throw new CertificateException("Unable to extract Spiffe from the
certificate");
+ }
+
+ private static X509Certificate[] castCertsToX509(Certificate[]
clientCertificateChain)
+ {
+ return Arrays.asList(clientCertificateChain).toArray(new
X509Certificate[0]);
+ }
+}
diff --git a/src/java/org/apache/cassandra/config/Config.java
b/src/java/org/apache/cassandra/config/Config.java
index e254e54e05..5e904bbf3e 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -77,7 +77,7 @@ public class Config
public static final String PROPERTY_PREFIX = "cassandra.";
public String cluster_name = "Test Cluster";
- public String authenticator;
+ public ParameterizedClass authenticator;
public String authorizer;
public String role_manager;
public ParameterizedClass crypto_provider;
@@ -213,7 +213,7 @@ public class Config
public boolean listen_interface_prefer_ipv6 = false;
public String broadcast_address;
public boolean listen_on_broadcast_address = false;
- public String internode_authenticator;
+ public ParameterizedClass internode_authenticator;
public boolean traverse_auth_from_root = false;
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index 558f4a349b..88c0ab9e7d 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -420,7 +420,8 @@ public class DatabaseDescriptor
}
}
- private static void setConfig(Config config)
+ @VisibleForTesting
+ public static void setConfig(Config config)
{
conf = config;
}
diff --git a/src/java/org/apache/cassandra/config/ParameterizedClass.java
b/src/java/org/apache/cassandra/config/ParameterizedClass.java
index 9b001786e3..2d0390ef44 100644
--- a/src/java/org/apache/cassandra/config/ParameterizedClass.java
+++ b/src/java/org/apache/cassandra/config/ParameterizedClass.java
@@ -17,11 +17,14 @@
*/
package org.apache.cassandra.config;
+import java.lang.reflect.Constructor;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.base.Objects;
+import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.utils.Shared;
import static org.apache.cassandra.utils.Shared.Scope.SIMULATION;
@@ -40,6 +43,12 @@ public class ParameterizedClass
// for snakeyaml
}
+ public ParameterizedClass(String class_name)
+ {
+ this.class_name = class_name;
+ this.parameters = Collections.emptyMap();
+ }
+
public ParameterizedClass(String class_name, Map<String, String>
parameters)
{
this.class_name = class_name;
@@ -53,6 +62,46 @@ public class ParameterizedClass
p.containsKey(PARAMETERS) ? (Map<String,
String>)((List<?>)p.get(PARAMETERS)).get(0) : null);
}
+ static public <K> K newInstance(ParameterizedClass parameterizedClass,
List<String> searchPackages)
+ {
+ Exception last = null;
+ if (searchPackages == null || searchPackages.isEmpty())
+ searchPackages = Collections.singletonList("");
+ for (String searchPackage : searchPackages)
+ {
+ try
+ {
+ if (!searchPackage.isEmpty() && !searchPackage.endsWith("."))
+ searchPackage = searchPackage + '.';
+ String name = searchPackage + parameterizedClass.class_name;
+ Class<?> providerClass = Class.forName(name);
+ try
+ {
+ Constructor<?> constructor =
providerClass.getConstructor(Map.class);
+ K instance = (K)
constructor.newInstance(parameterizedClass.parameters);
+ return instance;
+ }
+ catch (Exception constructorEx)
+ {
+ //no-op
+ }
+ // fallback to no arg constructor if no params present
+ if (parameterizedClass.parameters == null ||
parameterizedClass.parameters.isEmpty())
+ {
+ Constructor<?> constructor =
providerClass.getConstructor();
+ K instance = (K) constructor.newInstance();
+ return instance;
+ }
+ }
+ // there are about 5 checked exceptions that could be thrown here.
+ catch (Exception e)
+ {
+ last = e;
+ }
+ }
+ throw new ConfigurationException("Unable to create parameterized class
" + parameterizedClass.class_name, last);
+ }
+
@Override
public boolean equals(Object that)
{
diff --git
a/src/java/org/apache/cassandra/cql3/statements/AddIdentityStatement.java
b/src/java/org/apache/cassandra/cql3/statements/AddIdentityStatement.java
new file mode 100644
index 0000000000..b11801d4ea
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/AddIdentityStatement.java
@@ -0,0 +1,86 @@
+/*
+ * 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.cql3.statements;
+
+import org.apache.cassandra.audit.AuditLogContext;
+import org.apache.cassandra.audit.AuditLogEntryType;
+import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.auth.RoleResource;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.exceptions.RequestValidationException;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.transport.messages.ResultMessage;
+
+/**
+ * Cqlsh statement to add identity into roles_to_identity table for storing
authorized identities for mTLS connections.
+ * Performs some checks before adding the identity to roles table.
+ *
+ * EX: ADD IDENTITY 'testIdentity' TO ROLE 'testRole'
+ */
+public class AddIdentityStatement extends AuthenticationStatement
+{
+ final String identity;
+ final String role;
+ final boolean ifNotExists;
+
+ public AddIdentityStatement(String identity, String role, boolean
ifNotExists)
+ {
+ this.role = role;
+ this.identity = identity;
+ this.ifNotExists = ifNotExists;
+ }
+
+ @Override
+ public void authorize(ClientState state)
+ {
+ checkPermission(state, Permission.CREATE,
state.getUser().getPrimaryRole());
+ }
+
+ @Override
+ public void validate(ClientState state)
+ {
+ state.ensureNotAnonymous();
+
+ if
(!DatabaseDescriptor.getRoleManager().isExistingRole(RoleResource.role(role)))
+ {
+ throw new InvalidRequestException(String.format("Can not add
identity for non-existent role '%s'", role));
+ }
+
+ if (!ifNotExists &&
DatabaseDescriptor.getRoleManager().isExistingIdentity(identity))
+ throw new InvalidRequestException(String.format("%s already
exists", identity));
+ }
+
+ @Override
+ public AuditLogContext getAuditLogContext()
+ {
+ return new AuditLogContext(AuditLogEntryType.CREATE_IDENTITY);
+ }
+
+ @Override
+ public ResultMessage execute(ClientState state) throws
RequestExecutionException, RequestValidationException
+ {
+ if(!ifNotExists ||
!DatabaseDescriptor.getRoleManager().isExistingIdentity(identity))
+ {
+ DatabaseDescriptor.getRoleManager().addIdentity(identity, role);
+ }
+ return null;
+ }
+}
diff --git
a/src/java/org/apache/cassandra/cql3/statements/DropIdentityStatement.java
b/src/java/org/apache/cassandra/cql3/statements/DropIdentityStatement.java
new file mode 100644
index 0000000000..732f28a489
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/statements/DropIdentityStatement.java
@@ -0,0 +1,79 @@
+/*
+ * 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.cql3.statements;
+
+import org.apache.cassandra.audit.AuditLogContext;
+import org.apache.cassandra.audit.AuditLogEntryType;
+import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.exceptions.RequestValidationException;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.transport.messages.ResultMessage;
+
+/**
+ * Cqlsh statement to remove identity from identity_to_roles table.
+ * Ex: DROP IDENTITY 'testIdentity'
+ */
+public class DropIdentityStatement extends AuthenticationStatement
+{
+ final String identity;
+ final boolean ifExists;
+
+ public DropIdentityStatement(String identity, boolean ifExists)
+ {
+ this.identity = identity;
+ this.ifExists = ifExists;
+ }
+
+ @Override
+ public void authorize(ClientState state)
+ {
+ checkPermission(state, Permission.DROP,
state.getUser().getPrimaryRole());
+ }
+
+ @Override
+ public void validate(ClientState state)
+ {
+ state.ensureNotAnonymous();
+
+ if (!ifExists &&
!DatabaseDescriptor.getRoleManager().isExistingIdentity(identity))
+ {
+ throw new InvalidRequestException(String.format("identity '%s'
doesn't exist", identity));
+ }
+ }
+
+ @Override
+ public AuditLogContext getAuditLogContext()
+ {
+ return new AuditLogContext(AuditLogEntryType.DROP_IDENTITY);
+ }
+
+ @Override
+ public ResultMessage execute(ClientState state) throws
RequestExecutionException, RequestValidationException
+ {
+ // not rejected in validate()
+ if(!ifExists ||
DatabaseDescriptor.getRoleManager().isExistingIdentity(identity))
+ {
+ DatabaseDescriptor.getRoleManager().dropIdentity(identity);
+ }
+ return null;
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/StorageService.java
b/src/java/org/apache/cassandra/service/StorageService.java
index d4ada4e2a2..c3c5fbba09 100644
--- a/src/java/org/apache/cassandra/service/StorageService.java
+++ b/src/java/org/apache/cassandra/service/StorageService.java
@@ -1092,6 +1092,7 @@ public class StorageService extends
NotificationBroadcasterSupport implements IE
if (DatabaseDescriptor.getReplaceTokens().size() > 0 ||
DatabaseDescriptor.getReplaceNode() != null)
throw new RuntimeException("Replace method removed; use " +
REPLACE_ADDRESS.getKey() + " system property instead.");
+ DatabaseDescriptor.getInternodeAuthenticator().setupInternode();
MessagingService.instance().listen();
UUID localHostId = SystemKeyspace.getOrInitializeLocalHostId();
@@ -1371,7 +1372,6 @@ public class StorageService extends
NotificationBroadcasterSupport implements IE
DatabaseDescriptor.getRoleManager().setup();
DatabaseDescriptor.getAuthenticator().setup();
- DatabaseDescriptor.getInternodeAuthenticator().setupInternode();
DatabaseDescriptor.getAuthorizer().setup();
DatabaseDescriptor.getNetworkAuthorizer().setup();
AuthCacheService.initializeAndRegisterCaches();
diff --git a/test/conf/cassandra-mtls-backward-compatibility.yaml
b/test/conf/cassandra-mtls-backward-compatibility.yaml
new file mode 100644
index 0000000000..fe1b8ea326
--- /dev/null
+++ b/test/conf/cassandra-mtls-backward-compatibility.yaml
@@ -0,0 +1,76 @@
+#
+# 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.
+#
+
+#
+# This file is used for testing mTLS authenticators
+#
+cluster_name: Test Cluster
+memtable_allocation_type: offheap_objects
+commitlog_sync: batch
+commitlog_sync_batch_window_in_ms: 1.0
+commitlog_segment_size: 5MiB
+commitlog_directory: build/test/cassandra/commitlog
+cdc_raw_directory: build/test/cassandra/cdc_raw
+cdc_enabled: false
+hints_directory: build/test/cassandra/hints
+partitioner: org.apache.cassandra.dht.ByteOrderedPartitioner
+listen_address: 127.0.0.1
+storage_port: 7012
+ssl_storage_port: 17012
+start_native_transport: true
+native_transport_port: 9042
+column_index_size: 4KiB
+saved_caches_directory: build/test/cassandra/saved_caches
+data_file_directories:
+ - build/test/cassandra/data
+disk_access_mode: mmap
+seed_provider:
+ - class_name: org.apache.cassandra.locator.SimpleSeedProvider
+ parameters:
+ - seeds: "127.0.0.1:7012"
+endpoint_snitch: org.apache.cassandra.locator.SimpleSnitch
+dynamic_snitch: true
+incremental_backups: true
+concurrent_compactors: 4
+compaction_throughput: 0MiB/s
+row_cache_class_name: org.apache.cassandra.cache.OHCProvider
+row_cache_size: 16MiB
+user_defined_functions_enabled: true
+scripted_user_defined_functions_enabled: true
+prepared_statements_cache_size: 1MiB
+corrupted_tombstone_strategy: exception
+stream_entire_sstables: true
+stream_throughput_outbound: 23841858MiB/s
+sasi_indexes_enabled: true
+materialized_views_enabled: true
+drop_compact_storage_enabled: true
+file_cache_enabled: true
+auto_hints_cleanup_enabled: true
+default_keyspace_rf: 1
+
+server_encryption_options:
+ internode_encryption: none
+ keystore: test/conf/cassandra_ssl_test.keystore
+ keystore_password: cassandra
+ outbound_keystore: test/conf/cassandra_ssl_test_outbound.keystore
+ outbound_keystore_password: cassandra
+ truststore: test/conf/cassandra_ssl_test.truststore
+ truststore_password: cassandra
+ require_client_auth: true
+internode_authenticator:
org.apache.cassandra.auth.AllowAllInternodeAuthenticator
+authenticator: org.apache.cassandra.auth.AllowAllAuthenticator
diff --git a/test/conf/cassandra-mtls.yaml b/test/conf/cassandra-mtls.yaml
new file mode 100644
index 0000000000..924f55cedf
--- /dev/null
+++ b/test/conf/cassandra-mtls.yaml
@@ -0,0 +1,89 @@
+#
+# 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.
+#
+
+#
+# This file is used for testing mTLS authenticators
+#
+cluster_name: Test Cluster
+memtable_allocation_type: offheap_objects
+commitlog_sync: batch
+commitlog_sync_batch_window_in_ms: 1.0
+commitlog_segment_size: 5MiB
+commitlog_directory: build/test/cassandra/commitlog
+cdc_raw_directory: build/test/cassandra/cdc_raw
+cdc_enabled: false
+hints_directory: build/test/cassandra/hints
+partitioner: org.apache.cassandra.dht.ByteOrderedPartitioner
+listen_address: 127.0.0.1
+storage_port: 7012
+ssl_storage_port: 17012
+start_native_transport: true
+native_transport_port: 9042
+column_index_size: 4KiB
+saved_caches_directory: build/test/cassandra/saved_caches
+data_file_directories:
+ - build/test/cassandra/data
+disk_access_mode: mmap
+seed_provider:
+ - class_name: org.apache.cassandra.locator.SimpleSeedProvider
+ parameters:
+ - seeds: "127.0.0.1:7012"
+endpoint_snitch: org.apache.cassandra.locator.SimpleSnitch
+dynamic_snitch: true
+incremental_backups: true
+concurrent_compactors: 4
+compaction_throughput: 0MiB/s
+row_cache_class_name: org.apache.cassandra.cache.OHCProvider
+row_cache_size: 16MiB
+prepared_statements_cache_size: 1MiB
+corrupted_tombstone_strategy: exception
+stream_entire_sstables: true
+stream_throughput_outbound: 23841858MiB/s
+sasi_indexes_enabled: true
+materialized_views_enabled: true
+drop_compact_storage_enabled: true
+file_cache_enabled: true
+auto_hints_cleanup_enabled: true
+default_keyspace_rf: 1
+
+client_encryption_options:
+ enabled: true
+ require_client_auth: true
+ keystore: test/conf/cassandra_ssl_test.keystore
+ keystore_password: cassandra
+ truststore: test/conf/cassandra_ssl_test.truststore
+ truststore_password: cassandra
+
+server_encryption_options:
+ internode_encryption: all
+ enabled: true
+ keystore: test/conf/cassandra_ssl_test.keystore
+ keystore_password: cassandra
+ outbound_keystore: test/conf/cassandra_ssl_test_outbound.keystore
+ outbound_keystore_password: cassandra
+ truststore: test/conf/cassandra_ssl_test.truststore
+ truststore_password: cassandra
+ require_client_auth: true
+internode_authenticator:
+ class_name : org.apache.cassandra.auth.MutualTlsInternodeAuthenticator
+ parameters :
+ validator_class_name: org.apache.cassandra.auth.SpiffeCertificateValidator
+authenticator:
+ class_name : org.apache.cassandra.auth.MutualTlsAuthenticator
+ parameters :
+ validator_class_name: org.apache.cassandra.auth.SpiffeCertificateValidator
diff --git a/test/conf/cassandra_ssl_test.truststore
b/test/conf/cassandra_ssl_test.truststore
index ab01af30cd..10abf12f53 100644
Binary files a/test/conf/cassandra_ssl_test.truststore and
b/test/conf/cassandra_ssl_test.truststore differ
diff --git a/test/conf/cassandra_ssl_test_outbound.keystore
b/test/conf/cassandra_ssl_test_outbound.keystore
index 7dbf466e5a..fc5d117b04 100644
Binary files a/test/conf/cassandra_ssl_test_outbound.keystore and
b/test/conf/cassandra_ssl_test_outbound.keystore differ
diff --git a/test/resources/auth/SampleInvalidCertificate.pem
b/test/resources/auth/SampleInvalidCertificate.pem
new file mode 100644
index 0000000000..dce4a10de4
--- /dev/null
+++ b/test/resources/auth/SampleInvalidCertificate.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDejCCAmICCQCHse88ehxeVjANBgkqhkiG9w0BAQsFADB/MQswCQYDVQQGEwJ1
+czELMAkGA1UECAwCY2ExEjAQBgNVBAcMCXN1bm55dmFsZTEOMAwGA1UECgwFYXBw
+bGUxDDAKBgNVBAsMA3BpZTESMBAGA1UEAwwJbG9jYWxob3N0MR0wGwYJKoZIhvcN
+AQkBFg50ZXN0QGFwcGxlLmNvbTAeFw0xOTAzMjEyMTI3NDNaFw0yMDAzMjAyMTI3
+NDNaMH8xCzAJBgNVBAYTAnVzMQswCQYDVQQIDAJjYTESMBAGA1UEBwwJc3Vubnl2
+YWxlMQ4wDAYDVQQKDAVhcHBsZTEMMAoGA1UECwwDcGllMRIwEAYDVQQDDAlsb2Nh
+bGhvc3QxHTAbBgkqhkiG9w0BCQEWDnRlc3RAYXBwbGUuY29tMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxOYvntrvB41GiOLP0U4vv/dRdotnX3bgXDG4
+na5SW4X7x8BbT6Kg2FVlgsEMk/26id/isAXuo8uWmyAwL0r522No5pXBlc6QzQr4
+VR8xAnLkBzuIH4R0eVOGOpf/k4IKem1dQjRap5SCKcMm8oVwGTwiDZNNCSb8uBSX
+opnINMa2GjWGoe1WLGBVaHm2gHMpcoyXIOgp1bIu6UohII713DXw1A3OfICa0JIE
+xYhfRr9wTNreIWc6lLEQkdFe7qTo/nn1bGJkkV/pcxw3GwB3arh+B2JtD3y+Tvw1
+EyLThaCkr5DoH7M4Rt1/plyJZ4sdEO47bpHhqVrkO7oKj0eY7QIDAQABMA0GCSqG
+SIb3DQEBCwUAA4IBAQCcdisPpyWK4YN/QEs5ao3o3cNO7XjBzndghsQqqPtZYh6t
+MHHEk9uR4lw6VChKE4KFquR3ukHTsR913i+EjdTXpstj1HenzlLsPG3D7C4CQRpT
+D7s9lgpF2W40y7ZlX07yRzZPJFZX4aApEQvALTIwloP7+EZVAkRDOxIX7CogJU0/
+ngiF5Scaxr61G4cuNLhxbJaw7OdYp/a/6Qh2hkHmrCSs8MfOozCN79HeAXjit/vZ
+eBMxx+8nPPjP7Um0GO2ESkWYvjlM5P/WjaySHbzwpUkVvBWKmY/lfgyKcR9pwFQy
+pwaGg9+XcoVrpW5RHjp/ue6xIxqB4T0RoY9kDDIm
+-----END CERTIFICATE-----
diff --git a/test/resources/auth/SampleMtlsClientCertificate.pem
b/test/resources/auth/SampleMtlsClientCertificate.pem
new file mode 100644
index 0000000000..ae02ba5929
--- /dev/null
+++ b/test/resources/auth/SampleMtlsClientCertificate.pem
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIFDDCCAvSgAwIBAgIJAI/Ret4ZZJ5sMA0GCSqGSIb3DQEBCwUAMCUxIzAhBgNV
+BAMMGm10bHMtYXV0aGVudGljYXRvci10ZXN0aW5nMB4XDTIzMDEwMzIyNTU1OVoX
+DTMyMTIzMTIyNTU1OVowJTEjMCEGA1UEAwwabXRscy1hdXRoZW50aWNhdG9yLXRl
+c3RpbmcwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzhMGvT9Sn2SXQ
+fSbacWj2E20RoZhnrBdzOX+CHX1Jx4juNjGIfJPPCwPJ4I2RvSou7Bw+QiwnWmy9
+8gkU1FK928VzzH5Gmw99LN8iqNCUdltylLMkr2dN2kgX0K2DC04ETK1Vosd1X6+a
+6QescH10I3zzEuw9KQg/CzvzqjeBgdWyXw3e8MPctMcM6S5TPiI/BqGEJNz3a0DV
+dFkXQL0y7ya7gqkRk+m4sK90NNcJk67b3Vbo8+FWmigOLoGYXP4HyoIHZVicpEp/
+a9YmcAQ6azabQdBzFWxGkAkijwtnVLYQtGMqiKZkbEstbzaIlCL+F3t3P+S64A+i
+dmo60pXkYaB0JQ8Kg8gI5yNy8IvKqUuzVJlGf5qCcoAV5udROi5GRLsQd904sWwq
+jQWSVR4HTy94Nrf0yUemACtwe+5XLSmW2rQaRTWVMh/vpd2EhMmjZ72FVBCk6pzk
+ZIKQJO1A6baU/At+8BV/LWopGxcIUn3Lzffx+xLVevNr6LuunKGQkt9XASH7pYV5
+KBt9gR6fAmp68n+Y7EE/2+9Rp7X6CNUec+4z+xGSZqfGGNB/KjpRyppN64oMf2xt
+gRzihN71ZQd/+Sc9nYqVcN5ps6SrnrKKoSIzZCuJQv8hQd4z4K8vlWj1cq7kLNXL
+JMhBLQSpUyHYtRInSJlgFpq+XplsQQIDAQABoz8wPTA7BgNVHREENDAyhjBzcGlm
+ZmU6Ly90ZXN0ZG9tYWluLmNvbS90ZXN0SWRlbnRpZmllci90ZXN0VmFsdWUwDQYJ
+KoZIhvcNAQELBQADggIBAKA+PEOS2ZH/tu5x4q+v28uFxo472itfgOlVWZrpQX6g
+B4HbPRv6lBZW/ko+jlJspIPiDmYTRFZQ4WKG0+sW0yW2k+sUQ1Ee+Aql6M0Gqi6P
+OdOmTai5+OfpcLLg0ANNohf9BKeavpqaTauNG1adnaIHKxcZjBJ305eOZvr2sgKz
+LtsqmnO54ADbBFsS+RIR4hacLs/P8gWr1j3Tujv37vddE3l76urGEIHx8x9gJydG
+++/mcrq6Oom6Npph5vWRLCU15WDWnIvttKcBNW7LcvaiaXQxX76gZvIkzLZr3w5z
+SZE1VI+rXZkluvqHIGSNxQGAcLwkDOimymFDUONZu7GMof6yi9zEHVR8kNOmb8Xe
+Nwr7DkfAuDvTzhau+/yNvcYW4LW/5aerw+1hQoD15HWAtjylbzXAAvsPX3VkfLJS
+p6iOMuZJ+p1veys1bMteVu95s9bD/aMicSV79n3QF7zfnsR1SowR3txPsgLOwxdP
+F/JfpUXG7OCzFM1WxUrUQR+VQ6YX4yM8m+EttXF5gXOzzBzb0LBrVw20VIlrvZXB
+vx4LWo9A5J2Gp/0IhwCrAU68Wjok9gP5HQ9/pSbFXDiffY+O2RflqdWJIfmdos4b
+JlxAXwXN/6yQp/Rj9Jz+rn2I8TmVMw6tFS5psh9nvXA7KLoFYoBBOsbL6P7HtEK0
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/test/resources/auth/SampleUnauthorizedMtlsClientCertificate.pem
b/test/resources/auth/SampleUnauthorizedMtlsClientCertificate.pem
new file mode 100644
index 0000000000..34f682ad4c
--- /dev/null
+++ b/test/resources/auth/SampleUnauthorizedMtlsClientCertificate.pem
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIFCTCCAvGgAwIBAgIJAKp41MPbHc4yMA0GCSqGSIb3DQEBCwUAMCAxHjAcBgNV
+BAMMFWF1dGhlbnRpY2F0b3ItdGVzdGluZzAeFw0yMzAyMDcyMTMwMzZaFw0zMzAy
+MDQyMTMwMzZaMCAxHjAcBgNVBAMMFWF1dGhlbnRpY2F0b3ItdGVzdGluZzCCAiIw
+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMYQ1POS39c4RyOFH9fjRGjlHV5i
+5eit5+HY5Y2ID4mSwY1EBEcWvZvJSeS0B1M/tj3DNNcVsYm/Cf1EnIKxzdonIN97
+LZFMf3IE1getxopPDryGfjFHBUVW+wGWS0w5214PAAPWRbYzzH7fiXkPnk1QTYYs
+EShHhV5+lTVETJb4PSqyc83jwDJYfiSmXPVSN7NH/gLs6CPqhjPwO520BXVyzF3D
+lRDkhYjzk8NMecdW3/Ztw9FJxS1EITyiAwcJcoCu+u0tX9XWQeUw9MAm2wPPp/iU
+BUCGQ86LY3szTmEcEG3EOFj0j+UrNJs01vGQf21o7JO+lDWhbuxncv7+zIxQqRA4
+1yaa7HxDNrSl/WmJvhAKKkuRtwuZhWb+IP0LEjsAVB+X5dGraVeYbpLGZJec2Xis
+X7v6UGua/L3LswjsovQIk8ou2j0LExEn+jOk+O8JLHzxrjBafSLS9D9g5akC4qor
+LopCMEfVEAcvscw7sadrUeyxVAVZE69Mp6Qn0PoV6IVMr36uzZhTMK123qS8ynho
+FzgA6IlrmM71UvlVPx+rpApKypMoPYVsU8nPMqVqwxQC1hR/TrWosL7nG/fUSrxe
+QCK9dLJhJ0v8CP2PwXGyDctdQj2BWxaTKmuvXFv1MjbxBNOZPcW4STuH8/RA3tmA
+tNfenfbuiDyC0lcvAgMBAAGjRjBEMEIGA1UdEQQ7MDmGN3NwaWZmZTovL3Rlc3Rk
+b21haW4uY29tL3Rlc3RJZGVudGlmaWVyL3Rlc3RVbmF1dGhvcml6ZWQwDQYJKoZI
+hvcNAQELBQADggIBAELimG+Aganv39Q2Rvc2rJKxhZQQak+G4qf1Ql7tzWdqIiBs
+4AUHI1Kc75SoIWKXtkDBdL28OdMqyEWoTJ4huJ+jJiBMYphaguQdXx0ZY/kmdiJb
+2zl6WFQMsZdfDRfMYypGNs/ieN65u4cnQKJxwME0OKdhflNuuydXxAAgUrygXSGN
+7J3GOhcPPAMiaxj+/aOeL/+HPt0tQKGrGC1W6zjrJznv1hD4pHo6Ch9GuTh1cWSc
+b6P4rSDb+NIshq6R+pB3FjcqT/lkewVAYlSTHI+yXt8sfLFqDJKUe0BlgNA0zTSA
+LuJNHzxtruIpMiRNy+SSxWoNc/R86eMnOxMBIp61wtonzlIhyb9VHy9kvpO9v0ru
+ijt2skEIAcNznxwX5nxNvG+byVytdNXpVZ1qSsC3QlS4cYhzMhpPG4X7S/O9cf03
+Sst3WKF7ZOKpmef5IxetlU+vbwfq86CzTzXP5wmXCK1y7Pys+jxlU6bzYkU1sKb2
+GPnkTU2gNncRmkatwtk+BhgVp6glDcjdN6sUY/SSAlLc5XiFz9rhvZa4+P8DjjoJ
+wJpzZWAqDShs0sJ6VvUFngw0sw/U2OmJcLHpoTTH/Iw8Qa+9p7rlRxvud8RIk7Mm
+0YXU84vP/QmCKhHfkbhoDRUTwCxRvP7PUj10Jtrrjj+X5p+QFyrVuBCojc8k
+-----END CERTIFICATE-----
diff --git a/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
b/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
index 0ced5a10e7..50d20ea883 100644
--- a/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
+++ b/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
@@ -69,7 +69,7 @@ public class AuditLoggerAuthTest
public static void setup() throws Exception
{
OverrideConfigurationLoader.override((config) -> {
- config.authenticator = "PasswordAuthenticator";
+ config.authenticator = new
ParameterizedClass("PasswordAuthenticator");
config.role_manager = "CassandraRoleManager";
config.authorizer = "CassandraAuthorizer";
config.audit_logging_options.enabled = true;
diff --git a/test/unit/org/apache/cassandra/auth/AuthConfigTest.java
b/test/unit/org/apache/cassandra/auth/AuthConfigTest.java
new file mode 100644
index 0000000000..b9bde913be
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/AuthConfigTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.auth;
+
+import java.io.IOException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.utils.MBeanWrapper;
+
+import static org.apache.cassandra.auth.AuthTestUtils.loadCertificateChain;
+import static
org.apache.cassandra.auth.IInternodeAuthenticator.InternodeConnectionDirection.INBOUND;
+import static org.apache.cassandra.config.YamlConfigurationLoaderTest.load;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class AuthConfigTest
+{
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void testNewInstanceForMutualTlsInternodeAuthenticator() throws
IOException, CertificateException
+ {
+ Config config = load("cassandra-mtls.yaml");
+ config.internode_authenticator.class_name =
"org.apache.cassandra.auth.MutualTlsInternodeAuthenticator";
+ config.internode_authenticator.parameters =
Collections.singletonMap("validator_class_name",
"org.apache.cassandra.auth.SpiffeCertificateValidator");
+ config.server_encryption_options =
config.server_encryption_options.withOutboundKeystore("test/conf/cassandra_ssl_test_outbound.keystore")
+
.withOutboundKeystorePassword("cassandra");
+ DatabaseDescriptor.setConfig(config);
+ MutualTlsInternodeAuthenticator authenticator =
ParameterizedClass.newInstance(config.internode_authenticator,
+
Arrays.asList("", "org.apache.cassandra.auth."));
+
+ InetAddressAndPort address = InetAddressAndPort.getByName("127.0.0.1");
+
+ Certificate[] authorizedCertificates =
loadCertificateChain("auth/SampleMtlsClientCertificate.pem");
+ assertTrue(authenticator.authenticate(address.getAddress(),
address.getPort(), authorizedCertificates, INBOUND));
+
+ Certificate[] unauthorizedCertificates =
loadCertificateChain("auth/SampleUnauthorizedMtlsClientCertificate.pem");
+ assertFalse(authenticator.authenticate(address.getAddress(),
address.getPort(), unauthorizedCertificates, INBOUND));
+ }
+
+ @Test
+ public void testNewInstanceForMutualTlsWithPasswordFallbackAuthenticator()
+ {
+ Config config = load("cassandra-mtls.yaml");
+ config.client_encryption_options.applyConfig();
+ config.authenticator.class_name =
"org.apache.cassandra.auth.MutualTlsWithPasswordFallbackAuthenticator";
+ config.authenticator.parameters =
Collections.singletonMap("validator_class_name",
"org.apache.cassandra.auth.SpiffeCertificateValidator");
+ DatabaseDescriptor.setConfig(config);
+ MutualTlsWithPasswordFallbackAuthenticator authenticator =
ParameterizedClass.newInstance(config.authenticator,
+
Arrays.asList("", "org.apache.cassandra.auth."));
+ assertNotNull(authenticator);
+ unregisterIdentitesCache();
+ }
+
+ @Test
+ public void testNewInstanceForMutualTlsAuthenticator() throws IOException,
CertificateException
+ {
+ Config config = load("cassandra-mtls.yaml");
+ config.client_encryption_options.applyConfig();
+ DatabaseDescriptor.setConfig(config);
+ MutualTlsAuthenticator authenticator =
ParameterizedClass.newInstance(config.authenticator,
+
Arrays.asList("", "org.apache.cassandra.auth."));
+ assertNotNull(authenticator);
+ unregisterIdentitesCache();
+ }
+
+ private void unregisterIdentitesCache()
+ {
+
MBeanWrapper.instance.unregisterMBean("org.apache.cassandra.auth:type=IdentitiesCache");
+ }
+}
diff --git a/test/unit/org/apache/cassandra/auth/AuthTestUtils.java
b/test/unit/org/apache/cassandra/auth/AuthTestUtils.java
index c78520e06f..e3ef505be9 100644
--- a/test/unit/org/apache/cassandra/auth/AuthTestUtils.java
+++ b/test/unit/org/apache/cassandra/auth/AuthTestUtils.java
@@ -18,7 +18,18 @@
package org.apache.cassandra.auth;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
import java.util.concurrent.Callable;
+import java.util.concurrent.TimeoutException;
import org.apache.cassandra.auth.jmx.AuthorizationProxy;
import org.apache.cassandra.cql3.QueryOptions;
@@ -32,8 +43,11 @@ import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.service.QueryState;
+import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.transport.messages.ResultMessage;
+import static org.junit.Assert.assertNotNull;
+
public class AuthTestUtils
{
@@ -70,6 +84,12 @@ public class AuthTestUtils
return QueryProcessor.executeInternal(query);
}
+ @Override
+ UntypedResultSet process(String query, ConsistencyLevel
consistencyLevel, ByteBuffer... values)
+ {
+ return QueryProcessor.executeInternal(query, (Object[]) values);
+ }
+
@Override
protected void scheduleSetupTask(final Callable<Void> setupTask)
{
@@ -171,4 +191,30 @@ public class AuthTestUtils
roleOptions.setOption(IRoleManager.Option.PASSWORD, "ignored");
return roleOptions;
}
+
+ // mTLS authenticators related utility methods
+ public static InetAddress getMockInetAddress() throws UnknownHostException
+ {
+ return InetAddress.getByName("127.0.0.1");
+ }
+
+ public static Certificate[] loadCertificateChain(final String path) throws
CertificateException
+ {
+ InputStream inputStream =
MutualTlsAuthenticator.class.getClassLoader().getResourceAsStream(path);
+ assertNotNull(inputStream);
+ Collection<? extends Certificate> c =
CertificateFactory.getInstance("X.509").generateCertificates(inputStream);
+ X509Certificate[] certs = new X509Certificate[c.size()];
+ for (int i = 0; i < certs.length; i++)
+ {
+ certs[i] = (X509Certificate) c.toArray()[i];
+ }
+ return certs;
+ }
+
+ public static void initializeIdentityRolesTable(final String identity)
throws IOException, TimeoutException
+ {
+ StorageService.instance.truncate(SchemaConstants.AUTH_KEYSPACE_NAME,
AuthKeyspace.IDENTITY_TO_ROLES);
+ String insertQuery = "Insert into %s.%s (identity, role) values ('%s',
'readonly_user');";
+ QueryProcessor.process(String.format(insertQuery,
SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.IDENTITY_TO_ROLES, identity),
ConsistencyLevel.ONE);
+ }
}
diff --git
a/test/unit/org/apache/cassandra/auth/MutualTlsAuthenticatorTest.java
b/test/unit/org/apache/cassandra/auth/MutualTlsAuthenticatorTest.java
new file mode 100644
index 0000000000..0dc7984f26
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/MutualTlsAuthenticatorTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.auth;
+
+import java.io.IOException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.TimeoutException;
+
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.exceptions.AuthenticationException;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.schema.SchemaConstants;
+import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.utils.MBeanWrapper;
+
+import static org.apache.cassandra.auth.AuthTestUtils.getMockInetAddress;
+import static
org.apache.cassandra.auth.AuthTestUtils.initializeIdentityRolesTable;
+import static org.apache.cassandra.auth.AuthTestUtils.loadCertificateChain;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+
+@RunWith(Parameterized.class)
+public class MutualTlsAuthenticatorTest
+{
+ @Parameterized.Parameter(0)
+ public String certificatePath;
+ @Parameterized.Parameter(1)
+ public String identity;
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Parameterized.Parameters()
+ public static Collection<Object[]> versions()
+ {
+ return Collections.singletonList(new Object[]{
"auth/SampleMtlsClientCertificate.pem",
"spiffe://testdomain.com/testIdentifier/testValue" });
+ }
+
+ @BeforeClass
+ public static void setup()
+ {
+ SchemaLoader.loadSchema();
+ DatabaseDescriptor.daemonInitialization();
+ StorageService.instance.initServer(0);
+
((CassandraRoleManager)DatabaseDescriptor.getRoleManager()).loadIdentityStatement();
+ final Config config = DatabaseDescriptor.getRawConfig();
+ config.client_encryption_options =
config.client_encryption_options.withEnabled(true)
+
.withRequireClientAuth(true);
+ }
+
+ @After
+ public void after() throws IOException, TimeoutException
+ {
+
MBeanWrapper.instance.unregisterMBean("org.apache.cassandra.auth:type=IdentitiesCache");
+ StorageService.instance.truncate(SchemaConstants.AUTH_KEYSPACE_NAME,
AuthKeyspace.IDENTITY_TO_ROLES);
+ }
+
+ String getValidatorClass()
+ {
+ return "org.apache.cassandra.auth.SpiffeCertificateValidator";
+ }
+
+ @Test
+ public void testAuthorizedUsers() throws CertificateException,
IOException, TimeoutException
+ {
+ initializeIdentityRolesTable(identity);
+ Certificate[] chain = loadCertificateChain(certificatePath);
+
+ // Verify authenticated user is as expected
+ IAuthenticator mutualTlsAuthenticator =
createAndInitializeMtlsAuthenticator();
+ IAuthenticator.SaslNegotiator saslNegotiator =
mutualTlsAuthenticator.newSaslNegotiator(getMockInetAddress(), chain);
+ AuthenticatedUser authenticatedUser =
saslNegotiator.getAuthenticatedUser();
+ assertNotNull(authenticatedUser);
+ assertEquals("readonly_user", authenticatedUser.getName());
+ }
+
+ @Test
+ public void testUnauthorizedUsers() throws CertificateException,
IOException, TimeoutException
+ {
+ // As identity of certificate is not added to identity_role_table,
connection should fail
+ Certificate[] chain = loadCertificateChain(certificatePath);
+ IAuthenticator mutualTlsAuthenticator =
createAndInitializeMtlsAuthenticator();
+ IAuthenticator.SaslNegotiator saslNegotiator =
mutualTlsAuthenticator.newSaslNegotiator(getMockInetAddress(), chain);
+ expectedException.expect(AuthenticationException.class);
+ expectedException.expectMessage(String.format("Certificate identity
'%s' not authorized", identity));
+ saslNegotiator.getAuthenticatedUser();
+ }
+
+ @Test
+ public void testInvalidUsers() throws CertificateException, IOException,
TimeoutException
+ {
+ initializeIdentityRolesTable(identity);
+ Certificate[] clientCertificates =
loadCertificateChain("auth/SampleInvalidCertificate.pem");
+ IAuthenticator mutualTlsAuthenticator =
createAndInitializeMtlsAuthenticator();
+ IAuthenticator.SaslNegotiator saslNegotiator =
mutualTlsAuthenticator.newSaslNegotiator(getMockInetAddress(),
clientCertificates);
+ expectedException.expect(AuthenticationException.class);
+ expectedException.expectMessage("Unable to extract Spiffe from the
certificate");
+ saslNegotiator.getAuthenticatedUser();
+ }
+
+ @Test
+ public void testChangeInValidUrns() throws CertificateException,
IOException, TimeoutException
+ {
+ DatabaseDescriptor.setCredentialsValidity(10);
+ initializeIdentityRolesTable(identity);
+ Certificate[] chain = loadCertificateChain(certificatePath);
+ IAuthenticator mutualTlsAuthenticator =
createAndInitializeMtlsAuthenticator();
+ IAuthenticator.SaslNegotiator saslNegotiator =
mutualTlsAuthenticator.newSaslNegotiator(getMockInetAddress(), chain);
+ assertEquals("readonly_user",
saslNegotiator.getAuthenticatedUser().getName());
+ // following call truncates identity table. After removing the
identity of certificate, we should get
+ // authentication exception
+ initializeIdentityRolesTable("another_id");
+ expectedException.expect(AuthenticationException.class);
+ expectedException.expectMessage(String.format("Certificate identity
'%s' not authorized", identity));
+ saslNegotiator.getAuthenticatedUser();
+ }
+
+ @Test
+ public void testValidatorClassNameIsNotSet()
+ {
+ expectedException.expect(ConfigurationException.class);
+
expectedException.expectMessage("authenticator.parameters.validator_class_name
is not set");
+ new MutualTlsAuthenticator(Collections.emptyMap());
+ }
+
+ @Test
+ public void testAddingAndRemovingIdentitiesToTableReflectsInCache() throws
IOException, TimeoutException
+ {
+ DatabaseDescriptor.setCredentialsValidity(10);
+ String identity1 = "id1";
+ String identity2 = "id2";
+
+ initializeIdentityRolesTable(identity1);
+ MutualTlsAuthenticator.IdentityCache urnCache = new
MutualTlsAuthenticator.IdentityCache();
+ assertEquals("readonly_user", urnCache.get(identity1));
+
+ initializeIdentityRolesTable(identity2);
+ assertNull(urnCache.get(identity1));
+ assertEquals("readonly_user", urnCache.get(identity2));
+ }
+
+ MutualTlsAuthenticator createAndInitializeMtlsAuthenticator()
+ {
+ Map<String, String> parameters =
Collections.singletonMap("validator_class_name", getValidatorClass());
+ MutualTlsAuthenticator mutualTlsAuthenticator = new
MutualTlsAuthenticator(parameters);
+ mutualTlsAuthenticator.setup();
+ return mutualTlsAuthenticator;
+ }
+}
diff --git
a/test/unit/org/apache/cassandra/auth/MutualTlsInternodeAuthenticatorTest.java
b/test/unit/org/apache/cassandra/auth/MutualTlsInternodeAuthenticatorTest.java
new file mode 100644
index 0000000000..37f8d062f9
--- /dev/null
+++
b/test/unit/org/apache/cassandra/auth/MutualTlsInternodeAuthenticatorTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.auth;
+
+import java.io.IOException;
+import java.net.UnknownHostException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeoutException;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import org.apache.cassandra.SchemaLoader;
+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.service.StorageService;
+
+import static org.apache.cassandra.auth.AuthTestUtils.loadCertificateChain;
+import static
org.apache.cassandra.auth.IInternodeAuthenticator.InternodeConnectionDirection.INBOUND;
+import static
org.apache.cassandra.auth.IInternodeAuthenticator.InternodeConnectionDirection.OUTBOUND;
+import static
org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CONFIG;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(Parameterized.class)
+public class MutualTlsInternodeAuthenticatorTest
+{
+ private static final String VALIDATOR_CLASS_NAME = "validator_class_name";
+ private static final String TRUSTED_PEER_IDENTITIES =
"trusted_peer_identities";
+ private static final String NODE_IDENTITY = "node_identity";
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Parameterized.Parameter(0)
+ public String certificatePath;
+ @Parameterized.Parameter(1)
+ public String identity;
+
+ @Parameterized.Parameters()
+ public static Collection<Object[]> versions()
+ {
+ return Collections.singletonList(new Object[]{
"auth/SampleMtlsClientCertificate.pem",
"spiffe://testdomain.com/testIdentifier/testValue" });
+ }
+
+ @BeforeClass
+ public static void initialize()
+ {
+ CASSANDRA_CONFIG.setString("cassandra-mtls.yaml");
+ SchemaLoader.loadSchema();
+ DatabaseDescriptor.daemonInitialization();
+ StorageService.instance.initServer(0);
+ }
+
+ @Before
+ public void before()
+ {
+ Config config = DatabaseDescriptor.getRawConfig();
+ config.server_encryption_options =
config.server_encryption_options.withOutboundKeystore("test/conf/cassandra_ssl_test_outbound.keystore")
+
.withOutboundKeystorePassword("cassandra");
+ }
+
+ String getValidatorClass()
+ {
+ return "org.apache.cassandra.auth.SpiffeCertificateValidator";
+ }
+
+ @Test
+ public void
testAuthenticateWithoutCertificatesShouldThrowUnsupportedOperation() throws
UnknownHostException
+ {
+ InetAddressAndPort address = InetAddressAndPort.getByName("127.0.0.1");
+ IInternodeAuthenticator authenticator = new
MutualTlsInternodeAuthenticator(getParams());
+ expectedException.expect(UnsupportedOperationException.class);
+ expectedException.expectMessage("mTLS Authenticator only supports
certificate based authenticate method");
+ authenticator.authenticate(address.getAddress(), address.getPort());
+ }
+
+ @Test
+ public void testAuthenticationOfOutboundConnectionsShouldBeSuccess()
throws UnknownHostException
+ {
+ InetAddressAndPort address = InetAddressAndPort.getByName("127.0.0.1");
+ IInternodeAuthenticator authenticator = new
MutualTlsInternodeAuthenticator(getParams());
+ assertTrue(authenticator.authenticate(address.getAddress(),
address.getPort(), new Certificate[0], OUTBOUND));
+ }
+
+ @Test
+ public void testAuthorizedUsersTrustedPeersConfigured() throws
UnknownHostException, CertificateException
+ {
+ Map<String, String> params = new HashMap<>(getParams());
+ params.put(TRUSTED_PEER_IDENTITIES,
"spiffe://testdomain.com/testIdentifier/testValue");
+ InetAddressAndPort address = InetAddressAndPort.getByName("127.0.0.1");
+ IInternodeAuthenticator authenticator = new
MutualTlsInternodeAuthenticator(params);
+ Certificate[] clientCertificates =
loadCertificateChain(certificatePath);
+ assertTrue(authenticator.authenticate(address.getAddress(),
address.getPort(), clientCertificates, INBOUND));
+ }
+
+ @Test
+ public void testAuthorizedUsersTrustedPeersNotConfigured() throws
IOException, CertificateException
+ {
+ InetAddressAndPort address = InetAddressAndPort.getByName("127.0.0.1");
+ IInternodeAuthenticator authenticator = new
MutualTlsInternodeAuthenticator(getParams());
+ Certificate[] clientCertificates =
loadCertificateChain(certificatePath);
+ assertTrue(authenticator.authenticate(address.getAddress(),
address.getPort(), clientCertificates, INBOUND));
+ }
+
+ @Test
+ public void testNodeIdentityMismatch()
+ {
+ Map<String, String> params = new HashMap<>(getParams());
+ params.put(NODE_IDENTITY, "id1");
+ expectedException.expect(ConfigurationException.class);
+ expectedException.expectMessage("Configured node identity is not
matching identity extracted" +
+ "from the keystore");
+ new MutualTlsInternodeAuthenticator(params);
+ }
+
+ @Test
+ public void testUnauthorizedUser() throws IOException,
CertificateException, TimeoutException
+ {
+ InetAddressAndPort address = InetAddressAndPort.getByName("127.0.0.1");
+ Map<String, String> parameters = getParams();
+ IInternodeAuthenticator authenticator = new
MutualTlsInternodeAuthenticator(parameters);
+ Certificate[] clientCertificates =
loadCertificateChain("auth/SampleUnauthorizedMtlsClientCertificate.pem");
+ assertFalse(authenticator.authenticate(address.getAddress(),
address.getPort(), clientCertificates, INBOUND));
+ }
+
+ @Test
+ public void testNoValidatorClassNameInConfig()
+ {
+ Map<String, String> parameters = new HashMap<>(getParams());
+ parameters.put(VALIDATOR_CLASS_NAME, null);
+ expectedException.expect(ConfigurationException.class);
+
expectedException.expectMessage("internode_authenticator.parameters.validator_class_name
is not set");
+ new MutualTlsInternodeAuthenticator(parameters);
+ }
+
+
+ @Test
+ public void testNoIdentitiesInKeystore()
+ {
+ Config config = DatabaseDescriptor.getRawConfig();
+ config.server_encryption_options =
config.server_encryption_options.withOutboundKeystore("test/conf/cassandra_ssl_test.keystore")
+
.withOutboundKeystorePassword("cassandra");
+ expectedException.expect(ConfigurationException.class);
+ expectedException.expectMessage("No identity was extracted from the
outbound keystore 'test/conf/cassandra_ssl_test.keystore'");
+ new MutualTlsInternodeAuthenticator(getParams());
+ }
+
+ @Test
+ public void testGetIdentitiesFromKeystore()
+ {
+ List<String> identities = new
MutualTlsInternodeAuthenticator(getParams()).getIdentitiesFromKeyStore("test/conf/cassandra_ssl_test_outbound.keystore",
"cassandra", "JKS");
+ assertFalse(identities.isEmpty());
+ assertTrue(identities.contains(identity));
+ }
+
+ Map<String, String> getParams()
+ {
+ return Collections.singletonMap(VALIDATOR_CLASS_NAME,
getValidatorClass());
+ }
+}
diff --git
a/test/unit/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticatorTest.java
b/test/unit/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticatorTest.java
new file mode 100644
index 0000000000..f4269a72fa
--- /dev/null
+++
b/test/unit/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticatorTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.auth;
+
+import java.io.InputStream;
+import java.net.UnknownHostException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.Collections;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.service.StorageService;
+
+import static org.apache.cassandra.auth.AuthTestUtils.getMockInetAddress;
+import static org.junit.Assert.assertNotNull;
+import static org.psjava.util.AssertStatus.assertTrue;
+
+public class MutualTlsWithPasswordFallbackAuthenticatorTest
+{
+ private static MutualTlsWithPasswordFallbackAuthenticator
fallbackAuthenticator;
+ private Certificate[] clientCertificatesCorp;
+
+ @BeforeClass
+ public static void initialize()
+ {
+ SchemaLoader.loadSchema();
+ DatabaseDescriptor.daemonInitialization();
+ StorageService.instance.initServer(0);
+ Config config = DatabaseDescriptor.getRawConfig();
+ config.client_encryption_options =
config.client_encryption_options.withEnabled(true)
+
.withRequireClientAuth(true);
+ Map<String, String> parameters =
Collections.singletonMap("validator_class_name",
"org.apache.cassandra.auth.SpiffeCertificateValidator");
+ fallbackAuthenticator = new
MutualTlsWithPasswordFallbackAuthenticator(parameters);
+ fallbackAuthenticator.setup();
+ }
+
+ @Before
+ public void setup() throws CertificateException
+ {
+ InputStream inputStreamCorp =
getClass().getClassLoader().getResourceAsStream("auth/SampleMtlsClientCertificate.pem");
+ assertNotNull(inputStreamCorp);
+ Certificate corpCertificate =
CertificateFactory.getInstance("X.509").generateCertificate(inputStreamCorp);
+ clientCertificatesCorp = new Certificate[]{ corpCertificate };
+ }
+
+ @Test
+ public void testFallbackToPasswordAuthentication() throws
UnknownHostException
+ {
+ // If client certificate chain is not present fallback to password
authentication
+ IAuthenticator.SaslNegotiator passwordNegotiator =
fallbackAuthenticator.newSaslNegotiator(getMockInetAddress());
+ assertTrue(passwordNegotiator instanceof
PasswordAuthenticator.PlainTextSaslAuthenticator);
+
+ // If client certificate chain is null fallback to password
authentication
+ IAuthenticator.SaslNegotiator passwordNegotiator1 =
fallbackAuthenticator.newSaslNegotiator(getMockInetAddress(), null);
+ assertTrue(passwordNegotiator1 instanceof
PasswordAuthenticator.PlainTextSaslAuthenticator);
+
+ // If client certificate chain length is zero fallback to password
authentication
+ IAuthenticator.SaslNegotiator passwordNegotiator2 =
fallbackAuthenticator.newSaslNegotiator(getMockInetAddress(), new
Certificate[0]);
+ assertTrue(passwordNegotiator2 instanceof
PasswordAuthenticator.PlainTextSaslAuthenticator);
+ }
+
+ @Test
+ public void testUsesMtlsAuthenticationWhenCertificatesPresent() throws
UnknownHostException
+ {
+ // If client certificate chain present and valid use mTLS
authentication
+ IAuthenticator.SaslNegotiator mutualtlsAuthenticator =
fallbackAuthenticator.newSaslNegotiator(getMockInetAddress(),
clientCertificatesCorp);
+ assertTrue(mutualtlsAuthenticator instanceof
MutualTlsAuthenticator.CertificateNegotiator);
+ }
+}
diff --git
a/test/unit/org/apache/cassandra/auth/SpiffeCertificateValidatorTest.java
b/test/unit/org/apache/cassandra/auth/SpiffeCertificateValidatorTest.java
new file mode 100644
index 0000000000..fb1083c3f0
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/SpiffeCertificateValidatorTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.auth;
+
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import org.apache.cassandra.exceptions.AuthenticationException;
+
+import static org.apache.cassandra.auth.AuthTestUtils.loadCertificateChain;
+import static org.junit.Assert.assertEquals;
+
+public class SpiffeCertificateValidatorTest
+{
+ private static final String CERTIFICATE_PATH =
"auth/SampleMtlsClientCertificate.pem";
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void getIdentityShouldReturnSpiffeTest() throws CertificateException
+ {
+ SpiffeCertificateValidator validator = new
SpiffeCertificateValidator();
+ Certificate[] chain = loadCertificateChain(CERTIFICATE_PATH);
+ String spiffe = validator.identity(chain);
+ assertEquals("spiffe://testdomain.com/testIdentifier/testValue",
spiffe);
+ }
+
+ @Test
+ public void getIdentityShouldThrowExceptionOnNoSpiffeInSAN() throws
CertificateException
+ {
+ SpiffeCertificateValidator validator = new
SpiffeCertificateValidator();
+ String invalidCertificate = "auth/SampleInvalidCertificate.pem";
+ Certificate[] chain = loadCertificateChain(invalidCertificate);
+ expectedException.expectMessage("Unable to extract Spiffe from the
certificate");
+ expectedException.expect(AuthenticationException.class);
+ validator.identity(chain);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/config/ConfigCompatabilityTest.java
b/test/unit/org/apache/cassandra/config/ConfigCompatabilityTest.java
index 4a620334c3..6db68ec6b7 100644
--- a/test/unit/org/apache/cassandra/config/ConfigCompatabilityTest.java
+++ b/test/unit/org/apache/cassandra/config/ConfigCompatabilityTest.java
@@ -88,6 +88,14 @@ public class ConfigCompatabilityTest
.add("streaming_socket_timeout_in_ms") // CASSANDRA-12229
.build();
+ private static final Set<String> EXPECTED_FOR_50 =
ImmutableSet.<String>builder()
+ // Switched
to a parameterized class that can construct from a bare string
+
.add("internode_authenticator types do not match;
org.apache.cassandra.config.ParameterizedClass != java.lang.String")
+
.add("authenticator types do not match;
org.apache.cassandra.config.ParameterizedClass != java.lang.String")
+
.add("Property internode_authenticator used to be a value-type, but now is
nested type class org.apache.cassandra.config.ParameterizedClass")
+
.add("Property authenticator used to be a value-type, but now is nested type
class org.apache.cassandra.config.ParameterizedClass")
+ .build();
+
/**
* Not all converts make sense as backwards compatible as they use things
like String to handle the conversion more
* generically.
@@ -97,13 +105,13 @@ public class ConfigCompatabilityTest
@Test
public void diff_3_0() throws IOException
{
- diff(TEST_DIR + "/version=3.0.0-alpha1.yml", REMOVED_IN_40);
+ diff(TEST_DIR + "/version=3.0.0-alpha1.yml", REMOVED_IN_40,
EXPECTED_FOR_50);
}
@Test
public void diff_3_11() throws IOException
{
- diff(TEST_DIR + "/version=3.11.0.yml", REMOVED_IN_40);
+ diff(TEST_DIR + "/version=3.11.0.yml", REMOVED_IN_40, EXPECTED_FOR_50);
}
@Test
@@ -111,10 +119,10 @@ public class ConfigCompatabilityTest
{
diff(TEST_DIR + "/version=4.0-alpha1.yml",
ImmutableSet.<String>builder()
.addAll(WINDOWS)
- .build());
+ .build(),
EXPECTED_FOR_50);
}
- private void diff(String original, Set<String> ignore) throws IOException
+ private void diff(String original, Set<String> ignore, Set<String>
expectedErrors) throws IOException
{
Class<Config> type = Config.class;
ClassTree previous = load(original);
@@ -124,6 +132,7 @@ public class ConfigCompatabilityTest
Set<String> errors = new HashSet<>();
diff(loader, replacements, previous, type, "", missing, errors);
missing = Sets.difference(missing, ignore);
+ errors = Sets.difference(errors, expectedErrors);
StringBuilder msg = new StringBuilder();
if (!missing.isEmpty())
msg.append(String.format("Unable to find the following
properties:\n%s", String.join("\n", new TreeSet<>(missing))));
diff --git
a/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java
b/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java
index 1f7819820c..efa47b05d7 100644
--- a/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java
+++ b/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java
@@ -18,6 +18,7 @@
package org.apache.cassandra.config;
+import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
@@ -26,6 +27,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
import com.google.common.collect.ImmutableMap;
@@ -41,6 +43,7 @@ import static
org.apache.cassandra.config.YamlConfigurationLoader.SYSTEM_PROPERT
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -387,7 +390,41 @@ public class YamlConfigurationLoaderTest
return YamlConfigurationLoader.fromMap(builder.build(), Config.class);
}
- private static Config load(String path)
+ @Test
+ public void
testBackwardCompatibilityOfInternodeAuthenticatorPropertyAsMap()
+ {
+ Config config = load("cassandra-mtls.yaml");
+ assertEquals(config.internode_authenticator.class_name,
"org.apache.cassandra.auth.MutualTlsInternodeAuthenticator");
+ assertFalse(config.internode_authenticator.parameters.isEmpty());
+
assertEquals(config.internode_authenticator.parameters.get("validator_class_name"),
"org.apache.cassandra.auth.SpiffeCertificateValidator");
+ }
+
+ @Test
+ public void
testBackwardCompatibilityOfInternodeAuthenticatorPropertyAsString()
+ {
+ Config config = load("cassandra-mtls-backward-compatibility.yaml");
+ assertEquals(config.internode_authenticator.class_name,
"org.apache.cassandra.auth.AllowAllInternodeAuthenticator");
+ assertTrue(config.internode_authenticator.parameters.isEmpty());
+ }
+
+ @Test
+ public void testBackwardCompatibilityOfAuthenticatorPropertyAsMap()
+ {
+ Config config = load("cassandra-mtls.yaml");
+ assertEquals(config.authenticator.class_name,
"org.apache.cassandra.auth.MutualTlsAuthenticator");
+ assertFalse(config.authenticator.parameters.isEmpty());
+
assertEquals(config.authenticator.parameters.get("validator_class_name"),
"org.apache.cassandra.auth.SpiffeCertificateValidator");
+ }
+
+ @Test
+ public void testBackwardCompatibilityOfAuthenticatorPropertyAsString()
throws IOException, TimeoutException
+ {
+ Config config = load("cassandra-mtls-backward-compatibility.yaml");
+ assertEquals(config.authenticator.class_name,
"org.apache.cassandra.auth.AllowAllAuthenticator");
+ assertTrue(config.authenticator.parameters.isEmpty());
+ }
+
+ public static Config load(String path)
{
URL url =
YamlConfigurationLoaderTest.class.getClassLoader().getResource(path);
if (url == null)
diff --git
a/test/unit/org/apache/cassandra/cql3/statements/AddIdentityStatementTest.java
b/test/unit/org/apache/cassandra/cql3/statements/AddIdentityStatementTest.java
new file mode 100644
index 0000000000..e77ae35413
--- /dev/null
+++
b/test/unit/org/apache/cassandra/cql3/statements/AddIdentityStatementTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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.cql3.statements;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.auth.AuthCacheService;
+import org.apache.cassandra.auth.AuthKeyspace;
+import org.apache.cassandra.auth.AuthTestUtils;
+import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.auth.CassandraRoleManager;
+import org.apache.cassandra.auth.RoleResource;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.exceptions.UnauthorizedException;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.service.QueryState;
+
+import static org.apache.cassandra.auth.AuthKeyspace.IDENTITY_TO_ROLES;
+import static org.apache.cassandra.schema.SchemaConstants.AUTH_KEYSPACE_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class AddIdentityStatementTest
+{
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ private static final String USER_ROLE = "cassandra";
+ private static final String IDENTITY =
"spiffe://testdomain.com/testIdentifier/testValue";
+ private static final String ADD_QUERY = String.format("ADD IDENTITY '%s'
TO ROLE '%s';", IDENTITY, USER_ROLE);
+ private static void setupPrivilegedUser()
+ {
+ QueryProcessor.executeInternal(String.format("INSERT INTO %s.%s (role,
is_superuser, can_login, salted_hash) "
+ + "VALUES ('%s', true,
true, '%s')",
+ AUTH_KEYSPACE_NAME,
+ AuthKeyspace.ROLES,
+
CassandraRoleManager.DEFAULT_SUPERUSER_NAME,
+ "xxx"));
+ }
+
+ @BeforeClass
+ public static void defineSchema() throws ConfigurationException
+ {
+ SchemaLoader.prepareServer();
+ SchemaLoader.setupAuth(new AuthTestUtils.LocalCassandraRoleManager(),
+ new AuthTestUtils.LocalPasswordAuthenticator(),
+ new AuthTestUtils.LocalCassandraAuthorizer(),
+ new
AuthTestUtils.LocalCassandraNetworkAuthorizer());
+ AuthCacheService.initializeAndRegisterCaches();
+ setupPrivilegedUser();
+ }
+
+ @Before
+ public void clear()
+ {
+
Keyspace.open(AUTH_KEYSPACE_NAME).getColumnFamilyStore(IDENTITY_TO_ROLES).truncateBlocking();
+ }
+
+ @Test
+ public void testAddIdentityStatementParsing()
+ {
+ CQLStatement.Raw statement = QueryProcessor.parseStatement(ADD_QUERY);
+ assertTrue(statement instanceof AddIdentityStatement);
+ AddIdentityStatement addIdentityStatement =
(AddIdentityStatement)statement;
+ assertEquals(IDENTITY, addIdentityStatement.identity);
+ assertEquals(USER_ROLE, addIdentityStatement.role);
+ }
+
+ @Test
+ public void testAddingValidIdentity()
+ {
+ QueryProcessor.process(ADD_QUERY, ConsistencyLevel.QUORUM,
getClientState(), 10L);
+ assertEquals(USER_ROLE,
DatabaseDescriptor.getRoleManager().roleForIdentity(IDENTITY));
+ }
+
+ @Test
+ public void testAddingExistingIdentity()
+ {
+ QueryProcessor.process(ADD_QUERY, ConsistencyLevel.QUORUM,
getClientState(), 10L);
+ expectedException.expect(InvalidRequestException.class);
+ expectedException.expectMessage(IDENTITY +" already exists");
+ QueryProcessor.process(ADD_QUERY, ConsistencyLevel.QUORUM,
getClientState(), 10L);
+ }
+
+ @Test
+ public void testAddIdentityOnlyWhenNotPresent()
+ {
+ DatabaseDescriptor.getRoleManager().addIdentity(IDENTITY, USER_ROLE);
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Identity is already associated with
another role, cannot associate it with role read_write_user");
+ DatabaseDescriptor.getRoleManager().addIdentity(IDENTITY,
"read_write_user");
+ }
+
+ @Test
+ public void testAnonymousUser()
+ {
+ expectedException.expect(UnauthorizedException.class);
+ expectedException.expectMessage("You have not logged in");
+ QueryProcessor.executeInternal(ADD_QUERY);
+ }
+
+ @Test
+ public void testAddingNonExistentRole()
+ {
+ String query = String.format("ADD IDENTITY '%s' TO ROLE
'non-existing-role';", IDENTITY);
+ expectedException.expect(InvalidRequestException.class);
+ expectedException.expectMessage("Can not add identity for non-existent
role 'non-existing-role'");
+ QueryProcessor.process(query, ConsistencyLevel.QUORUM,
getClientState(), 10L);
+ }
+
+ @Test
+ public void testUsersWithNoPrevilegesCannotAddIdentitiess()
+ {
+ // Added user to roles table
+ AuthenticatedUser authenticatedUser = new
AuthenticatedUser("readwrite_user");
+ DatabaseDescriptor.getRoleManager().createRole(authenticatedUser,
RoleResource.role("readwrite_user"), AuthTestUtils.getLoginRoleOptions());
+ ClientState state = ClientState.forInternalCalls();
+ state.login(authenticatedUser);
+
+ String query = String.format("ADD IDENTITY '%s' TO ROLE
'readwrite_user';", IDENTITY);
+ expectedException.expect(UnauthorizedException.class);
+ expectedException.expectMessage("User readwrite_user does not have
sufficient privileges to perform the requested operation");
+ QueryProcessor.process(query, ConsistencyLevel.QUORUM, new
QueryState(state), 10L);
+ }
+
+
+ @Test
+ public void creatingRoleWithIdentitiesAlreadyExisting()
+ {
+ DatabaseDescriptor.getRoleManager().addIdentity(IDENTITY,
"readwrite_user");
+ AuthenticatedUser authenticatedUser = new
AuthenticatedUser("cassandra");
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Cannot create a role 'readwrite_user'
when identities already exists for it");
+ DatabaseDescriptor.getRoleManager().createRole(authenticatedUser,
RoleResource.role("readwrite_user"), AuthTestUtils.getLoginRoleOptions());
+ }
+
+ @Test
+ public void ifNotExistsTest()
+ {
+ // Assert that identity is not present in the table
+
assertNull(DatabaseDescriptor.getRoleManager().roleForIdentity(IDENTITY));
+
+ String addQueryWithIfNotExists = String.format("ADD IDENTITY IF NOT
EXISTS '%s' TO ROLE '%s';", IDENTITY, USER_ROLE);
+
+ // Identity not in the table & add identity query with IF NOT EXISTS
should succeed
+ QueryProcessor.process(addQueryWithIfNotExists,
ConsistencyLevel.QUORUM, getClientState(), 10L);
+ assertEquals(USER_ROLE,
DatabaseDescriptor.getRoleManager().roleForIdentity(IDENTITY));
+
+ // Identity in the table & add identity query with IF NOT EXISTS
should succeed
+ QueryProcessor.process(addQueryWithIfNotExists,
ConsistencyLevel.QUORUM, getClientState(), 10L);
+ assertEquals(USER_ROLE,
DatabaseDescriptor.getRoleManager().roleForIdentity(IDENTITY));
+
+ clear();
+ String addQueryWithOutIfNotExists = String.format("ADD IDENTITY '%s'
TO ROLE '%s';", IDENTITY, USER_ROLE);
+ // Identity not in the table & add identity query without IF NOT
EXISTS should succeed
+ QueryProcessor.process(addQueryWithOutIfNotExists,
ConsistencyLevel.QUORUM, getClientState(), 10L);
+ assertEquals(USER_ROLE,
DatabaseDescriptor.getRoleManager().roleForIdentity(IDENTITY));
+
+ // Identity in the table & add identity query without IF NOT EXISTS
should fail
+ expectedException.expect(InvalidRequestException.class);
+ expectedException.expectMessage(IDENTITY + " already exists");
+ QueryProcessor.process(addQueryWithOutIfNotExists,
ConsistencyLevel.QUORUM, getClientState(), 10L);
+ }
+
+ static QueryState getClientState()
+ {
+ ClientState state = ClientState.forInternalCalls();
+ state.login(new
AuthenticatedUser(CassandraRoleManager.DEFAULT_SUPERUSER_NAME));
+ return new QueryState(state);
+ }
+}
diff --git
a/test/unit/org/apache/cassandra/cql3/statements/DropIdentityStatementTest.java
b/test/unit/org/apache/cassandra/cql3/statements/DropIdentityStatementTest.java
new file mode 100644
index 0000000000..7521c95e85
--- /dev/null
+++
b/test/unit/org/apache/cassandra/cql3/statements/DropIdentityStatementTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.cql3.statements;
+
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import org.apache.cassandra.auth.AuthTestUtils;
+import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.auth.RoleResource;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.exceptions.UnauthorizedException;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.service.QueryState;
+
+import static org.apache.cassandra.auth.AuthKeyspace.IDENTITY_TO_ROLES;
+import static
org.apache.cassandra.cql3.statements.AddIdentityStatementTest.defineSchema;
+import static
org.apache.cassandra.cql3.statements.AddIdentityStatementTest.getClientState;
+import static org.apache.cassandra.schema.SchemaConstants.AUTH_KEYSPACE_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class DropIdentityStatementTest
+{
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ private static final String IDENTITY =
"spiffe://testdomain.com/testIdentifier/testValue";
+ private static final String DROP_QUERY = String.format("DROP IDENTITY
'%s'", IDENTITY);
+
+ @BeforeClass
+ public static void beforeClasss() throws ConfigurationException
+ {
+ defineSchema();
+ }
+
+ @Before
+ public void clear()
+ {
+
Keyspace.open(AUTH_KEYSPACE_NAME).getColumnFamilyStore(IDENTITY_TO_ROLES).truncateBlocking();
+ }
+
+
+ @Test
+ public void testDropIdentityStatementParsing()
+ {
+ CQLStatement.Raw statement = QueryProcessor.parseStatement(DROP_QUERY);
+ assertTrue(statement instanceof DropIdentityStatement);
+ DropIdentityStatement dropIdentityStatement = (DropIdentityStatement)
statement;
+ assertEquals(IDENTITY, dropIdentityStatement.identity);
+ assertFalse(dropIdentityStatement.ifExists);
+ }
+
+ @Test
+ public void testDroppingValidIdentity()
+ {
+ DatabaseDescriptor.getRoleManager().addIdentity(IDENTITY, "cassandra");
+ QueryProcessor.process(DROP_QUERY, ConsistencyLevel.QUORUM,
getClientState(), 10L);
+
assertFalse(DatabaseDescriptor.getRoleManager().isExistingIdentity(IDENTITY));
+ }
+
+ @Test
+ public void testAnonymousUser()
+ {
+ expectedException.expect(UnauthorizedException.class);
+ expectedException.expectMessage("You have not logged in");
+ QueryProcessor.executeInternal(DROP_QUERY);
+ }
+
+ @Test
+ public void testUsersWithoutPrevilegesCannotDropIdentities()
+ {
+ // Added user to roles table
+ AuthenticatedUser authenticatedUser = new
AuthenticatedUser("readwrite_user");
+ DatabaseDescriptor.getRoleManager().createRole(authenticatedUser,
RoleResource.role("readwrite_user"), AuthTestUtils.getLoginRoleOptions());
+ ClientState state = ClientState.forInternalCalls();
+ state.login(authenticatedUser);
+
+ String query = String.format("DROP IDENTITY '%s';", IDENTITY);
+ expectedException.expect(UnauthorizedException.class);
+ expectedException.expectMessage("User readwrite_user does not have
sufficient privileges to perform the requested operation");
+ QueryProcessor.process(query, ConsistencyLevel.QUORUM, new
QueryState(state), 10L);
+ }
+
+ @Test
+ public void dropRoleSholdRemoveAllAssociatedIdentities()
+ {
+ // Create a new user and associate identities to the user
+ AuthenticatedUser authenticatedUser = new
AuthenticatedUser("readwrite_user");
+ DatabaseDescriptor.getRoleManager().createRole(authenticatedUser,
RoleResource.role("readwrite_user"), AuthTestUtils.getLoginRoleOptions());
+ DatabaseDescriptor.getRoleManager().addIdentity(IDENTITY,
"readwrite_user");
+ String identity1 = "spiffe://testdomain.com/testIdentifier/testValue1";
+ DatabaseDescriptor.getRoleManager().addIdentity(identity1,
"readwrite_user");
+
+ // Dropping role should remove identities associated with it
+ String query = "DROP ROLE readwrite_user";
+ QueryProcessor.process(query, ConsistencyLevel.QUORUM,
getClientState(), 10L);
+
+ Map<String, String > m =
DatabaseDescriptor.getRoleManager().authorizedIdentities();
+ assertFalse(m.containsKey(IDENTITY));
+ assertFalse(m.containsKey(identity1));
+ }
+
+ @Test
+ public void ifExistsTest()
+ {
+ // Assert that identity is not present in the table
+
assertNull(DatabaseDescriptor.getRoleManager().roleForIdentity(IDENTITY));
+
+ String dropQueryWithIfExists = String.format("DROP IDENTITY IF EXISTS
'%s'", IDENTITY);
+
+ // Identity in the table & IF EXISTS in query should succeed
+ DatabaseDescriptor.getRoleManager().addIdentity(IDENTITY, "cassandra");
+ QueryProcessor.process(dropQueryWithIfExists, ConsistencyLevel.QUORUM,
getClientState(), 10L);
+
+ // Identity not in the table & IF EXISTS in query should succeed
+ QueryProcessor.process(dropQueryWithIfExists, ConsistencyLevel.QUORUM,
getClientState(), 10L);
+
+ String dropQueryWithOutIfExists = String.format("DROP IDENTITY '%s'",
IDENTITY);
+ // Identity in the table & no IF EXISTS in query should succeed
+ DatabaseDescriptor.getRoleManager().addIdentity(IDENTITY, "cassandra");
+ QueryProcessor.process(dropQueryWithOutIfExists,
ConsistencyLevel.QUORUM, getClientState(), 10L);
+
+ // Identity not in the table & no IF EXISTS in query should fail
+ expectedException.expect(InvalidRequestException.class);
+ expectedException.expectMessage(String.format("identity '%s' doesn't
exist", IDENTITY));
+ QueryProcessor.process(dropQueryWithOutIfExists,
ConsistencyLevel.QUORUM, getClientState(), 10L);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/transport/CQLUserAuditTest.java
b/test/unit/org/apache/cassandra/transport/CQLUserAuditTest.java
index 8bc9f28dd2..6b7323c9ab 100644
--- a/test/unit/org/apache/cassandra/transport/CQLUserAuditTest.java
+++ b/test/unit/org/apache/cassandra/transport/CQLUserAuditTest.java
@@ -64,7 +64,7 @@ public class CQLUserAuditTest
public static void setup() throws Exception
{
OverrideConfigurationLoader.override((config) -> {
- config.authenticator = "PasswordAuthenticator";
+ config.authenticator = new
ParameterizedClass("PasswordAuthenticator");
config.role_manager = "CassandraRoleManager";
config.diagnostic_events_enabled = true;
config.audit_logging_options.enabled = true;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]