mirror of https://github.com/apache/cassandra
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:
parent
192a639088
commit
f078c02cb5
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -420,7 +420,8 @@ public class DatabaseDescriptor
|
|||
}
|
||||
}
|
||||
|
||||
private static void setConfig(Config config)
|
||||
@VisibleForTesting
|
||||
public static void setConfig(Config config)
|
||||
{
|
||||
conf = config;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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-----
|
||||
|
|
@ -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-----
|
||||
|
|
@ -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-----
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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))));
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue