Skip to content

Commit

Permalink
Add support for Amazon Keyspaces
Browse files Browse the repository at this point in the history
including different authentication means.

Also fix logging of confidential data in SessionHolder
  • Loading branch information
maximevw committed Dec 15, 2024
1 parent 027e414 commit 80837fe
Show file tree
Hide file tree
Showing 19 changed files with 1,027 additions and 105 deletions.
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Created by https://www.toptal.com/developers/gitignore/api/macos,java,maven,intellij,eclipse
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,java,maven,intellij,eclipse
# Created by https://www.toptal.com/developers/gitignore/api/java,macos,maven,dotenv,eclipse,intellij
# Edit at https://www.toptal.com/developers/gitignore?templates=java,macos,maven,dotenv,eclipse,intellij

### dotenv ###
.env

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

## [4.13.1] - 2024-09-04
### Fixed
Expand Down
42 changes: 42 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@

<!-- Versions for dependencies -->
<approvaltests.version>24.3.0</approvaltests.version>
<aws-secretsmanager.version>2.29.1</aws-secretsmanager.version>
<aws-sigv4-auth-cassandra.version>4.0.9</aws-sigv4-auth-cassandra.version>
<checkstyle.version>9.3</checkstyle.version>
<caffeine.version>2.9.3</caffeine.version>
<cassandra-driver-krb5.version>3.0.0</cassandra-driver-krb5.version>
Expand All @@ -131,6 +133,7 @@
<javax-jsr305.version>3.0.2</javax-jsr305.version>
<semver4j.version>5.3.0</semver4j.version>
<!-- Versions for test dependencies -->
<dotenv.version>2.3.2</dotenv.version>
<hamcrest.version>2.2</hamcrest.version>
<junit5.version>5.10.3</junit5.version>
<junit-platform.version>1.10.3</junit-platform.version>
Expand Down Expand Up @@ -225,6 +228,31 @@
<version>${javax-jsr305.version}</version>
</dependency>

<!-- AWS Auth provider support -->
<dependency>
<groupId>software.aws.mcs</groupId>
<artifactId>aws-sigv4-auth-cassandra-java-driver-plugin</artifactId>
<version>${aws-sigv4-auth-cassandra.version}</version>
<exclusions>
<!-- Dependencies already provided through import of AWS Secret manager below, exclude them here to
ensure the Amazon Secrets manager client works with the appropriate dependencies versions. -->
<exclusion>
<artifactId>profiles</artifactId>
<groupId>software.amazon.awssdk</groupId>
</exclusion>
<exclusion>
<artifactId>auth</artifactId>
<groupId>software.amazon.awssdk</groupId>
</exclusion>
</exclusions>
</dependency>

<!-- AWS Secret manager -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>secretsmanager</artifactId>
<version>${aws-secretsmanager.version}</version>
</dependency>

