Skip to content

Commit 281c1dc

Browse files
jcshepherdsmiklosovic
authored andcommitted
Enable IAuthenticator to declare supported and alterable role options
With negotiated authentication (CEP-50), nodes may be configured with multiple authenticators. Prior to this change, a number of areas in the code assumed that there was a single configured authenticator and contained logic that switched depending on the authenticator type. This logic won't work when multiple authenticators can be configured. This change eliminates most calls to DataDescriptor.getAuthenticator(), by enabling individual authenticators to declare the role attributes they support, requiring callers to specify the type of authenticator they're looking for, and directly returning whether the node can enforce authn or not rather than inferring it by the presence of an authenticator. Testing done: Unit tests for auth and config packages; d-tests for auth-related functionality (e.g. ColumnMasks). patch by Joel Shepherd; reviewed by Stefan Miklosovic, Andy Tolbert for CASSANDRA-20834
1 parent ba2af93 commit 281c1dc

14 files changed

Lines changed: 347 additions & 74 deletions

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
6.0-alpha2
2+
* Enable IAuthenticator to declare supported and alterable role options (CASSANDRA-20834)
23
* Avoid capturing lambda allocation in UnfilteredSerializer.deserializeRowBody (CASSANDRA-21289)
34
* Avoid Cell iterator allocation for alive rows in MetricsRecording transformation of ReadCommand (CASSANDRA-21288)
45
* Reuse a single TrackedDataInputPlus instance per UnfilteredSerializer (CASSANDRA-21296)

src/java/org/apache/cassandra/auth/AuthConfig.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import java.util.List;
2222

23+
import com.google.common.annotations.VisibleForTesting;
24+
2325
import org.slf4j.Logger;
2426
import org.slf4j.LoggerFactory;
2527

@@ -38,6 +40,16 @@ public final class AuthConfig
3840

3941
private static boolean initialized;
4042

