Skip to content

Commit 80837fe

Browse files
committed
Add support for Amazon Keyspaces
including different authentication means. Also fix logging of confidential data in SessionHolder
1 parent 027e414 commit 80837fe

19 files changed

+1027
-105
lines changed

.gitignore

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
# Created by https://www.toptal.com/developers/gitignore/api/macos,java,maven,intellij,eclipse
2-
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,java,maven,intellij,eclipse
1+
# Created by https://www.toptal.com/developers/gitignore/api/java,macos,maven,dotenv,eclipse,intellij
2+
# Edit at https://www.toptal.com/developers/gitignore?templates=java,macos,maven,dotenv,eclipse,intellij
3+
4+
### dotenv ###
5+
.env
36

47
### Eclipse ###
58
.metadata

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1010
- Add implementation for the method `CassandraPreparedStatement.setArray(int, Array)`.
1111
- Add a parameter `serialconsistency` to specify the default serial consistency level used by the connection.
1212
- Add support for the special CQL command `SERIAL CONSISTENCY [level]` in `CassandraStatement`.
13+
- Add support for Amazon Keyspaces:
14+
- Add specific `jdbc:cassandra:aws` protocol to ease connection to Amazon Keyspaces.
15+
- Add support for Amazon Signature V4 authentication provider.
16+
- Add support to retrieve connection password from Amazon Secrets manager.
1317
### Fixed
1418
- Do not try to register codecs again (if already done previously) on a pre-existing session to avoid warnings in logs.
19+
- Fix some logging in `SessionHolder` to not leak connection credentials.
1520

1621
## [4.13.1] - 2024-09-04
1722
### Fixed

pom.xml

+42
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@
120120

121121
<!-- Versions for dependencies -->
122122
<approvaltests.version>24.3.0</approvaltests.version>
123+
<aws-secretsmanager.version>2.29.1</aws-secretsmanager.version>
124+
<aws-sigv4-auth-cassandra.version>4.0.9</aws-sigv4-auth-cassandra.version>
123125
<checkstyle.version>9.3</checkstyle.version>
124126
<caffeine.version>2.9.3</caffeine.version>
125127
<cassandra-driver-krb5.version>3.0.0</cassandra-driver-krb5.version>
@@ -131,6 +133,7 @@
131133
<javax-jsr305.version>3.0.2</javax-jsr305.version>
132134
<semver4j.version>5.3.0</semver4j.version>
133135
<!-- Versions for test dependencies -->
136+
<dotenv.version>2.3.2</dotenv.version>
134137
<hamcrest.version>2.2</hamcrest.version>
135138
<junit5.version>5.10.3</junit5.version>
136139
<junit-platform.version>1.10.3</junit-platform.version>
@@ -225,6 +228,31 @@
225228
<version>${javax-jsr305.version}</version>
226229
</dependency>
227230

231+
<!-- AWS Auth provider support -->
232+
<dependency>
233+
<groupId>software.aws.mcs</groupId>
234+
<artifactId>aws-sigv4-auth-cassandra-java-driver-plugin</artifactId>
235+
<version>${aws-sigv4-auth-cassandra.version}</version>
236+
<exclusions>
237+
<!-- Dependencies already provided through import of AWS Secret manager below, exclude them here to
238+
ensure the Amazon Secrets manager client works with the appropriate dependencies versions. -->
239+
<exclusion>
240+
<artifactId>profiles</artifactId>
241+
<groupId>software.amazon.awssdk</groupId>
242+
</exclusion>
243+
<exclusion>
244+
<artifactId>auth</artifactId>
245+
<groupId>software.amazon.awssdk</groupId>
246+
</exclusion>
247+
</exclusions>
248+
</dependency>
249+
250+
<!-- AWS Secret manager -->
251+
<dependency>
252+
<groupId>software.amazon.awssdk</groupId>
253+
<artifactId>secretsmanager</artifactId>
254+
<version>${aws-secretsmanager.version}</version>
255+
</dependency>
228256

229257
<!-- Unit tests libraries -->
230258
<dependency>
@@ -288,6 +316,13 @@
288316
<version>${testcontainers.version}</version>
289317
<scope>test</scope>
290318
</dependency>
319+
<!-- LocalStack containers for tests using Amazon Secrets manager -->
320+
<dependency>
321+
<groupId>org.testcontainers</groupId>
322+
<artifactId>localstack</artifactId>
323+
<version>${testcontainers.version}</version>
324+
<scope>test</scope>
325+
</dependency>
291326
<!-- Astra SDK for integration tests with AstraDB -->
292327
<dependency>
293328
<groupId>com.datastax.astra</groupId>
@@ -323,6 +358,13 @@
323358
<version>${lombok.version}</version>
324359
<scope>test</scope>
325360
</dependency>
361+
<!-- Dotenv for tests only running locally (integration with DBaaS) -->
362+
<dependency>
363+
<groupId>io.github.cdimascio</groupId>
364+
<artifactId>dotenv-java</artifactId>
365+
<version>${dotenv.version}</version>
366+
<scope>test</scope>
367+
</dependency>
326368
</dependencies>
327369