<!-- Unit tests libraries -->
<dependency>
Expand Down Expand Up @@ -288,6 +316,13 @@
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<!-- LocalStack containers for tests using Amazon Secrets manager -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>localstack</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<!-- Astra SDK for integration tests with AstraDB -->
<dependency>
<groupId>com.datastax.astra</groupId>
Expand Down Expand Up @@ -323,6 +358,13 @@
<version>${lombok.version}</version>
<scope>test</scope>
</dependency>
<!-- Dotenv for tests only running locally (integration with DBaaS) -->
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
<version>${dotenv.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
41 changes: 41 additions & 0 deletions src/main/java/com/ing/data/cassandra/jdbc/CassandraConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLNonTransientConnectionException;
Expand All @@ -66,6 +67,7 @@
import static com.ing.data.cassandra.jdbc.CassandraResultSet.DEFAULT_CONCURRENCY;
import static com.ing.data.cassandra.jdbc.CassandraResultSet.DEFAULT_HOLDABILITY;
import static com.ing.data.cassandra.jdbc.CassandraResultSet.DEFAULT_TYPE;
import static com.ing.data.cassandra.jdbc.utils.AwsUtil.AWS_KEYSPACES_HOSTS_REGEX;
import static com.ing.data.cassandra.jdbc.utils.DriverUtil.safelyRegisterCodecs;
import static com.ing.data.cassandra.jdbc.utils.DriverUtil.toStringWithoutSensitiveValues;
import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.ALWAYS_AUTOCOMMIT;
Expand Down Expand Up @@ -125,6 +127,10 @@ public class CassandraConnection extends AbstractConnection implements Connectio
* The connection URL.
*/
protected String url;
/**
* Whether the connection is bound to an Amazon Keyspaces database.
*/
private Boolean connectedToAmazonKeyspaces;

private final SessionHolder sessionHolder;
private final Session cSession;
Expand Down Expand Up @@ -251,6 +257,41 @@ public CassandraConnection(final Session cSession, final String currentKeyspace,
safelyRegisterCodecs(cSession, codecs);
}

/**
* Checks whether the current connection is not bound to an Amazon Keyspaces instance.
* <p>
* The main purpose of this method is to adapt the values returned by the methods of
* {@link CassandraDatabaseMetaData} when the used database is Amazon Keyspaces since this one does not
* implement all the Cassandra features. See
* <a href="https://docs.aws.amazon.com/keyspaces/latest/devguide/keyspaces-vs-cassandra.html">comparison
* between Cassandra and Keyspaces</a>.
* </p>
*
* @return {@code true} if the connection is not bound to Amazon Keyspaces, {@code false} otherwise.
*/
public boolean isNotConnectedToAmazonKeyspaces() {
if (this.connectedToAmazonKeyspaces == null) {
this.connectedToAmazonKeyspaces = false;
// Valid Amazon Keyspaces endpoints are available here:
// https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.endpoints.html
if (this.url.matches(AWS_KEYSPACES_HOSTS_REGEX)) {
this.connectedToAmazonKeyspaces = true;
} else {
// Check for the existence of the keyspace 'system_schema_mcs' (specific to Amazon Keyspaces).
try (final CassandraStatement stmt = (CassandraStatement) this.createStatement()) {
stmt.execute(
"SELECT keyspace_name FROM system_schema.keyspaces WHERE keyspace_name = 'system_schema_mcs'");
final ResultSet rs = stmt.getResultSet();
this.connectedToAmazonKeyspaces = rs != null && rs.next();
} catch (final Exception e) {
LOG.debug("Failed to check existing keyspaces to determine if the connection is bound to AWS: {}",
e.getMessage());
}
}
}
return !this.connectedToAmazonKeyspaces;
}

/**
* Checks whether the connection is closed.
*
Expand Down
124 changes: 124 additions & 0 deletions src/main/java/com/ing/data/cassandra/jdbc/CassandraDataSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import com.ing.data.cassandra.jdbc.optionset.Liquibase;
import com.ing.data.cassandra.jdbc.utils.ContactPoint;
import org.apache.commons.lang3.StringUtils;
import software.amazon.awssdk.regions.Region;

import javax.sql.ConnectionPoolDataSource;
import javax.sql.DataSource;
Expand All @@ -48,6 +49,9 @@
import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.NO_INTERFACE;
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.PROTOCOL;
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_ACTIVE_PROFILE;
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_AWS_REGION;
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_AWS_SECRET_NAME;
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_AWS_SECRET_REGION;
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_CLOUD_SECURE_CONNECT_BUNDLE;
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_COMPLIANCE_MODE;
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_CONFIG_FILE;
Expand All @@ -70,6 +74,7 @@
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_TCP_NO_DELAY;
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_USER;
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_USE_KERBEROS;
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.TAG_USE_SIG_V4;
import static com.ing.data.cassandra.jdbc.utils.JdbcUrlUtil.createSubName;

/**
Expand Down Expand Up @@ -827,6 +832,125 @@ public void setConfigurationFile(final Path configurationFilePath) {
this.setConfigurationFile(configurationFilePath.toString());
}

/**
* Gets the AWS region of the contact point of the Amazon Keyspaces instance.
*
* @return The AWS region.
*/
public String getAwsRegion() {
return this.properties.getProperty(TAG_AWS_REGION);
}

/**
* Sets the AWS region of the contact point of the Amazon Keyspaces instance.
*
* @param region The string representation of the region.
*/
public void setAwsRegion(final String region) {
this.setDataSourceProperty(TAG_AWS_REGION, region);
}

/**
* Sets the AWS region of the contact point of the Amazon Keyspaces instance.
*
* @param region The AWS region.
*/
public void setAwsRegion(final Region region) {
if (region == null) {
this.setAwsRegion((String) null);
} else {
this.setDataSourceProperty(TAG_AWS_REGION, region.id());
}
}

/**
* Gets the AWS region of the Amazon Secret Manager in which the credentials of the user used for the connection
* are stored. If not defined, the value is the one returned by {@link #getAwsRegion()}.
*
* @return .
*/
public String getAwsSecretRegion() {
return (String) this.properties.getOrDefault(TAG_AWS_SECRET_REGION,
this.properties.getProperty(TAG_AWS_REGION));
}

/**
* Sets the AWS region of the Amazon Secret Manager in which the credentials of the user used for the connection
* are stored.
*
* @param region The string representation of the region.
*/
public void setAwsSecretRegion(final String region) {
this.setDataSourceProperty(TAG_AWS_SECRET_REGION, region);
}

/**
* Sets the AWS region of the Amazon Secret Manager in which the credentials of the user used for the connection
* are stored.
*
* @param region The AWS region.
*/
public void setAwsSecretRegion(final Region region) {
if (region == null) {
this.setAwsSecretRegion((String) null);
} else {
this.setDataSourceProperty(TAG_AWS_SECRET_REGION, region.id());
}
}

/**
* Gets the name of the secret, stored in Amazon Secret Manager, containing the credentials of the user used for
* the connection.
*
* @return The name of the secret.
*/
public String getAwsSecretName() {
return this.properties.getProperty(TAG_AWS_SECRET_NAME);
}

/**
* Sets the name of the secret, stored in Amazon Secret Manager, containing the credentials of the user used for
* the connection.
*
* @param secretName The name of the secret.
*/
public void setAwsSecretName(final String secretName) {
this.setDataSourceProperty(TAG_AWS_SECRET_NAME, secretName);
}

/**
* Gets whether the Amazon Signature V4 auth provider is enabled.
* <p>
* The default value is {@code false}.
* See <a href="https://docs.datastax.com/en/developer/java-driver/latest/manual/core/authentication/">
* Authentication reference</a> and
* <a href="https://github.com/aws/aws-sigv4-auth-cassandra-java-driver-plugin">
* Amazon Signature V4 authenticator plugin for Java driver</a> for further information.
* </p>
*
* @return {@code true} if the Amazon Signature V4 auth provider is enabled, {@code false} otherwise.
*/
public boolean isSigV4AuthProviderEnabled() {
return (boolean) this.properties.getOrDefault(TAG_USE_SIG_V4, false);
}

/**
* Sets whether the Amazon Signature V4 auth provider is enabled.
* <p>
* This will enable the Amazon Signature V4 {@link AuthProvider} implementation for the connection using the
* AWS region defined in the property {@link #setAwsRegion(String)} (or {@link #setAwsRegion(Region)}).
* See <a href="https://docs.datastax.com/en/developer/java-driver/latest/manual/core/authentication/">
* Authentication reference</a> and
* <a href="https://github.com/aws/aws-sigv4-auth-cassandra-java-driver-plugin">
* Amazon Signature V4 authenticator plugin for Java driver</a> for further information.
* </p>
*
* @param enabled Whether the Amazon Signature V4 auth provider is enabled.
*/
public void setSigV4AuthProviderEnabled(final boolean enabled) {
this.setDataSourceProperty(TAG_USE_SIG_V4, enabled);
}

private void setDataSourceProperty(final String propertyName, final Object value) {
if (value == null) {
this.properties.remove(propertyName);
Expand Down
Loading

0 comments on commit 80837fe

Please sign in to comment.