43+
/**
44+
* Resets the initialized flag, enabling AuthConfig to be reconfigured multiple times within a single
45+
* test case.
46+
*/
47+
@VisibleForTesting
48+
static void reset()
49+
{
50+
initialized = false;
51+
}
52+
4153
public static void applyAuth()
4254
{
4355
// some tests need this

src/java/org/apache/cassandra/auth/CassandraRoleManager.java

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.util.ArrayList;
2222
import java.util.Arrays;
2323
import java.util.Collections;
24-
import java.util.EnumSet;
2524
import java.util.HashMap;
2625
import java.util.HashSet;
2726
import java.util.List;
@@ -91,29 +90,16 @@
9190
import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
9291

9392
/**
94-
* Responsible for the creation, maintenance and deletion of roles
95-
* for the purposes of authentication and authorization.
96-
* Role data is stored internally, using the roles and role_members tables
97-
* in the system_auth keyspace.
93+
* Responsible for the creation, maintenance and deletion of roles for the purposes of authentication and
94+
* authorization. Role data is stored internally, using the roles and role_members tables in the system_auth
95+
* keyspace.
9896
*
99-
* Additionally, if org.apache.cassandra.auth.PasswordAuthenticator is used,
100-
* encrypted passwords are also stored in the system_auth.roles table. This
101-
* coupling between the IAuthenticator and IRoleManager implementations exists
102-
* because setting a role's password via CQL is done with a CREATE ROLE or
103-
* ALTER ROLE statement, the processing of which is handled by IRoleManager.
104-
* As IAuthenticator is concerned only with credentials checking and has no
105-
* means to modify passwords, PasswordAuthenticator depends on
106-
* CassandraRoleManager for those functions.
107-
*
108-
* Alternative IAuthenticator implementations may be used in conjunction with
109-
* CassandraRoleManager, but WITH PASSWORD = 'password' will not be supported
110-
* in CREATE/ALTER ROLE statements.
111-
*
112-
* Such a configuration could be implemented using a custom IRoleManager that
113-
* extends CassandraRoleManager and which includes Option.PASSWORD in the {@code Set<Option>}
114-
* returned from supportedOptions/alterableOptions. Any additional processing
115-
* of the password itself (such as storing it in an alternative location) would
116-
* be added in overridden createRole and alterRole implementations.
97+
* Authenticators (implementations of {@link IAuthenticator}) can specify additional attributes to be stored.
98+
* For example, {@link org.apache.cassandra.auth.PasswordAuthenticator}, stores encrypted passwords in the
99+
* system_auth.roles table. This coupling between the IAuthenticator and IRoleManager implementations exists because
100+
* setting a role's password via CQL is done with a CREATE ROLE or ALTER ROLE statement, the processing of which is
101+
* handled by IRoleManager. Authenticators depend on CassandraRoleManager for those functions because IAuthenticator
102+
* is concerned only with credentials checking and has no means to directly modify passwords.
117103
*/
118104
public class CassandraRoleManager implements IRoleManager, CassandraRoleManagerMBean
119105
{
@@ -123,8 +109,24 @@ public class CassandraRoleManager implements IRoleManager, CassandraRoleManagerM
123109
public static final String DEFAULT_SUPERUSER_NAME = "cassandra";
124110
public static final String DEFAULT_SUPERUSER_PASSWORD = "cassandra";
125111

112+
/**
113+
* Role options which are supported for all authentication mechanisms. IAuthenticator implementations can declare
114+
* additional supported role options via {@link IAuthenticator#getSupportedRoleOptions()}.
115+
*/
116+
@VisibleForTesting
117+
static final Set<Option> DEFAULT_SUPPORTED_ROLE_OPTIONS = Set.of(Option.LOGIN, Option.SUPERUSER);
118+
119+
/**
120+
* User-alterable role options which are supported for all authentication mechanisms. IAuthenticator
121+
* implementations can declare additional alterable role options via
122+
* {@link IAuthenticator#getAlterableRoleOptions()}.
123+
*/
124+
@VisibleForTesting
125+
static final Set<Option> DEFAULT_ALTERABLE_ROLE_OPTIONS = Set.of();
126+
126127
@VisibleForTesting
127128
static final String PARAM_INVALID_ROLE_DISCONNECT_TASK_PERIOD = "invalid_role_disconnect_task_period";
129+
128130
@VisibleForTesting
129131
static final String PARAM_INVALID_ROLE_DISCONNECT_TASK_MAX_JITTER = "invalid_role_disconnect_task_max_jitter";
130132

@@ -191,17 +193,21 @@ public CassandraRoleManager()
191193

192194
public CassandraRoleManager(Map<String, String> parameters)
193195
{
194-
Set<Option> allowedOptions = DatabaseDescriptor.getAuthenticator() instanceof PasswordAuthenticator
195-
? EnumSet.of(Option.LOGIN, Option.SUPERUSER, Option.PASSWORD, Option.HASHED_PASSWORD, Option.GENERATED_PASSWORD, Option.GENERATED_NAME)
196-
: EnumSet.of(Option.LOGIN, Option.SUPERUSER);
196+
Set<Option> supportedOptions = Stream.concat(
197+
DEFAULT_SUPPORTED_ROLE_OPTIONS.stream(),
198+
DatabaseDescriptor.getAuthenticator().getSupportedRoleOptions().stream()
199+
.filter(Objects::nonNull))
200+
.collect(Collectors.toSet());
197201

198202
if (Guardrails.roleNamePolicy.getGenerator() != NoOpGenerator.INSTANCE)
199-
allowedOptions.add(Option.OPTIONS);
203+
supportedOptions.add(Option.OPTIONS);
204+
205+
this.supportedOptions = Set.copyOf(supportedOptions);
200206

201-
supportedOptions = ImmutableSet.copyOf(allowedOptions);
202-
alterableOptions = DatabaseDescriptor.getAuthenticator() instanceof PasswordAuthenticator
203-
? ImmutableSet.of(Option.PASSWORD, Option.HASHED_PASSWORD, Option.GENERATED_PASSWORD)
204-
: ImmutableSet.<Option>of();
207+
alterableOptions = Stream.concat(DEFAULT_ALTERABLE_ROLE_OPTIONS.stream(),
208+
DatabaseDescriptor.getAuthenticator().getAlterableRoleOptions().stream()
209+
.filter(Objects::nonNull))
210+
.collect(Collectors.toUnmodifiableSet());
205211

206212
// Inherit parsing and validation from existing config parser
207213
invalidClientDisconnectPeriodMillis = new DurationSpec.LongMillisecondsBound(parameters.getOrDefault(PARAM_INVALID_ROLE_DISCONNECT_TASK_PERIOD, "0h")).toMilliseconds();
@@ -505,8 +511,8 @@ public boolean isExistingRole(RoleResource role)
505511

506512
public Set<? extends IResource> protectedResources()
507513
{
508-
return ImmutableSet.of(DataResource.table(SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES),
509-
DataResource.table(SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLE_MEMBERS));
514+
return Set.of(DataResource.table(SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES),
515+
DataResource.table(SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLE_MEMBERS));
510516
}
511517

512518
public void validateConfiguration() throws ConfigurationException

src/java/org/apache/cassandra/auth/IAuthenticator.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,29 @@ default boolean supportsEarlyAuthentication()
6666
*/
6767
Set<? extends IResource> protectedResources();
6868

69+
/**
70+
* Additional set of IRoleManager.Options used by this authenticator and supported by CREATE ROLE and ALTER ROLE
71+
* statements. These are in addition to any default options supported by the role manager.
72+
*
73+
* @return A set of IRoleManager.Options that this authenticator requires support for.
74+
*/
75+
default Set<IRoleManager.Option> getSupportedRoleOptions()
76+
{
77+
return Set.of();
78+
}
79+
80+
/**
81+
* Additional set of IRoleManager.Options used by this authenticator that users are allowed to alter via
82+
* ALTER ROLE statements. These are in addition to any default alterable options supported by the role manager.
83+
* Alterable role options must also be supported role options.
84+
*
85+
* @return A set of supported role options that users are allowed to alter.
86+
*/
87+
default Set<IRoleManager.Option> getAlterableRoleOptions()
88+
{
89+
return Set.of();
90+
}
91+
6992
/**
7093
* Validates configuration of IAuthenticator implementation (if configurable).
7194
*

src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,16 @@ public class MutualTlsAuthenticator implements IAuthenticator
7878
private static final Logger logger = LoggerFactory.getLogger(MutualTlsAuthenticator.class);
7979
private static final NoSpamLogger nospamLogger = NoSpamLogger.getLogger(logger, 1L, TimeUnit.MINUTES);
8080
private static final String VALIDATOR_CLASS_NAME = "validator_class_name";
81-
private static final String CACHE_NAME = "IdentitiesCache";
81+
8282
private final IdentityCache identityCache = new IdentityCache();
8383
private final MutualTlsCertificateValidator certificateValidator;
8484
private static final Set<AuthenticationMode> AUTHENTICATION_MODES = Collections.singleton(MTLS);
8585
private final MutualTlsCertificateValidityPeriodValidator certificateValidityPeriodValidator;
8686
private final DurationSpec.IntMinutesBound certificateValidityWarnThreshold;
8787

88+
@VisibleForTesting
89+
static final String CACHE_NAME = "IdentitiesCache";
90+
8891
// key for the 'identity' value in AuthenticatedUser metadata map.
8992
public static final String METADATA_IDENTITY_KEY = "identity";
9093

src/java/org/apache/cassandra/auth/NetworkPermissionsCache.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public NetworkPermissionsCache(INetworkAuthorizer authorizer)
3636
DatabaseDescriptor::getRolesCacheActiveUpdate,
3737
authorizer::authorize,
3838
authorizer.bulkLoader(),
39-
() -> DatabaseDescriptor.getAuthenticator().requireAuthentication());
39+
DatabaseDescriptor::isAuthenticationRequired);
4040

4141
MBeanWrapper.instance.registerMBean(this, MBEAN_NAME_BASE + DEPRECATED_CACHE_NAME);
4242
}

src/java/org/apache/cassandra/auth/PasswordAuthenticator.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121
import java.nio.charset.StandardCharsets;
2222
import java.util.Arrays;
2323
import java.util.Collections;
24+
import java.util.EnumSet;
2425
import java.util.HashMap;
2526
import java.util.Map;
2627
import java.util.Set;
2728
import java.util.function.Supplier;
2829

2930
import com.google.common.annotations.VisibleForTesting;
30-
import com.google.common.collect.ImmutableSet;
3131
import com.google.common.collect.Lists;
3232

3333
import org.mindrot.jbcrypt.BCrypt;
@@ -76,6 +76,19 @@ public class PasswordAuthenticator implements IAuthenticator, AuthCache.BulkLoad
7676
public static final String PASSWORD_KEY = "password";
7777
private static final Set<AuthenticationMode> AUTHENTICATION_MODES = Collections.singleton(AuthenticationMode.PASSWORD);
7878

79+
@VisibleForTesting
80+
static final Set<IRoleManager.Option> SUPPORTED_ROLE_OPTIONS =
81+
EnumSet.of(IRoleManager.Option.PASSWORD,
82+
IRoleManager.Option.HASHED_PASSWORD,
83+
IRoleManager.Option.GENERATED_PASSWORD,
84+
IRoleManager.Option.GENERATED_NAME);
85+
86+
@VisibleForTesting
87+
static final Set<IRoleManager.Option> ALTERABLE_ROLE_OPTIONS =
88+
EnumSet.of(IRoleManager.Option.PASSWORD,
89+
IRoleManager.Option.HASHED_PASSWORD,
90+
IRoleManager.Option.GENERATED_PASSWORD);
91+
7992
static final byte NUL = 0;
8093
private SelectStatement authenticateStatement;
8194

@@ -87,7 +100,26 @@ public PasswordAuthenticator()
87100
AuthCacheService.instance.register(cache);
88101
}
89102

103+
/**
104+
* {@inheritDoc}
105+
*/
106+
@Override
107+
public Set<IRoleManager.Option> getSupportedRoleOptions()
108+
{
109+
return SUPPORTED_ROLE_OPTIONS;
110+
}
111+
112+
/**
113+
* {@inheritDoc}
114+
*/
115+
@Override
116+
public Set<IRoleManager.Option> getAlterableRoleOptions()
117+
{
118+
return ALTERABLE_ROLE_OPTIONS;
119+
}
120+
90121
// No anonymous access.
122+
@Override
91123
public boolean requireAuthentication()
92124
{
93125
return true;
@@ -207,7 +239,7 @@ ResultMessage.Rows select(SelectStatement statement, QueryOptions options)
207239
public Set<DataResource> protectedResources()
208240
{
209241
// Also protected by CassandraRoleManager, but the duplication doesn't hurt and is more explicit
210-
return ImmutableSet.of(DataResource.table(SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES));
242+
return Set.of(DataResource.table(SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES));
211243
}
212244

213245
public void validateConfiguration() throws ConfigurationException
@@ -347,6 +379,7 @@ private CredentialsCache(PasswordAuthenticator authenticator)
347379
// invalidate the key if the sentinel is loaded during a refresh
348380
}
349381

382+
@Override
350383
public void invalidateCredentials(String roleName)
351384
{
352385
invalidate(roleName);

src/java/org/apache/cassandra/auth/Roles.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public class Roles
3636

3737
private static final Role NO_ROLE = new Role("", false, false, Collections.emptyMap(), Collections.emptySet());
3838

39-
public static final RolesCache cache = new RolesCache(DatabaseDescriptor.getRoleManager(), () -> DatabaseDescriptor.getAuthenticator().requireAuthentication());
39+
public static final RolesCache cache = new RolesCache(DatabaseDescriptor.getRoleManager(), DatabaseDescriptor::isAuthenticationRequired);
4040

4141
/** Use {@link AuthCacheService#initializeAndRegisterCaches} rather than calling this directly */
4242
public static void init()

src/java/org/apache/cassandra/config/DatabaseDescriptor.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2037,16 +2037,52 @@ public static void setCryptoProvider(AbstractCryptoProvider cryptoProvider)
20372037
DatabaseDescriptor.cryptoProvider = cryptoProvider;
20382038
}
20392039

2040+
/**
2041+
* Returns the authenticator configured for this node.
2042+
*/
20402043
public static IAuthenticator getAuthenticator()
20412044
{
20422045
return authenticator;
20432046
}
20442047

2048+
/**
2049+
* Returns an authenticator configured for this node, if it is of the requested type.
2050+
* @param clazz The class of the requested authenticator: e.g. PasswordAuthenticator.class.
2051+
* @return An Optional of the configured authenticator, if it is of the requested type; otherwise
2052+
* returns an empty Optional.
2053+
*/
2054+
public static <T extends IAuthenticator> Optional<T> getAuthenticator(Class<T> clazz)
2055+
{
2056+
return hasAuthenticator(clazz) ? Optional.of(clazz.cast(authenticator)) : Optional.empty();
2057+
}
2058+
2059+
/**
2060+
* Sets the authenticator used by this node to authenticate clients.
2061+
*/
20452062
public static void setAuthenticator(IAuthenticator authenticator)
20462063
{
20472064
DatabaseDescriptor.authenticator = authenticator;
20482065
}
20492066

2067+
/**
2068+
* Indicates if this node uses an authenticator that requires authentication.
2069+
*/
2070+
public static boolean isAuthenticationRequired()
2071+
{
2072+
return authenticator.requireAuthentication();
2073+
}
2074+
2075+
/**
2076+
* Indicates if this node is configured with an authenticator of the specified type.
2077+
* @param clazz The class of the authenticator.
2078+
* @return True if this node has an authenticator of the specified type, false otherwise.
2079+
*/
2080+
private static boolean hasAuthenticator(Class<? extends IAuthenticator> clazz)
2081+
{
2082+
return clazz.isAssignableFrom(authenticator.getClass());
2083+
}
2084+
2085+
20502086
public static IAuthorizer getAuthorizer()
20512087
{
20522088
return authorizer;

src/java/org/apache/cassandra/db/virtual/CredentialsCacheKeysTable.java

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
import java.util.Optional;
2121

22-
import org.apache.cassandra.auth.IAuthenticator;
2322
import org.apache.cassandra.auth.PasswordAuthenticator;
2423
import org.apache.cassandra.config.DatabaseDescriptor;
2524
import org.apache.cassandra.db.marshal.UTF8Type;
@@ -31,7 +30,7 @@ final class CredentialsCacheKeysTable extends AbstractMutableVirtualTable
3130
private static final String ROLE = "role";
3231

3332
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
34-
private final Optional<PasswordAuthenticator> passwordAuthenticatorOptional;
33+
private final Optional<PasswordAuthenticator> maybePasswordAuthenticator;
3534

3635
CredentialsCacheKeysTable(String keyspace)
3736
{
@@ -42,18 +41,14 @@ final class CredentialsCacheKeysTable extends AbstractMutableVirtualTable
4241
.addPartitionKeyColumn(ROLE, UTF8Type.instance)
4342
.build());
4443

45-
IAuthenticator authenticator = DatabaseDescriptor.getAuthenticator();
46-
if (authenticator instanceof PasswordAuthenticator)
47-
this.passwordAuthenticatorOptional = Optional.of((PasswordAuthenticator) authenticator);
48-
else
49-
this.passwordAuthenticatorOptional = Optional.empty();
44+
maybePasswordAuthenticator = DatabaseDescriptor.getAuthenticator(PasswordAuthenticator.class);
5045
}
5146

5247
public DataSet data()
5348
{
5449
SimpleDataSet result = new SimpleDataSet(metadata());
5550

56-
passwordAuthenticatorOptional
51+
maybePasswordAuthenticator
5752
.ifPresent(passwordAuthenticator -> passwordAuthenticator.getCredentialsCache().getAll()
5853
.forEach((roleName, ignored) -> result.row(roleName)));
5954

@@ -65,14 +60,14 @@ protected void applyPartitionDeletion(ColumnValues partitionKey)
6560
{
6661
String roleName = partitionKey.value(0);
6762

68-
passwordAuthenticatorOptional
63+
maybePasswordAuthenticator
6964
.ifPresent(passwordAuthenticator -> passwordAuthenticator.getCredentialsCache().invalidate(roleName));
7065
}
7166

7267
@Override
7368
public void truncate()
7469
{
75-
passwordAuthenticatorOptional
70+
maybePasswordAuthenticator
7671
.ifPresent(passwordAuthenticator -> passwordAuthenticator.getCredentialsCache().invalidate());
7772
}
7873
}

0 commit comments

Comments
 (0)