diff --git a/CHANGES.txt b/CHANGES.txt index 9c7ca4e5d9..a6107c29a8 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 5.1 + * Allow CQL client certificate authentication to work without sending an AUTHENTICATE request (CASSANDRA-18857) * Extend nodetool tpstats and system_views.thread_pools with detailed pool parameters (CASSANDRA-19289) * Remove dependency on Sigar in favor of OSHI (CASSANDRA-16565) * Simplify the bind marker and Term logic (CASSANDRA-18813) diff --git a/src/java/org/apache/cassandra/auth/AuthCacheService.java b/src/java/org/apache/cassandra/auth/AuthCacheService.java index f6ee02e3a0..2ebc8a9924 100644 --- a/src/java/org/apache/cassandra/auth/AuthCacheService.java +++ b/src/java/org/apache/cassandra/auth/AuthCacheService.java @@ -77,4 +77,18 @@ public class AuthCacheService Roles.init(); } } + + /** + * Invalidates all registered caches, which is useful for tests that make changes to roles, identities, cidr + * permissions and so on. + */ + @VisibleForTesting + public synchronized void invalidateCaches() + { + logger.info("Bulk invalidating {} auth cache(s)", caches.size()); + for (AuthCache, ?> cache : caches) + { + cache.invalidate(); + } + } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/auth/IAuthenticator.java b/src/java/org/apache/cassandra/auth/IAuthenticator.java index 349aaf4fc2..89c08243f2 100644 --- a/src/java/org/apache/cassandra/auth/IAuthenticator.java +++ b/src/java/org/apache/cassandra/auth/IAuthenticator.java @@ -28,11 +28,31 @@ import org.apache.cassandra.exceptions.ConfigurationException; public interface IAuthenticator { /** - * Whether or not the authenticator requires explicit login. + * Whether the authenticator requires explicit login. * If false will instantiate user with AuthenticatedUser.ANONYMOUS_USER. */ boolean requireAuthentication(); + /** + * Whether the authenticator supports 'early' authentication, meaning that it can be authenticated without + * the need to send an AUTHENTICATE request to the client after receiveing a STARTUP message from the client. + *
+ * An example use case of this would be if the client could be authenticated using certificates present + * on a TLS-encrypted connection, as is done in {@link MutualTlsAuthenticator}. In this case, an AUTHENTICATE + * message is not needed because the client can be identified by information present in its provided certificate. + *
+ * If an authenticator supports requires early authentication, this should be true;
+ * otherwise if a client cannot authenticate using its connection details (e.g. a certificate), an AUTHENTICATE
+ * will be sent to the client. This may confuse a driver implementation into thinking an authenticator is needed,
+ * when instead the real issue is that something is missing on the connection, such as a certificate.
+ *
+ * If false (default behavior), an AUTHENTICATE request will be sent to the client.
+ */
+ default boolean supportsEarlyAuthentication()
+ {
+ return false;
+ }
+
/**
* Set of resources that should be made inaccessible to users and only accessible internally.
*
@@ -145,5 +165,19 @@ public interface IAuthenticator
* @throws AuthenticationException
*/
public AuthenticatedUser getAuthenticatedUser() throws AuthenticationException;
+
+ /**
+ * Whether it is determined that an AUTHENTICATE message should be sent to properly negotiate.
+ * This is used in conjunction with {@link #supportsEarlyAuthentication()} in determining if authentication
+ * can be done early, for example if the client can be authenticated using client certificates when handling
+ * a STARTUP message.
+ *
+ * If true (default behavior), an AUTHENTICATE message will be sent in response to a STARTUP,
+ * otherwise {@link #evaluateResponse(byte[])} will be called with an empty byte array when handling STARTUP.
+ */
+ default boolean shouldSendAuthenticateMessage()
+ {
+ return true;
+ }
}
}
diff --git a/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java b/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java
index 01ff6919fa..8c379a6dbd 100644
--- a/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java
@@ -90,6 +90,12 @@ public class MutualTlsAuthenticator implements IAuthenticator
return true;
}
+ @Override
+ public boolean supportsEarlyAuthentication()
+ {
+ return true;
+ }
+
@Override
public Set extends IResource> protectedResources()
{
@@ -149,6 +155,12 @@ public class MutualTlsAuthenticator implements IAuthenticator
return null;
}
+ @Override
+ public boolean shouldSendAuthenticateMessage()
+ {
+ return false;
+ }
+
@Override
public boolean isComplete()
{
@@ -158,6 +170,11 @@ public class MutualTlsAuthenticator implements IAuthenticator
@Override
public AuthenticatedUser getAuthenticatedUser() throws AuthenticationException
{
+ if (clientCertificateChain == null || clientCertificateChain.length == 0)
+ {
+ throw new AuthenticationException("No certificate present on connection");
+ }
+
if (!certificateValidator.isValidCertificate(clientCertificateChain))
{
String message = "Invalid or not supported certificate";
diff --git a/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java b/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java
index 4c86f6a3fe..f10b7eb1a0 100644
--- a/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java
@@ -46,13 +46,21 @@ public class MutualTlsWithPasswordFallbackAuthenticator extends PasswordAuthenti
mutualTlsAuthenticator.setup();
}
+ @Override
+ public boolean supportsEarlyAuthentication()
+ {
+ return true;
+ }
+
@Override
public SaslNegotiator newSaslNegotiator(InetAddress clientAddress, Certificate[] certificates)
{
if (certificates == null || certificates.length == 0)
{
+ // If no certificates present, fallback to PasswordAuthentication
return newSaslNegotiator(clientAddress);
}
+ // Otherwise attempt to authenticate using the client-provided certificate.
return mutualTlsAuthenticator.newSaslNegotiator(clientAddress, certificates);
}
diff --git a/src/java/org/apache/cassandra/transport/SimpleClient.java b/src/java/org/apache/cassandra/transport/SimpleClient.java
index 0fa3a7af31..8209f5b2f3 100644
--- a/src/java/org/apache/cassandra/transport/SimpleClient.java
+++ b/src/java/org/apache/cassandra/transport/SimpleClient.java
@@ -416,12 +416,13 @@ public class SimpleClient implements Closeable
}
break;
case SUPPORTED:
+ case ERROR:
// just pass through
results.add(response);
break;
default:
throw new ProtocolException(String.format("Unexpected %s response expecting " +
- "READY, AUTHENTICATE or SUPPORTED",
+ "READY, AUTHENTICATE, ERROR or SUPPORTED",
response.header.type));
}
}
diff --git a/src/java/org/apache/cassandra/transport/messages/AuthResponse.java b/src/java/org/apache/cassandra/transport/messages/AuthResponse.java
index 81002c8281..e24b3393a1 100644
--- a/src/java/org/apache/cassandra/transport/messages/AuthResponse.java
+++ b/src/java/org/apache/cassandra/transport/messages/AuthResponse.java
@@ -20,11 +20,6 @@ package org.apache.cassandra.transport.messages;
import java.nio.ByteBuffer;
import io.netty.buffer.ByteBuf;
-import org.apache.cassandra.auth.AuthEvents;
-import org.apache.cassandra.auth.AuthenticatedUser;
-import org.apache.cassandra.auth.IAuthenticator;
-import org.apache.cassandra.exceptions.AuthenticationException;
-import org.apache.cassandra.metrics.ClientMetrics;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.*;
@@ -71,29 +66,16 @@ public class AuthResponse extends Message.Request
@Override
protected Response execute(QueryState queryState, long queryStartNanoTime, boolean traceRequest)
{
- try
+ return AuthUtil.handleLogin(connection, queryState, token, (negotiationComplete, challenge) ->
{
- IAuthenticator.SaslNegotiator negotiator = ((ServerConnection) connection).getSaslNegotiator(queryState);
- byte[] challenge = negotiator.evaluateResponse(token);
- if (negotiator.isComplete())
+ if (negotiationComplete)
{
- AuthenticatedUser user = negotiator.getAuthenticatedUser();
- queryState.getClientState().login(user);
- ClientMetrics.instance.markAuthSuccess();
- AuthEvents.instance.notifyAuthSuccess(queryState);
- // authentication is complete, send a ready message to the client
return new AuthSuccess(challenge);
}
else
{
return new AuthChallenge(challenge);
}
- }
- catch (AuthenticationException e)
- {
- ClientMetrics.instance.markAuthFailure();
- AuthEvents.instance.notifyAuthFailure(queryState, e);
- return ErrorMessage.fromException(e);
- }
+ });
}
}
diff --git a/src/java/org/apache/cassandra/transport/messages/AuthUtil.java b/src/java/org/apache/cassandra/transport/messages/AuthUtil.java
new file mode 100644
index 0000000000..1009984eac
--- /dev/null
+++ b/src/java/org/apache/cassandra/transport/messages/AuthUtil.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.transport.messages;
+
+import java.util.function.BiFunction;
+
+import org.apache.cassandra.auth.AuthEvents;
+import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.auth.IAuthenticator;
+import org.apache.cassandra.exceptions.AuthenticationException;
+import org.apache.cassandra.metrics.ClientMetrics;
+import org.apache.cassandra.service.QueryState;
+import org.apache.cassandra.transport.Connection;
+import org.apache.cassandra.transport.Message.Response;
+import org.apache.cassandra.transport.ServerConnection;
+
+public final class AuthUtil
+{
+
+ /**
+ * Attempts to authenticate a user on the current connection by negotiating with the underlying
+ * {@link org.apache.cassandra.auth.IAuthenticator.SaslNegotiator} using the given state of the connection
+ * (e.g. client certificates), the provided query state and the provided token.
+ *
+ * If unsuccessful an {@link ErrorMessage} is returned and the authentication failure is recorded. + *
+ * If negotiation is complete, a successful login is recorded and the given function is called with
+ * true and the challenge returned from
+ * {@link org.apache.cassandra.auth.IAuthenticator.SaslNegotiator#evaluateResponse(byte[])}.
+ *
+ * If negotiation is incomplete,
+ * Note to use this appropriately, {@link #requireNetwork} should be given a server configurator configured
+ * with {@link Server.Builder#withTlsEncryptionPolicy(EncryptionOptions.TlsEncryptionPolicy)} using
+ * {@link org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy#ENCRYPTED}.
+ */
+ protected static void requireNativeProtocolClientEncryption()
+ {
+ DatabaseDescriptor.updateNativeProtocolEncryptionOptions((encryptionOptions ->
+ encryptionOptions.withEnabled(true)
+ .withKeyStore("test/conf/cassandra_ssl_test.keystore")
+ .withKeyStorePassword("cassandra")
+ .withTrustStore("test/conf/cassandra_ssl_test.truststore")
+ .withTrustStorePassword("cassandra")
+ .withRequireEndpointVerification(false)
+ .withRequireClientAuth(EncryptionOptions.ClientAuth.OPTIONAL)));
+ }
+
+
/**
* Initialize Native Transport for test that need it.
*/
@@ -2205,7 +2233,8 @@ public abstract class CQLTester
}
@FunctionalInterface
- public interface CheckedFunction {
+ public interface CheckedFunction
+ {
void apply() throws Throwable;
}
diff --git a/test/unit/org/apache/cassandra/transport/AuthenticationTest.java b/test/unit/org/apache/cassandra/transport/AuthenticationTest.java
new file mode 100644
index 0000000000..b770b8e37a
--- /dev/null
+++ b/test/unit/org/apache/cassandra/transport/AuthenticationTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.transport;
+
+import java.io.IOException;
+import java.util.function.Consumer;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.exceptions.AuthenticationException;
+import org.apache.cassandra.service.QueryState;
+import org.apache.cassandra.transport.messages.AuthResponse;
+import org.apache.cassandra.transport.messages.AuthSuccess;
+import org.apache.cassandra.transport.messages.AuthenticateMessage;
+import org.apache.cassandra.transport.messages.ErrorMessage;
+import org.apache.cassandra.transport.messages.StartupMessage;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static org.apache.cassandra.auth.AuthTestUtils.getToken;
+import static org.apache.cassandra.transport.messages.StartupMessage.CQL_VERSION;
+import static org.junit.Assert.fail;
+import static org.psjava.util.AssertStatus.assertTrue;
+
+/**
+ * Verifies that {@link StartupMessage#execute(QueryState, long)} will prompt a client for authentication if an
+ * authenticator is configured. For this test {@link org.apache.cassandra.auth.PasswordAuthenticator} is used.
+ */
+public class AuthenticationTest extends CQLTester
+{
+
+ @BeforeClass
+ public static void setup()
+ {
+ requireNetwork();
+ requireAuthentication();
+ }
+
+ @Before
+ public void initNetwork()
+ {
+ reinitializeNetwork();
+ }
+
+ @Test
+ public void testSuccessfulAuth()
+ {
+ testAuthResponse((client) -> {
+ AuthResponse authResponse = new AuthResponse(getToken("cassandra", "cassandra"));
+ Message.Response authResponseResponse = client.execute(authResponse);
+
+ if (!(authResponseResponse instanceof AuthSuccess))
+ {
+ fail("Expected an AUTH_SUCCESS in response to a AUTH_RESPONSE with default credentials, got: " + authResponseResponse);
+ }
+ });
+ }
+
+ @Test
+ public void testUnsuccessfulAuth()
+ {
+ testAuthResponse((client) -> {
+ AuthResponse authResponse = new AuthResponse(getToken("cassandra", "badpw"));
+ Message.Response response = client.execute(authResponse, false);
+
+ if (response instanceof ErrorMessage)
+ {
+ ErrorMessage errorMessage = (ErrorMessage) response;
+ assertTrue(errorMessage.error instanceof AuthenticationException, "Expected an AuthenticationException, got: " + errorMessage.error);
+ }
+ else
+ {
+ fail("Expected an ErrorMessage but got: " + response);
+ }
+ });
+ }
+
+ public void testAuthResponse(Consumer
+ * This authenticator has an interesting property such that its underlying
+ * {@link IAuthenticator.SaslNegotiator#shouldSendAuthenticateMessage()} will only return false if a client
+ * certificate is present, otherwise it will return true, which should cause Cassandra to return an AUTHENTICATE
+ * message in response to a STARTUP request sent by a client which will provoke the normal authentication flow.
+ */
+public class MutualTlsWithPasswordFallbackAuthenticatorEarlyAuthenticationTest extends EarlyAuthenticationTest
+{
+
+ @BeforeClass
+ public static void setup()
+ {
+ setupWithAuthenticator(new AuthTestUtils.LocalMutualTlsWithPasswordFallbackAuthenticator(authenticatorParams));
+ }
+
+ @Test
+ @Override
+ public void testNoClientCertificatePresented()
+ {
+ /*
+ * given server is configured with a Fallback password authenticator in that it supports early certificate
+ * authentication, but the sasl negotiator is determined based on the presence of client certificates
+ *
+ * When connecting without a client certificate, we expect the server to prompt us to authenticate.
+ */
+ testStartupResponse(false, startupResponse -> {
+ if (!(startupResponse instanceof AuthenticateMessage))
+ {
+ fail("Expected an AUTHENTICATE in response to a STARTUP, got: " + startupResponse);
+ }
+ });
+ }
+}
diff --git a/test/unit/org/apache/cassandra/transport/SimpleClientSslContextFactory.java b/test/unit/org/apache/cassandra/transport/SimpleClientSslContextFactory.java
new file mode 100644
index 0000000000..709cc50f70
--- /dev/null
+++ b/test/unit/org/apache/cassandra/transport/SimpleClientSslContextFactory.java
@@ -0,0 +1,68 @@
+/*
+ * 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.transport;
+
+import java.util.Map;
+import javax.net.ssl.SSLException;
+
+import io.netty.handler.ssl.CipherSuiteFilter;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import org.apache.cassandra.config.EncryptionOptions;
+import org.apache.cassandra.security.FileBasedSslContextFactory;
+
+/**
+ * A custom implementation of {@link FileBasedSslContextFactory} to be used by tests utilizing {@link SimpleClient}.
+ *
+ * Provides a subtly different implementation of {@link #createNettySslContext(EncryptionOptions.ClientAuth, SocketType, CipherSuiteFilter)}
+ * that only configures an {@link SslContext} for clients and most importantly only configures a key manager if an
+ * outbound keystore is configured, where the existing implementation always does this. This is useful for tests
+ * that try to create a client that uses encryption but does not provide a certificate.
+ */
+public class SimpleClientSslContextFactory extends FileBasedSslContextFactory
+{
+
+ public SimpleClientSslContextFactory(Mapfalse and the challenge is passed to the given function.
+ *
+ * @param connection The connection to authenticate
+ * @param queryState The current query state
+ * @param token The token provided in an {@link AuthResponse} from the client (or empty
+ * if not handling an AuthResponse).
+ * @param messageToSendBasedOnNegotiation Determines what response to return on based on whether sasl negotiation
+ * is complete (1st parameter) and the challenege token returned from the
+ * negotiator (2nd parameter).
+ * @return the response to send back to the client.
+ */
+ static Response handleLogin(Connection connection, QueryState queryState, byte[] token,
+ BiFunctiontrue.
+ */
+public class EarlyAuthenticationTest extends CQLTester
+{
+
+ // configures server with client encryption enabled.
+ static final Consumer