Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions conf/cassandra.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,32 @@ role_manager:
# invalid_role_disconnect_task_period: 4h
# invalid_role_disconnect_task_max_jitter: 1h

# Creates the initial role on a cluster which has no roles yet, implementing IDefaultRoleInitializer.
# Most functions of the IRoleManager require an authenticated login, so a cluster with no roles has no way
# to create the first one; this option controls how that role is bootstrapped.
#
# Defaults to PasswordDefaultRoleInitializer, which creates a 'cassandra' superuser whose password is also
# 'cassandra'. That password is a published constant, so deployments using it must rotate or drop the role
# before the native transport is reachable.
#
# MutualTlsDefaultRoleInitializer instead creates the role with no password at all and maps a client
# certificate identity onto it, so there is no credential to guess. It requires an authenticator supporting
# mutual TLS, such as MutualTlsAuthenticator or MutualTlsWithPasswordFallbackAuthenticator.
#
# default_role_initializer:
# class_name: PasswordDefaultRoleInitializer
# parameters:
# role: cassandra
# # Either a plaintext password or a password_hash may be given; if password_hash is set, password is ignored.
# password: cassandra
# # password_hash: "$2a$04$wsvzFamDJPDrTwMjgfcgpO.mKc.CMEuHBFZSjhGz2Ts6.v8PUO2rC"
#
# default_role_initializer:
# class_name: MutualTlsDefaultRoleInitializer
# parameters:
# role: cassandra_mtls
# identity: "spiffe1"

# Network authorization backend, implementing INetworkAuthorizer; used to restrict user
# access to certain DCs
# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllNetworkAuthorizer,
Expand Down
25 changes: 24 additions & 1 deletion src/java/org/apache/cassandra/auth/AuthConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ public static void applyAuth()

DatabaseDescriptor.setAuthorizer(authorizer);

// default role initializer: bootstraps the first role on a cluster which has none yet. Instantiated
// before the role manager because the role manager depends on it (see IRoleManager#defaultRoleInitializer).

IDefaultRoleInitializer defaultRoleInitializer = authInstantiate(conf.default_role_initializer,
IDefaultRoleInitializer.class,
PasswordDefaultRoleInitializer.instance);
DatabaseDescriptor.setDefaultRoleInitializer(defaultRoleInitializer);

// role manager

IRoleManager roleManager = authInstantiate(conf.role_manager, IRoleManager.class, CassandraRoleManager.class);
Expand Down Expand Up @@ -140,18 +148,23 @@ public static void applyAuth()
authenticator.validateConfiguration();
authorizer.validateConfiguration();
roleManager.validateConfiguration();
defaultRoleInitializer.validateConfiguration();
networkAuthorizer.validateConfiguration();
cidrAuthorizer.validateConfiguration();
DatabaseDescriptor.getInternodeAuthenticator().validateConfiguration();
}

private static <T> T authInstantiate(ParameterizedClass authCls, Class<T> expectedType, Class<? extends T> defaultCls) {
public static <T> T authInstantiate(ParameterizedClass authCls, Class<T> expectedType, Class<? extends T> defaultCls)
{
if (authCls != null && authCls.class_name != null)
{
String authPackage = AuthConfig.class.getPackage().getName();
return ParameterizedClass.newInstance(authCls, List.of("", authPackage), expectedType);
}

if (defaultCls == null)
return null;

// for now, this has to stay and can not be replaced by ParameterizedClass.newInstance as above
// due to that failing for simulator dtests. See CASSANDRA-20450 for more information.
try
Expand All @@ -163,4 +176,14 @@ private static <T> T authInstantiate(ParameterizedClass authCls, Class<T> expect
throw new ConfigurationException("Failed to instantiate " + defaultCls.getName(), e);
}
}

public static <T> T authInstantiate(ParameterizedClass authCls, Class<T> expectedType, T defaultInstance)
{
if (authCls != null && authCls.class_name != null)
{
String authPackage = AuthConfig.class.getPackage().getName();
return ParameterizedClass.newInstance(authCls, List.of("", authPackage), expectedType);
}
return defaultInstance;
}
}
74 changes: 22 additions & 52 deletions src/java/org/apache/cassandra/auth/CassandraRoleManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.transport.messages.ResultMessage;
import org.apache.cassandra.utils.ByteBufferUtil;
Expand Down Expand Up @@ -106,9 +105,6 @@ public class CassandraRoleManager implements IRoleManager, CassandraRoleManagerM
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";

