diff --git a/docs/src/main/sphinx/connector/cassandra.md b/docs/src/main/sphinx/connector/cassandra.md index 0a5c98fc61174..631606239e71c 100644 --- a/docs/src/main/sphinx/connector/cassandra.md +++ b/docs/src/main/sphinx/connector/cassandra.md @@ -49,6 +49,7 @@ The following configuration properties are available: | `cassandra.native-protocol-port` | The Cassandra server port running the native client protocol, defaults to `9042`. | | `cassandra.consistency-level` | Consistency levels in Cassandra refer to the level of consistency to be used for both read and write operations. More information about consistency levels can be found in the [Cassandra consistency] documentation. This property defaults to a consistency level of `ONE`. Possible values include `ALL`, `EACH_QUORUM`, `QUORUM`, `LOCAL_QUORUM`, `ONE`, `TWO`, `THREE`, `LOCAL_ONE`, `ANY`, `SERIAL`, `LOCAL_SERIAL`. | | `cassandra.allow-drop-table` | Enables {doc}`/sql/drop-table` operations. Defaults to `false`. | +| `cassandra.security` | Should be set to `PASSWORD` for password based authentication. Defaults to `NONE`. | | `cassandra.username` | Username used for authentication to the Cassandra cluster. This is a global setting used for all connections, regardless of the user connected to Trino. | | `cassandra.password` | Password used for authentication to the Cassandra cluster. This is a global setting used for all connections, regardless of the user connected to Trino. | | `cassandra.protocol-version` | It is possible to override the protocol version for older Cassandra clusters. By default, the value corresponds to the default protocol version used in the underlying Cassandra java driver. Possible values include `V3`, `V4`, `V5`, `V6`. | diff --git a/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/CassandraClientConfig.java b/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/CassandraClientConfig.java index c476eda315145..66534e44d119d 100644 --- a/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/CassandraClientConfig.java +++ b/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/CassandraClientConfig.java @@ -20,9 +20,7 @@ import com.google.common.collect.ImmutableList; import io.airlift.configuration.Config; import io.airlift.configuration.ConfigDescription; -import io.airlift.configuration.ConfigSecuritySensitive; import io.airlift.configuration.DefunctConfig; -import io.airlift.configuration.validation.FileExists; import io.airlift.units.Duration; import io.airlift.units.MaxDuration; import io.airlift.units.MinDuration; @@ -31,10 +29,10 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; -import java.io.File; import java.util.List; import java.util.Optional; +import static io.trino.plugin.cassandra.CassandraClientConfig.CassandraAuthenticationType.NONE; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MINUTES; @@ -55,6 +53,12 @@ }) public class CassandraClientConfig { + public enum CassandraAuthenticationType + { + NONE, + PASSWORD; + } + private ConsistencyLevel consistencyLevel = ConsistencyLevel.ONE; private int fetchSize = 5_000; private List contactPoints = ImmutableList.of(); @@ -64,8 +68,6 @@ public class CassandraClientConfig private int batchSize = 100; private Long splitsPerNode; private boolean allowDropTable; - private String username; - private String password; private Duration clientReadTimeout = new Duration(12_000, MILLISECONDS); private Duration clientConnectTimeout = new Duration(5_000, MILLISECONDS); private Integer clientSoLinger; @@ -79,10 +81,7 @@ public class CassandraClientConfig private Duration speculativeExecutionDelay = new Duration(500, MILLISECONDS); private ProtocolVersion protocolVersion; private boolean tlsEnabled; - private File keystorePath; - private String keystorePassword; - private File truststorePath; - private String truststorePassword; + private CassandraAuthenticationType authenticationType = NONE; @NotNull @Size(min = 1) @@ -201,31 +200,6 @@ public CassandraClientConfig setAllowDropTable(boolean allowDropTable) return this; } - public String getUsername() - { - return username; - } - - @Config("cassandra.username") - public CassandraClientConfig setUsername(String username) - { - this.username = username; - return this; - } - - public String getPassword() - { - return password; - } - - @Config("cassandra.password") - @ConfigSecuritySensitive - public CassandraClientConfig setPassword(String password) - { - this.password = password; - return this; - } - @MinDuration("1ms") @MaxDuration("1h") public Duration getClientReadTimeout() @@ -392,53 +366,15 @@ public CassandraClientConfig setTlsEnabled(boolean tlsEnabled) return this; } - public Optional<@FileExists File> getKeystorePath() - { - return Optional.ofNullable(keystorePath); - } - - @Config("cassandra.tls.keystore-path") - public CassandraClientConfig setKeystorePath(File keystorePath) - { - this.keystorePath = keystorePath; - return this; - } - - public Optional getKeystorePassword() - { - return Optional.ofNullable(keystorePassword); - } - - @Config("cassandra.tls.keystore-password") - @ConfigSecuritySensitive - public CassandraClientConfig setKeystorePassword(String keystorePassword) - { - this.keystorePassword = keystorePassword; - return this; - } - - public Optional<@FileExists File> getTruststorePath() - { - return Optional.ofNullable(truststorePath); - } - - @Config("cassandra.tls.truststore-path") - public CassandraClientConfig setTruststorePath(File truststorePath) - { - this.truststorePath = truststorePath; - return this; - } - - public Optional getTruststorePassword() + public CassandraAuthenticationType getAuthenticationType() { - return Optional.ofNullable(truststorePassword); + return authenticationType; } - @Config("cassandra.tls.truststore-password") - @ConfigSecuritySensitive - public CassandraClientConfig setTruststorePassword(String truststorePassword) + @Config("cassandra.security") + public CassandraClientConfig setAuthenticationType(CassandraAuthenticationType authenticationType) { - this.truststorePassword = truststorePassword; + this.authenticationType = authenticationType; return this; } } diff --git a/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/CassandraClientModule.java b/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/CassandraClientModule.java index 0ac8a8ce78613..e015cbd788b19 100644 --- a/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/CassandraClientModule.java +++ b/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/CassandraClientModule.java @@ -27,41 +27,38 @@ import com.google.inject.Provides; import com.google.inject.Scopes; import com.google.inject.Singleton; +import com.google.inject.multibindings.ProvidesIntoSet; +import io.airlift.configuration.AbstractConfigurationAwareModule; import io.airlift.json.JsonCodec; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.cassandra.v4_4.CassandraTelemetry; import io.trino.plugin.cassandra.ptf.Query; -import io.trino.spi.TrinoException; +import io.trino.plugin.cassandra.tls.CassandraTlsModule; import io.trino.spi.function.table.ConnectorTableFunction; import io.trino.spi.procedure.Procedure; import io.trino.spi.type.Type; import io.trino.spi.type.TypeId; import io.trino.spi.type.TypeManager; -import javax.net.ssl.SSLContext; - -import java.io.File; -import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; -import java.security.GeneralSecurityException; import java.time.Duration; import java.util.List; -import java.util.Optional; +import java.util.Set; import static com.google.common.base.Preconditions.checkArgument; import static com.google.inject.multibindings.Multibinder.newSetBinder; +import static io.airlift.configuration.ConditionalModule.conditionalModule; import static io.airlift.configuration.ConfigBinder.configBinder; import static io.airlift.json.JsonBinder.jsonBinder; import static io.airlift.json.JsonCodecBinder.jsonCodecBinder; import static io.trino.plugin.base.ClosingBinder.closingBinder; -import static io.trino.plugin.base.ssl.SslUtils.createSSLContext; -import static io.trino.plugin.cassandra.CassandraErrorCode.CASSANDRA_SSL_INITIALIZATION_FAILURE; +import static io.trino.plugin.cassandra.CassandraClientConfig.CassandraAuthenticationType.PASSWORD; import static java.util.Objects.requireNonNull; public class CassandraClientModule - implements Module + extends AbstractConfigurationAwareModule { private final TypeManager typeManager; @@ -71,7 +68,7 @@ public CassandraClientModule(TypeManager typeManager) } @Override - public void configure(Binder binder) + public void setup(Binder binder) { binder.bind(TypeManager.class).toInstance(typeManager); @@ -89,8 +86,19 @@ public void configure(Binder binder) configBinder(binder).bindConfig(CassandraClientConfig.class); + install(conditionalModule( + CassandraClientConfig.class, + CassandraClientConfig::isTlsEnabled, + new CassandraTlsModule())); + + install(conditionalModule( + CassandraClientConfig.class, + config -> config.getAuthenticationType() == PASSWORD, + new PasswordAuthenticationModule())); + jsonCodecBinder(binder).bindListJsonCodec(ExtraColumnMetadata.class); jsonBinder(binder).addDeserializerBinding(Type.class).to(TypeDeserializer.class); + newSetBinder(binder, CassandraSessionConfigurator.class); closingBinder(binder).registerCloseable(CassandraSession.class); } @@ -119,68 +127,21 @@ protected Type _deserialize(String value, DeserializationContext context) public static CassandraSession createCassandraSession( CassandraTypeManager cassandraTypeManager, CassandraClientConfig config, + Set sessionConfigurators, JsonCodec> extraColumnMetadataCodec, OpenTelemetry openTelemetry) { requireNonNull(extraColumnMetadataCodec, "extraColumnMetadataCodec is null"); CqlSessionBuilder cqlSessionBuilder = CqlSession.builder(); - ProgrammaticDriverConfigLoaderBuilder driverConfigLoaderBuilder = DriverConfigLoader.programmaticBuilder(); - // allow the retrieval of metadata for the system keyspaces - driverConfigLoaderBuilder.withStringList(DefaultDriverOption.METADATA_SCHEMA_REFRESHED_KEYSPACES, List.of()); - - if (config.getProtocolVersion() != null) { - driverConfigLoaderBuilder.withString(DefaultDriverOption.PROTOCOL_VERSION, config.getProtocolVersion().name()); - } List contactPoints = requireNonNull(config.getContactPoints(), "contactPoints is null"); checkArgument(!contactPoints.isEmpty(), "empty contactPoints"); - driverConfigLoaderBuilder.withString(DefaultDriverOption.RECONNECTION_POLICY_CLASS, com.datastax.oss.driver.internal.core.connection.ExponentialReconnectionPolicy.class.getName()); - driverConfigLoaderBuilder.withDuration(DefaultDriverOption.RECONNECTION_BASE_DELAY, Duration.ofMillis(500)); - driverConfigLoaderBuilder.withDuration(DefaultDriverOption.RECONNECTION_MAX_DELAY, Duration.ofMillis(10_000)); - driverConfigLoaderBuilder.withString(DefaultDriverOption.RETRY_POLICY_CLASS, config.getRetryPolicy().getPolicyClass().getName()); - - driverConfigLoaderBuilder.withString(DefaultDriverOption.LOAD_BALANCING_POLICY_CLASS, DefaultLoadBalancingPolicy.class.getName()); - if (config.isUseDCAware()) { - requireNonNull(config.getDcAwareLocalDC(), "DCAwarePolicy localDC is null"); - driverConfigLoaderBuilder.withString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER, config.getDcAwareLocalDC()); - - if (config.getDcAwareUsedHostsPerRemoteDc() > 0) { - driverConfigLoaderBuilder.withInt(DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_MAX_NODES_PER_REMOTE_DC, config.getDcAwareUsedHostsPerRemoteDc()); - if (config.isDcAwareAllowRemoteDCsForLocal()) { - driverConfigLoaderBuilder.withBoolean(DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_ALLOW_FOR_LOCAL_CONSISTENCY_LEVELS, true); - } - } + for (CassandraSessionConfigurator sessionConfigurator : sessionConfigurators) { + sessionConfigurator.configure(cqlSessionBuilder); } - driverConfigLoaderBuilder.withDuration(DefaultDriverOption.REQUEST_TIMEOUT, config.getClientReadTimeout().toJavaTime()); - driverConfigLoaderBuilder.withDuration(DefaultDriverOption.CONNECTION_CONNECT_TIMEOUT, config.getClientConnectTimeout().toJavaTime()); - if (config.getClientSoLinger() != null) { - driverConfigLoaderBuilder.withInt(DefaultDriverOption.SOCKET_LINGER_INTERVAL, config.getClientSoLinger()); - } - if (config.isTlsEnabled()) { - buildSslContext(config.getKeystorePath(), config.getKeystorePassword(), config.getTruststorePath(), config.getTruststorePassword()) - .ifPresent(cqlSessionBuilder::withSslContext); - } - - if (config.getUsername() != null && config.getPassword() != null) { - cqlSessionBuilder.withAuthCredentials(config.getUsername(), config.getPassword()); - } - - driverConfigLoaderBuilder.withInt(DefaultDriverOption.REQUEST_PAGE_SIZE, config.getFetchSize()); - driverConfigLoaderBuilder.withString(DefaultDriverOption.REQUEST_CONSISTENCY, config.getConsistencyLevel().name()); - - if (config.getSpeculativeExecutionLimit().isPresent()) { - driverConfigLoaderBuilder.withString(DefaultDriverOption.SPECULATIVE_EXECUTION_POLICY_CLASS, com.datastax.oss.driver.internal.core.specex.ConstantSpeculativeExecutionPolicy.class.getName()); - // maximum number of executions - driverConfigLoaderBuilder.withInt(DefaultDriverOption.SPECULATIVE_EXECUTION_MAX, config.getSpeculativeExecutionLimit().get()); - // delay before a new execution is launched - driverConfigLoaderBuilder.withDuration(DefaultDriverOption.SPECULATIVE_EXECUTION_DELAY, Duration.ofMillis(config.getSpeculativeExecutionDelay().toMillis())); - } - - cqlSessionBuilder.withConfigLoader(driverConfigLoaderBuilder.build()); - return new CassandraSession( cassandraTypeManager, extraColumnMetadataCodec, @@ -193,21 +154,72 @@ public static CassandraSession createCassandraSession( config.getNoHostAvailableRetryTimeout()); } - private static Optional buildSslContext( - Optional keystorePath, - Optional keystorePassword, - Optional truststorePath, - Optional truststorePassword) + @ProvidesIntoSet + @Singleton + public CassandraSessionConfigurator configurationLoaderConfigurator(CassandraClientConfig config) + { + return builder -> { + ProgrammaticDriverConfigLoaderBuilder driverConfigLoaderBuilder = DriverConfigLoader.programmaticBuilder(); + // allow the retrieval of metadata for the system keyspaces + driverConfigLoaderBuilder.withStringList(DefaultDriverOption.METADATA_SCHEMA_REFRESHED_KEYSPACES, List.of()); + + if (config.getProtocolVersion() != null) { + driverConfigLoaderBuilder.withString(DefaultDriverOption.PROTOCOL_VERSION, config.getProtocolVersion().name()); + } + + driverConfigLoaderBuilder.withString(DefaultDriverOption.RECONNECTION_POLICY_CLASS, com.datastax.oss.driver.internal.core.connection.ExponentialReconnectionPolicy.class.getName()); + driverConfigLoaderBuilder.withDuration(DefaultDriverOption.RECONNECTION_BASE_DELAY, Duration.ofMillis(500)); + driverConfigLoaderBuilder.withDuration(DefaultDriverOption.RECONNECTION_MAX_DELAY, Duration.ofMillis(10_000)); + driverConfigLoaderBuilder.withString(DefaultDriverOption.RETRY_POLICY_CLASS, config.getRetryPolicy().getPolicyClass().getName()); + + driverConfigLoaderBuilder.withString(DefaultDriverOption.LOAD_BALANCING_POLICY_CLASS, DefaultLoadBalancingPolicy.class.getName()); + if (config.isUseDCAware()) { + requireNonNull(config.getDcAwareLocalDC(), "DCAwarePolicy localDC is null"); + driverConfigLoaderBuilder.withString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER, config.getDcAwareLocalDC()); + + if (config.getDcAwareUsedHostsPerRemoteDc() > 0) { + driverConfigLoaderBuilder.withInt(DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_MAX_NODES_PER_REMOTE_DC, config.getDcAwareUsedHostsPerRemoteDc()); + if (config.isDcAwareAllowRemoteDCsForLocal()) { + driverConfigLoaderBuilder.withBoolean(DefaultDriverOption.LOAD_BALANCING_DC_FAILOVER_ALLOW_FOR_LOCAL_CONSISTENCY_LEVELS, true); + } + } + } + + driverConfigLoaderBuilder.withDuration(DefaultDriverOption.REQUEST_TIMEOUT, config.getClientReadTimeout().toJavaTime()); + driverConfigLoaderBuilder.withDuration(DefaultDriverOption.CONNECTION_CONNECT_TIMEOUT, config.getClientConnectTimeout().toJavaTime()); + if (config.getClientSoLinger() != null) { + driverConfigLoaderBuilder.withInt(DefaultDriverOption.SOCKET_LINGER_INTERVAL, config.getClientSoLinger()); + } + + driverConfigLoaderBuilder.withInt(DefaultDriverOption.REQUEST_PAGE_SIZE, config.getFetchSize()); + driverConfigLoaderBuilder.withString(DefaultDriverOption.REQUEST_CONSISTENCY, config.getConsistencyLevel().name()); + + if (config.getSpeculativeExecutionLimit().isPresent()) { + driverConfigLoaderBuilder.withString(DefaultDriverOption.SPECULATIVE_EXECUTION_POLICY_CLASS, com.datastax.oss.driver.internal.core.specex.ConstantSpeculativeExecutionPolicy.class.getName()); + // maximum number of executions + driverConfigLoaderBuilder.withInt(DefaultDriverOption.SPECULATIVE_EXECUTION_MAX, config.getSpeculativeExecutionLimit().get()); + // delay before a new execution is launched + driverConfigLoaderBuilder.withDuration(DefaultDriverOption.SPECULATIVE_EXECUTION_DELAY, Duration.ofMillis(config.getSpeculativeExecutionDelay().toMillis())); + } + + builder.withConfigLoader(driverConfigLoaderBuilder.build()); + }; + } + + private static class PasswordAuthenticationModule + implements Module { - if (keystorePath.isEmpty() && truststorePath.isEmpty()) { - return Optional.empty(); + @Override + public void configure(Binder binder) + { + configBinder(binder).bindConfig(CassandraPasswordConfig.class); } - try { - return Optional.of(createSSLContext(keystorePath, keystorePassword, truststorePath, truststorePassword)); - } - catch (GeneralSecurityException | IOException e) { - throw new TrinoException(CASSANDRA_SSL_INITIALIZATION_FAILURE, e); + @ProvidesIntoSet + @Singleton + public CassandraSessionConfigurator passwordAuthenticationConfigurator(CassandraPasswordConfig config) + { + return builder -> builder.withAuthCredentials(config.getUsername(), config.getPassword()); } } diff --git a/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/CassandraPasswordConfig.java b/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/CassandraPasswordConfig.java new file mode 100644 index 0000000000000..6a88dcc704512 --- /dev/null +++ b/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/CassandraPasswordConfig.java @@ -0,0 +1,51 @@ +/* + * Licensed 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 io.trino.plugin.cassandra; + +import io.airlift.configuration.Config; +import io.airlift.configuration.ConfigSecuritySensitive; +import jakarta.validation.constraints.NotNull; + +public class CassandraPasswordConfig +{ + private String username; + private String password; + + @NotNull + public String getUsername() + { + return username; + } + + @Config("cassandra.username") + public CassandraPasswordConfig setUsername(String username) + { + this.username = username; + return this; + } + + @NotNull + public String getPassword() + { + return password; + } + + @Config("cassandra.password") + @ConfigSecuritySensitive + public CassandraPasswordConfig setPassword(String password) + { + this.password = password; + return this; + } +} diff --git a/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/CassandraSessionConfigurator.java b/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/CassandraSessionConfigurator.java new file mode 100644 index 0000000000000..39ee571252acb --- /dev/null +++ b/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/CassandraSessionConfigurator.java @@ -0,0 +1,21 @@ +/* + * Licensed 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 io.trino.plugin.cassandra; + +import com.datastax.oss.driver.api.core.CqlSessionBuilder; + +public interface CassandraSessionConfigurator +{ + void configure(CqlSessionBuilder builder); +} diff --git a/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/tls/CassandraTlsConfig.java b/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/tls/CassandraTlsConfig.java new file mode 100644 index 0000000000000..28d03c2ed0e93 --- /dev/null +++ b/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/tls/CassandraTlsConfig.java @@ -0,0 +1,79 @@ +/* + * Licensed 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 io.trino.plugin.cassandra.tls; + +import io.airlift.configuration.Config; +import io.airlift.configuration.ConfigSecuritySensitive; +import io.airlift.configuration.validation.FileExists; + +import java.io.File; +import java.util.Optional; + +public class CassandraTlsConfig +{ + private File keystorePath; + private String keystorePassword; + private File truststorePath; + private String truststorePassword; + + public Optional<@FileExists File> getKeystorePath() + { + return Optional.ofNullable(keystorePath); + } + + @Config("cassandra.tls.keystore-path") + public CassandraTlsConfig setKeystorePath(File keystorePath) + { + this.keystorePath = keystorePath; + return this; + } + + public Optional getKeystorePassword() + { + return Optional.ofNullable(keystorePassword); + } + + @Config("cassandra.tls.keystore-password") + @ConfigSecuritySensitive + public CassandraTlsConfig setKeystorePassword(String keystorePassword) + { + this.keystorePassword = keystorePassword; + return this; + } + + public Optional<@FileExists File> getTruststorePath() + { + return Optional.ofNullable(truststorePath); + } + + @Config("cassandra.tls.truststore-path") + public CassandraTlsConfig setTruststorePath(File truststorePath) + { + this.truststorePath = truststorePath; + return this; + } + + public Optional getTruststorePassword() + { + return Optional.ofNullable(truststorePassword); + } + + @Config("cassandra.tls.truststore-password") + @ConfigSecuritySensitive + public CassandraTlsConfig setTruststorePassword(String truststorePassword) + { + this.truststorePassword = truststorePassword; + return this; + } +} diff --git a/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/tls/CassandraTlsModule.java b/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/tls/CassandraTlsModule.java new file mode 100644 index 0000000000000..395d2877e33fd --- /dev/null +++ b/plugin/trino-cassandra/src/main/java/io/trino/plugin/cassandra/tls/CassandraTlsModule.java @@ -0,0 +1,68 @@ +/* + * Licensed 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 io.trino.plugin.cassandra.tls; + +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Singleton; +import com.google.inject.multibindings.ProvidesIntoSet; +import io.trino.plugin.cassandra.CassandraSessionConfigurator; +import io.trino.spi.TrinoException; + +import javax.net.ssl.SSLContext; + +import java.io.File; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Optional; + +import static io.airlift.configuration.ConfigBinder.configBinder; +import static io.trino.plugin.base.ssl.SslUtils.createSSLContext; +import static io.trino.plugin.cassandra.CassandraErrorCode.CASSANDRA_SSL_INITIALIZATION_FAILURE; + +public class CassandraTlsModule + implements Module +{ + @Override + public void configure(Binder binder) + { + configBinder(binder).bindConfig(CassandraTlsConfig.class); + } + + @ProvidesIntoSet + @Singleton + public CassandraSessionConfigurator tlsConfigurator(CassandraTlsConfig config) + { + return builder -> buildSslContext(config.getKeystorePath(), config.getKeystorePassword(), config.getTruststorePath(), config.getTruststorePassword()) + .ifPresent(builder::withSslContext); + } + + private static Optional buildSslContext( + Optional keystorePath, + Optional keystorePassword, + Optional truststorePath, + Optional truststorePassword) + { + if (keystorePath.isEmpty() && truststorePath.isEmpty()) { + return Optional.empty(); + } + + try { + return Optional.of(createSSLContext(keystorePath, keystorePassword, truststorePath, truststorePassword)); + } + catch (GeneralSecurityException | IOException e) { + throw new TrinoException(CASSANDRA_SSL_INITIALIZATION_FAILURE, e); + } + } +} diff --git a/plugin/trino-cassandra/src/test/java/io/trino/plugin/cassandra/TestCassandraClientConfig.java b/plugin/trino-cassandra/src/test/java/io/trino/plugin/cassandra/TestCassandraClientConfig.java index ac6619239d494..25df43dff039d 100644 --- a/plugin/trino-cassandra/src/test/java/io/trino/plugin/cassandra/TestCassandraClientConfig.java +++ b/plugin/trino-cassandra/src/test/java/io/trino/plugin/cassandra/TestCassandraClientConfig.java @@ -21,13 +21,13 @@ import org.junit.jupiter.api.Test; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Map; import static io.airlift.configuration.testing.ConfigAssertions.assertFullMapping; import static io.airlift.configuration.testing.ConfigAssertions.assertRecordedDefaults; import static io.airlift.configuration.testing.ConfigAssertions.recordDefaults; +import static io.trino.plugin.cassandra.CassandraClientConfig.CassandraAuthenticationType.NONE; +import static io.trino.plugin.cassandra.CassandraClientConfig.CassandraAuthenticationType.PASSWORD; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; @@ -47,8 +47,6 @@ public void testDefaults() .setBatchSize(100) .setSplitsPerNode(null) .setAllowDropTable(false) - .setUsername(null) - .setPassword(null) .setClientReadTimeout(new Duration(12_000, MILLISECONDS)) .setClientConnectTimeout(new Duration(5_000, MILLISECONDS)) .setClientSoLinger(null) @@ -62,19 +60,13 @@ public void testDefaults() .setSpeculativeExecutionDelay(new Duration(500, MILLISECONDS)) .setProtocolVersion(null) .setTlsEnabled(false) - .setKeystorePath(null) - .setKeystorePassword(null) - .setTruststorePath(null) - .setTruststorePassword(null)); + .setAuthenticationType(NONE)); } @Test public void testExplicitPropertyMappings() throws IOException { - Path keystoreFile = Files.createTempFile(null, null); - Path truststoreFile = Files.createTempFile(null, null); - Map properties = ImmutableMap.builder() .put("cassandra.contact-points", "host1,host2") .put("cassandra.native-protocol-port", "9999") @@ -85,8 +77,6 @@ public void testExplicitPropertyMappings() .put("cassandra.batch-size", "999") .put("cassandra.splits-per-node", "10000") .put("cassandra.allow-drop-table", "true") - .put("cassandra.username", "my_username") - .put("cassandra.password", "my_password") .put("cassandra.client.read-timeout", "11ms") .put("cassandra.client.connect-timeout", "22ms") .put("cassandra.client.so-linger", "33") @@ -100,10 +90,7 @@ public void testExplicitPropertyMappings() .put("cassandra.speculative-execution.delay", "101s") .put("cassandra.protocol-version", "V3") .put("cassandra.tls.enabled", "true") - .put("cassandra.tls.keystore-path", keystoreFile.toString()) - .put("cassandra.tls.keystore-password", "keystore-password") - .put("cassandra.tls.truststore-path", truststoreFile.toString()) - .put("cassandra.tls.truststore-password", "truststore-password") + .put("cassandra.security", "PASSWORD") .buildOrThrow(); CassandraClientConfig expected = new CassandraClientConfig() @@ -116,8 +103,6 @@ public void testExplicitPropertyMappings() .setBatchSize(999) .setSplitsPerNode(10_000L) .setAllowDropTable(true) - .setUsername("my_username") - .setPassword("my_password") .setClientReadTimeout(new Duration(11, MILLISECONDS)) .setClientConnectTimeout(new Duration(22, MILLISECONDS)) .setClientSoLinger(33) @@ -130,11 +115,8 @@ public void testExplicitPropertyMappings() .setSpeculativeExecutionLimit(10) .setSpeculativeExecutionDelay(new Duration(101, SECONDS)) .setProtocolVersion(DefaultProtocolVersion.V3) - .setTlsEnabled(true) - .setKeystorePath(keystoreFile.toFile()) - .setKeystorePassword("keystore-password") - .setTruststorePath(truststoreFile.toFile()) - .setTruststorePassword("truststore-password"); + .setAuthenticationType(PASSWORD) + .setTlsEnabled(true); assertFullMapping(properties, expected); } diff --git a/plugin/trino-cassandra/src/test/java/io/trino/plugin/cassandra/TestCassandraPasswordConfig.java b/plugin/trino-cassandra/src/test/java/io/trino/plugin/cassandra/TestCassandraPasswordConfig.java new file mode 100644 index 0000000000000..401d2b5092284 --- /dev/null +++ b/plugin/trino-cassandra/src/test/java/io/trino/plugin/cassandra/TestCassandraPasswordConfig.java @@ -0,0 +1,49 @@ +/* + * Licensed 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 io.trino.plugin.cassandra; + +import com.google.common.collect.ImmutableMap; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static io.airlift.configuration.testing.ConfigAssertions.assertFullMapping; +import static io.airlift.configuration.testing.ConfigAssertions.assertRecordedDefaults; +import static io.airlift.configuration.testing.ConfigAssertions.recordDefaults; + +final class TestCassandraPasswordConfig +{ + @Test + void testDefaults() + { + assertRecordedDefaults(recordDefaults(CassandraPasswordConfig.class) + .setUsername(null) + .setPassword(null)); + } + + @Test + void testExplicitPropertyMappings() + { + Map properties = ImmutableMap.builder() + .put("cassandra.username", "my_username") + .put("cassandra.password", "my_password") + .buildOrThrow(); + + CassandraPasswordConfig expected = new CassandraPasswordConfig() + .setUsername("my_username") + .setPassword("my_password"); + + assertFullMapping(properties, expected); + } +} diff --git a/plugin/trino-cassandra/src/test/java/io/trino/plugin/cassandra/tls/TestCassandraTlsConfig.java b/plugin/trino-cassandra/src/test/java/io/trino/plugin/cassandra/tls/TestCassandraTlsConfig.java new file mode 100644 index 0000000000000..02a1d858e5f82 --- /dev/null +++ b/plugin/trino-cassandra/src/test/java/io/trino/plugin/cassandra/tls/TestCassandraTlsConfig.java @@ -0,0 +1,62 @@ +/* + * Licensed 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 io.trino.plugin.cassandra.tls; + +import com.google.common.collect.ImmutableMap; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import static io.airlift.configuration.testing.ConfigAssertions.assertFullMapping; +import static io.airlift.configuration.testing.ConfigAssertions.assertRecordedDefaults; +import static io.airlift.configuration.testing.ConfigAssertions.recordDefaults; + +final class TestCassandraTlsConfig +{ + @Test + void testDefaults() + { + assertRecordedDefaults(recordDefaults(CassandraTlsConfig.class) + .setKeystorePath(null) + .setKeystorePassword(null) + .setTruststorePath(null) + .setTruststorePassword(null)); + } + + @Test + void testExplicitPropertyMappings() + throws IOException + { + Path keystoreFile = Files.createTempFile(null, null); + Path truststoreFile = Files.createTempFile(null, null); + + Map properties = ImmutableMap.builder() + .put("cassandra.tls.keystore-path", keystoreFile.toString()) + .put("cassandra.tls.keystore-password", "keystore-password") + .put("cassandra.tls.truststore-path", truststoreFile.toString()) + .put("cassandra.tls.truststore-password", "truststore-password") + .buildOrThrow(); + + CassandraTlsConfig expected = new CassandraTlsConfig() + .setKeystorePath(keystoreFile.toFile()) + .setKeystorePassword("keystore-password") + .setTruststorePath(truststoreFile.toFile()) + .setTruststorePassword("truststore-password"); + + assertFullMapping(properties, expected); + } +}