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 <djoshi@apache.org>
This commit is contained in:
jkonisa 2023-05-22 22:15:22 -07:00 committed by Dinesh Joshi
parent 192a639088
commit f078c02cb5
42 changed files with 2618 additions and 23 deletions

View File

@ -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

View File

@ -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

View File

@ -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`.

View File

@ -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;

View File

@ -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

View File

@ -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),

View File

@ -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

View File

@ -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));
}
}

View File

@ -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)
{

View File

@ -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()
{

View File

@ -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)
{
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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]);
}
}

View File

@ -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;

View File

@ -420,7 +420,8 @@ public class DatabaseDescriptor
}
}
private static void setConfig(Config config)
@VisibleForTesting
public static void setConfig(Config config)
{
conf = config;
}

View File

@ -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)
{

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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

View File

@ -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

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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;

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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))));

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;