/**
* Role options which are supported for all authentication mechanisms. IAuthenticator implementations can declare
* additional supported role options via {@link IAuthenticator#getSupportedRoleOptions()}.
Expand Down Expand Up @@ -217,6 +213,21 @@ public CassandraRoleManager(Map<String, String> parameters)
MBeanWrapper.instance.registerMBean(this, MBEAN_NAME);
}

/**
* The default role initializer is configured as a top-level {@code default_role_initializer} option and
* applied by {@link AuthConfig#applyAuth()}. Returning it here lets the startup logic (see
* {@link #setup(boolean)}, {@link #hasExistingRoles()} and {@link #consistencyForRoleWrite(String)}) reach the
* configured initializer through the role manager, and lets custom {@link IRoleManager} implementations
* override how they integrate it. Falls back to the historical password initializer when auth setup has not
* run, e.g. in tests which do not call {@link AuthConfig#applyAuth()}.
*/
@Override
public IDefaultRoleInitializer defaultRoleInitializer()
{
IDefaultRoleInitializer initializer = DatabaseDescriptor.getDefaultRoleInitializer();
return initializer == null ? PasswordDefaultRoleInitializer.instance : initializer;
}

@Override
public void setup(boolean asyncRoleSetup)
{
Expand All @@ -228,7 +239,7 @@ public void setup(boolean asyncRoleSetup)
try
{
// Try to set up synchronously
setupDefaultRole();
defaultRoleInitializer().setupDefaultRole();
return;
}
catch (Throwable t)
Expand All @@ -237,7 +248,7 @@ public void setup(boolean asyncRoleSetup)
}
}
scheduleSetupTask(() -> {
setupDefaultRole();
defaultRoleInitializer().setupDefaultRole();
return null;
});
}
Expand Down Expand Up @@ -519,51 +530,10 @@ public void validateConfiguration() throws ConfigurationException
{
}

/*
* Create the default superuser role to bootstrap role creation on a clean system. Preemptively
* gives the role the default password so PasswordAuthenticator can be used to log in (if
* configured)
*/
private static void setupDefaultRole()
{
if (ClusterMetadata.current().tokenMap.tokens().isEmpty())
throw new IllegalStateException("CassandraRoleManager skipped default role setup: no known tokens in ring");

try
{
if (!hasExistingRoles())
{
QueryProcessor.process(createDefaultRoleQuery(),
consistencyForRoleWrite(DEFAULT_SUPERUSER_NAME));
logger.info("Created default superuser role '{}'", DEFAULT_SUPERUSER_NAME);
}
}
catch (RequestExecutionException e)
{
logger.warn("CassandraRoleManager skipped default role setup: some nodes were not ready");
throw e;
}
}

@VisibleForTesting
public static String createDefaultRoleQuery()
{
return String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) VALUES ('%s', true, true, '%s') USING TIMESTAMP 0",
SchemaConstants.AUTH_KEYSPACE_NAME,
AuthKeyspace.ROLES,
DEFAULT_SUPERUSER_NAME,
escape(hashpw(DEFAULT_SUPERUSER_PASSWORD)));
}

@VisibleForTesting
public static boolean hasExistingRoles() throws RequestExecutionException
{
// Try looking up the 'cassandra' default role first, to avoid the range query if possible.
String defaultSUQuery = String.format("SELECT * FROM %s.%s WHERE role = '%s'", SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES, DEFAULT_SUPERUSER_NAME);
String allUsersQuery = String.format("SELECT * FROM %s.%s LIMIT 1", SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES);
return !QueryProcessor.process(defaultSUQuery, ConsistencyLevel.ONE).isEmpty()
|| !QueryProcessor.process(defaultSUQuery, ConsistencyLevel.QUORUM).isEmpty()
|| !QueryProcessor.process(allUsersQuery, ConsistencyLevel.QUORUM).isEmpty();
return DatabaseDescriptor.getRoleManager().defaultRoleInitializer().hasExistingRoles();
}

protected void scheduleSetupTask(final Callable<Void> setupTask)
Expand Down Expand Up @@ -771,12 +741,12 @@ private void enforcePasswordUpdateRateLimit(AuthenticatedUser performer, String
throw new OverloadedException(failure);
}

private static String hashpw(String password)
static String hashpw(String password)
{
return BCrypt.hashpw(password, PasswordSaltSupplier.get());
}