328370
<build>

src/main/java/com/ing/data/cassandra/jdbc/CassandraConnection.java

+41
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
import java.sql.Connection;
4242
import java.sql.DatabaseMetaData;
43+
import java.sql.ResultSet;
4344
import java.sql.SQLException;
4445
import java.sql.SQLFeatureNotSupportedException;
4546
import java.sql.SQLNonTransientConnectionException;
@@ -66,6 +67,7 @@
6667
import static com.ing.data.cassandra.jdbc.CassandraResultSet.DEFAULT_CONCURRENCY;
6768
import static com.ing.data.cassandra.jdbc.CassandraResultSet.DEFAULT_HOLDABILITY;
6869
import static com.ing.data.cassandra.jdbc.CassandraResultSet.DEFAULT_TYPE;
70+
import static com.ing.data.cassandra.jdbc.utils.AwsUtil.AWS_KEYSPACES_HOSTS_REGEX;
6971
import static com.ing.data.cassandra.jdbc.utils.DriverUtil.safelyRegisterCodecs;
7072
import static com.ing.data.cassandra.jdbc.utils.DriverUtil.toStringWithoutSensitiveValues;
7173
import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.ALWAYS_AUTOCOMMIT;
@@ -125,6 +127,10 @@ public class CassandraConnection extends AbstractConnection implements Connectio
125127
* The connection URL.
126128
*/
127129
protected String url;
130+
/**
131+
* Whether the connection is bound to an Amazon Keyspaces database.
132+
*/
133+
private Boolean connectedToAmazonKeyspaces;
128134

129135
private final SessionHolder sessionHolder;
130136
private final Session cSession;
@@ -251,6 +257,41 @@ public CassandraConnection(final Session cSession, final String currentKeyspace,
251257
safelyRegisterCodecs(cSession, codecs);
252258
}
253259

