diff --git a/CHANGES.txt b/CHANGES.txt index 8a7f5d5e22..f22e088193 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 4.1 + * Add guardrails for read/write consistency levels (CASSANDRA-17188) * Add guardrail for SELECT IN terms and their cartesian product (CASSANDRA-17187) * remove unused imports in cqlsh.py and cqlshlib (CASSANDRA-17413) * deprecate property windows_timer_interval (CASSANDRA-17404) diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml index ef03795526..8df5771060 100644 --- a/conf/cassandra.yaml +++ b/conf/cassandra.yaml @@ -1604,7 +1604,8 @@ drop_compact_storage_enabled: false # The two thresholds default to -1 to disable. # materialized_views_per_table_warn_threshold: -1 # materialized_views_per_table_fail_threshold: -1 -# Guardrail to ignore or reject properties when creating tables. By default all properties are allowed. +# Guardrail to warn about, ignore or reject properties when creating tables. By default all properties are allowed. +# table_properties_warned: [] # table_properties_ignored: [] # table_properties_disallowed: [] # Guardrail to allow/disallow user-provided timestamps. Defaults to true. @@ -1625,6 +1626,12 @@ drop_compact_storage_enabled: false # The two thresholds default to -1 to disable. # in_select_cartesian_product_warn_threshold: -1 # in_select_cartesian_product_fail_threshold: -1 +# Guardrail to warn about or reject read consistency levels. By default, all consistency levels are allowed. +# read_consistency_levels_warned: [] +# read_consistency_levels_disallowed: [] +# Guardrail to warn about or reject write consistency levels. By default, all consistency levels are allowed. +# write_consistency_levels_warned: [] +# write_consistency_levels_disallowed: [] # Startup Checks are executed as part of Cassandra startup process, not all of them # are configurable (so you can disable them) but these which are enumerated bellow. diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java index 4c0f29a07f..671e49dd0e 100644 --- a/src/java/org/apache/cassandra/config/Config.java +++ b/src/java/org/apache/cassandra/config/Config.java @@ -788,8 +788,13 @@ public class Config public volatile int partition_keys_in_select_fail_threshold = DISABLED_GUARDRAIL; public volatile int in_select_cartesian_product_warn_threshold = DISABLED_GUARDRAIL; public volatile int in_select_cartesian_product_fail_threshold = DISABLED_GUARDRAIL; + public volatile Set table_properties_warned = Collections.emptySet(); public volatile Set table_properties_ignored = Collections.emptySet(); public volatile Set table_properties_disallowed = Collections.emptySet(); + public volatile Set read_consistency_levels_warned = Collections.emptySet(); + public volatile Set read_consistency_levels_disallowed = Collections.emptySet(); + public volatile Set write_consistency_levels_warned = Collections.emptySet(); + public volatile Set write_consistency_levels_disallowed = Collections.emptySet(); public volatile boolean user_timestamps_enabled = true; public volatile boolean read_before_write_list_operations_enabled = true; diff --git a/src/java/org/apache/cassandra/config/GuardrailsOptions.java b/src/java/org/apache/cassandra/config/GuardrailsOptions.java index d4de0f2c4a..a8b7adac73 100644 --- a/src/java/org/apache/cassandra/config/GuardrailsOptions.java +++ b/src/java/org/apache/cassandra/config/GuardrailsOptions.java @@ -18,6 +18,7 @@ package org.apache.cassandra.config; +import java.util.Collections; import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; @@ -27,6 +28,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.cql3.statements.schema.TableAttributes; +import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.guardrails.GuardrailsConfig; @@ -64,12 +66,17 @@ public class GuardrailsOptions implements GuardrailsConfig validateIntThreshold(config.columns_per_table_warn_threshold, config.columns_per_table_fail_threshold, "columns_per_table"); validateIntThreshold(config.secondary_indexes_per_table_warn_threshold, config.secondary_indexes_per_table_fail_threshold, "secondary_indexes_per_table"); validateIntThreshold(config.materialized_views_per_table_warn_threshold, config.materialized_views_per_table_fail_threshold, "materialized_views_per_table"); + config.table_properties_warned = validateTableProperties(config.table_properties_warned, "table_properties_warned"); config.table_properties_ignored = validateTableProperties(config.table_properties_ignored, "table_properties_ignored"); config.table_properties_disallowed = validateTableProperties(config.table_properties_disallowed, "table_properties_disallowed"); validateIntThreshold(config.page_size_warn_threshold, config.page_size_fail_threshold, "page_size"); validateIntThreshold(config.partition_keys_in_select_warn_threshold, config.partition_keys_in_select_fail_threshold, "partition_keys_in_select"); validateIntThreshold(config.in_select_cartesian_product_warn_threshold, config.in_select_cartesian_product_fail_threshold, "in_select_cartesian_product"); + config.read_consistency_levels_warned = validateConsistencyLevels(config.read_consistency_levels_warned, "read_consistency_levels_warned"); + config.read_consistency_levels_disallowed = validateConsistencyLevels(config.read_consistency_levels_disallowed, "read_consistency_levels_disallowed"); + config.write_consistency_levels_warned = validateConsistencyLevels(config.write_consistency_levels_warned, "write_consistency_levels_warned"); + config.write_consistency_levels_disallowed = validateConsistencyLevels(config.write_consistency_levels_disallowed, "write_consistency_levels_disallowed"); } @Override @@ -266,6 +273,20 @@ public class GuardrailsOptions implements GuardrailsConfig x -> config.page_size_fail_threshold = x); } + @Override + public Set getTablePropertiesWarned() + { + return config.table_properties_warned; + } + + public void setTablePropertiesWarned(Set properties) + { + updatePropertyWithLogging("table_properties_warned", + validateTableProperties(properties, "table_properties_warned"), + () -> config.table_properties_warned, + x -> config.table_properties_warned = x); + } + @Override public Set getTablePropertiesIgnored() { @@ -347,6 +368,61 @@ public class GuardrailsOptions implements GuardrailsConfig x -> config.in_select_cartesian_product_fail_threshold = x); } + public Set getReadConsistencyLevelsWarned() + { + return config.read_consistency_levels_warned; + } + + public void setReadConsistencyLevelsWarned(Set consistencyLevels) + { + updatePropertyWithLogging("read_consistency_levels_warned", + validateConsistencyLevels(consistencyLevels, "read_consistency_levels_warned"), + () -> config.read_consistency_levels_warned, + x -> config.read_consistency_levels_warned = x); + } + + @Override + public Set getReadConsistencyLevelsDisallowed() + { + return config.read_consistency_levels_disallowed; + } + + public void setReadConsistencyLevelsDisallowed(Set consistencyLevels) + { + updatePropertyWithLogging("read_consistency_levels_disallowed", + validateConsistencyLevels(consistencyLevels, "read_consistency_levels_disallowed"), + () -> config.read_consistency_levels_disallowed, + x -> config.read_consistency_levels_disallowed = x); + } + + @Override + public Set getWriteConsistencyLevelsWarned() + { + return config.write_consistency_levels_warned; + } + + public void setWriteConsistencyLevelsWarned(Set consistencyLevels) + { + updatePropertyWithLogging("write_consistency_levels_warned", + validateConsistencyLevels(consistencyLevels, "write_consistency_levels_warned"), + () -> config.write_consistency_levels_warned, + x -> config.write_consistency_levels_warned = x); + } + + @Override + public Set getWriteConsistencyLevelsDisallowed() + { + return config.write_consistency_levels_disallowed; + } + + public void setWriteConsistencyLevelsDisallowed(Set consistencyLevels) + { + updatePropertyWithLogging("write_consistency_levels_disallowed", + validateConsistencyLevels(consistencyLevels, "write_consistency_levels_disallowed"), + () -> config.write_consistency_levels_disallowed, + x -> config.write_consistency_levels_disallowed = x); + } + private static void updatePropertyWithLogging(String propertyName, T newValue, Supplier getter, Consumer setter) { T oldValue = getter.get(); @@ -415,4 +491,12 @@ public class GuardrailsOptions implements GuardrailsConfig return lowerCaseProperties; } + + private static Set validateConsistencyLevels(Set consistencyLevels, String name) + { + if (consistencyLevels == null) + throw new IllegalArgumentException(format("Invalid value for %s: null is not allowed", name)); + + return consistencyLevels.isEmpty() ? Collections.emptySet() : Sets.immutableEnumSet(consistencyLevels); + } } diff --git a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java index 4f537ad910..89ece02c4d 100644 --- a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java @@ -31,6 +31,7 @@ import org.slf4j.helpers.MessageFormatter; import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; +import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.ColumnMetadata; @@ -410,6 +411,9 @@ public class BatchStatement implements CQLStatement if (options.getSerialConsistency() == null) throw new InvalidRequestException("Invalid empty serial consistency level"); + Guardrails.writeConsistencyLevels.guard(EnumSet.of(options.getConsistency(), options.getSerialConsistency()), + queryState.getClientState()); + if (hasConditions) return executeWithConditions(options, queryState, queryStartNanoTime); diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java index 52578a6318..7a467ce543 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java @@ -460,6 +460,9 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa if (options.getConsistency() == null) throw new InvalidRequestException("Invalid empty consistency level"); + Guardrails.writeConsistencyLevels.guard(EnumSet.of(options.getConsistency(), options.getSerialConsistency()), + queryState.getClientState()); + return hasConditions() ? executeWithCondition(queryState, options, queryStartNanoTime) : executeWithoutCondition(queryState, options, queryStartNanoTime); diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java index e5f4d75334..54ea8c8981 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java @@ -240,6 +240,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement checkNotNull(cl, "Invalid empty consistency level"); cl.validateForRead(); + Guardrails.readConsistencyLevels.guard(EnumSet.of(cl), state.getClientState()); int nowInSec = options.getNowInSeconds(state); int userLimit = getLimit(options); diff --git a/src/java/org/apache/cassandra/db/ConsistencyLevel.java b/src/java/org/apache/cassandra/db/ConsistencyLevel.java index ccbd9181a6..843ccb9249 100644 --- a/src/java/org/apache/cassandra/db/ConsistencyLevel.java +++ b/src/java/org/apache/cassandra/db/ConsistencyLevel.java @@ -18,6 +18,8 @@ package org.apache.cassandra.db; +import java.util.Locale; + import com.carrotsearch.hppc.ObjectIntHashMap; import org.apache.cassandra.locator.Endpoints; import org.apache.cassandra.locator.InOurDc; @@ -81,6 +83,11 @@ public enum ConsistencyLevel return codeIdx[code]; } + public static ConsistencyLevel fromString(String str) + { + return valueOf(str.toUpperCase(Locale.US)); + } + public static int quorumFor(AbstractReplicationStrategy replicationStrategy) { return (replicationStrategy.getReplicationFactor().allReplicas / 2) + 1; diff --git a/src/java/org/apache/cassandra/db/guardrails/Guardrails.java b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java index 13ac27c406..b100df5272 100644 --- a/src/java/org/apache/cassandra/db/guardrails/Guardrails.java +++ b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java @@ -18,13 +18,18 @@ package org.apache.cassandra.db.guardrails; +import java.util.Collections; import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; +import org.apache.commons.lang3.StringUtils; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.GuardrailsOptions; +import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.utils.MBeanWrapper; @@ -101,10 +106,11 @@ public final class Guardrails implements GuardrailsMBean threshold, what)); /** - * Guardrail ignoring/disallowing the usage of certain table properties. + * Guardrail warning about, ignoring or rejecting the usage of certain table properties. */ public static final Values tableProperties = - new Values<>(state -> CONFIG_PROVIDER.getOrCreate(state).getTablePropertiesIgnored(), + new Values<>(state -> CONFIG_PROVIDER.getOrCreate(state).getTablePropertiesWarned(), + state -> CONFIG_PROVIDER.getOrCreate(state).getTablePropertiesIgnored(), state -> CONFIG_PROVIDER.getOrCreate(state).getTablePropertiesDisallowed(), "Table Properties"); @@ -161,6 +167,23 @@ public final class Guardrails implements GuardrailsMBean : format("Aborting query because the cartesian product of the IN restrictions on %s " + "produces %d values, this exceeds fail threshold of %s.", what, value, threshold)); + /** + * Guardrail on read consistency levels. + */ + public static final Values readConsistencyLevels = + new Values<>(state -> CONFIG_PROVIDER.getOrCreate(state).getReadConsistencyLevelsWarned(), + state -> Collections.emptySet(), + state -> CONFIG_PROVIDER.getOrCreate(state).getReadConsistencyLevelsDisallowed(), + "read consistency levels"); + + /** + * Guardrail on write consistency levels. + */ + public static final Values writeConsistencyLevels = + new Values<>(state -> CONFIG_PROVIDER.getOrCreate(state).getWriteConsistencyLevelsWarned(), + state -> Collections.emptySet(), + state -> CONFIG_PROVIDER.getOrCreate(state).getWriteConsistencyLevelsDisallowed(), + "write consistency levels"); private Guardrails() { @@ -280,6 +303,35 @@ public final class Guardrails implements GuardrailsMBean DEFAULT_CONFIG.setMaterializedViewsPerTableThreshold(warn, fail); } + @Override + public Set getTablePropertiesWarned() + { + return DEFAULT_CONFIG.getTablePropertiesWarned(); + } + + @Override + public String getTablePropertiesWarnedCSV() + { + return toCSV(DEFAULT_CONFIG.getTablePropertiesWarned()); + } + + public void setTablePropertiesWarned(String... properties) + { + setTablePropertiesWarned(ImmutableSet.copyOf(properties)); + } + + @Override + public void setTablePropertiesWarned(Set properties) + { + DEFAULT_CONFIG.setTablePropertiesWarned(properties); + } + + @Override + public void setTablePropertiesWarnedCSV(String properties) + { + setTablePropertiesWarned(fromCSV(properties)); + } + @Override public Set getTablePropertiesDisallowed() { @@ -368,6 +420,7 @@ public final class Guardrails implements GuardrailsMBean DEFAULT_CONFIG.setPageSizeThreshold(warn, fail); } + @Override public boolean getReadBeforeWriteListOperationsEnabled() { return DEFAULT_CONFIG.getReadBeforeWriteListOperationsEnabled(); @@ -415,13 +468,118 @@ public final class Guardrails implements GuardrailsMBean DEFAULT_CONFIG.setInSelectCartesianProductThreshold(warn, fail); } + public Set getReadConsistencyLevelsWarned() + { + return DEFAULT_CONFIG.getReadConsistencyLevelsWarned(); + } + + @Override + public String getReadConsistencyLevelsWarnedCSV() + { + return toCSV(DEFAULT_CONFIG.getReadConsistencyLevelsWarned(), ConsistencyLevel::toString); + } + + @Override + public void setReadConsistencyLevelsWarned(Set consistencyLevels) + { + DEFAULT_CONFIG.setReadConsistencyLevelsWarned(consistencyLevels); + } + + @Override + public void setReadConsistencyLevelsWarnedCSV(String consistencyLevels) + { + DEFAULT_CONFIG.setReadConsistencyLevelsWarned(fromCSV(consistencyLevels, ConsistencyLevel::fromString)); + } + + @Override + public Set getReadConsistencyLevelsDisallowed() + { + return DEFAULT_CONFIG.getReadConsistencyLevelsDisallowed(); + } + + @Override + public String getReadConsistencyLevelsDisallowedCSV() + { + return toCSV(DEFAULT_CONFIG.getReadConsistencyLevelsDisallowed(), ConsistencyLevel::toString); + } + + @Override + public void setReadConsistencyLevelsDisallowed(Set consistencyLevels) + { + DEFAULT_CONFIG.setReadConsistencyLevelsDisallowed(consistencyLevels); + } + + @Override + public void setReadConsistencyLevelsDisallowedCSV(String consistencyLevels) + { + DEFAULT_CONFIG.setReadConsistencyLevelsDisallowed(fromCSV(consistencyLevels, ConsistencyLevel::fromString)); + } + + @Override + public Set getWriteConsistencyLevelsWarned() + { + return DEFAULT_CONFIG.getWriteConsistencyLevelsWarned(); + } + + @Override + public String getWriteConsistencyLevelsWarnedCSV() + { + return toCSV(DEFAULT_CONFIG.getWriteConsistencyLevelsWarned(), ConsistencyLevel::toString); + } + + @Override + public void setWriteConsistencyLevelsWarned(Set consistencyLevels) + { + DEFAULT_CONFIG.setWriteConsistencyLevelsWarned(consistencyLevels); + } + + @Override + public void setWriteConsistencyLevelsWarnedCSV(String consistencyLevels) + { + DEFAULT_CONFIG.setWriteConsistencyLevelsWarned(fromCSV(consistencyLevels, ConsistencyLevel::fromString)); + } + + @Override + public Set getWriteConsistencyLevelsDisallowed() + { + return DEFAULT_CONFIG.getWriteConsistencyLevelsDisallowed(); + } + + @Override + public String getWriteConsistencyLevelsDisallowedCSV() + { + return toCSV(DEFAULT_CONFIG.getWriteConsistencyLevelsDisallowed(), ConsistencyLevel::toString); + } + + @Override + public void setWriteConsistencyLevelsDisallowed(Set consistencyLevels) + { + DEFAULT_CONFIG.setWriteConsistencyLevelsDisallowed(consistencyLevels); + } + + @Override + public void setWriteConsistencyLevelsDisallowedCSV(String consistencyLevels) + { + DEFAULT_CONFIG.setWriteConsistencyLevelsDisallowed(fromCSV(consistencyLevels, ConsistencyLevel::fromString)); + } + private static String toCSV(Set values) { - return values == null ? "" : String.join(",", values); + return values == null || values.isEmpty() ? "" : String.join(",", values); + } + + private static String toCSV(Set values, Function formatter) + { + return values == null || values.isEmpty() ? "" : values.stream().map(formatter).collect(Collectors.joining(",")); } private static Set fromCSV(String csv) { - return csv == null ? null : ImmutableSet.copyOf(csv.split(",")); + return StringUtils.isEmpty(csv) ? Collections.emptySet() : ImmutableSet.copyOf(csv.split(",")); + } + + private static Set fromCSV(String csv, Function parser) + { + return StringUtils.isEmpty(csv) ? Collections.emptySet() : fromCSV(csv).stream().map(parser).collect(Collectors.toSet()); } } diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java index 2586436a0a..fc671e7f5d 100644 --- a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java +++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java @@ -20,6 +20,8 @@ package org.apache.cassandra.db.guardrails; import java.util.Set; +import org.apache.cassandra.db.ConsistencyLevel; + /** * Configuration settings for guardrails. * @@ -111,6 +113,11 @@ public interface GuardrailsConfig */ int getMaterializedViewsPerTableFailThreshold(); + /** + * @return The table properties that are warned about when creating or altering a table. + */ + Set getTablePropertiesWarned(); + /** * @return The table properties that are ignored when creating or altering a table. */ @@ -156,4 +163,24 @@ public interface GuardrailsConfig * -1 means disabled. */ public int getInSelectCartesianProductFailThreshold(); + + /** + * @return The consistency levels that are warned about when reading. + */ + Set getReadConsistencyLevelsWarned(); + + /** + * @return The consistency levels that are disallowed when reading. + */ + Set getReadConsistencyLevelsDisallowed(); + + /** + * @return The consistency levels that are warned about when writing. + */ + Set getWriteConsistencyLevelsWarned(); + + /** + * @return The consistency levels that are disallowed when writing. + */ + Set getWriteConsistencyLevelsDisallowed(); } diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java index b6ed551948..6b59b05141 100644 --- a/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java +++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java @@ -20,6 +20,8 @@ package org.apache.cassandra.db.guardrails; import java.util.Set; +import org.apache.cassandra.db.ConsistencyLevel; + /** * JMX entrypoint for updating the default guardrails configuration parsed from {@code cassandra.yaml}. *

