diff --git a/CHANGES.txt b/CHANGES.txt index a16f5f3698..2a09415064 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 4.1 + * Runtime-configurable YAML option to prohibit USE statements (CASSANDRA-17318) * When streaming sees a ClosedChannelException this triggers the disk failure policy (CASSANDRA-17116) * Add a virtual table for exposing prepared statements metrics (CASSANDRA-17224) * Remove python 2.x support from cqlsh (CASSANDRA-17242) diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml index ae7d598cb3..283095a558 100644 --- a/conf/cassandra.yaml +++ b/conf/cassandra.yaml @@ -1550,6 +1550,9 @@ enable_transient_replication: false # 'ALTER ... DROP COMPACT STORAGE' is considered experimental and is not recommended for production use. enable_drop_compact_storage: false +# Whether or not USE is allowed. This is enabled by default to avoid failure on upgrade. +#use_statements_enabled: true + # When the client triggers a protocol exception or unknown issue (Cassandra bug) we increment # a client metric showing this; this logic will exclude specific subnets from updating these # metrics diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java index f0e52a2fa2..d7100e1657 100644 --- a/src/java/org/apache/cassandra/config/Config.java +++ b/src/java/org/apache/cassandra/config/Config.java @@ -411,6 +411,8 @@ public class Config public volatile boolean enable_drop_compact_storage = false; + public volatile boolean use_statements_enabled = true; + /** * Optionally disable asynchronous UDF execution. * Disabling asynchronous UDF execution also implicitly disables the security-manager! diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java index 3968484d10..471102d7bf 100644 --- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java +++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java @@ -70,7 +70,6 @@ import org.apache.cassandra.security.EncryptionContext; import org.apache.cassandra.security.SSLFactory; import org.apache.cassandra.service.CacheService.CacheType; import org.apache.cassandra.service.paxos.Paxos; -import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.FBUtilities; import org.apache.commons.lang3.ArrayUtils; @@ -3810,4 +3809,18 @@ public class DatabaseDescriptor conf.minimum_keyspace_rf = value; } + + public static boolean getUseStatementsEnabled() + { + return conf.use_statements_enabled; + } + + public static void setUseStatementsEnabled(boolean enabled) + { + if (enabled != conf.use_statements_enabled) + { + logger.info("Setting use_statements_enabled to {}", enabled); + conf.use_statements_enabled = enabled; + } + } } diff --git a/src/java/org/apache/cassandra/cql3/statements/UseStatement.java b/src/java/org/apache/cassandra/cql3/statements/UseStatement.java index a0928e857c..c7f0c24852 100644 --- a/src/java/org/apache/cassandra/cql3/statements/UseStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/UseStatement.java @@ -19,8 +19,10 @@ package org.apache.cassandra.cql3.statements; import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.UnauthorizedException; import org.apache.cassandra.transport.messages.ResultMessage; @@ -29,6 +31,7 @@ import org.apache.cassandra.service.QueryState; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; import static org.apache.cassandra.utils.Clock.Global.nanoTime; public class UseStatement extends CQLStatement.Raw implements CQLStatement @@ -50,12 +53,15 @@ public class UseStatement extends CQLStatement.Raw implements CQLStatement state.validateLogin(); } + @Override public void validate(ClientState state) throws InvalidRequestException { + checkTrue(DatabaseDescriptor.getUseStatementsEnabled(), "USE statements prohibited. (see use_statements_enabled in cassandra.yaml)"); } public ResultMessage execute(QueryState state, QueryOptions options, long queryStartNanoTime) throws InvalidRequestException { + QueryProcessor.metrics.useStatementsExecuted.inc(); state.getClientState().setKeyspace(keyspace); return new ResultMessage.SetKeyspace(keyspace); } diff --git a/src/java/org/apache/cassandra/metrics/CQLMetrics.java b/src/java/org/apache/cassandra/metrics/CQLMetrics.java index 1020e927db..ce91333942 100644 --- a/src/java/org/apache/cassandra/metrics/CQLMetrics.java +++ b/src/java/org/apache/cassandra/metrics/CQLMetrics.java @@ -32,6 +32,8 @@ public class CQLMetrics public final Counter preparedStatementsExecuted; public final Counter preparedStatementsEvicted; + public final Counter useStatementsExecuted; + public final Gauge preparedStatementsCount; public final Gauge preparedStatementsRatio; @@ -41,13 +43,9 @@ public class CQLMetrics preparedStatementsExecuted = Metrics.counter(factory.createMetricName("PreparedStatementsExecuted")); preparedStatementsEvicted = Metrics.counter(factory.createMetricName("PreparedStatementsEvicted")); - preparedStatementsCount = Metrics.register(factory.createMetricName("PreparedStatementsCount"), new Gauge() - { - public Integer getValue() - { - return QueryProcessor.preparedStatementsCount(); - } - }); + useStatementsExecuted = Metrics.counter(factory.createMetricName("UseStatementsExecuted")); + + preparedStatementsCount = Metrics.register(factory.createMetricName("PreparedStatementsCount"), QueryProcessor::preparedStatementsCount); preparedStatementsRatio = Metrics.register(factory.createMetricName("PreparedStatementsRatio"), new RatioGauge() { public Ratio getRatio() diff --git a/src/java/org/apache/cassandra/service/StorageProxy.java b/src/java/org/apache/cassandra/service/StorageProxy.java index 4780e45b87..e273348a7d 100644 --- a/src/java/org/apache/cassandra/service/StorageProxy.java +++ b/src/java/org/apache/cassandra/service/StorageProxy.java @@ -416,7 +416,6 @@ public class StorageProxy implements StorageProxyMBean * {@link ConsistencyLevel#LOCAL_SERIAL}). * @param consistencyForReplayCommits the consistency for the commit phase of "replayed" in-progress operations. * @param consistencyForCommit the consistency for the commit phase of _this_ operation update. - * @param state the client state. * @param queryStartNanoTime the nano time for the start of the query this is part of. This is the base time for * timeouts. * @param casMetrics the metrics to update for this operation. @@ -2981,4 +2980,16 @@ public class StorageProxy implements StorageProxyMBean { return Paxos.getPaxosVariant().toString(); } + + @Override + public boolean getUseStatementsEnabled() + { + return DatabaseDescriptor.getUseStatementsEnabled(); + } + + @Override + public void setUseStatementsEnabled(boolean enabled) + { + DatabaseDescriptor.setUseStatementsEnabled(enabled); + } } diff --git a/src/java/org/apache/cassandra/service/StorageProxyMBean.java b/src/java/org/apache/cassandra/service/StorageProxyMBean.java index 5ce1b9e924..cce7ff0225 100644 --- a/src/java/org/apache/cassandra/service/StorageProxyMBean.java +++ b/src/java/org/apache/cassandra/service/StorageProxyMBean.java @@ -121,4 +121,7 @@ public interface StorageProxyMBean void setPaxosVariant(String variant); String getPaxosVariant(); + + boolean getUseStatementsEnabled(); + void setUseStatementsEnabled(boolean enabled); } diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/UseTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/UseTest.java index e1498b65a3..b2a40a3243 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/UseTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/UseTest.java @@ -17,15 +17,41 @@ */ package org.apache.cassandra.cql3.validation.operations; +import org.assertj.core.api.Assertions; import org.junit.Test; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.exceptions.InvalidRequestException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; public class UseTest extends CQLTester { @Test public void testUseStatementWithBindVariable() throws Throwable { + DatabaseDescriptor.setUseStatementsEnabled(true); assertInvalidSyntaxMessage("Bind variables cannot be used for keyspace names", "USE ?"); } + + @Test + public void shouldRejectUseStatementWhenProhibited() throws Throwable + { + long useCountBefore = QueryProcessor.metrics.useStatementsExecuted.getCount(); + + try + { + DatabaseDescriptor.setUseStatementsEnabled(false); + execute("USE cql_test_keyspace"); + fail("expected USE statement to fail with use_statements_enabled = false"); + } + catch (InvalidRequestException e) + { + assertEquals(useCountBefore, QueryProcessor.metrics.useStatementsExecuted.getCount()); + Assertions.assertThat(e).hasMessageContaining("USE statements prohibited"); + } + } } diff --git a/test/unit/org/apache/cassandra/metrics/CQLMetricsTest.java b/test/unit/org/apache/cassandra/metrics/CQLMetricsTest.java index 3971b9f1e1..e14861ed3a 100644 --- a/test/unit/org/apache/cassandra/metrics/CQLMetricsTest.java +++ b/test/unit/org/apache/cassandra/metrics/CQLMetricsTest.java @@ -20,12 +20,14 @@ package org.apache.cassandra.metrics; import java.io.IOException; +import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import com.datastax.driver.core.Cluster; import com.datastax.driver.core.PreparedStatement; import com.datastax.driver.core.Session; +import com.datastax.driver.core.exceptions.InvalidQueryException; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.schema.Schema; @@ -33,12 +35,12 @@ import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.service.EmbeddedCassandraService; -import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class CQLMetricsTest extends SchemaLoader { - private static EmbeddedCassandraService cassandra; - private static Cluster cluster; private static Session session; @@ -47,7 +49,7 @@ public class CQLMetricsTest extends SchemaLoader { Schema.instance.clear(); - cassandra = new EmbeddedCassandraService(); + EmbeddedCassandraService cassandra = new EmbeddedCassandraService(); cassandra.start(); cluster = Cluster.builder().addContactPoint("127.0.0.1").withPort(DatabaseDescriptor.getNativeTransportPort()).build(); @@ -57,11 +59,34 @@ public class CQLMetricsTest extends SchemaLoader session.execute("CREATE TABLE IF NOT EXISTS junit.metricstest (id int PRIMARY KEY, val text);"); } + @Test + public void testConnectionWithUseDisabled() + { + long useCountBefore = QueryProcessor.metrics.useStatementsExecuted.getCount(); + DatabaseDescriptor.setUseStatementsEnabled(false); + + try (Session ignored = cluster.connect("junit")) + { + fail("expected USE statement to fail with use_statements_enabled = false"); + } + catch (InvalidQueryException e) + { + Assert.assertEquals(useCountBefore, QueryProcessor.metrics.useStatementsExecuted.getCount()); + assertTrue(e.getMessage().contains("USE statements prohibited")); + } + finally + { + DatabaseDescriptor.setUseStatementsEnabled(true); + } + } + @Test public void testPreparedStatementsCount() { int n = QueryProcessor.metrics.preparedStatementsCount.getValue(); + long useCountBefore = QueryProcessor.metrics.useStatementsExecuted.getCount(); session.execute("use junit"); + Assert.assertEquals(useCountBefore + 1, QueryProcessor.metrics.useStatementsExecuted.getCount()); session.prepare("SELECT * FROM junit.metricstest WHERE id = ?"); assertEquals(n+2, (int) QueryProcessor.metrics.preparedStatementsCount.getValue()); } @@ -104,15 +129,15 @@ public class CQLMetricsTest extends SchemaLoader clearMetrics(); PreparedStatement metricsStatement = session.prepare("INSERT INTO junit.metricstest (id, val) VALUES (?, ?)"); - assertEquals(Double.NaN, QueryProcessor.metrics.preparedStatementsRatio.getValue()); + assertEquals(Double.NaN, QueryProcessor.metrics.preparedStatementsRatio.getValue(), 0.0); for (int i = 0; i < 10; i++) session.execute(metricsStatement.bind(i, "val" + i)); - assertEquals(1.0, QueryProcessor.metrics.preparedStatementsRatio.getValue()); + assertEquals(1.0, QueryProcessor.metrics.preparedStatementsRatio.getValue(), 0.0); for (int i = 0; i < 10; i++) session.execute(String.format("INSERT INTO junit.metricstest (id, val) VALUES (%d, '%s')", i, "val" + i)); - assertEquals(0.5, QueryProcessor.metrics.preparedStatementsRatio.getValue()); + assertEquals(0.5, QueryProcessor.metrics.preparedStatementsRatio.getValue(), 0.0); } private void clearMetrics()