260+
/**
261+
* Checks whether the current connection is not bound to an Amazon Keyspaces instance.
262+
* <p>
263+
* The main purpose of this method is to adapt the values returned by the methods of
264+
* {@link CassandraDatabaseMetaData} when the used database is Amazon Keyspaces since this one does not
265+
* implement all the Cassandra features. See
266+
* <a href="https://docs.aws.amazon.com/keyspaces/latest/devguide/keyspaces-vs-cassandra.html">comparison
267+
* between Cassandra and Keyspaces</a>.
268+
* </p>
269+
*
270+
* @return {@code true} if the connection is not bound to Amazon Keyspaces, {@code false} otherwise.
271+
*/
272+
public boolean isNotConnectedToAmazonKeyspaces() {
273+
if (this.connectedToAmazonKeyspaces == null) {
274+
this.connectedToAmazonKeyspaces = false;
275+
// Valid Amazon Keyspaces endpoints are available here:
276+
// https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.endpoints.html
277+
if (this.url.matches(AWS_KEYSPACES_HOSTS_REGEX)) {
278+
this.connectedToAmazonKeyspaces = true;
279+
} else {
280+
// Check for the existence of the keyspace 'system_schema_mcs' (specific to Amazon Keyspaces).
281+
try (final CassandraStatement stmt = (CassandraStatement) this.createStatement()) {
282+
stmt.execute(
283+
"SELECT keyspace_name FROM system_schema.keyspaces WHERE keyspace_name = 'system_schema_mcs'");
284+
final ResultSet rs = stmt.getResultSet();
285+
this.connectedToAmazonKeyspaces = rs != null && rs.next();
286+
} catch (final Exception e) {
287+
LOG.debug("Failed to check existing keyspaces to determine if the connection is bound to AWS: {}",
288+
e.getMessage());
289+
}
290+
}
291+
}
292+
return !this.connectedToAmazonKeyspaces;
293+
}
294+
254295
/**
255296
* Checks whether the connection is closed.
256297
*

src/main/java/com/ing/data/cassandra/jdbc/CassandraDataSource.java

+124
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.ing.data.cassandra.jdbc.optionset.Liquibase;
3131
import com.ing.data.cassandra.jdbc.utils.ContactPoint;
3232
import org.apache.commons.lang3.StringUtils;
33+
import software.amazon.awssdk.regions.Region;
3334

3435
import javax.sql.ConnectionPoolDataSource;
3536
import javax.sql.DataSource;
@@ -48,6 +49,9 @@
4849
import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.NO_INTERFACE;
4950
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.PROTOCOL;
5051
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_ACTIVE_PROFILE;
52+
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_AWS_REGION;
53+
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_AWS_SECRET_NAME;
54+
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_AWS_SECRET_REGION;
5155
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_CLOUD_SECURE_CONNECT_BUNDLE;
5256
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_COMPLIANCE_MODE;
5357
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_CONFIG_FILE;
@@ -70,6 +74,7 @@
7074
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_TCP_NO_DELAY;
7175
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_USER;
7276
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_USE_KERBEROS;
77+
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_USE_SIG_V4;
7378
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.createSubName;
7479

7580
/**
@@ -827,6 +832,125 @@ public void setConfigurationFile(final Path configurationFilePath) {
827832
this.setConfigurationFile(configurationFilePath.toString());
828833
}
829834

835+
/**
836+
* Gets the AWS region of the contact point of the Amazon Keyspaces instance.
837+
*
838+
* @return The AWS region.
839+
*/
840+
public String getAwsRegion() {
841+
return this.properties.getProperty(TAG_AWS_REGION);
842+
}
843+
844+
/**
845+
* Sets the AWS region of the contact point of the Amazon Keyspaces instance.
846+
*
847+
* @param region The string representation of the region.
848+
*/
849+
public void setAwsRegion(final String region) {
850+
this.setDataSourceProperty(TAG_AWS_REGION, region);
851+
}
852+
853+
/**
854+
* Sets the AWS region of the contact point of the Amazon Keyspaces instance.
855+
*
856+
* @param region The AWS region.
857+
*/
858+
public void setAwsRegion(final Region region) {
859+
if (region == null) {
860+
this.setAwsRegion((String) null);
861+
} else {
862+
this.setDataSourceProperty(TAG_AWS_REGION, region.id());
863+
}
864+
}
865+
866+
/**
867+
* Gets the AWS region of the Amazon Secret Manager in which the credentials of the user used for the connection
868+
* are stored. If not defined, the value is the one returned by {@link #getAwsRegion()}.
869+
*
870+
* @return .
871+
*/
872+
public String getAwsSecretRegion() {
873+
return (String) this.properties.getOrDefault(TAG_AWS_SECRET_REGION,
874+
this.properties.getProperty(TAG_AWS_REGION));
875+
}
876+
877+
/**
878+
* Sets the AWS region of the Amazon Secret Manager in which the credentials of the user used for the connection
879+
* are stored.
880+
*
881+
* @param region The string representation of the region.
882+
*/
883+
public void setAwsSecretRegion(final String region) {
884+
this.setDataSourceProperty(TAG_AWS_SECRET_REGION, region);
885+
}
886+
887+
/**
888+
* Sets the AWS region of the Amazon Secret Manager in which the credentials of the user used for the connection
889+
* are stored.
890+
*
891+
* @param region The AWS region.
892+
*/
893+
public void setAwsSecretRegion(final Region region) {
894+
if (region == null) {
895+
this.setAwsSecretRegion((String) null);
896+
} else {
897+
this.setDataSourceProperty(TAG_AWS_SECRET_REGION, region.id());
898+
}
899+
}
900+
901+
/**
902+
* Gets the name of the secret, stored in Amazon Secret Manager, containing the credentials of the user used for
903+
* the connection.
904+
*
905+
* @return The name of the secret.
906+
*/
907+
public String getAwsSecretName() {
908+
return this.properties.getProperty(TAG_AWS_SECRET_NAME);
909+
}
910+
911+
/**
912+
* Sets the name of the secret, stored in Amazon Secret Manager, containing the credentials of the user used for
913+
* the connection.
914+
*
915+
* @param secretName The name of the secret.
916+
*/
917+
public void setAwsSecretName(final String secretName) {
918+
this.setDataSourceProperty(TAG_AWS_SECRET_NAME, secretName);
919+
}
920+
921+
/**
922+
* Gets whether the Amazon Signature V4 auth provider is enabled.
923+
* <p>
924+
* The default value is {@code false}.
925+
* See <a href="https://docs.datastax.com/en/developer/java-driver/latest/manual/core/authentication/">
926+
* Authentication reference</a> and
927+
* <a href="https://github.com/aws/aws-sigv4-auth-cassandra-java-driver-plugin">
928+
* Amazon Signature V4 authenticator plugin for Java driver</a> for further information.
929+
* </p>
930+
*
931+
* @return {@code true} if the Amazon Signature V4 auth provider is enabled, {@code false} otherwise.
932+
*/
933+
public boolean isSigV4AuthProviderEnabled() {
934+
return (boolean) this.properties.getOrDefault(TAG_USE_SIG_V4, false);
935+
}
936+
937+
/**
938+
* Sets whether the Amazon Signature V4 auth provider is enabled.
939+
* <p>
940+
* This will enable the Amazon Signature V4 {@link AuthProvider} implementation for the connection using the
941+
* AWS region defined in the property {@link #setAwsRegion(String)} (or {@link #setAwsRegion(Region)}).
942+
* See <a href="https://docs.datastax.com/en/developer/java-driver/latest/manual/core/authentication/">
943+
* Authentication reference</a> and
944+
* <a href="https://github.com/aws/aws-sigv4-auth-cassandra-java-driver-plugin">
945+
* Amazon Signature V4 authenticator plugin for Java driver</a> for further information.
946+
* </p>
947+
*
948+
* @param enabled Whether the Amazon Signature V4 auth provider is enabled.
949+
*/
950+
public void setSigV4AuthProviderEnabled(final boolean enabled) {
951+
this.setDataSourceProperty(TAG_USE_SIG_V4, enabled);
952+
}
953+
830954
private void setDataSourceProperty(final String propertyName, final Object value) {
831955
if (value == null) {
832956
this.properties.remove(propertyName);

0 commit comments

Comments
 (0)