private static String escape(String name)
static String escape(String name)
{
return StringUtils.replace(name, "'", "''");
}
Expand All @@ -789,14 +759,14 @@ private static ByteBuffer byteBuf(String str)
/** Allows selective overriding of the consistency level for specific roles. */
protected static ConsistencyLevel consistencyForRoleWrite(String role)
{
return role.equals(DEFAULT_SUPERUSER_NAME) ?
return role.equals(DatabaseDescriptor.getRoleManager().defaultRoleInitializer().defaultRoleName()) ?
DEFAULT_SUPERUSER_CONSISTENCY_LEVEL :
CassandraAuthorizer.authWriteConsistencyLevel();
}

protected static ConsistencyLevel consistencyForRoleRead(String role)
{
return role.equals(DEFAULT_SUPERUSER_NAME) ?
return role.equals(DatabaseDescriptor.getRoleManager().defaultRoleInitializer().defaultRoleName()) ?
DEFAULT_SUPERUSER_CONSISTENCY_LEVEL :
CassandraAuthorizer.authReadConsistencyLevel();
}
Expand Down
113 changes: 113 additions & 0 deletions src/java/org/apache/cassandra/auth/IDefaultRoleInitializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* 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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.tcm.ClusterMetadata;

import static org.apache.cassandra.auth.CassandraRoleManager.escape;

/**
* Creates the initial role on a cluster which has no roles yet, so that there is some
* identity available to authenticate as and grant permissions from. Selected via
* {@code default_role_initializer} option in cassandra.yaml and instantiated by
* {@link AuthConfig#applyAuth()}
* <p>
* Implementations decide both what the role is called and how it is authenticated.
* See {@link PasswordDefaultRoleInitializer} which gives the role a password, and
* {@link MutualTlsDefaultRoleInitializer} which gives no password and instead
* maps a client certificate identity to itself.
*/
public interface IDefaultRoleInitializer
{
Logger logger = LoggerFactory.getLogger(IDefaultRoleInitializer.class);

/**
* Creates the default role.
* When using this in connection with CassandraRoleManager, every node runs this independently during initial
* startup so implementations must write at {@link CassandraRoleManager#consistencyForRoleWrite(String)} to
* avoid concurrent duplicate creation and must use {@code USING TIMESTAMP 0} so that any operator changes
* to the role later supersede it.
* <p>
* The caller retries on failure so this may be invoked more than once on a node: it must not fail
*
* @throws RequestExecutionException if not enough nodes are available
* yet which the caller treats as a signal to reschedule
*/
void createDefaultRole();

/**
* The name of the role {@link #createDefaultRole()} creates.
*/
String defaultRoleName();

/**
* Validates configuration of the IDefaultRoleInitializer implementation (if configurable).
* <p>
* Called by {@link AuthConfig#applyAuth()} after the authenticator, authorizer and role manager have been
* set, so implementations may inspect those to reject combinations which would leave the cluster with no
* usable login.
*
* @throws ConfigurationException when there is a configuration error.
*/
default void validateConfiguration() throws ConfigurationException
{
}

/*
* Create the default superuser role to bootstrap role creation on a clean system. Preemptively
* gives the role the default password so PasswordAuthenticator can be used to log in (if
* configured)
*/
default void setupDefaultRole()
{
if (ClusterMetadata.current().tokenMap.tokens().isEmpty())
throw new IllegalStateException("CassandraRoleManager skipped default role setup: no known tokens in ring");

try
{
if (!hasExistingRoles())
{
createDefaultRole();
}
}
catch (RequestExecutionException e)
{
logger.warn("CassandraRoleManager skipped default role setup: some nodes were not ready");
throw e;
}
}

default boolean hasExistingRoles()
{
// Try looking up the configured default role first, to avoid the range query if possible.
String defaultRoleQuery = String.format("SELECT * FROM %s.%s WHERE role = '%s'", SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES, escape(defaultRoleName()));
String allUsersQuery = String.format("SELECT * FROM %s.%s LIMIT 1", SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES);
return !QueryProcessor.process(defaultRoleQuery, ConsistencyLevel.ONE).isEmpty()
|| !QueryProcessor.process(defaultRoleQuery, ConsistencyLevel.QUORUM).isEmpty()
|| !QueryProcessor.process(allUsersQuery, ConsistencyLevel.QUORUM).isEmpty();
}
}
4 changes: 4 additions & 0 deletions src/java/org/apache/cassandra/auth/IRoleManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -330,4 +330,8 @@ default void dropIdentity(String identity)

}

default IDefaultRoleInitializer defaultRoleInitializer()
{
return PasswordDefaultRoleInitializer.instance;
}
}
Loading