@@ -135,6 +137,26 @@ public interface GuardrailsMBean */ void setMaterializedViewsPerTableThreshold(int warn, int fail); + /** + * @return properties that are warned about when creating or altering a table. + */ + Set getTablePropertiesWarned(); + + /** + * @return Comma-separated list of properties that are warned about when creating or altering a table. + */ + String getTablePropertiesWarnedCSV(); + + /** + * @param properties properties that are warned about when creating or altering a table. + */ + void setTablePropertiesWarned(Set properties); + + /** + * @param properties Comma-separated list of properties that are warned about when creating or altering a table. + */ + void setTablePropertiesWarnedCSV(String properties); + /** * @return properties that are not allowed when creating or altering a table. */ @@ -260,4 +282,84 @@ public interface GuardrailsMBean * -1 means disabled. */ public void setInSelectCartesianProductThreshold(int warn, int fail); + + /** + * @return consistency levels that are warned about when reading. + */ + Set getReadConsistencyLevelsWarned(); + + /** + * @return Comma-separated list of consistency levels that are warned about when reading. + */ + String getReadConsistencyLevelsWarnedCSV(); + + /** + * @param consistencyLevels consistency levels that are warned about when reading. + */ + void setReadConsistencyLevelsWarned(Set consistencyLevels); + + /** + * @param consistencyLevels Comma-separated list of consistency levels that are warned about when reading. + */ + void setReadConsistencyLevelsWarnedCSV(String consistencyLevels); + + /** + * @return consistency levels that are not allowed when reading. + */ + Set getReadConsistencyLevelsDisallowed(); + + /** + * @return Comma-separated list of consistency levels that are not allowed when reading. + */ + String getReadConsistencyLevelsDisallowedCSV(); + + /** + * @param consistencyLevels consistency levels that are not allowed when reading. + */ + void setReadConsistencyLevelsDisallowed(Set consistencyLevels); + + /** + * @param consistencyLevels Comma-separated list of consistency levels that are not allowed when reading. + */ + void setReadConsistencyLevelsDisallowedCSV(String consistencyLevels); + + /** + * @return consistency levels that are warned about when writing. + */ + Set getWriteConsistencyLevelsWarned(); + + /** + * @return Comma-separated list of consistency levels that are warned about when writing. + */ + String getWriteConsistencyLevelsWarnedCSV(); + + /** + * @param consistencyLevels consistency levels that are warned about when writing. + */ + void setWriteConsistencyLevelsWarned(Set consistencyLevels); + + /** + * @param consistencyLevels Comma-separated list of consistency levels that are warned about when writing. + */ + void setWriteConsistencyLevelsWarnedCSV(String consistencyLevels); + + /** + * @return consistency levels that are not allowed when writing. + */ + Set getWriteConsistencyLevelsDisallowed(); + + /** + * @return Comma-separated list of consistency levels that are not allowed when writing. + */ + String getWriteConsistencyLevelsDisallowedCSV(); + + /** + * @param consistencyLevels consistency levels that are not allowed when writing. + */ + void setWriteConsistencyLevelsDisallowed(Set consistencyLevels); + + /** + * @param consistencyLevels Comma-separated list of consistency levels that are not allowed when writing. + */ + void setWriteConsistencyLevelsDisallowedCSV(String consistencyLevels); } diff --git a/src/java/org/apache/cassandra/db/guardrails/Values.java b/src/java/org/apache/cassandra/db/guardrails/Values.java index f1a62bf338..36bb171896 100644 --- a/src/java/org/apache/cassandra/db/guardrails/Values.java +++ b/src/java/org/apache/cassandra/db/guardrails/Values.java @@ -31,12 +31,14 @@ import org.apache.cassandra.service.ClientState; import static java.lang.String.format; /** - * A guardrail that warns about but ignores some specific values, and rejects the use of some other values. + * A guardrail that warns about some specific values, warns about but ignores some other values, and/or rejects the use + * of some other values. * - * @param The type of the values of which certain are disallowed. + * @param The type of the values of which certain are warned, ignored and/or disallowed. */ public class Values extends Guardrail { + private final Function> warnedValues; private final Function> ignoredValues; private final Function> disallowedValues; private final String what; @@ -44,22 +46,45 @@ public class Values extends Guardrail /** * Creates a new values guardrail. * - * @param ignoredValues a {@link ClientState}-based of the values that are ignored. - * @param disallowedValues a {@link ClientState}-based of the values that are disallowed. + * @param warnedValues a {@link ClientState}-based provider of the values for which a warning is triggered. + * @param ignoredValues a {@link ClientState}-based provider of the values that are ignored. + * @param disallowedValues a {@link ClientState}-based provider of the values that are disallowed. * @param what The feature that is guarded by this guardrail (for reporting in error messages). */ - public Values(Function> ignoredValues, + public Values(Function> warnedValues, + Function> ignoredValues, Function> disallowedValues, String what) { + this.warnedValues = warnedValues; this.ignoredValues = ignoredValues; this.disallowedValues = disallowedValues; this.what = what; } /** - * Triggers a warning for each of the provided values that are disallowed by this guardrail and triggers an action - * to ignore them. If the values are disallowed it will abort the operation. + * Triggers a warning for each of the provided values that is discouraged by this guardrail. If any of the values + * is disallowed it will abort the operation. + *

+ * This assumes that there aren't any values to be ignored, thus it doesn't require an ignore action. If this is + * not the case and the provided value is set up to be ignored this will throw an assertion error. + * + * @param values The values to check. + * @param state The client state, used to skip the check if the query is internal or is done by a superuser. + * A {@code null} value means that the check should be done regardless of the query. + */ + public void guard(Set values, @Nullable ClientState state) + { + guard(values, x -> { + throw new AssertionError(format("There isn't an ignore action for %s, but value %s is setup to be ignored", + what, x)); + }, state); + } + + /** + * Triggers a warning for each of the provided values that is discouraged by this guardrail. Also triggers a warning + * for each of the provided values that is ignored by this guardrail and triggers the provided action to ignore it. + * If any of the values is disallowed it will abort the operation. * * @param values The values to check. * @param ignoreAction An action called on the subset of {@code values} that should be ignored. This action @@ -86,5 +111,11 @@ public class Values extends Guardrail toIgnore.stream().sorted().collect(Collectors.toList()), what, ignored)); toIgnore.forEach(ignoreAction); } + + Set warned = warnedValues.apply(state); + Set toWarn = Sets.intersection(values, warned); + if (!toWarn.isEmpty()) + warn(format("Provided values %s are not recommended for %s (warned values are: %s)", + toWarn.stream().sorted().collect(Collectors.toList()), what, warned)); } } diff --git a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java index da06cb1d06..c1e771be9f 100644 --- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java +++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java @@ -97,10 +97,12 @@ public class DatabaseDescriptorRefTest "org.apache.cassandra.config.EncryptionOptions$ServerEncryptionOptions$OutgoingEncryptedPortSource", "org.apache.cassandra.db.guardrails.GuardrailsConfig", "org.apache.cassandra.db.guardrails.GuardrailsConfigMBean", + "org.apache.cassandra.db.guardrails.GuardrailsConfig$ConsistencyLevels", "org.apache.cassandra.db.guardrails.GuardrailsConfig$IntThreshold", "org.apache.cassandra.db.guardrails.GuardrailsConfig$TableProperties", "org.apache.cassandra.config.GuardrailsOptions", "org.apache.cassandra.config.GuardrailsOptions$Config", + "org.apache.cassandra.config.GuardrailsOptions$ConsistencyLevels", "org.apache.cassandra.config.GuardrailsOptions$IntThreshold", "org.apache.cassandra.config.GuardrailsOptions$TableProperties", "org.apache.cassandra.config.GuardrailsOptions$Threshold", diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailConsistencyLevelsTester.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailConsistencyLevelsTester.java new file mode 100644 index 0000000000..c81f49d3f5 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailConsistencyLevelsTester.java @@ -0,0 +1,131 @@ +/* + * 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.db.guardrails; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableSet; +import org.junit.Before; +import org.junit.Test; + +import org.apache.cassandra.db.ConsistencyLevel; + +/** + * Utilty class for testing the guardrails for read/write consistency levels. + */ +public abstract class GuardrailConsistencyLevelsTester extends GuardrailTester +{ + private final String warnedPropertyName; + private final String disallowePropertyName; + private final Function> warnedGetter; + private final Function> disallowedGetter; + private final Function warnedCSVGetter; + private final Function disallowedCSVGetter; + private final BiConsumer> warnedSetter; + private final BiConsumer> disallowedSetter; + private final BiConsumer warnedCSVSetter; + private final BiConsumer disallowedCSVSetter; + + public GuardrailConsistencyLevelsTester(String warnedPropertyName, + String disallowePropertyName, + Function> warnedGetter, + Function> disallowedGetter, + Function warnedCSVGetter, + Function disallowedCSVGetter, + BiConsumer> warnedSetter, + BiConsumer> disallowedSetter, + BiConsumer warnedCSVSetter, + BiConsumer disallowedCSVSetter) + { + this.warnedPropertyName = warnedPropertyName; + this.disallowePropertyName = disallowePropertyName; + this.warnedGetter = warnedGetter; + this.disallowedGetter = disallowedGetter; + this.warnedCSVGetter = g -> sortCSV(warnedCSVGetter.apply(g)); + this.disallowedCSVGetter = g -> sortCSV(disallowedCSVGetter.apply(g)); + this.warnedSetter = warnedSetter; + this.disallowedSetter = disallowedSetter; + this.warnedCSVSetter = warnedCSVSetter; + this.disallowedCSVSetter = disallowedCSVSetter; + } + + @Before + public void before() + { + warnConsistencyLevels(); + disableConsistencyLevels(); + } + + protected void warnConsistencyLevels(ConsistencyLevel... consistencyLevels) + { + warnedSetter.accept(guardrails(), ImmutableSet.copyOf(consistencyLevels)); + } + + protected void disableConsistencyLevels(ConsistencyLevel... consistencyLevels) + { + disallowedSetter.accept(guardrails(), ImmutableSet.copyOf(consistencyLevels)); + } + + @Test + public void testConfigValidation() + { + String message = "Invalid value for %s: null is not allowed"; + assertInvalidProperty(warnedSetter, null, message, warnedPropertyName); + assertInvalidProperty(disallowedSetter, null, message, disallowePropertyName); + + assertValidProperty(Collections.emptySet()); + assertValidProperty(EnumSet.allOf(ConsistencyLevel.class)); + + assertValidPropertyCSV(""); + assertValidPropertyCSV(EnumSet.allOf(ConsistencyLevel.class) + .stream() + .map(ConsistencyLevel::toString) + .collect(Collectors.joining(","))); + + assertInvalidPropertyCSV("invalid", "INVALID"); + assertInvalidPropertyCSV("ONE,invalid1,invalid2", "INVALID1"); + assertInvalidPropertyCSV("invalid1,invalid2,ONE", "INVALID1"); + assertInvalidPropertyCSV("invalid1,ONE,invalid2", "INVALID1"); + } + + private void assertValidProperty(Set properties) + { + assertValidProperty(warnedSetter, warnedGetter, properties); + assertValidProperty(disallowedSetter, disallowedGetter, properties); + } + + private void assertValidPropertyCSV(String csv) + { + csv = sortCSV(csv); + assertValidProperty(warnedCSVSetter, warnedCSVGetter, csv); + assertValidProperty(disallowedCSVSetter, disallowedCSVGetter, csv); + } + + private void assertInvalidPropertyCSV(String properties, String rejected) + { + String message = "No enum constant org.apache.cassandra.db.ConsistencyLevel.%s"; + assertInvalidProperty(warnedCSVSetter, properties, message, rejected); + assertInvalidProperty(disallowedCSVSetter, properties, message, rejected); + } +} diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailReadConsistencyLevelsTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailReadConsistencyLevelsTest.java new file mode 100644 index 0000000000..dccd306aab --- /dev/null +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailReadConsistencyLevelsTest.java @@ -0,0 +1,118 @@ +/* + * 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.db.guardrails; + +import org.junit.Test; + +import org.apache.cassandra.db.ConsistencyLevel; + +import static java.lang.String.format; +import static org.apache.cassandra.db.ConsistencyLevel.ALL; +import static org.apache.cassandra.db.ConsistencyLevel.EACH_QUORUM; +import static org.apache.cassandra.db.ConsistencyLevel.LOCAL_ONE; +import static org.apache.cassandra.db.ConsistencyLevel.LOCAL_QUORUM; +import static org.apache.cassandra.db.ConsistencyLevel.ONE; +import static org.apache.cassandra.db.ConsistencyLevel.QUORUM; + +/** + * Tests the guardrail for read consistency levels, {@link Guardrails#readConsistencyLevels}. + */ +public class GuardrailReadConsistencyLevelsTest extends GuardrailConsistencyLevelsTester +{ + public GuardrailReadConsistencyLevelsTest() + { + super("read_consistency_levels_warned", + "read_consistency_levels_disallowed", + Guardrails::getReadConsistencyLevelsWarned, + Guardrails::getReadConsistencyLevelsDisallowed, + Guardrails::getReadConsistencyLevelsWarnedCSV, + Guardrails::getReadConsistencyLevelsDisallowedCSV, + Guardrails::setReadConsistencyLevelsWarned, + Guardrails::setReadConsistencyLevelsDisallowed, + Guardrails::setReadConsistencyLevelsWarnedCSV, + Guardrails::setReadConsistencyLevelsDisallowedCSV); + } + + @Test + public void testSelect() throws Throwable + { + createTable("CREATE TABLE IF NOT EXISTS %s (k INT, c INT, v INT, PRIMARY KEY(k, c))"); + + execute("INSERT INTO %s (k, c, v) VALUES (0, 0, 0)"); + execute("INSERT INTO %s (k, c, v) VALUES (0, 1, 1)"); + execute("INSERT INTO %s (k, c, v) VALUES (1, 0, 2)"); + execute("INSERT INTO %s (k, c, v) VALUES (1, 1, 3)"); + + testQuery("SELECT * FROM %s"); + testQuery("SELECT * FROM %s WHERE k = 0"); + testQuery("SELECT * FROM %s WHERE k = 0 AND c = 0"); + } + + private void testQuery(String query) throws Throwable + { + testQuery(query, ONE); + testQuery(query, ALL); + testQuery(query, QUORUM); + testQuery(query, EACH_QUORUM); + testQuery(query, LOCAL_ONE); + testQuery(query, LOCAL_QUORUM); + } + + private void testQuery(String query, ConsistencyLevel cl) throws Throwable + { + warnConsistencyLevels(); + disableConsistencyLevels(); + assertValid(query, cl); + + warnConsistencyLevels(cl); + assertWarns(query, cl); + + disableConsistencyLevels(cl); + assertFails(query, cl); + } + + private void assertValid(String query, ConsistencyLevel cl) throws Throwable + { + assertValid(() -> execute(userClientState, query, cl)); + } + + private void assertWarns(String query, ConsistencyLevel cl) throws Throwable + { + assertWarns(() -> execute(userClientState, query, cl), + format("Provided values [%s] are not recommended for read consistency levels (warned values are: %s)", + cl, guardrails().getReadConsistencyLevelsWarned())); + + assertExcludedUsers(query, cl); + } + + private void assertFails(String query, ConsistencyLevel cl) throws Throwable + { + assertFails(() -> execute(userClientState, query, cl), + format("Provided values [%s] are not allowed for read consistency levels (disallowed values are: %s)", + cl, guardrails().getReadConsistencyLevelsDisallowed())); + + assertExcludedUsers(query, cl); + } + + private void assertExcludedUsers(String query, ConsistencyLevel cl) throws Throwable + { + assertValid(() -> execute(superClientState, query, cl)); + assertValid(() -> execute(systemClientState, query, cl)); + } +} diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablePropertiesTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablePropertiesTest.java index 03e94dca10..018700a4be 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablePropertiesTest.java +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablePropertiesTest.java @@ -45,51 +45,81 @@ public class GuardrailTablePropertiesTest extends GuardrailTester "WHERE pk IS NOT null and ck IS NOT null PRIMARY KEY(ck, pk) %s"; private static final String ALTER_VIEW = "ALTER MATERIALIZED VIEW %s.%s WITH %s"; + private static final String WARNED_PROPERTY_NAME = "table_properties_warned"; private static final String IGNORED_PROPERTY_NAME = "table_properties_ignored"; private static final String DISALLOWED_PROPERTY_NAME = "table_properties_disallowed"; @Before public void before() { - // only allow "gc_grace_seconds" and "comments" - Set allowed = new HashSet<>(Arrays.asList("gc_grace_seconds", "comment")); + // only allow "gc_grace_seconds", "comments" and "default_time_to_live" + Set allowed = new HashSet<>(Arrays.asList("gc_grace_seconds", "comment", "default_time_to_live")); guardrails().setTablePropertiesDisallowed(TableAttributes.validKeywords() .stream() .filter(p -> !allowed.contains(p)) .map(String::toUpperCase) .collect(Collectors.toSet())); - // but actually warn about "comment" - guardrails().setTablePropertiesIgnored(Collections.singleton("comment")); + // but actually ignore "comment" and warn about "default_time_to_live" + guardrails().setTablePropertiesIgnored("comment"); + guardrails().setTablePropertiesWarned("default_time_to_live"); } @Test public void testConfigValidation() { String message = "Invalid value for %s: null is not allowed"; + assertInvalidProperty(Guardrails::setTablePropertiesWarned, (Set) null, message, WARNED_PROPERTY_NAME); assertInvalidProperty(Guardrails::setTablePropertiesIgnored, (Set) null, message, IGNORED_PROPERTY_NAME); assertInvalidProperty(Guardrails::setTablePropertiesDisallowed, (Set) null, message, DISALLOWED_PROPERTY_NAME); assertValidProperty(Collections.emptySet()); assertValidProperty(TableAttributes.allKeywords()); + + assertValidPropertyCSV(""); + assertValidPropertyCSV(String.join(",", TableAttributes.allKeywords())); + assertInvalidProperty(Collections.singleton("invalid"), Collections.singleton("invalid")); assertInvalidProperty(ImmutableSet.of("comment", "invalid1", "invalid2"), ImmutableSet.of("invalid1", "invalid2")); assertInvalidProperty(ImmutableSet.of("invalid1", "invalid2", "comment"), ImmutableSet.of("invalid1", "invalid2")); assertInvalidProperty(ImmutableSet.of("invalid1", "comment", "invalid2"), ImmutableSet.of("invalid1", "invalid2")); + + assertInvalidPropertyCSV("invalid", "[invalid]"); + assertInvalidPropertyCSV("comment,invalid1,invalid2", "[invalid1, invalid2]"); + assertInvalidPropertyCSV("invalid1,invalid2,comment", "[invalid1, invalid2]"); + assertInvalidPropertyCSV("invalid1,comment,invalid2", "[invalid1, invalid2]"); } private void assertValidProperty(Set properties) { - assertValidProperty(Guardrails::setTablePropertiesIgnored, properties); - assertValidProperty(Guardrails::setTablePropertiesDisallowed, properties); + assertValidProperty(Guardrails::setTablePropertiesWarned, Guardrails::getTablePropertiesWarned, properties); + assertValidProperty(Guardrails::setTablePropertiesIgnored, Guardrails::getTablePropertiesIgnored, properties); + assertValidProperty(Guardrails::setTablePropertiesDisallowed, Guardrails::getTablePropertiesDisallowed, properties); + } + + private void assertValidPropertyCSV(String csv) + { + csv = sortCSV(csv); + assertValidProperty(Guardrails::setTablePropertiesWarnedCSV, g -> sortCSV(g.getTablePropertiesWarnedCSV()), csv); + assertValidProperty(Guardrails::setTablePropertiesIgnoredCSV, g -> sortCSV(g.getTablePropertiesIgnoredCSV()), csv); + assertValidProperty(Guardrails::setTablePropertiesDisallowedCSV, g -> sortCSV(g.getTablePropertiesDisallowedCSV()), csv); } private void assertInvalidProperty(Set properties, Set rejected) { String message = "Invalid value for %s: '%s' do not parse as valid table properties"; + assertInvalidProperty(Guardrails::setTablePropertiesWarned, properties, message, WARNED_PROPERTY_NAME, rejected); assertInvalidProperty(Guardrails::setTablePropertiesIgnored, properties, message, IGNORED_PROPERTY_NAME, rejected); assertInvalidProperty(Guardrails::setTablePropertiesDisallowed, properties, message, DISALLOWED_PROPERTY_NAME, rejected); } + private void assertInvalidPropertyCSV(String properties, String rejected) + { + String message = "Invalid value for %s: '%s' do not parse as valid table properties"; + assertInvalidProperty(Guardrails::setTablePropertiesWarnedCSV, properties, message, WARNED_PROPERTY_NAME, rejected); + assertInvalidProperty(Guardrails::setTablePropertiesIgnoredCSV, properties, message, IGNORED_PROPERTY_NAME, rejected); + assertInvalidProperty(Guardrails::setTablePropertiesDisallowedCSV, properties, message, DISALLOWED_PROPERTY_NAME, rejected); + } + @Test public void testTableProperties() throws Throwable { @@ -112,6 +142,13 @@ public class GuardrailTablePropertiesTest extends GuardrailTester keyspace(), tableName.get()).one().getString("comment")); + // default_time_to_live is "warned". So it should warn, and getting the default ttl on the created table should + // not be empty, since we don't ignore it. + assertWarns(() -> tableName.set(createTableWithProperties("with default_time_to_live = 1000")), "[default_time_to_live]"); + assertEquals(1000, executeNet("SELECT default_time_to_live FROM system_schema.tables WHERE keyspace_name=? AND table_name=?", + keyspace(), + tableName.get()).one().getInt("default_time_to_live")); + // alter column is allowed assertValid(this::createTableWithProperties); assertValid("ALTER TABLE %s ADD v1 int"); @@ -130,8 +167,6 @@ public class GuardrailTablePropertiesTest extends GuardrailTester // alter mv properties except "gc_grace_seconds" is not allowed assertValid(() -> alterViewWithProperties("gc_grace_seconds = 1000")); - assertFails(() -> alterViewWithProperties("compaction = { 'class': 'SizeTieredCompactionStrategy' } AND default_time_to_live = 1"), - "[compaction, default_time_to_live]"); assertFails(() -> alterViewWithProperties("compaction = { 'class': 'SizeTieredCompactionStrategy' } AND crc_check_chance = 1"), "[compaction, crc_check_chance]"); } diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailTester.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTester.java index 9e3a9f56f6..797476de61 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/GuardrailTester.java +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTester.java @@ -22,11 +22,15 @@ import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.util.Collections; import java.util.List; +import java.util.TreeSet; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import com.google.common.collect.ImmutableSet; +import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; @@ -37,13 +41,16 @@ import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.view.View; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.index.sasi.SASIIndex; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.ClientWarn; import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.transport.messages.ResultMessage; +import org.apache.cassandra.utils.Clock; import org.assertj.core.api.Assertions; import static java.lang.String.format; @@ -105,6 +112,12 @@ public abstract class GuardrailTester extends CQLTester setter.accept(guardrails(), value); } + protected void assertValidProperty(BiConsumer setter, Function getter, T value) + { + setter.accept(guardrails(), value); + assertEquals(value, getter.apply(guardrails())); + } + protected void assertInvalidProperty(BiConsumer setter, T value, String message, @@ -222,6 +235,23 @@ public abstract class GuardrailTester extends CQLTester assertFails(() -> execute(userClientState, query), messages); } + protected void assertThrows(CheckedFunction function, Class exception, String message) + { + try + { + function.apply(); + fail("Expected to fail, but it did not"); + } + catch (Throwable e) + { + if (!exception.isAssignableFrom(e.getClass())) + Assert.fail(format("Expected to fail with %s but got %s", exception.getName(), e.getClass().getName())); + + assertTrue(format("Error message '%s' does not contain expected message '%s'", e.getMessage(), message), + e.getMessage().contains(message)); + } + } + private void assertWarnings(String... messages) { List warnings = getWarnings(); @@ -282,6 +312,32 @@ public abstract class GuardrailTester extends CQLTester } protected ResultMessage execute(ClientState state, String query, List values) + { + QueryOptions options = QueryOptions.forInternalCalls(values); + + return execute(state, query, options); + } + + protected ResultMessage execute(ClientState state, String query, ConsistencyLevel cl) + { + return execute(state, query, cl, null); + } + + protected ResultMessage execute(ClientState state, String query, ConsistencyLevel cl, ConsistencyLevel serialCl) + { + QueryOptions options = QueryOptions.create(cl, + Collections.emptyList(), + false, + 10, + null, + serialCl, + ProtocolVersion.CURRENT, + KEYSPACE); + + return execute(state, query, options); + } + + protected ResultMessage execute(ClientState state, String query, QueryOptions options) { QueryState queryState = new QueryState(state); @@ -289,8 +345,11 @@ public abstract class GuardrailTester extends CQLTester CQLStatement statement = QueryProcessor.parseStatement(formattedQuery, queryState.getClientState()); statement.validate(state); - QueryOptions options = QueryOptions.forInternalCalls(values); + return statement.execute(queryState, options, Clock.Global.nanoTime()); + } - return statement.executeLocally(queryState, options); + protected static String sortCSV(String csv) + { + return String.join(",", (new TreeSet<>(ImmutableSet.copyOf((csv.split(",")))))); } } diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailWriteConsistencyLevelsTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailWriteConsistencyLevelsTest.java new file mode 100644 index 0000000000..3c38a26ee2 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailWriteConsistencyLevelsTest.java @@ -0,0 +1,208 @@ +/* + * 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.db.guardrails; + +import org.junit.Before; +import org.junit.Test; + +import org.apache.cassandra.db.ConsistencyLevel; + +import static java.lang.String.format; +import static org.apache.cassandra.db.ConsistencyLevel.ALL; +import static org.apache.cassandra.db.ConsistencyLevel.ANY; +import static org.apache.cassandra.db.ConsistencyLevel.LOCAL_ONE; +import static org.apache.cassandra.db.ConsistencyLevel.LOCAL_QUORUM; +import static org.apache.cassandra.db.ConsistencyLevel.LOCAL_SERIAL; +import static org.apache.cassandra.db.ConsistencyLevel.ONE; +import static org.apache.cassandra.db.ConsistencyLevel.QUORUM; +import static org.apache.cassandra.db.ConsistencyLevel.SERIAL; + +/** + * Tests the guardrail for write consistency levels, {@link Guardrails#writeConsistencyLevels}. + */ +public class GuardrailWriteConsistencyLevelsTest extends GuardrailConsistencyLevelsTester +{ + public GuardrailWriteConsistencyLevelsTest() + { + super("write_consistency_levels_warned", + "write_consistency_levels_disallowed", + Guardrails::getWriteConsistencyLevelsWarned, + Guardrails::getWriteConsistencyLevelsDisallowed, + Guardrails::getWriteConsistencyLevelsWarnedCSV, + Guardrails::getWriteConsistencyLevelsDisallowedCSV, + Guardrails::setWriteConsistencyLevelsWarned, + Guardrails::setWriteConsistencyLevelsDisallowed, + Guardrails::setWriteConsistencyLevelsWarnedCSV, + Guardrails::setWriteConsistencyLevelsDisallowedCSV); + } + + @Before + public void before() + { + super.before(); + createTable("CREATE TABLE IF NOT EXISTS %s (k INT, c INT, v TEXT, PRIMARY KEY(k, c))"); + } + + @Test + public void testInsert() throws Throwable + { + testQuery("INSERT INTO %s (k, c, v) VALUES (1, 2, 'val')"); + testLWTQuery("INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') IF NOT EXISTS"); + } + + @Test + public void testUpdate() throws Throwable + { + testQuery("UPDATE %s SET v = 'val2' WHERE k = 1 AND c = 2"); + testLWTQuery("UPDATE %s SET v = 'val2' WHERE k = 1 AND c = 2 IF EXISTS"); + } + + @Test + public void testDelete() throws Throwable + { + testQuery("DELETE FROM %s WHERE k=1"); + testLWTQuery("DELETE FROM %s WHERE k=1 AND c=2 IF EXISTS"); + } + + @Test + public void testBatch() throws Throwable + { + testQuery("BEGIN BATCH INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') APPLY BATCH"); + testQuery("BEGIN BATCH UPDATE %s SET v = 'val2' WHERE k = 1 AND c = 2 APPLY BATCH"); + testQuery("BEGIN BATCH DELETE FROM %s WHERE k=1 APPLY BATCH"); + + testLWTQuery("BEGIN BATCH INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') IF NOT EXISTS APPLY BATCH"); + testLWTQuery("BEGIN BATCH UPDATE %s SET v = 'val2' WHERE k = 1 AND c = 2 IF EXISTS APPLY BATCH"); + testLWTQuery("BEGIN BATCH DELETE FROM %s WHERE k=1 AND c=2 IF EXISTS APPLY BATCH"); + } + + private void testQuery(String query) throws Throwable + { + testQuery(query, ONE); + testQuery(query, ALL); + testQuery(query, ANY); + testQuery(query, QUORUM); + testQuery(query, LOCAL_ONE); + testQuery(query, LOCAL_QUORUM); + } + + private void testQuery(String query, ConsistencyLevel cl) throws Throwable + { + warnConsistencyLevels(); + disableConsistencyLevels(); + assertValid(query, cl, null); + + warnConsistencyLevels(cl); + assertWarns(query, cl, null, cl); + + warnConsistencyLevels(); + disableConsistencyLevels(cl); + assertFails(query, cl, null, cl); + } + + private void testLWTQuery(String query) throws Throwable + { + testLWTQuery(query, ONE); + testLWTQuery(query, ALL); + testLWTQuery(query, QUORUM); + testLWTQuery(query, LOCAL_ONE); + testLWTQuery(query, LOCAL_QUORUM); + } + + private void testLWTQuery(String query, ConsistencyLevel cl) throws Throwable + { + disableConsistencyLevels(); + + warnConsistencyLevels(); + assertValid(query, cl, SERIAL); + assertValid(query, cl, LOCAL_SERIAL); + assertValid(query, cl, null); + + warnConsistencyLevels(cl); + assertWarns(query, cl, SERIAL, cl); + assertWarns(query, cl, LOCAL_SERIAL, cl); + assertWarns(query, cl, null, cl); + + warnConsistencyLevels(SERIAL); + assertWarns(query, cl, SERIAL, SERIAL); + assertValid(query, cl, LOCAL_SERIAL); + assertWarns(query, cl, null, SERIAL); + + warnConsistencyLevels(LOCAL_SERIAL); + assertValid(query, cl, SERIAL); + assertWarns(query, cl, LOCAL_SERIAL, LOCAL_SERIAL); + assertValid(query, cl, null); + + warnConsistencyLevels(SERIAL, LOCAL_SERIAL); + assertWarns(query, cl, SERIAL, SERIAL); + assertWarns(query, cl, LOCAL_SERIAL, LOCAL_SERIAL); + assertWarns(query, cl, null, SERIAL); + + warnConsistencyLevels(); + + disableConsistencyLevels(cl); + assertFails(query, cl, SERIAL, cl); + assertFails(query, cl, LOCAL_SERIAL, cl); + assertFails(query, cl, null, cl); + + disableConsistencyLevels(SERIAL); + assertFails(query, cl, SERIAL, SERIAL); + assertValid(query, cl, LOCAL_SERIAL); + assertFails(query, cl, null, SERIAL); + + disableConsistencyLevels(LOCAL_SERIAL); + assertValid(query, cl, SERIAL); + assertFails(query, cl, LOCAL_SERIAL, LOCAL_SERIAL); + assertValid(query, cl, null); + + disableConsistencyLevels(SERIAL, LOCAL_SERIAL); + assertFails(query, cl, SERIAL, SERIAL); + assertFails(query, cl, LOCAL_SERIAL, LOCAL_SERIAL); + assertFails(query, cl, null, SERIAL); + } + + private void assertValid(String query, ConsistencyLevel cl, ConsistencyLevel serialCl) throws Throwable + { + assertValid(() -> execute(userClientState, query, cl, serialCl)); + } + + private void assertWarns(String query, ConsistencyLevel cl, ConsistencyLevel serialCl, ConsistencyLevel warnedCl) throws Throwable + { + assertWarns(() -> execute(userClientState, query, cl, serialCl), + format("Provided values [%s] are not recommended for write consistency levels (warned values are: %s)", + warnedCl, guardrails().getWriteConsistencyLevelsWarned())); + + assertExcludedUsers(query, cl, serialCl); + } + + private void assertFails(String query, ConsistencyLevel cl, ConsistencyLevel serialCl, ConsistencyLevel rejectedCl) throws Throwable + { + assertFails(() -> execute(userClientState, query, cl, serialCl), + format("Provided values [%s] are not allowed for write consistency levels (disallowed values are: %s)", + rejectedCl, guardrails().getWriteConsistencyLevelsDisallowed())); + + assertExcludedUsers(query, cl, serialCl); + } + + private void assertExcludedUsers(String query, ConsistencyLevel cl, ConsistencyLevel serialCl) throws Throwable + { + assertValid(() -> execute(superClientState, query, cl, serialCl)); + assertValid(() -> execute(systemClientState, query, cl, serialCl)); + } +} diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailsTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailsTest.java index e7eaeb4016..6cebccca2f 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/GuardrailsTest.java +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailsTest.java @@ -18,10 +18,12 @@ package org.apache.cassandra.db.guardrails; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import java.util.function.Consumer; @@ -155,67 +157,30 @@ public class GuardrailsTest extends GuardrailTester } @Test - public void testDisallowedValues() throws Throwable + public void testValuesWarned() throws Throwable { - // Using a sorted set below to ensure the order in the error message checked below are not random - Values disallowed = new Values<>(state -> Collections.emptySet(), - state -> insertionOrderedSet(4, 6, 20), - "integer"); + // Using a sorted set below to ensure the order in the warning message checked below is not random + Values warned = new Values<>(state -> insertionOrderedSet(4, 6, 20), + state -> Collections.emptySet(), + state -> Collections.emptySet(), + "integer"); Consumer action = i -> Assert.fail("The ignore action shouldn't have been triggered"); - assertValid(() -> disallowed.guard(set(3), action, userClientState)); - assertFails(() -> disallowed.guard(set(4), action, userClientState), - "Provided values [4] are not allowed for integer (disallowed values are: [4, 6, 20])"); - assertValid(() -> disallowed.guard(set(10), action, userClientState)); - assertFails(() -> disallowed.guard(set(20), action, userClientState), - "Provided values [20] are not allowed for integer (disallowed values are: [4, 6, 20])"); - assertValid(() -> disallowed.guard(set(200), action, userClientState)); - assertValid(() -> disallowed.guard(set(1, 2, 3), action, userClientState)); - - assertFails(() -> disallowed.guard(set(4, 6), action, null), - "Provided values [4, 6] are not allowed for integer (disallowed values are: [4, 6, 20])"); - assertFails(() -> disallowed.guard(set(4, 5, 6, 7), action, null), - "Provided values [4, 6] are not allowed for integer (disallowed values are: [4, 6, 20])"); + assertValid(() -> warned.guard(set(3), action, userClientState)); + assertWarns(() -> warned.guard(set(4), action, userClientState), + "Provided values [4] are not recommended for integer (warned values are: [4, 6, 20])"); + assertWarns(() -> warned.guard(set(4, 6), action, null), + "Provided values [4, 6] are not recommended for integer (warned values are: [4, 6, 20])"); + assertWarns(() -> warned.guard(set(4, 5, 6, 7), action, null), + "Provided values [4, 6] are not recommended for integer (warned values are: [4, 6, 20])"); } @Test - public void testDisallowedValuesUsers() throws Throwable - { - Values disallowed = new Values<>(state -> Collections.emptySet(), - state -> Collections.singleton(2), - "integer"); - - Consumer action = i -> Assert.fail("The ignore action shouldn't have been triggered"); - assertValid(() -> disallowed.guard(set(1), action, null)); - assertValid(() -> disallowed.guard(set(1), action, userClientState)); - assertValid(() -> disallowed.guard(set(1), action, systemClientState)); - assertValid(() -> disallowed.guard(set(1), action, superClientState)); - - String message = "Provided values [2] are not allowed for integer (disallowed values are: [2])"; - assertFails(() -> disallowed.guard(set(2), action, null), message); - assertFails(() -> disallowed.guard(set(2), action, userClientState), message); - assertValid(() -> disallowed.guard(set(2), action, systemClientState)); - assertValid(() -> disallowed.guard(set(2), action, superClientState)); - - Set allowedValues = set(1); - assertValid(() -> disallowed.guard(allowedValues, action, null)); - assertValid(() -> disallowed.guard(allowedValues, action, userClientState)); - assertValid(() -> disallowed.guard(allowedValues, action, systemClientState)); - assertValid(() -> disallowed.guard(allowedValues, action, superClientState)); - - Set disallowedValues = set(2); - message = "Provided values [2] are not allowed for integer (disallowed values are: [2])"; - assertFails(() -> disallowed.guard(disallowedValues, action, null), message); - assertFails(() -> disallowed.guard(disallowedValues, action, userClientState), message); - assertValid(() -> disallowed.guard(disallowedValues, action, systemClientState)); - assertValid(() -> disallowed.guard(disallowedValues, action, superClientState)); - } - - @Test - public void testIgnoredValues() throws Throwable + public void testValuesIgnored() throws Throwable { // Using a sorted set below to ensure the order in the error message checked below are not random - Values ignored = new Values<>(state -> insertionOrderedSet(4, 6, 20), + Values ignored = new Values<>(state -> Collections.emptySet(), + state -> insertionOrderedSet(4, 6, 20), state -> Collections.emptySet(), "integer"); @@ -237,6 +202,71 @@ public class GuardrailsTest extends GuardrailTester "Ignoring provided values [4, 6] as they are not supported for integer (ignored values are: [4, 6, 20])"); assertEquals(set(4, 6), triggeredOn); triggeredOn.clear(); + + assertThrows(() -> ignored.guard(set(4), userClientState), + AssertionError.class, + "There isn't an ignore action for integer, but value 4 is setup to be ignored"); + } + + @Test + public void testValuesDisallowed() throws Throwable + { + // Using a sorted set below to ensure the order in the error message checked below are not random + Values disallowed = new Values<>(state -> Collections.emptySet(), + state -> Collections.emptySet(), + state -> insertionOrderedSet(4, 6, 20), + "integer"); + + Consumer action = i -> Assert.fail("The ignore action shouldn't have been triggered"); + assertValid(() -> disallowed.guard(set(3), action, userClientState)); + assertFails(() -> disallowed.guard(set(4), action, userClientState), + "Provided values [4] are not allowed for integer (disallowed values are: [4, 6, 20])"); + assertValid(() -> disallowed.guard(set(10), action, userClientState)); + assertFails(() -> disallowed.guard(set(20), action, userClientState), + "Provided values [20] are not allowed for integer (disallowed values are: [4, 6, 20])"); + assertValid(() -> disallowed.guard(set(200), action, userClientState)); + assertValid(() -> disallowed.guard(set(1, 2, 3), action, userClientState)); + + assertFails(() -> disallowed.guard(set(4, 6), action, null), + "Provided values [4, 6] are not allowed for integer (disallowed values are: [4, 6, 20])"); + assertFails(() -> disallowed.guard(set(4, 5, 6, 7), action, null), + "Provided values [4, 6] are not allowed for integer (disallowed values are: [4, 6, 20])"); + } + + @Test + public void testValuesUsers() throws Throwable + { + Values disallowed = new Values<>(state -> Collections.singleton(2), + state -> Collections.singleton(3), + state -> Collections.singleton(4), + "integer"); + + Consumer action = i -> Assert.fail("The ignore action shouldn't have been triggered"); + + assertValid(() -> disallowed.guard(set(1), action, null)); + assertValid(() -> disallowed.guard(set(1), action, userClientState)); + assertValid(() -> disallowed.guard(set(1), action, systemClientState)); + assertValid(() -> disallowed.guard(set(1), action, superClientState)); + + String message = "Provided values [2] are not recommended for integer (warned values are: [2])"; + assertWarns(() -> disallowed.guard(set(2), action, null), message); + assertWarns(() -> disallowed.guard(set(2), action, userClientState), message); + assertValid(() -> disallowed.guard(set(2), action, systemClientState)); + assertValid(() -> disallowed.guard(set(2), action, superClientState)); + + message = "Ignoring provided values [3] as they are not supported for integer (ignored values are: [3])"; + List triggeredOn = new ArrayList<>(); + assertWarns(() -> disallowed.guard(set(3), triggeredOn::add, null), message); + assertWarns(() -> disallowed.guard(set(3), triggeredOn::add, userClientState), message); + assertValid(() -> disallowed.guard(set(3), triggeredOn::add, systemClientState)); + assertValid(() -> disallowed.guard(set(3), triggeredOn::add, superClientState)); + Assert.assertEquals(list(3, 3), triggeredOn); + + message = "Provided values [4] are not allowed for integer (disallowed values are: [4])"; + assertFails(() -> disallowed.guard(set(4), action, null), message); + assertFails(() -> disallowed.guard(set(4), action, userClientState), message); + assertValid(() -> disallowed.guard(set(4), action, systemClientState)); + assertValid(() -> disallowed.guard(set(4), action, superClientState)); } private static Set set(Integer value)