You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by ad...@apache.org on 2021/12/03 15:28:31 UTC

[cassandra] branch trunk updated: Add guardrails framework prototype

This is an automated email from the ASF dual-hosted git repository.

adelapena pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 7871a28  Add guardrails framework prototype
7871a28 is described below

commit 7871a2854217acc89d2f885619243a442cf68e22
Author: Andrés de la Peña <a....@gmail.com>
AuthorDate: Wed Nov 3 10:10:02 2021 +0000

    Add guardrails framework prototype
    
    patch by Andrés de la Peña; reviewed by David Capwell and Stefan Miklosovic for CASSANDRA-17147
    
    Co-authored-by: Sylvain Lebresne <le...@gmail.com>
    Co-authored-by: Andrés de la Peña <a....@gmail.com>
    Co-authored-by: Aleksandr Sorokoumov <al...@gmail.com>
    Co-authored-by: Zhao Yang <ja...@gmail.com>
    Co-authored-by: Jakub Żytka <ja...@datastax.com>
---
 CHANGES.txt                                        |   1 +
 conf/cassandra.yaml                                |  31 +++
 .../cassandra/auth/CassandraRoleManager.java       |   2 +-
 src/java/org/apache/cassandra/config/Config.java   |   3 +
 .../cassandra/config/DatabaseDescriptor.java       |  19 ++
 .../apache/cassandra/config/GuardrailsOptions.java | 262 ++++++++++++++++++
 .../cql3/statements/ModificationStatement.java     |   4 +
 .../cql3/statements/PropertyDefinitions.java       |  13 +
 .../statements/schema/AlterSchemaStatement.java    |   2 +-
 .../statements/schema/AlterTableStatement.java     |  22 ++
 .../cql3/statements/schema/AlterViewStatement.java |  14 +
 .../statements/schema/CreateIndexStatement.java    |  20 ++
 .../statements/schema/CreateTableStatement.java    |  30 +++
 .../statements/schema/CreateViewStatement.java     |  22 ++
 .../cql3/statements/schema/TableAttributes.java    |  11 +
 .../cassandra/db/guardrails/DisableFlag.java       |  81 ++++++
 .../apache/cassandra/db/guardrails/Guardrail.java  |  82 ++++++
 .../apache/cassandra/db/guardrails/Guardrails.java | 280 +++++++++++++++++++
 .../cassandra/db/guardrails/GuardrailsConfig.java  |  82 ++++++
 .../db/guardrails/GuardrailsConfigProvider.java    |  78 ++++++
 .../cassandra/db/guardrails/GuardrailsMBean.java   | 173 ++++++++++++
 .../apache/cassandra/db/guardrails/Threshold.java  | 153 +++++++++++
 .../org/apache/cassandra/db/guardrails/Values.java | 103 +++++++
 .../org/apache/cassandra/schema/TableMetadata.java |   5 +
 .../org/apache/cassandra/service/ClientState.java  |  31 ++-
 .../config/DatabaseDescriptorRefTest.java          |   8 +
 .../guardrails/GuardrailColumnsPerTableTest.java   | 190 +++++++++++++
 .../GuardrailSecondaryIndexesPerTable.java         | 122 +++++++++
 .../guardrails/GuardrailTablePropertiesTest.java   | 181 +++++++++++++
 .../db/guardrails/GuardrailTablesLimitTest.java    | 115 ++++++++
 .../cassandra/db/guardrails/GuardrailTester.java   | 284 +++++++++++++++++++
 .../db/guardrails/GuardrailUserTimestampsTest.java | 124 +++++++++
 .../db/guardrails/GuardrailViewsPerTableTest.java  | 125 +++++++++
 .../guardrails/GuardrailsConfigProviderTest.java   |  79 ++++++
 .../cassandra/db/guardrails/GuardrailsTest.java    | 300 +++++++++++++++++++++
 .../cassandra/db/guardrails/ThresholdTester.java   | 188 +++++++++++++
 36 files changed, 3236 insertions(+), 4 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index c8cc544..14a1623 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 4.1
+ * Add guardrails framework (CASSANDRA-17147)
  * Harden resource management on SSTable components to prevent future leaks (CASSANDRA-17174)
  * Make nodes more resilient to local unrelated files during startup (CASSANDRA-17082)
  * repair prepare message would produce a wrong error message if network timeout happened rather than reply wait timeout (CASSANDRA-16992)
diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index ecacfed..a9a1f9c 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -1571,3 +1571,34 @@ enable_drop_compact_storage: false
 #     row_index_size:
 #         warn_threshold_kb: 0
 #         abort_threshold_kb: 0
+
+# Guardrails settings.
+# guardrails:
+# Whether guardrails are enabled or not. Guardrails are disabled by default.
+# enabled: false
+# Guardrail to warn or abort when creating more user tables than threshold.
+# The two thresholds default to -1 to disable.
+#     tables:
+#         warn_threshold: -1
+#         abort_threshold: -1
+# Guardrail to warn or abort when creating/altering a table with more columns per table than threshold.
+# The two thresholds default to -1 to disable.
+#     columns_per_table:
+#         warn_threshold: -1
+#         abort_threshold: -1
+# Guardrail to warn or abort when creating more secondary indexes per table than threshold.
+# The two thresholds default to -1 to disable.
+#     secondary_indexes_per_table:
+#         warn_threshold: -1
+#         abort_threshold: -1
+# Guardrail to warn or abort when creating more materialized views per table than threshold.
+# The two thresholds default to -1 to disable.
+#     materialized_views_per_table:
+#         warn_threshold: -1
+#         abort_threshold: -1
+# Guardrail to ignore or reject properties when creating tables. By default all properties are allowed.
+#     table_properties:
+#         ignored: []
+#         disallowed: []
+# Guardrail to allow/disallow user-provided timestamps. Defaults to true.
+#     user_timestamps_enabled: true
diff --git a/src/java/org/apache/cassandra/auth/CassandraRoleManager.java b/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
index b813b55..802b998 100644
--- a/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
+++ b/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
@@ -80,7 +80,7 @@ public class CassandraRoleManager implements IRoleManager
 {
     private static final Logger logger = LoggerFactory.getLogger(CassandraRoleManager.class);
 
-    static final String DEFAULT_SUPERUSER_NAME = "cassandra";
+    public static final String DEFAULT_SUPERUSER_NAME = "cassandra";
     static final String DEFAULT_SUPERUSER_PASSWORD = "cassandra";
 
     /**
diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java
index 38fba4a..cdf83b8 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -563,6 +563,9 @@ public class Config
      */
     public volatile double range_tombstone_list_growth_factor = 1.5;
 
+    /** The configuration for guardrails. */
+    public final GuardrailsOptions guardrails = new GuardrailsOptions();
+
     /**
      * @deprecated migrate to {@link DatabaseDescriptor#isClientInitialized()}
      */
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index 8265fd2..8ed8612 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -365,6 +365,8 @@ public class DatabaseDescriptor
         applyEncryptionContext();
 
         applySslContext();
+
+        applyGuardrails();
     }
 
     private static void applySimpleConfig()
@@ -872,6 +874,23 @@ public class DatabaseDescriptor
         config.track_warnings.validate("track_warnings");
     }
 
+    public static GuardrailsOptions getGuardrailsConfig()
+    {
+        return conf.guardrails;
+    }
+
+    private static void applyGuardrails()
+    {
+        try
+        {
+            conf.guardrails.validate();
+        }
+        catch (IllegalArgumentException e)
+        {
+            throw new ConfigurationException("Invalid guardrails configuration: " + e.getMessage(), e);
+        }
+    }
+
     private static String storagedirFor(String type)
     {
         return storagedir(type + "_directory") + File.pathSeparator() + type;
diff --git a/src/java/org/apache/cassandra/config/GuardrailsOptions.java b/src/java/org/apache/cassandra/config/GuardrailsOptions.java
new file mode 100644
index 0000000..a6892d4
--- /dev/null
+++ b/src/java/org/apache/cassandra/config/GuardrailsOptions.java
@@ -0,0 +1,262 @@
+/*
+ * 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.config;
+
+import java.util.Collections;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+import com.google.common.collect.Sets;
+
+import org.apache.cassandra.cql3.statements.schema.TableAttributes;
+import org.apache.cassandra.db.guardrails.Guardrails;
+import org.apache.cassandra.db.guardrails.GuardrailsConfig;
+import org.apache.cassandra.db.guardrails.Values;
+
+import static java.lang.String.format;
+import static java.util.stream.Collectors.toSet;
+
+/**
+ * Configuration settings for guardrails populated from the Yaml file.
+ *
+ * <p>Note that the settings here must only be used to build the {@link GuardrailsConfig} class and not directly by the
+ * code checking each guarded constraint. That code should use the higher level abstractions defined in
+ * {@link Guardrails}).
+ *
+ * <p>This contains a main setting, {@code enabled}, controlling if guardrails are globally active or not, and
+ * individual settings to control each guardrail.
+ *
+ * <p>We have 2 variants of guardrails, soft (warn) and hard (abort) limits, each guardrail having either one of the
+ * variants or both. Note in particular that hard limits only make sense for guardrails triggering during query
+ * execution. For other guardrails, say one triggering during compaction, aborting that compaction does not make sense.
+ *
+ * <p>Additionally, each individual setting should have a specific value (typically -1 for numeric settings),
+ * that allows to disable the corresponding guardrail.
+ */
+public class GuardrailsOptions implements GuardrailsConfig
+{
+    public volatile boolean enabled = false;
+    public final IntThreshold tables = new IntThreshold();
+    public final IntThreshold columns_per_table = new IntThreshold();
+    public final IntThreshold secondary_indexes_per_table = new IntThreshold();
+    public final IntThreshold materialized_views_per_table = new IntThreshold();
+    public final TableProperties table_properties = new TableProperties();
+    public volatile boolean user_timestamps_enabled = true;
+
+    public void validate()
+    {
+        tables.validate("guardrails.tables");
+        columns_per_table.validate("guardrails.columns_per_table");
+        secondary_indexes_per_table.validate("guardrails.secondary_indexes_per_table");
+        materialized_views_per_table.validate("guardrails.materialized_views_per_table");
+        table_properties.validate("guardrails.table_properties");
+    }
+
+    @Override
+    public boolean getEnabled()
+    {
+        return enabled;
+    }
+
+    /**
+     * Enable/disable guardrails.
+     *
+     * @param enabled {@code true} for enabling guardrails, {@code false} for disabling them.
+     */
+    public void setEnabled(boolean enabled)
+    {
+        this.enabled = enabled;
+    }
+
+    @Override
+    public IntThreshold getTables()
+    {
+        return tables;
+    }
+
+    @Override
+    public IntThreshold getColumnsPerTable()
+    {
+        return columns_per_table;
+    }
+
+    @Override
+    public IntThreshold getSecondaryIndexesPerTable()
+    {
+        return secondary_indexes_per_table;
+    }
+
+    @Override
+    public IntThreshold getMaterializedViewsPerTable()
+    {
+        return materialized_views_per_table;
+    }
+
+    @Override
+    public TableProperties getTableProperties()
+    {
+        return table_properties;
+    }
+
+    @Override
+    public boolean getUserTimestampsEnabled()
+    {
+        return user_timestamps_enabled;
+    }
+
+    public void setUserTimestampsEnabled(boolean enabled)
+    {
+        user_timestamps_enabled = enabled;
+    }
+
+    public static abstract class Threshold implements org.apache.cassandra.db.guardrails.Threshold.Config
+    {
+        public static final long DISABLED = -1;
+
+        public volatile long warn_threshold = DISABLED;
+        public volatile long abort_threshold = DISABLED;
+
+        @Override
+        public long getWarnThreshold()
+        {
+            return warn_threshold;
+        }
+
+        @Override
+        public long getAbortThreshold()
+        {
+            return abort_threshold;
+        }
+
+        public void setThresholds(long warn, long abort)
+        {
+            validateStrictlyPositive(warn, "warn threshold");
+            validateStrictlyPositive(abort, "abort threshold");
+            validateWarnLowerThanAbort(warn, abort, null);
+            warn_threshold = warn;
+            abort_threshold = abort;
+        }
+
+        public void validate(String name)
+        {
+            validateStrictlyPositive(warn_threshold, name + ".warn_threshold");
+            validateStrictlyPositive(abort_threshold, name + ".abort_threshold");
+            validateWarnLowerThanAbort(warn_threshold, abort_threshold, name);
+        }
+
+        public abstract long maxValue();
+        public abstract boolean allowZero();
+
+        private void validateStrictlyPositive(long value, String name)
+        {
+            if (value > maxValue())
+                throw new IllegalArgumentException(format("Invalid value %d for %s: maximum allowed value is %d",
+                                                          value, name, maxValue()));
+
+            if (value == 0 && !allowZero())
+                throw new IllegalArgumentException(format("Invalid value for %s: 0 is not allowed; " +
+                                                          "if attempting to disable use %d",
+                                                          name, DISABLED));
+
+            // We allow -1 as a general "disabling" flag. But reject anything lower to avoid mistakes.
+            if (value < DISABLED)
+                throw new IllegalArgumentException(format("Invalid value %d for %s: negative values are not allowed, " +
+                                                          "outside of %d which disables the guardrail",
+                                                          value, name, DISABLED));
+        }
+
+        private void validateWarnLowerThanAbort(long warnValue, long abortValue, @Nullable String name)
+        {
+            if (warnValue == DISABLED || abortValue == DISABLED)
+                return;
+
+            if (abortValue < warnValue)
+                throw new IllegalArgumentException(format("The warn threshold %d%s should be lower than the abort " +
+                                                          "threshold %d",
+                                                          warnValue,
+                                                          name == null ? "" : " for " + name,
+                                                          abortValue));
+        }
+    }
+
+    public static class IntThreshold extends Threshold
+    {
+        @Override
+        public long maxValue()
+        {
+            return Integer.MAX_VALUE;
+        }
+
+        @Override
+        public boolean allowZero()
+        {
+            return false;
+        }
+    }
+
+    public static class TableProperties implements Values.Config<String>
+    {
+        public volatile Set<String> ignored = Collections.emptySet();
+        public volatile Set<String> disallowed = Collections.emptySet();
+
+        @Override
+        public Set<String> getIgnored()
+        {
+            return ignored;
+        }
+
+        @Override
+        public Set<String> getDisallowed()
+        {
+            return disallowed;
+        }
+
+        public void setIgnoredValues(Set<String> values)
+        {
+            this.ignored = validateTableProperties(values, "ignored properties");
+        }
+
+        public void setDisallowedValues(Set<String> values)
+        {
+            this.disallowed = validateTableProperties(values, "disallowed properties");
+        }
+
+        private Set<String> validateTableProperties(Set<String> properties, String name)
+        {
+            if (properties == null)
+                throw new IllegalArgumentException(format("Invalid value for %s: null is not allowed", name));
+
+            Set<String> lowerCaseProperties = properties.stream().map(String::toLowerCase).collect(toSet());
+
+            Set<String> diff = Sets.difference(lowerCaseProperties, TableAttributes.allKeywords());
+
+            if (!diff.isEmpty())
+                throw new IllegalArgumentException(format("Invalid value for %s: '%s' do not parse as valid table properties",
+                                                          name, diff.toString()));
+
+            return lowerCaseProperties;
+        }
+
+        public void validate(String name)
+        {
+            validateTableProperties(ignored, name + ".ignored");
+            validateTableProperties(disallowed, name + ".disallowed");
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
index 21ddf52..b6abdca 100644
--- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
@@ -26,6 +26,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.db.guardrails.Guardrails;
 import org.apache.cassandra.db.marshal.ValueAccessor;
 import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.schema.Schema;
@@ -269,6 +270,9 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
         checkFalse(isVirtual() && attrs.isTimestampSet(), "Custom timestamp is not supported by virtual tables");
         checkFalse(isVirtual() && attrs.isTimeToLiveSet(), "Expiring columns are not supported by virtual tables");
         checkFalse(isVirtual() && hasConditions(), "Conditional updates are not supported by virtual tables");
+
+        if (attrs.isTimestampSet())
+            Guardrails.userTimestampsEnabled.ensureEnabled(state);
     }
 
     public RegularAndStaticColumns updatedColumns()
diff --git a/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java b/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java
index 590910f..c777172 100644
--- a/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java
+++ b/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java
@@ -59,6 +59,19 @@ public class PropertyDefinitions
         }
     }
 
+    /**
+     * Returns the name of all the properties that are updated by this object.
+     */
+    public Set<String> updatedProperties()
+    {
+        return properties.keySet();
+    }
+
+    public void removeProperty(String name)
+    {
+        properties.remove(name);
+    }
+
     protected String getSimple(String name) throws SyntaxException
     {
         Object val = properties.get(name);
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java
index 124d04c..3b40c68 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java
@@ -44,7 +44,7 @@ abstract public class AlterSchemaStatement implements CQLStatement.SingleKeyspac
         this.keyspaceName = keyspaceName;
     }
 
-    public final void validate(ClientState state)
+    public void validate(ClientState state)
     {
         // no-op; validation is performed while executing the statement, in apply()
     }
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java
index c8cf045..386dbae 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java
@@ -42,6 +42,7 @@ import org.apache.cassandra.cql3.CQLStatement;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.cql3.QualifiedName;
 import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.guardrails.Guardrails;
 import org.apache.cassandra.db.marshal.AbstractType;
 
 import org.apache.cassandra.exceptions.InvalidRequestException;
@@ -163,6 +164,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
         }
 
         private final Collection<Column> newColumns;
+        private ClientState state;
 
         private AddColumns(String keyspaceName, String tableName, Collection<Column> newColumns)
         {
@@ -170,11 +172,23 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
             this.newColumns = newColumns;
         }
 
+        @Override
+        public void validate(ClientState state)
+        {
+            super.validate(state);
+
+            // save the query state to use it for guardrails validation in #apply
+            this.state = state;
+        }
+
         public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table)
         {
             TableMetadata.Builder tableBuilder = table.unbuild();
             Views.Builder viewsBuilder = keyspace.views.unbuild();
             newColumns.forEach(c -> addColumn(keyspace, table, c, tableBuilder, viewsBuilder));
+
+            Guardrails.columnsPerTable.guard(tableBuilder.numColumns(), tableName, state);
+
             TableMetadata tableMetadata = tableBuilder.build();
             tableMetadata.validate();
 
@@ -391,6 +405,14 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
             this.attrs = attrs;
         }
 
+        @Override
+        public void validate(ClientState state)
+        {
+            super.validate(state);
+
+            Guardrails.tableProperties.guard(attrs.updatedProperties(), attrs::removeProperty, state);
+        }
+
         public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table)
         {
             attrs.validate();
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java
index 3493eb0..d6a0a35 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java
@@ -22,6 +22,7 @@ import org.apache.cassandra.audit.AuditLogEntryType;
 import org.apache.cassandra.auth.Permission;
 import org.apache.cassandra.cql3.CQLStatement;
 import org.apache.cassandra.cql3.QualifiedName;
+import org.apache.cassandra.db.guardrails.Guardrails;
 import org.apache.cassandra.schema.*;
 import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
 import org.apache.cassandra.service.ClientState;
@@ -33,6 +34,7 @@ public final class AlterViewStatement extends AlterSchemaStatement
 {
     private final String viewName;
     private final TableAttributes attrs;
+    private ClientState state;
 
     public AlterViewStatement(String keyspaceName, String viewName, TableAttributes attrs)
     {
@@ -41,6 +43,15 @@ public final class AlterViewStatement extends AlterSchemaStatement
         this.attrs = attrs;
     }
 
+    @Override
+    public void validate(ClientState state)
+    {
+        super.validate(state);
+
+        // save the query state to use it for guardrails validation in #apply
+        this.state = state;
+    }
+
     public Keyspaces apply(Keyspaces schema)
     {
         KeyspaceMetadata keyspace = schema.getNullable(keyspaceName);
@@ -54,6 +65,9 @@ public final class AlterViewStatement extends AlterSchemaStatement
 
         attrs.validate();
 
+        // Guardrails on table properties
+        Guardrails.tableProperties.guard(attrs.updatedProperties(), attrs::removeProperty, state);
+
         TableParams params = attrs.asAlteredTableParams(view.metadata.params);
 
         if (params.gcGraceSeconds == 0)
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java
index d501423..ae60f24 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java
@@ -19,6 +19,7 @@ package org.apache.cassandra.cql3.statements.schema;
 
 import java.util.*;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 
@@ -31,6 +32,7 @@ import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.cql3.QualifiedName;
 import org.apache.cassandra.cql3.statements.schema.IndexTarget.Type;
 import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.guardrails.Guardrails;
 import org.apache.cassandra.db.marshal.MapType;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.index.sasi.SASIIndex;
@@ -52,6 +54,8 @@ public final class CreateIndexStatement extends AlterSchemaStatement
     private final IndexAttributes attrs;
     private final boolean ifNotExists;
 
+    private ClientState state;
+
     public CreateIndexStatement(String keyspaceName,
                                 String tableName,
                                 String indexName,
@@ -67,6 +71,15 @@ public final class CreateIndexStatement extends AlterSchemaStatement
         this.ifNotExists = ifNotExists;
     }
 
+    @Override
+    public void validate(ClientState state)
+    {
+        super.validate(state);
+
+        // save the query state to use it for guardrails validation in #apply
+        this.state = state;
+    }
+
     public Keyspaces apply(Keyspaces schema)
     {
         attrs.validate();
@@ -99,6 +112,13 @@ public final class CreateIndexStatement extends AlterSchemaStatement
         if (Keyspace.open(table.keyspace).getReplicationStrategy().hasTransientReplicas())
             throw new InvalidRequestException("Secondary indexes are not supported on transiently replicated keyspaces");
 
+        // guardrails to limit number of secondary indexes per table.
+        Guardrails.secondaryIndexesPerTable.guard(table.indexes.size() + 1,
+                                                  Strings.isNullOrEmpty(indexName)
+                                                  ? String.format("on table %s", table.name)
+                                                  : String.format("%s on table %s", indexName, table.name),
+                                                  state);
+
         List<IndexTarget> indexTargets = Lists.newArrayList(transform(rawIndexTargets, t -> t.prepare(table)));
 
         if (indexTargets.isEmpty() && !attrs.isCustom)
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java
index b2a8e96..c765696 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java
@@ -33,6 +33,8 @@ import org.apache.cassandra.auth.IResource;
 import org.apache.cassandra.auth.Permission;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.*;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.guardrails.Guardrails;
 import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.exceptions.AlreadyExistsException;
 import org.apache.cassandra.schema.*;
@@ -118,6 +120,34 @@ public final class CreateTableStatement extends AlterSchemaStatement
         return schema.withAddedOrUpdated(keyspace.withSwapped(keyspace.tables.with(table)));
     }
 
+    @Override
+    public void validate(ClientState state)
+    {
+        super.validate(state);
+
+        // since there are multiple guardrails guarding this, we first check that guardrails are globally enabled as an
+        // optimization for the case where they are disabled, so we don't have to do the same check on every guardrail
+        if (Guardrails.enabled(state))
+        {
+            // Guardrails on table properties
+            Guardrails.tableProperties.guard(attrs.updatedProperties(), attrs::removeProperty, state);
+
+            // Guardrail on columns per table
+            Guardrails.columnsPerTable.guard(rawColumns.size(), tableName, state);
+
+            // Guardrails on number of tables
+            if (Guardrails.tablesLimit.enabled(state))
+            {
+                int totalUserTables = Schema.instance.getUserKeyspaces()
+                                                     .stream()
+                                                     .map(Keyspace::open)
+                                                     .mapToInt(keyspace -> keyspace.getColumnFamilyStores().size())
+                                                     .sum();
+                Guardrails.tablesLimit.guard(totalUserTables + 1, tableName, state);
+            }
+        }
+    }
+
     SchemaChange schemaChangeEvent(KeyspacesDiff diff)
     {
         return new SchemaChange(Change.CREATED, Target.TABLE, keyspaceName, tableName);
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java
index 9d6bac3..26ffeee 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.cql3.statements.schema;
 import java.util.*;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 
 import org.apache.cassandra.audit.AuditLogContext;
@@ -31,6 +32,7 @@ import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
 import org.apache.cassandra.cql3.selection.RawSelector;
 import org.apache.cassandra.cql3.selection.Selectable;
 import org.apache.cassandra.cql3.statements.StatementType;
+import org.apache.cassandra.db.guardrails.Guardrails;
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.ReversedType;
 import org.apache.cassandra.db.view.View;
@@ -65,6 +67,8 @@ public final class CreateViewStatement extends AlterSchemaStatement
 
     private final boolean ifNotExists;
 
+    private ClientState state;
+
     public CreateViewStatement(String keyspaceName,
                                String tableName,
                                String viewName,
@@ -96,6 +100,15 @@ public final class CreateViewStatement extends AlterSchemaStatement
         this.ifNotExists = ifNotExists;
     }
 
+    @Override
+    public void validate(ClientState state)
+    {
+        super.validate(state);
+
+        // save the query state to use it for guardrails validation in #apply
+        this.state = state;
+    }
+
     public Keyspaces apply(Keyspaces schema)
     {
         if (!DatabaseDescriptor.getEnableMaterializedViews())
@@ -137,6 +150,15 @@ public final class CreateViewStatement extends AlterSchemaStatement
         if (table.isView())
             throw ire("Materialized views cannot be created against other materialized views");
 
+        // Guardrails on table properties
+        Guardrails.tableProperties.guard(attrs.updatedProperties(), attrs::removeProperty, state);
+
+        // Guardrail to limit number of mvs per table
+        Iterable<ViewMetadata> tableViews = keyspace.views.forTable(table.id);
+        Guardrails.materializedViewsPerTable.guard(Iterables.size(tableViews) + 1,
+                                                   String.format("%s on table %s", viewName, table.name),
+                                                   state);
+
         if (table.params.gcGraceSeconds == 0)
         {
             throw ire("Cannot create materialized view '%s' for base table " +
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java
index 126e6d7..caeb720 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java
@@ -21,6 +21,7 @@ import java.util.Map;
 import java.util.Set;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 
 import org.apache.cassandra.cql3.statements.PropertyDefinitions;
 import org.apache.cassandra.exceptions.ConfigurationException;
@@ -83,6 +84,16 @@ public final class TableAttributes extends PropertyDefinitions
         }
     }
 
+    public static Set<String> validKeywords()
+    {
+        return ImmutableSet.copyOf(validKeywords);
+    }
+
+    public static Set<String> allKeywords()
+    {
+        return Sets.union(validKeywords, obsoleteKeywords);
+    }
+
     private TableParams build(TableParams.Builder builder)
     {
         if (hasOption(Option.BLOOM_FILTER_FP_CHANCE))
diff --git a/src/java/org/apache/cassandra/db/guardrails/DisableFlag.java b/src/java/org/apache/cassandra/db/guardrails/DisableFlag.java
new file mode 100644
index 0000000..1d34514
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/DisableFlag.java
@@ -0,0 +1,81 @@
+/*
+ * 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.function.Predicate;
+import javax.annotation.Nullable;
+
+import org.apache.cassandra.service.ClientState;
+
+/**
+ * A guardrail that completely disables the use of a particular feature.
+ *
+ * <p>Note that this guardrail only aborts operations (if the feature is disabled) so is only meant for
+ * query-based guardrails (we're happy to reject queries deemed dangerous, but we don't want to create a guardrail
+ * that breaks compaction for instance).
+ */
+public class DisableFlag extends Guardrail
+{
+    private final Predicate<ClientState> disabled;
+    private final String what;
+
+    /**
+     * Creates a new {@link DisableFlag} guardrail.
+     *
+     * @param disabled a {@link ClientState}-based supplier of boolean indicating whether the feature guarded by this
+     *                 guardrail must be disabled.
+     * @param what     The feature that is guarded by this guardrail (for reporting in error messages),
+     *                 {@link DisableFlag#ensureEnabled(String, ClientState)} can specify a different {@code what}.
+     */
+    public DisableFlag(Predicate<ClientState> disabled, String what)
+    {
+        this.disabled = disabled;
+        this.what = what;
+    }
+
+    /**
+     * Aborts the operation if this guardrail is disabled.
+     *
+     * <p>This must be called when the feature guarded by this guardrail is used to ensure such use is in fact
+     * allowed.
+     *
+     * @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 ensureEnabled(@Nullable ClientState state)
+    {
+        ensureEnabled(what, state);
+    }
+
+    /**
+     * Aborts the operation if this guardrail is disabled.
+     *
+     * <p>This must be called when the feature guarded by this guardrail is used to ensure such use is in fact
+     * allowed.
+     *
+     * @param what  The feature that is guarded by this guardrail (for reporting in error messages).
+     * @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 ensureEnabled(String what, @Nullable ClientState state)
+    {
+        if (enabled(state) && disabled.test(state))
+            abort(what + " is not allowed");
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/Guardrail.java b/src/java/org/apache/cassandra/db/guardrails/Guardrail.java
new file mode 100644
index 0000000..749fddf
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/Guardrail.java
@@ -0,0 +1,82 @@
+/*
+ * 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.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.service.ClientWarn;
+import org.apache.cassandra.tracing.Tracing;
+import org.apache.cassandra.utils.NoSpamLogger;
+
+/**
+ * General class defining a given guardrail that guards against some particular usage/condition.
+ * <p>
+ * Some guardrails only emit warnings when triggered, while others abort the query that triggers them. Some may do one
+ * or the other based on specific threshold. The queries are aborted with an {@link InvalidRequestException}.
+ * <p>
+ * Note that all the defined classes support live updates, which is why each guardrail class constructor takes
+ * suppliers of the condition the guardrail acts on rather than the condition itself. This implies that said suppliers
+ * should be fast and non-blocking to avoid surprises.
+ */
+public abstract class Guardrail
+{
+    protected static final NoSpamLogger logger = NoSpamLogger.getLogger(LoggerFactory.getLogger(Guardrail.class),
+                                                                        10, TimeUnit.MINUTES);
+
+    /**
+     * Checks whether this guardrail is enabled or not. This will be enabled if guardrails are enabled
+     * ({@link Guardrails#enabled(ClientState)}) and if the authenticated user (if specified) is not system nor
+     * superuser.
+     *
+     * @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.
+     * @return {@code true} if this guardrail is enabled, {@code false} otherwise.
+     */
+    public boolean enabled(@Nullable ClientState state)
+    {
+        return Guardrails.enabled(state) && (state == null || state.isOrdinaryUser());
+    }
+
+    protected void warn(String message)
+    {
+        logger.warn(message);
+        // Note that ClientWarn will simply ignore the message if we're not running this as part of a user query
+        // (the internal "state" will be null)
+        ClientWarn.instance.warn(message);
+        // Similarly, tracing will also ignore the message if we're not running tracing on the current thread.
+        Tracing.trace(message);
+    }
+
+    protected void abort(String message)
+    {
+        logger.error(message);
+        // Note that ClientWarn will simply ignore the message if we're not running this as part of a user query
+        // (the internal "state" will be null)
+        ClientWarn.instance.warn(message);
+        // Similarly, tracing will also ignore the message if we're not running tracing on the current thread.
+        Tracing.trace(message);
+
+        throw new InvalidRequestException(message);
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/Guardrails.java b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java
new file mode 100644
index 0000000..62b01ab
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java
@@ -0,0 +1,280 @@
+/*
+ * 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.Set;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.GuardrailsOptions;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.utils.MBeanWrapper;
+
+import static java.lang.String.format;
+
+/**
+ * Entry point for Guardrails, storing the defined guardrails and providing a few global methods over them.
+ */
+public final class Guardrails implements GuardrailsMBean
+{
+    public static final String MBEAN_NAME = "org.apache.cassandra.db:type=Guardrails";
+
+    private static final GuardrailsConfigProvider CONFIG_PROVIDER = GuardrailsConfigProvider.instance;
+    private static final GuardrailsOptions DEFAULT_CONFIG = DatabaseDescriptor.getGuardrailsConfig();
+
+    @VisibleForTesting
+    static final Guardrails instance = new Guardrails();
+
+    /**
+     * Guardrail on the total number of tables on user keyspaces.
+     */
+    public static final Threshold tablesLimit =
+    new Threshold(state -> CONFIG_PROVIDER.getOrCreate(state).getTables(),
+                  (isWarning, what, value, threshold) ->
+                  isWarning ? format("Creating table %s, current number of tables %s exceeds warning threshold of %s.",
+                                     what, value, threshold)
+                            : format("Cannot have more than %s tables, aborting the creation of table %s",
+                                     threshold, what));
+
+    /**
+     * Guardrail on the number of columns per table.
+     */
+    public static final Threshold columnsPerTable =
+    new Threshold(state -> CONFIG_PROVIDER.getOrCreate(state).getColumnsPerTable(),
+                  (isWarning, what, value, threshold) ->
+                  isWarning ? format("The table %s has %s columns, this exceeds the warning threshold of %s.",
+                                     what, value, threshold)
+                            : format("Tables cannot have more than %s columns, but %s provided for table %s",
+                                     threshold, value, what));
+
+    public static final Threshold secondaryIndexesPerTable =
+    new Threshold(state -> CONFIG_PROVIDER.getOrCreate(state).getSecondaryIndexesPerTable(),
+                  (isWarning, what, value, threshold) ->
+                  isWarning ? format("Creating secondary index %s, current number of indexes %s exceeds warning threshold of %s.",
+                                     what, value, threshold)
+                            : format("Tables cannot have more than %s secondary indexes, aborting the creation of secondary index %s",
+                                     threshold, what));
+
+    /**
+     * Guardrail on the number of materialized views per table.
+     */
+    public static final Threshold materializedViewsPerTable =
+    new Threshold(state -> CONFIG_PROVIDER.getOrCreate(state).getMaterializedViewsPerTable(),
+                  (isWarning, what, value, threshold) ->
+                  isWarning ? format("Creating materialized view %s, current number of views %s exceeds warning threshold of %s.",
+                                     what, value, threshold)
+                            : format("Tables cannot have more than %s materialized views, aborting the creation of materialized view %s",
+                                     threshold, what));
+
+    /**
+     * Guardrail ignoring/disallowing the usage of certain table properties.
+     */
+    public static final Values<String> tableProperties =
+    new Values<>(state -> CONFIG_PROVIDER.getOrCreate(state).getTableProperties(),
+                 "Table Properties");
+
+    /**
+     * Guardrail disabling user-provided timestamps.
+     */
+    public static final DisableFlag userTimestampsEnabled =
+    new DisableFlag(state -> !CONFIG_PROVIDER.getOrCreate(state).getUserTimestampsEnabled(),
+                    "User provided timestamps (USING TIMESTAMP)");
+
+    private Guardrails()
+    {
+        MBeanWrapper.instance.registerMBean(this, MBEAN_NAME);
+    }
+
+    /**
+     * Whether guardrails are enabled.
+     *
+     * @return {@code true} if guardrails are enabled and daemon is initialized,
+     * {@code false} otherwise (in which case no guardrail will trigger).
+     */
+    public static boolean enabled(ClientState state)
+    {
+        return CONFIG_PROVIDER.getOrCreate(state).getEnabled() && DatabaseDescriptor.isDaemonInitialized();
+    }
+
+    @Override
+    public boolean getEnabled()
+    {
+        return DEFAULT_CONFIG.getEnabled();
+    }
+
+    @Override
+    public void setEnabled(boolean enabled)
+    {
+        DEFAULT_CONFIG.setEnabled(enabled);
+    }
+
+    @Override
+    public int getTablesWarnThreshold()
+    {
+        return (int) DEFAULT_CONFIG.getTables().getWarnThreshold();
+    }
+
+    @Override
+    public int getTablesAbortThreshold()
+    {
+        return (int) DEFAULT_CONFIG.getTables().getAbortThreshold();
+    }
+
+    @Override
+    public void setTablesThreshold(int warn, int abort)
+    {
+        DEFAULT_CONFIG.getTables().setThresholds(warn, abort);
+    }
+
+    @Override
+    public int getColumnsPerTableWarnThreshold()
+    {
+        return (int) DEFAULT_CONFIG.getColumnsPerTable().getWarnThreshold();
+    }
+
+    @Override
+    public int getColumnsPerTableAbortThreshold()
+    {
+        return (int) DEFAULT_CONFIG.getColumnsPerTable().getAbortThreshold();
+    }
+
+    @Override
+    public void setColumnsPerTableThreshold(int warn, int abort)
+    {
+        DEFAULT_CONFIG.getColumnsPerTable().setThresholds(warn, abort);
+    }
+
+    @Override
+    public int getSecondaryIndexesPerTableWarnThreshold()
+    {
+        return (int) DEFAULT_CONFIG.getSecondaryIndexesPerTable().getWarnThreshold();
+    }
+
+    @Override
+    public int getSecondaryIndexesPerTableAbortThreshold()
+    {
+        return (int) DEFAULT_CONFIG.getSecondaryIndexesPerTable().getAbortThreshold();
+    }
+
+    @Override
+    public void setSecondaryIndexesPerTableThreshold(int warn, int abort)
+    {
+        DEFAULT_CONFIG.getSecondaryIndexesPerTable().setThresholds(warn, abort);
+    }
+
+    @Override
+    public int getMaterializedViewsPerTableWarnThreshold()
+    {
+        return (int) DEFAULT_CONFIG.getMaterializedViewsPerTable().getWarnThreshold();
+    }
+
+    @Override
+    public int getMaterializedViewsPerTableAbortThreshold()
+    {
+        return (int) DEFAULT_CONFIG.getMaterializedViewsPerTable().getAbortThreshold();
+    }
+
+    @Override
+    public void setMaterializedViewsPerTableThreshold(int warn, int abort)
+    {
+        DEFAULT_CONFIG.getMaterializedViewsPerTable().setThresholds(warn, abort);
+    }
+
+    @Override
+    public Set<String> getTablePropertiesDisallowed()
+    {
+        return DEFAULT_CONFIG.getTableProperties().getDisallowed();
+    }
+
+    @Override
+    public String getTablePropertiesDisallowedCSV()
+    {
+        return toCSV(DEFAULT_CONFIG.getTableProperties().getDisallowed());
+    }
+
+    public void setTablePropertiesDisallowed(String... properties)
+    {
+        setTablePropertiesDisallowed(ImmutableSet.copyOf(properties));
+    }
+
+    @Override
+    public void setTablePropertiesDisallowed(Set<String> properties)
+    {
+        DEFAULT_CONFIG.getTableProperties().setDisallowedValues(properties);
+    }
+
+    @Override
+    public void setTablePropertiesDisallowedCSV(String properties)
+    {
+        setTablePropertiesDisallowed(fromCSV(properties));
+    }
+
+    @Override
+    public Set<String> getTablePropertiesIgnored()
+    {
+        return DEFAULT_CONFIG.getTableProperties().getIgnored();
+    }
+
+    @Override
+    public String getTablePropertiesIgnoredCSV()
+    {
+        return toCSV(DEFAULT_CONFIG.getTableProperties().getIgnored());
+    }
+
+    public void setTablePropertiesIgnored(String... properties)
+    {
+        setTablePropertiesIgnored(ImmutableSet.copyOf(properties));
+    }
+
+    @Override
+    public void setTablePropertiesIgnored(Set<String> properties)
+    {
+        DEFAULT_CONFIG.getTableProperties().setIgnoredValues(properties);
+    }
+
+    @Override
+    public void setTablePropertiesIgnoredCSV(String properties)
+    {
+        setTablePropertiesIgnored(fromCSV(properties));
+    }
+
+    @Override
+    public boolean getUserTimestampsEnabled()
+    {
+        return DEFAULT_CONFIG.getUserTimestampsEnabled();
+    }
+
+    @Override
+    public void setUserTimestampsEnabled(boolean enabled)
+    {
+        DEFAULT_CONFIG.setUserTimestampsEnabled(enabled);
+    }
+
+    private static String toCSV(Set<String> values)
+    {
+        return values == null ? "" : String.join(",", values);
+    }
+
+    private static Set<String> fromCSV(String csv)
+    {
+        return csv == null ? null : ImmutableSet.copyOf(csv.split(","));
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java
new file mode 100644
index 0000000..8f64638
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+/**
+ * Configuration settings for guardrails.
+ *
+ * <p>Note that the settings here must only be used by the {@link Guardrails} class and not directly by the code
+ * checking each guarded constraint (which, again, should use the higher level abstractions defined in
+ * {@link Guardrails}).
+ *
+ * <p>This contains a main setting, {@code enabled}, controlling if guardrails are globally active or not, and
+ * individual settings to control each guardrail.
+ *
+ * <p>We have 2 variants of guardrails, soft (warn) and hard (abort) limits, each guardrail having either one of the
+ * variants or both. Note in particular that hard limits only make sense for guardrails triggering during query
+ * execution. For other guardrails, say one triggering during compaction, aborting that compaction does not make sense.
+ *
+ * <p>Additionally, each individual setting should have a specific value (typically -1 for numeric settings),
+ * that allows to disable the corresponding guardrail.
+ * <p>
+ * This configuration is offered as an interface so different implementations of {@link GuardrailsConfigProvider} can
+ * provide different implementations of this config. However, this mechanism for guardrails config pluggability is not
+ * officially supported and this interface may change in a minor release.
+ */
+public interface GuardrailsConfig
+{
+    /**
+     * Whether guardrails are enabled or not.
+     *
+     * @return {@code true} if guardrails are enabled, {@code false} otherwise
+     */
+    boolean getEnabled();
+
+    /**
+     * @return The threshold to warn or abort when creating more tables than threshold.
+     */
+    Threshold.Config getTables();
+
+    /**
+     * @return The threshold to warn or abort when creating more columns per table than threshold.
+     */
+    Threshold.Config getColumnsPerTable();
+
+    /**
+     * @return The threshold to warn or abort when creating more secondary indexes per table than threshold.
+     */
+    Threshold.Config getSecondaryIndexesPerTable();
+
+    /**
+     * @return The threshold to warn or abort when creating more materialized views per table than threshold.
+     */
+    Threshold.Config getMaterializedViewsPerTable();
+
+    /**
+     * @return The table properties that are ignored/disallowed when creating or altering a table.
+     */
+    Values.Config<String> getTableProperties();
+
+    /**
+     * Returns whether user-provided timestamps are allowed.
+     *
+     * @return {@code true} if user-provided timestamps are allowed, {@code false} otherwise.
+     */
+    boolean getUserTimestampsEnabled();
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfigProvider.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfigProvider.java
new file mode 100644
index 0000000..6128764
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfigProvider.java
@@ -0,0 +1,78 @@
+/*
+ * 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 javax.annotation.Nullable;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.utils.FBUtilities;
+
+/**
+ * Provider of {@link GuardrailsConfig}s for a {@link ClientState}.
+ * <p>
+ * The {@link Default} implementation always retuns the {@link GuardrailsConfig} parsed from {@code cassandra.yaml},
+ * but different implementations can return different configurations based on the specified {@link ClientState}.
+ * <p>
+ * Custom implementations can be specified at runtime with the system property {@link #CUSTOM_IMPLEMENTATION_PROPERTY}.
+ * These configurations can be used to read the guardrails configuration from some other source, and provide different
+ * configurations depending on the {@link ClientState} or some other factors. However, this mechanism for pluggability
+ * and the related {@link GuardrailsConfig} interface are not officially supported and may change in a minor release.
+ */
+public interface GuardrailsConfigProvider
+{
+    public static final String CUSTOM_IMPLEMENTATION_PROPERTY = "cassandra.custom_guardrails_config_provider_class";
+
+    static final GuardrailsConfigProvider instance = System.getProperty(CUSTOM_IMPLEMENTATION_PROPERTY) == null
+                                                     ? new Default()
+                                                     : build(System.getProperty(CUSTOM_IMPLEMENTATION_PROPERTY));
+
+    /**
+     * Returns the {@link GuardrailsConfig} to be used for the specified {@link ClientState}.
+     *
+     * @param state a client state, maybe {@code null} if the guardrails check for which we are getting the config is
+     *              for a background process that is not associated to a user query.
+     * @return the configuration to be used for {@code state}
+     */
+    GuardrailsConfig getOrCreate(@Nullable ClientState state);
+
+    /**
+     * Creates an instance of the custom guardrails config provider of the given class.
+     *
+     * @param customImpl the fully qualified classname of the guardrails config provider to be instantiated
+     * @return a new instance of the specified custom guardrails config provider of the given class.
+     */
+    static GuardrailsConfigProvider build(String customImpl)
+    {
+        return FBUtilities.construct(customImpl, "custom guardrails config provider");
+    }
+
+    /**
+     * Default implementation of {@link GuardrailsConfigProvider} that always returns the {@link GuardrailsConfig}
+     * parsed from {@code cassandra.yaml}, independently of the {@link ClientState}.
+     */
+    class Default implements GuardrailsConfigProvider
+    {
+        @Override
+        public GuardrailsConfig getOrCreate(@Nullable ClientState state)
+        {
+            return DatabaseDescriptor.getGuardrailsConfig();
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java
new file mode 100644
index 0000000..a5f6c00
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java
@@ -0,0 +1,173 @@
+/*
+ * 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.Set;
+
+/**
+ * JMX entrypoint for updating the default guardrails configuration parsed from {@code cassandra.yaml}.
+ * <p>
+ * This is different to just exposing {@link GuardrailsConfig} in that the methods here should be JMX-friendly.
+ *
+ * <p>For consistency, guardrails based on a simple numeric threshold should use the naming scheme
+ * {@code <whatIsGuarded>WarnThreshold} for soft limits and {@code <whatIsGuarded>AbortThreshold} for hard
+ * ones, and if the value has a unit, that unit should be added at the end (for instance,
+ * {@code <whatIsGuarded>AbortThresholdInKb}). For "boolean" guardrails that disable a feature, use
+ * {@code <whatIsGuardedEnabled}. Other type of guardrails can use appropriate suffixes but should start with
+ * {@code <whatIsGuarded>}.
+ */
+public interface GuardrailsMBean
+{
+    /**
+     * Whether guardrails are enabled or not.
+     *
+     * @return {@code true} if guardrails are enabled, {@code false} otherwise
+     */
+    boolean getEnabled();
+
+    /**
+     * Enable/disable guardrails.
+     *
+     * @param enabled {@code true} for enabling guardrails, {@code false} for disabling them.
+     */
+    void setEnabled(boolean enabled);
+
+    /**
+     * @return The threshold to warn when creating more tables than threshold.
+     * -1 means disabled.
+     */
+    int getTablesWarnThreshold();
+
+    /**
+     * @return The threshold to prevent creating more tables than threshold.
+     * -1 means disabled.
+     */
+    int getTablesAbortThreshold();
+
+    /**
+     * @param warn The threshold to warn when creating more tables than threshold. -1 means disabled.
+     * @param abort The threshold to prevent creating more tables than threshold. -1 means disabled.
+     */
+    void setTablesThreshold(int warn, int abort);
+
+    /**
+     * @return The threshold to warn when having more columns per table than threshold.
+     * -1 means disabled.
+     */
+    int getColumnsPerTableWarnThreshold();
+
+    /**
+     * @return The threshold to prevent having more columns per table than threshold. -1 means disabled.
+     */
+    int getColumnsPerTableAbortThreshold();
+
+    /**
+     * @param warn The threshold to warn when having more columns per table than threshold. -1 means disabled.
+     * @param abort The threshold to prevent having more columns per table than threshold. -1 means disabled.
+     */
+    void setColumnsPerTableThreshold(int warn, int abort);
+
+    /**
+     * @return The threshold to warn when creating more secondary indexes per table than threshold. -1 means disabled.
+     */
+    int getSecondaryIndexesPerTableWarnThreshold();
+
+    /**
+     * @return The threshold to prevent creating more secondary indexes per table than threshold. -1 means disabled.
+     */
+    int getSecondaryIndexesPerTableAbortThreshold();
+
+    /**
+     * @param warn The threshold to warn when creating more secondary indexes per table than threshold. -1 means disabled.
+     * @param abort The threshold to prevent creating more secondary indexes per table than threshold. -1 means disabled.
+     */
+    void setSecondaryIndexesPerTableThreshold(int warn, int abort);
+
+    /**
+     * @return The threshold to warn when creating more materialized views per table than threshold.
+     * -1 means disabled.
+     */
+    int getMaterializedViewsPerTableWarnThreshold();
+
+    /**
+     * @return The threshold to prevent creating more materialized views per table than threshold.
+     * -1 means disabled.
+     */
+    int getMaterializedViewsPerTableAbortThreshold();
+
+    /**
+     * @param warn The threshold to warn when creating more materialized views per table than threshold. -1 means disabled.
+     * @param abort The threshold to prevent creating more materialized views per table than threshold. -1 means disabled.
+     */
+    void setMaterializedViewsPerTableThreshold(int warn, int abort);
+
+    /**
+     * @return properties that are not allowed when creating or altering a table.
+     */
+    Set<String> getTablePropertiesDisallowed();
+
+    /**
+     * @return Comma-separated list of properties that are not allowed when creating or altering a table.
+     */
+    String getTablePropertiesDisallowedCSV();
+
+    /**
+     * @param properties properties that are not allowed when creating or altering a table.
+     */
+    void setTablePropertiesDisallowed(Set<String> properties);
+
+    /**
+     * @param properties Comma-separated list of properties that are not allowed when creating or altering a table.
+     */
+    void setTablePropertiesDisallowedCSV(String properties);
+
+    /**
+     * @return properties that are ignored when creating or altering a table.
+     */
+    Set<String> getTablePropertiesIgnored();
+
+    /**
+     * @return Comma-separated list of properties that are ignored when creating or altering a table.
+     */
+    String getTablePropertiesIgnoredCSV();
+
+    /**
+     * @param properties properties that are ignored when creating or altering a table.
+     */
+    void setTablePropertiesIgnored(Set<String> properties);
+
+    /**
+     * @param properties Comma-separated list of properties that are ignored when creating or altering a table.
+     */
+    void setTablePropertiesIgnoredCSV(String properties);
+
+    /**
+     * Returns whether user-provided timestamps are allowed.
+     *
+     * @return {@code true} if user-provided timestamps are allowed, {@code false} otherwise.
+     */
+    boolean getUserTimestampsEnabled();
+
+    /**
+     * Sets whether user-provided timestamps are allowed.
+     *
+     * @param enabled {@code true} if user-provided timestamps are allowed, {@code false} otherwise.
+     */
+    void setUserTimestampsEnabled(boolean enabled);
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/Threshold.java b/src/java/org/apache/cassandra/db/guardrails/Threshold.java
new file mode 100644
index 0000000..7115b9c
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/Threshold.java
@@ -0,0 +1,153 @@
+/*
+ * 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.function.Function;
+import javax.annotation.Nullable;
+
+import org.apache.cassandra.service.ClientState;
+
+/**
+ * A guardrail based on numeric threshold(s).
+ *
+ * <p>A {@link Threshold} guardrail defines (up to) 2 thresholds, one at which a warning is issued, and a higher one
+ * at which the operation is aborted with an exception. Only one of those thresholds can be activated if desired.
+ *
+ * <p>This guardrail only handles guarding positive values.
+ */
+public class Threshold extends Guardrail
+{
+    private final Function<ClientState, Config> configProvider;
+    private final ErrorMessageProvider messageProvider;
+
+    /**
+     * Creates a new threshold guardrail.
+     *
+     * @param configProvider  a {@link ClientState}-based provider of {@link Config}s.
+     * @param messageProvider a function to generate the warning or error message if the guardrail is triggered
+     */
+    public Threshold(Function<ClientState, Config> configProvider, ErrorMessageProvider messageProvider)
+    {
+        this.configProvider = configProvider;
+        this.messageProvider = messageProvider;
+    }
+
+    private String errMsg(boolean isWarning, String what, long value, long thresholdValue)
+    {
+        return messageProvider.createMessage(isWarning,
+                                             what,
+                                             value,
+                                             thresholdValue);
+    }
+
+    private long abortValue(Config config)
+    {
+        long abortValue = config.getAbortThreshold();
+        return abortValue < 0 ? Long.MAX_VALUE : abortValue;
+    }
+
+    private long warnValue(Config config)
+    {
+        long warnValue = config.getWarnThreshold();
+        return warnValue < 0 ? Long.MAX_VALUE : warnValue;
+    }
+
+    @Override
+    public boolean enabled(@Nullable ClientState state)
+    {
+        if (!super.enabled(state))
+            return false;
+
+        Config config = configProvider.apply(state);
+        return config.getAbortThreshold() >= 0 || config.getWarnThreshold() >= 0;
+    }
+
+    /**
+     * Apply the guardrail to the provided value, warning or aborting if appropriate.
+     *
+     * @param value The value to check.
+     * @param what  A string describing what {@code value} is a value of. This is used in the error message if the
+     *              guardrail is triggered. For instance, say the guardrail guards the size of column values, then this
+     *              argument must describe which column of which row is triggering the guardrail for convenience.
+     * @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(long value, String what, @Nullable ClientState state)
+    {
+        if (!enabled(state))
+            return;
+
+        Config config = configProvider.apply(state);
+
+        long abortValue = abortValue(config);
+        if (value > abortValue)
+        {
+            triggerAbort(value, abortValue, what);
+            return;
+        }
+
+        long warnValue = warnValue(config);
+        if (value > warnValue)
+            triggerWarn(value, warnValue, what);
+    }
+
+    private void triggerAbort(long value, long abortValue, String what)
+    {
+        abort(errMsg(false, what, value, abortValue));
+    }
+
+    private void triggerWarn(long value, long warnValue, String what)
+    {
+        warn(errMsg(true, what, value, warnValue));
+    }
+
+    /**
+     * A function used to build the error message of a triggered {@link Threshold} guardrail.
+     */
+    interface ErrorMessageProvider
+    {
+        /**
+         * Called when the guardrail is triggered to build the corresponding error message.
+         *
+         * @param isWarning Whether the trigger is a warning one; otherwise it is an abort one.
+         * @param what      A string, provided by the call to the {@link #guard} method, describing what the guardrail
+         *                  has been applied to (and that has triggered it).
+         * @param value     The value that triggered the guardrail (as a string).
+         * @param threshold The threshold that was passed to trigger the guardrail (as a string).
+         */
+        String createMessage(boolean isWarning, String what, long value, long threshold);
+    }
+
+    /**
+     * Configuration class containing the thresholds to be used to check if the guarded value should trigger a warning
+     * or abort the operation.
+     */
+    public interface Config
+    {
+        /**
+         * @return The threshold to warn when the guarded value exceeds it. A negative value means disabled.
+         */
+        public long getWarnThreshold();
+
+        /**
+         * @return The threshold to abort the operation when the guarded value exceeds it. A negative value means disabled.
+         */
+        public long getAbortThreshold();
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/Values.java b/src/java/org/apache/cassandra/db/guardrails/Values.java
new file mode 100644
index 0000000..969b1f6
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/Values.java
@@ -0,0 +1,103 @@
+/*
+ * 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.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+import com.google.common.collect.Sets;
+
+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.
+ *
+ * @param <T> The type of the values of which certain are disallowed.
+ */
+public class Values<T> extends Guardrail
+{
+    private final Function<ClientState, Config<T>> configProvider;
+    private final String what;
+
+    /**
+     * Creates a new values guardrail.
+     *
+     * @param configProvider a {@link ClientState}-based provider of {@link Config}s.
+     * @param what           The feature that is guarded by this guardrail (for reporting in error messages).
+     */
+    public Values(Function<ClientState, Config<T>> configProvider, String what)
+    {
+        this.configProvider = configProvider;
+        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.
+     *
+     * @param values       The values to check.
+     * @param ignoreAction An action called on the subset of {@code values} that should be ignored. This action
+     *                     should do whatever is necessary to make sure the value is ignored.
+     * @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<T> values, Consumer<T> ignoreAction, @Nullable ClientState state)
+    {
+        if (!enabled(state))
+            return;
+
+        Config<T> config = configProvider.apply(state);
+
+        Set<T> disallowed = config.getDisallowed();
+        Set<T> toDisallow = Sets.intersection(values, disallowed);
+        if (!toDisallow.isEmpty())
+            abort(format("Provided values %s are not allowed for %s (disallowed values are: %s)",
+                         toDisallow.stream().sorted().collect(Collectors.toList()), what, disallowed.toString()));
+
+        Set<T> ignored = config.getIgnored();
+        Set<T> toIgnore = Sets.intersection(values, ignored);
+        if (!toIgnore.isEmpty())
+        {
+            warn(format("Ignoring provided values %s as they are not supported for %s (ignored values are: %s)",
+                        toIgnore.stream().sorted().collect(Collectors.toList()), what, ignored.toString()));
+            toIgnore.forEach(ignoreAction);
+        }
+    }
+
+    /**
+     * Configuration class containing the sets of values to ignore and/or reject.
+     */
+    public interface Config<T>
+    {
+        /**
+         * @return The values to be ignored.
+         */
+        Set<T> getIgnored();
+
+        /**
+         * @return The values to be rejected.
+         */
+        Set<T> getDisallowed();
+    }
+}
diff --git a/src/java/org/apache/cassandra/schema/TableMetadata.java b/src/java/org/apache/cassandra/schema/TableMetadata.java
index 1e85d88..7205b94 100644
--- a/src/java/org/apache/cassandra/schema/TableMetadata.java
+++ b/src/java/org/apache/cassandra/schema/TableMetadata.java
@@ -987,6 +987,11 @@ public class TableMetadata implements SchemaElement
             return columns.values();
         }
 
+        public int numColumns()
+        {
+            return columns.size();
+        }
+
         public Set<String> columnNames()
         {
             return columns.values().stream().map(c -> c.name.toString()).collect(toSet());
diff --git a/src/java/org/apache/cassandra/service/ClientState.java b/src/java/org/apache/cassandra/service/ClientState.java
index 5cd4d84..14a2ccf 100644
--- a/src/java/org/apache/cassandra/service/ClientState.java
+++ b/src/java/org/apache/cassandra/service/ClientState.java
@@ -23,7 +23,6 @@ import java.net.SocketAddress;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
-import java.util.ListIterator;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicLong;
@@ -492,9 +491,37 @@ public class ClientState
             throw new UnauthorizedException("You have to be logged in and not anonymous to perform this request");
     }
 
+    /**
+     * Checks if this user is an ordinary user (not a super or system user).
+     *
+     * @return {@code true} if this user is an ordinary user, {@code false} otherwise.
+     */
+    public boolean isOrdinaryUser()
+    {
+        return !isSuper() && !isSystem();
+    }
+
+    /**
+     * Checks if this user is a super user.
+     */
+    public boolean isSuper()
+    {
+        return !DatabaseDescriptor.getAuthenticator().requireAuthentication() || (user != null && user.isSuper());
+    }
+
+    /**
+     * Checks if the user is the system user.
+     *
+     * @return {@code true} if this user is the system user, {@code false} otherwise.
+     */
+    public boolean isSystem()
+    {
+        return isInternal;
+    }
+
     public void ensureIsSuperuser(String message)
     {
-        if (DatabaseDescriptor.getAuthenticator().requireAuthentication() && (user == null || !user.isSuper()))
+        if (!isSuper())
             throw new UnauthorizedException(message);
     }
 
diff --git a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
index 921ec04..7c398c4 100644
--- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
+++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
@@ -91,6 +91,12 @@ public class DatabaseDescriptorRefTest
     "org.apache.cassandra.config.EncryptionOptions$ServerEncryptionOptions",
     "org.apache.cassandra.config.EncryptionOptions$ServerEncryptionOptions$InternodeEncryption",
     "org.apache.cassandra.config.EncryptionOptions$ServerEncryptionOptions$OutgoingEncryptedPortSource",
+    "org.apache.cassandra.db.guardrails.GuardrailsConfig",
+    "org.apache.cassandra.db.guardrails.GuardrailsConfigMBean",
+    "org.apache.cassandra.config.GuardrailsOptions",
+    "org.apache.cassandra.config.GuardrailsOptions$IntThreshold",
+    "org.apache.cassandra.config.GuardrailsOptions$TableProperties",
+    "org.apache.cassandra.config.GuardrailsOptions$Threshold",
     "org.apache.cassandra.config.ReplicaFilteringProtectionOptions",
     "org.apache.cassandra.config.YamlConfigurationLoader",
     "org.apache.cassandra.config.YamlConfigurationLoader$PropertiesChecker",
@@ -109,6 +115,8 @@ public class DatabaseDescriptorRefTest
     "org.apache.cassandra.db.commitlog.CommitLogSegmentManagerStandard",
     "org.apache.cassandra.db.commitlog.CommitLog",
     "org.apache.cassandra.db.commitlog.CommitLogMBean",
+    "org.apache.cassandra.db.guardrails.Threshold$Config",
+    "org.apache.cassandra.db.guardrails.Values$Config",
     "org.apache.cassandra.dht.IPartitioner",
     "org.apache.cassandra.distributed.api.IInstance",
     "org.apache.cassandra.distributed.api.IIsolatedExecutor",
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnsPerTableTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnsPerTableTest.java
new file mode 100644
index 0000000..cd2338b
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnsPerTableTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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.config.DatabaseDescriptor;
+
+import static java.lang.String.format;
+
+/**
+ * Tests the guardrail for the number of columns in a table, {@link Guardrails#columnsPerTable}.
+ */
+public class GuardrailColumnsPerTableTest extends ThresholdTester
+{
+    private static final int COLUMNS_PER_TABLE_WARN_THRESHOLD = 2;
+    private static final int COLUMNS_PER_TABLE_ABORT_THRESHOLD = 4;
+
+    public GuardrailColumnsPerTableTest()
+    {
+        super(COLUMNS_PER_TABLE_WARN_THRESHOLD,
+              COLUMNS_PER_TABLE_ABORT_THRESHOLD,
+              DatabaseDescriptor.getGuardrailsConfig().getColumnsPerTable(),
+              Guardrails::setColumnsPerTableThreshold,
+              Guardrails::getColumnsPerTableWarnThreshold,
+              Guardrails::getColumnsPerTableAbortThreshold);
+    }
+
+    @Override
+    protected long currentValue()
+    {
+        return getCurrentColumnFamilyStore().metadata().columns().size();
+    }
+
+    @Test
+    public void testCreateTable() throws Throwable
+    {
+        // partition key on skinny table
+        assertCreateTableValid("CREATE TABLE %s (k1 int, v int, PRIMARY KEY((k1)))");
+        assertCreateTableWarns(3, "CREATE TABLE %s (k1 int, k2 int, v int, PRIMARY KEY((k1, k2)))");
+        assertCreateTableWarns(4, "CREATE TABLE %s (k1 int, k2 int, k3 int, v int, PRIMARY KEY((k1, k2, k3)))");
+        assertCreateTableAborts(5, "CREATE TABLE %s (k1 int, k2 int, k3 int, k4 int, v int, PRIMARY KEY((k1, k2, k3, k4)))");
+        assertCreateTableAborts(6, "CREATE TABLE %s (k1 int, k2 int, k3 int, k4 int, k5 int, v int, PRIMARY KEY((k1, k2, k3, k4, k5)))");
+
+        // partition key on wide table
+        assertCreateTableWarns(3, "CREATE TABLE %s (k1 int, c int, v int, PRIMARY KEY(k1, c))");
+        assertCreateTableWarns(4, "CREATE TABLE %s (k1 int, k2 int, c int, v int, PRIMARY KEY((k1, k2), c))");
+        assertCreateTableAborts(5, "CREATE TABLE %s (k1 int, k2 int, k3 int, c int, v int, PRIMARY KEY((k1, k2, k3), c))");
+        assertCreateTableAborts(6, "CREATE TABLE %s (k1 int, k2 int, k3 int, k4 int, c int, v int, PRIMARY KEY((k1, k2, k3, k4), c))");
+
+        // clustering key
+        assertCreateTableWarns(3, "CREATE TABLE %s (k int, c1 int, v int, PRIMARY KEY(k, c1))");
+        assertCreateTableWarns(4, "CREATE TABLE %s (k int, c1 int, c2 int, v int, PRIMARY KEY(k, c1, c2))");
+        assertCreateTableAborts(5, "CREATE TABLE %s (k int, c1 int, c2 int, c3 int, v int, PRIMARY KEY(k, c1, c2, c3))");
+        assertCreateTableAborts(6, "CREATE TABLE %s (k int, c1 int, c2 int, c3 int, c4 int, v int, PRIMARY KEY(k, c1, c2, c3, c4))");
+
+        // static column
+        assertCreateTableWarns(3, "CREATE TABLE %s (k int, c int, s1 int STATIC, PRIMARY KEY(k, c))");
+        assertCreateTableWarns(4, "CREATE TABLE %s (k int, c int, s1 int STATIC, s2 int STATIC, PRIMARY KEY(k, c))");
+        assertCreateTableAborts(5, "CREATE TABLE %s (k int, c int, s1 int STATIC, s2 int STATIC, s3 int STATIC, PRIMARY KEY(k, c))");
+        assertCreateTableAborts(6, "CREATE TABLE %s (k int, c int, s1 int STATIC, s2 int STATIC, s3 int STATIC, s4 int STATIC, PRIMARY KEY(k, c))");
+
+        // regular column on skinny table
+        assertCreateTableValid("CREATE TABLE %s (k int PRIMARY KEY, v1 int)");
+        assertCreateTableWarns(3, "CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int)");
+        assertCreateTableWarns(4, "CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int)");
+        assertCreateTableAborts(5, "CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int, v4 int)");
+        assertCreateTableAborts(6, "CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int, v4 int, v5 int)");
+
+        // regular column on wide table
+        assertCreateTableWarns(3, "CREATE TABLE %s (k int, c int, v1 int, PRIMARY KEY(k, c))");
+        assertCreateTableWarns(4, "CREATE TABLE %s (k int, c int, v1 int, v2 int, PRIMARY KEY(k, c))");
+        assertCreateTableAborts(5, "CREATE TABLE %s (k int, c int, v1 int, v2 int, v3 int, PRIMARY KEY(k, c))");
+        assertCreateTableAborts(6, "CREATE TABLE %s (k int, c int, v1 int, v2 int, v3 int, v4 int, PRIMARY KEY(k, c))");
+
+        // udt
+        String udt = createType("CREATE TYPE %s (a int, b int, c int, d int)");
+        assertValid(format("CREATE TABLE %s (k int PRIMARY KEY, v %s)", createTableName(), udt));
+    }
+
+    @Test
+    public void testAlterTableAddColumn() throws Throwable
+    {
+        // skinny table under threshold
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int)");
+        assertAddColumnWarns("ALTER TABLE %s ADD v2 int");
+        assertAddColumnWarns("ALTER TABLE %s ADD v3 int");
+        assertAddColumnAborts("ALTER TABLE %s ADD v4 int");
+
+        // skinny table at threshold
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int)");
+        assertAddColumnAborts("ALTER TABLE %s ADD v4 int");
+
+        // wide table
+        createTable("CREATE TABLE %s (k int, c int, v1 int, v2 int, PRIMARY KEY(k, c))");
+        assertAddColumnAborts("ALTER TABLE %s ADD v3 int");
+        assertAddColumnAborts("ALTER TABLE %s ADD s int STATIC");
+
+        // udt
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int)");
+        String udt = createType("CREATE TYPE %s (a int, b int, c int, d int)");
+        assertAddColumnWarns("ALTER TABLE %s ADD v2 " + udt);
+    }
+
+    /**
+     * Verifies that its possible to drop columns from a table that has more columns than the current threshold.
+     */
+    @Test
+    public void testAlterTableDropColumn() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int)");
+        guardrails().setColumnsPerTableThreshold(2, 2);
+        assertDropColumnValid("ALTER TABLE %s DROP v2");
+        assertDropColumnValid("ALTER TABLE %s DROP v3");
+        assertAddColumnAborts("ALTER TABLE %s ADD v2 int");
+    }
+
+    @Test
+    public void testExcludedUsers() throws Throwable
+    {
+        String table = keyspace() + '.' + createTableName();
+        testExcludedUsers(() -> format("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int, v4 int)", table),
+                          () -> format("ALTER TABLE %s ADD v5 int", table),
+                          () -> format("DROP TABLE %s", table));
+    }
+
+    private void assertCreateTableValid(String query) throws Throwable
+    {
+        assertThresholdValid(format(query, keyspace() + '.' + createTableName()));
+    }
+
+    private void assertDropColumnValid(String query) throws Throwable
+    {
+        assertValid(format(query, keyspace() + '.' + currentTable()));
+    }
+
+    private void assertCreateTableWarns(int numColumns, String query) throws Throwable
+    {
+        assertWarns(numColumns, query, createTableName());
+    }
+
+    private void assertAddColumnWarns(String query) throws Throwable
+    {
+        assertWarns(currentValue() + 1, query, currentTable());
+    }
+
+    private void assertWarns(long numColumns, String query, String tableName) throws Throwable
+    {
+        assertThresholdWarns(format("The table %s has %s columns, this exceeds the warning threshold of %s.",
+                                    tableName,
+                                    numColumns,
+                                    guardrails().getColumnsPerTableWarnThreshold()),
+                             format(query, keyspace() + '.' + tableName));
+    }
+
+    private void assertAddColumnAborts(String query) throws Throwable
+    {
+        assertAborts(currentValue() + 1, query, currentTable());
+    }
+
+    private void assertCreateTableAborts(long numColumns, String query) throws Throwable
+    {
+        assertAborts(numColumns, query, ABORT_TABLE);
+    }
+
+    private void assertAborts(long numColumns, String query, String tableName) throws Throwable
+    {
+        assertThresholdAborts(format("Tables cannot have more than %s columns, but %s provided for table %s",
+                                     guardrails().getColumnsPerTableAbortThreshold(),
+                                     numColumns,
+                                     tableName),
+                              format(query, keyspace() + '.' + tableName));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailSecondaryIndexesPerTable.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailSecondaryIndexesPerTable.java
new file mode 100644
index 0000000..debbfb1
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailSecondaryIndexesPerTable.java
@@ -0,0 +1,122 @@
+/*
+ * 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 com.google.common.base.Strings;
+import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+
+import static java.lang.String.format;
+
+/**
+ * Tests the guardrail for the number of secondary indexes in a table, {@link Guardrails#secondaryIndexesPerTable}.
+ */
+public class GuardrailSecondaryIndexesPerTable extends ThresholdTester
+{
+    private static final int INDEXES_PER_TABLE_WARN_THRESHOLD = 1;
+    private static final int INDEXES_PER_TABLE_ABORT_THRESHOLD = 3;
+
+    public GuardrailSecondaryIndexesPerTable()
+    {
+        super(INDEXES_PER_TABLE_WARN_THRESHOLD,
+              INDEXES_PER_TABLE_ABORT_THRESHOLD,
+              DatabaseDescriptor.getGuardrailsConfig().getSecondaryIndexesPerTable(),
+              Guardrails::setSecondaryIndexesPerTableThreshold,
+              Guardrails::getSecondaryIndexesPerTableWarnThreshold,
+              Guardrails::getSecondaryIndexesPerTableAbortThreshold);
+    }
+
+    @Override
+    protected long currentValue()
+    {
+        return getCurrentColumnFamilyStore().indexManager.listIndexes().size();
+    }
+
+    @Test
+    public void testCreateIndex() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int, v4 int)");
+        assertCreateIndexSucceeds("v1", "v1_idx");
+        assertCurrentValue(1);
+
+        assertCreateIndexWarns("v2", "");
+        assertCreateIndexWarns("v3", "v3_idx");
+        assertCreateIndexAborts("v4", "");
+        assertCreateIndexAborts("v2", "v2_idx");
+        assertCurrentValue(3);
+
+        // 2i guardrail will also affect custom indexes
+        assertCreateCustomIndexAborts("v2");
+
+        // drop the two first indexes, we should be able to create new indexes again
+        dropIndex(format("DROP INDEX %s.%s", keyspace(), "v3_idx"));
+        assertCurrentValue(2);
+
+        assertCreateIndexWarns("v3", "");
+        assertCreateCustomIndexAborts("v4");
+        assertCurrentValue(3);
+
+        // previous guardrail should not apply to another base table
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int, v4 int)");
+        assertCreateIndexSucceeds("v4", "");
+        assertCreateIndexWarns("v3", "");
+        assertCreateIndexWarns("v2", "");
+        assertCreateIndexAborts("v1", "");
+        assertCurrentValue(3);
+    }
+
+    @Test
+    public void testExcludedUsers() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int primary key, v1 int, v2 int)");
+        testExcludedUsers(() -> "CREATE INDEX excluded_1 ON %s(v1)",
+                          () -> "CREATE INDEX excluded_2 ON %s(v2)",
+                          () -> "DROP INDEX excluded_1",
+                          () -> "DROP INDEX excluded_2");
+    }
+
+    private void assertCreateIndexSucceeds(String column, String indexName) throws Throwable
+    {
+        assertThresholdValid(format("CREATE INDEX %s ON %s.%s(%s)", indexName, keyspace(), currentTable(), column));
+    }
+
+    private void assertCreateIndexWarns(String column, String indexName) throws Throwable
+    {
+        assertThresholdWarns(format("Creating secondary index %son table %s, current number of indexes %s exceeds warning threshold of %s.",
+                                    (Strings.isNullOrEmpty(indexName) ? "" : indexName + " "),
+                                    currentTable(),
+                                    currentValue() + 1,
+                                    guardrails().getSecondaryIndexesPerTableWarnThreshold()),
+                             format("CREATE INDEX %s ON %%s(%s)", indexName, column));
+    }
+
+    private void assertCreateIndexAborts(String column, String indexName) throws Throwable
+    {
+        assertThresholdAborts(format("aborting the creation of secondary index %son table %s",
+                                    Strings.isNullOrEmpty(indexName) ? "" : indexName + " ", currentTable()),
+                              format("CREATE INDEX %s ON %%s(%s)", indexName, column));
+    }
+
+    private void assertCreateCustomIndexAborts(String column) throws Throwable
+    {
+        assertThresholdAborts(format("aborting the creation of secondary index on table %s", currentTable()),
+                              format("CREATE CUSTOM INDEX ON %%s (%s) USING 'org.apache.cassandra.index.sasi.SASIIndex'", column));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablePropertiesTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablePropertiesTest.java
new file mode 100644
index 0000000..818a0b6
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablePropertiesTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+import com.google.common.collect.ImmutableSet;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.statements.schema.TableAttributes;
+
+import static java.lang.String.format;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests the guardrail for table properties, {@link Guardrails#tableProperties}.
+ */
+public class GuardrailTablePropertiesTest extends GuardrailTester
+{
+    private static final String CREATE_TABLE = "CREATE TABLE %s.%s(pk int, ck int, v int, PRIMARY KEY(pk, ck)) %s";
+    private static final String CREATE_VIEW = "CREATE MATERIALIZED VIEW %s.%s as SELECT * FROM %s.%s " +
+                                              "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";
+
+    @Before
+    public void before()
+    {
+        // only allow "gc_grace_seconds" and "comments"
+        Set<String> allowed = new HashSet<>(Arrays.asList("gc_grace_seconds", "comment"));
+        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"));
+    }
+
+    @Test
+    public void testConfigValidation()
+    {
+        String message = "Invalid value for %s properties: null is not allowed";
+        assertInvalidProperty(Guardrails::setTablePropertiesIgnored, (Set<String>) null, message, "ignored");
+        assertInvalidProperty(Guardrails::setTablePropertiesDisallowed, (Set<String>) null, message, "disallowed");
+
+        assertValidProperty(Collections.emptySet());
+        assertValidProperty(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"));
+    }
+
+    private void assertValidProperty(Set<String> properties)
+    {
+        assertValidProperty(Guardrails::setTablePropertiesIgnored, properties);
+        assertValidProperty(Guardrails::setTablePropertiesDisallowed, properties);
+    }
+
+    private void assertInvalidProperty(Set<String> properties, Set<String> rejected)
+    {
+        String message = "Invalid value for %s properties: '%s' do not parse as valid table properties";
+        assertInvalidProperty(Guardrails::setTablePropertiesIgnored, properties, message, "ignored", rejected);
+        assertInvalidProperty(Guardrails::setTablePropertiesDisallowed, properties, message, "disallowed", rejected);
+    }
+
+    @Test
+    public void testTableProperties() throws Throwable
+    {
+        // most table properties are not allowed
+        assertValid(this::createTableWithProperties);
+        assertAborts(() -> createTableWithProperties("with id = " + UUID.randomUUID()), "[id]");
+        assertAborts(() -> createTableWithProperties("with compression = { 'enabled': 'false' }"), "[compression]");
+        assertAborts(() -> createTableWithProperties("with compression = { 'enabled': 'false' } AND id = " + UUID.randomUUID()), "[compression, id]");
+        assertAborts(() -> createTableWithProperties("with compaction = { 'class': 'SizeTieredCompactionStrategy' }"), "[compaction]");
+        assertAborts(() -> createTableWithProperties("with gc_grace_seconds = 1000 and compression = { 'enabled': 'false' }"), "[compression]");
+
+        // though gc_grace_seconds alone is
+        assertValid(() -> createTableWithProperties("with gc_grace_seconds = 1000"));
+
+        // and comment is "ignored". So it should warn, and getting the comment on the created table should be empty,
+        // not the one we set.
+        AtomicReference<String> tableName = new AtomicReference<>();
+        assertWarns(() -> tableName.set(createTableWithProperties("with comment = 'my table'")), "[comment]");
+        assertEquals("", executeNet("SELECT comment FROM system_schema.tables WHERE keyspace_name=? AND table_name=?",
+                                    keyspace(),
+                                    tableName.get()).one().getString("comment"));
+
+        // alter column is allowed
+        assertValid(this::createTableWithProperties);
+        assertValid("ALTER TABLE %s ADD v1 int");
+        assertValid("ALTER TABLE %s DROP v1");
+        assertValid("ALTER TABLE %s RENAME pk to pk1");
+    }
+
+    @Test
+    public void testViewProperties() throws Throwable
+    {
+        // view properties is not allowed
+        createTableWithProperties();
+        assertValid(() -> createViewWithProperties(""));
+        assertAborts(() -> createViewWithProperties("with compression = { 'enabled': 'false' }"), "[compression]");
+        assertValid(() -> createViewWithProperties("with gc_grace_seconds = 1000"));
+
+        // alter mv properties except "gc_grace_seconds" is not allowed
+        assertValid(() -> alterViewWithProperties("gc_grace_seconds = 1000"));
+        assertAborts(() -> alterViewWithProperties("compaction = { 'class': 'SizeTieredCompactionStrategy' } AND default_time_to_live = 1"),
+                     "[compaction, default_time_to_live]");
+        assertAborts(() -> alterViewWithProperties("compaction = { 'class': 'SizeTieredCompactionStrategy' } AND crc_check_chance = 1"),
+                     "[compaction, crc_check_chance]");
+    }
+
+    @Test
+    public void testInvalidTableProperties()
+    {
+        assertConfigFails(c -> c.setTablePropertiesDisallowed("ID1", "gc_grace_seconds"), "[id1]");
+        assertConfigFails(c -> c.setTablePropertiesDisallowed("ID2", "Gc_Grace_Seconds"), "[id2]");
+        assertConfigFails(c -> c.setTablePropertiesIgnored("ID3", "gc_grace_seconds"), "[id3]");
+        assertConfigFails(c -> c.setTablePropertiesIgnored("ID4", "Gc_Grace_Seconds"), "[id4]");
+    }
+
+    @Test
+    public void testExcludedUsers() throws Throwable
+    {
+        testExcludedUsers(
+        () -> format(CREATE_TABLE, keyspace(), createTableName(), "WITH compaction = { 'class': 'SizeTieredCompactionStrategy' }"),
+        () -> format(CREATE_TABLE, keyspace(), createTableName(), "WITH gc_grace_seconds = 1000"),
+        () -> "ALTER TABLE %s WITH gc_grace_seconds = 1000 and default_time_to_live = 1000",
+        () -> "ALTER TABLE %s WITH compaction = { 'class': 'SizeTieredCompactionStrategy' }",
+        () -> format(CREATE_VIEW, keyspace(), createViewName(), keyspace(), currentTable(), "with compression = { 'enabled': 'false' }"),
+        () -> format(ALTER_VIEW, keyspace(), currentView(), "compaction = { 'class': 'SizeTieredCompactionStrategy' }"),
+        () -> format(ALTER_VIEW, keyspace(), currentView(), "gc_grace_seconds = 1000"),
+        () -> format(ALTER_VIEW, keyspace(), currentView(), "gc_grace_seconds = 1000 and crc_check_chance = 1"),
+        () -> format(ALTER_VIEW, keyspace(), currentView(), "compaction = { 'class': 'SizeTieredCompactionStrategy' }"));
+    }
+
+    private void createTableWithProperties()
+    {
+        createTableWithProperties("");
+    }
+
+    private String createTableWithProperties(String withClause)
+    {
+        String name = createTableName();
+        execute(userClientState, format(CREATE_TABLE, keyspace(), name, withClause));
+        return name;
+    }
+
+    private void createViewWithProperties(String withClause)
+    {
+        execute(userClientState, format(CREATE_VIEW, keyspace(), createViewName(), keyspace(), currentTable(), withClause));
+    }
+
+    private void alterViewWithProperties(String withClause)
+    {
+        execute(userClientState, format(ALTER_VIEW, keyspace(), currentView(), withClause));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablesLimitTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablesLimitTest.java
new file mode 100644
index 0000000..96f1681
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablesLimitTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.config.DatabaseDescriptor;
+import org.apache.cassandra.db.Keyspace;
+
+import static java.lang.String.format;
+
+/**
+ * Tests the guardrail for the max number of user tables, {@link Guardrails#tablesLimit}.
+ */
+public class GuardrailTablesLimitTest extends ThresholdTester
+{
+    private static final int TABLES_LIMIT_WARN_THRESHOLD = 1;
+    private static final int TABLES_LIMIT_ABORT_THRESHOLD = 2;
+
+    public GuardrailTablesLimitTest()
+    {
+        super(TABLES_LIMIT_WARN_THRESHOLD,
+              TABLES_LIMIT_ABORT_THRESHOLD,
+              DatabaseDescriptor.getGuardrailsConfig().getTables(),
+              Guardrails::setTablesThreshold,
+              Guardrails::getTablesWarnThreshold,
+              Guardrails::getTablesAbortThreshold);
+    }
+
+    @Override
+    protected long currentValue()
+    {
+        return Keyspace.open(keyspace()).getColumnFamilyStores().size();
+    }
+
+    @Test
+    public void testCreateTable() throws Throwable
+    {
+        // create tables until hitting the two warn/abort thresholds
+        String t1 = assertCreateTableValid();
+        String t2 = assertCreateTableWarns();
+        assertCreateTableAborts();
+
+        // drop a table and hit the warn/abort threshold again
+        dropTable(t2);
+        String t3 = assertCreateTableWarns();
+        assertCreateTableAborts();
+
+        // drop two tables and hit the warn/abort threshold again
+        dropTable(t1);
+        dropTable(t3);
+        assertCreateTableValid();
+        assertCreateTableWarns();
+        assertCreateTableAborts();
+
+        // test excluded users
+        testExcludedUsers(this::createTableQuery,
+                          this::createTableQuery,
+                          this::createTableQuery);
+    }
+
+    @Override
+    protected void dropTable(String tableName)
+    {
+        dropFormattedTable(format("DROP TABLE %s.%s", keyspace(), tableName));
+    }
+
+    private String assertCreateTableValid() throws Throwable
+    {
+        String tableName = createTableName();
+        assertThresholdValid(createTableQuery(tableName));
+        return tableName;
+    }
+
+    private String assertCreateTableWarns() throws Throwable
+    {
+        String tableName = createTableName();
+        assertThresholdWarns(format("Creating table %s, current number of tables 2 exceeds warning threshold of 1", tableName),
+                             createTableQuery(tableName));
+        return tableName;
+    }
+
+    private void assertCreateTableAborts() throws Throwable
+    {
+        String tableName = createTableName();
+        assertThresholdAborts(format("Cannot have more than 2 tables, aborting the creation of table %s", tableName),
+                              createTableQuery(tableName));
+    }
+
+    private String createTableQuery()
+    {
+        return createTableQuery(createTableName());
+    }
+
+    private String createTableQuery(String tableName)
+    {
+        return format("CREATE TABLE %s.%s (k1 int, v int, PRIMARY KEY((k1)))", keyspace(), tableName);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailTester.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTester.java
new file mode 100644
index 0000000..134b360
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTester.java
@@ -0,0 +1,284 @@
+/*
+ * 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.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+
+import com.datastax.driver.core.exceptions.InvalidQueryException;
+import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.auth.CassandraRoleManager;
+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.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.messages.ResultMessage;
+import org.assertj.core.api.Assertions;
+
+import static java.lang.String.format;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public abstract class GuardrailTester extends CQLTester
+{
+    // Name used when testing CREATE TABLE that should be aborted (we need to provide it as assertAborts, which
+    // is used to assert the failure, does not know that it is a CREATE TABLE and would thus reuse the name of the
+    // previously created table, which is not what we want).
+    protected static final String ABORT_TABLE = "abort_table_creation_test";
+
+    private static final String USERNAME = "guardrail_user";
+    private static final String PASSWORD = "guardrail_password";
+
+    protected static ClientState systemClientState, userClientState, superClientState;
+
+    @BeforeClass
+    public static void setUpClass()
+    {
+        CQLTester.setUpClass();
+        requireAuthentication();
+        requireNetwork();
+        guardrails().setEnabled(true);
+
+        systemClientState = ClientState.forInternalCalls();
+        userClientState = ClientState.forExternalCalls(InetSocketAddress.createUnresolved("127.0.0.1", 123));
+        superClientState = ClientState.forExternalCalls(InetSocketAddress.createUnresolved("127.0.0.1", 321));
+        superClientState.login(new AuthenticatedUser(CassandraRoleManager.DEFAULT_SUPERUSER_NAME));
+    }
+
+    /**
+     * Creates an ordinary user that is not excluded from guardrails, that is, a user that is not super not internal.
+     */
+    @Before
+    public void beforeGuardrailTest() throws Throwable
+    {
+        useSuperUser();
+        executeNet(format("CREATE USER IF NOT EXISTS %s WITH PASSWORD '%s'", USERNAME, PASSWORD));
+        executeNet(format("GRANT ALL ON KEYSPACE %s TO %s", KEYSPACE, USERNAME));
+        useUser(USERNAME, PASSWORD);
+
+        String useKeyspaceQuery = "USE " + keyspace();
+        execute(userClientState, useKeyspaceQuery);
+        execute(systemClientState, useKeyspaceQuery);
+        execute(superClientState, useKeyspaceQuery);
+    }
+
+    static Guardrails guardrails()
+    {
+        return Guardrails.instance;
+    }
+
+    protected <T> void assertValidProperty(BiConsumer<Guardrails, T> setter, T value)
+    {
+        setter.accept(guardrails(), value);
+    }
+
+    protected <T> void assertInvalidProperty(BiConsumer<Guardrails, T> setter,
+                                             T value,
+                                             String message,
+                                             Object... messageArgs)
+    {
+        Assertions.assertThatThrownBy(() -> setter.accept(guardrails(), value))
+                  .isInstanceOf(IllegalArgumentException.class)
+                  .hasMessage(format(message, messageArgs));
+    }
+
+    @SafeVarargs
+    protected final void testExcludedUsers(Supplier<String>... queries) throws Throwable
+    {
+        execute("USE " + keyspace());
+        assertSuperuserIsExcluded(queries);
+        assertInternalQueriesAreExcluded(queries);
+    }
+
+    @SafeVarargs
+    private final void assertInternalQueriesAreExcluded(Supplier<String>... queries) throws Throwable
+    {
+        for (Supplier<String> query : queries)
+        {
+            assertValid(() -> execute(systemClientState, query.get()));
+        }
+    }
+
+    @SafeVarargs
+    private final void assertSuperuserIsExcluded(Supplier<String>... queries) throws Throwable
+    {
+        for (Supplier<String> query : queries)
+        {
+            assertValid(() -> execute(superClientState, query.get()));
+        }
+    }
+
+    protected void assertValid(CheckedFunction function) throws Throwable
+    {
+        ClientWarn.instance.captureWarnings();
+        try
+        {
+            function.apply();
+            assertEmptyWarnings();
+        }
+        catch (InvalidRequestException e)
+        {
+            fail("Expected not to fail, but failed with error message: " + e.getMessage());
+        }
+        finally
+        {
+            ClientWarn.instance.resetWarnings();
+        }
+    }
+
+    protected void assertValid(String query) throws Throwable
+    {
+        assertValid(() -> execute(userClientState, query));
+    }
+
+    protected void assertWarns(CheckedFunction function, String message) throws Throwable
+    {
+        // We use client warnings to check we properly warn as this is the most convenient. Technically,
+        // this doesn't validate we also log the warning, but that's probably fine ...
+        ClientWarn.instance.captureWarnings();
+        try
+        {
+            function.apply();
+            assertWarnings(message);
+        }
+        finally
+        {
+            ClientWarn.instance.resetWarnings();
+        }
+    }
+
+    protected void assertWarns(String message, String query) throws Throwable
+    {
+        assertWarns(() -> execute(userClientState, query), message);
+    }
+
+    protected void assertAborts(CheckedFunction function, String message) throws Throwable
+    {
+        assertAborts(function, message, true);
+    }
+
+    protected void assertAborts(CheckedFunction function, String message, boolean thrown) throws Throwable
+    {
+        ClientWarn.instance.captureWarnings();
+        try
+        {
+            function.apply();
+
+            if (thrown)
+                fail("Expected to fail, but it did not");
+        }
+        catch (InvalidRequestException | InvalidQueryException e)
+        {
+            assertTrue("Expect no exception thrown", thrown);
+
+            assertTrue(format("Full error message '%s' does not contain expected message '%s'", e.getMessage(), message),
+                       e.getMessage().contains(message));
+
+            assertWarnings(message);
+        }
+        finally
+        {
+            ClientWarn.instance.resetWarnings();
+        }
+    }
+
+    protected void assertAborts(String message, String query) throws Throwable
+    {
+        assertAborts(() -> execute(userClientState, query), message);
+    }
+
+    private void assertWarnings(String message)
+    {
+        List<String> warnings = getWarnings();
+
+        assertFalse("Expected to warn, but no warning was received", warnings == null || warnings.isEmpty());
+        assertEquals(format("Got more thant 1 warning (got %d => %s)", warnings.size(), warnings),
+                     1,
+                     warnings.size());
+
+        String warning = warnings.get(0);
+        assertTrue(format("Warning log message '%s' does not contain expected message '%s'", warning, message),
+                   warning.contains(message));
+    }
+
+    private void assertEmptyWarnings()
+    {
+        List<String> warnings = getWarnings();
+
+        if (warnings == null) // will always be the case in practice currently, but being defensive if this change
+            warnings = Collections.emptyList();
+
+        assertTrue(format("Expect no warning messages but got %s", warnings), warnings.isEmpty());
+    }
+
+    private List<String> getWarnings()
+    {
+        List<String> warnings = ClientWarn.instance.getWarnings();
+
+        return warnings == null
+               ? Collections.emptyList()
+               : warnings.stream()
+                         .filter(w -> !w.equals(View.USAGE_WARNING) && !w.equals(SASIIndex.USAGE_WARNING))
+                         .collect(Collectors.toList());
+    }
+
+    protected void assertConfigFails(Consumer<Guardrails> consumer, String message)
+    {
+        try
+        {
+            consumer.accept(guardrails());
+            fail("Expected failure");
+        }
+        catch (IllegalArgumentException e)
+        {
+            String actualMessage = e.getMessage();
+            assertTrue(String.format("Failure message '%s' does not contain expected message '%s'", actualMessage, message),
+                       actualMessage.contains(message));
+        }
+    }
+
+    protected ResultMessage execute(ClientState state, String query)
+    {
+        QueryState queryState = new QueryState(state);
+
+        String formattedQuery = formatQuery(query);
+        CQLStatement statement = QueryProcessor.parseStatement(formattedQuery, queryState.getClientState());
+        statement.validate(state);
+
+        QueryOptions options = QueryOptions.forInternalCalls(Collections.emptyList());
+
+        return statement.executeLocally(queryState, options);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailUserTimestampsTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailUserTimestampsTest.java
new file mode 100644
index 0000000..20b6a52
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailUserTimestampsTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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;
+
+/**
+ * Tests the guardrail for disabling user-provided timestamps, {@link Guardrails#userTimestampsEnabled}.
+ */
+public class GuardrailUserTimestampsTest extends GuardrailTester
+{
+    @Before
+    public void setupTest()
+    {
+        createTable("CREATE TABLE IF NOT EXISTS %s (k INT, c INT, v TEXT, PRIMARY KEY(k, c))");
+    }
+
+    private void setGuardrail(boolean userTimestampsEnabled)
+    {
+        guardrails().setUserTimestampsEnabled(userTimestampsEnabled);
+    }
+
+    @Test
+    public void testInsertWithDisabledUserTimestamps() throws Throwable
+    {
+        setGuardrail(false);
+        assertAborts("INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') USING TIMESTAMP 1");
+    }
+
+    @Test
+    public void testInsertWithEnabledUserTimestamps() throws Throwable
+    {
+        setGuardrail(true);
+        assertValid("INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') USING TIMESTAMP 1");
+    }
+
+    @Test
+    public void testUpdateWithDisabledUserTimestamps() throws Throwable
+    {
+        setGuardrail(false);
+        assertAborts("UPDATE %s USING TIMESTAMP 1 SET v = 'val2' WHERE k = 1 and c = 2");
+    }
+
+    @Test
+    public void testUpdateWithEnabledUserTimestamps() throws Throwable
+    {
+        setGuardrail(true);
+        assertValid("UPDATE %s USING TIMESTAMP 1 SET v = 'val2' WHERE k = 1 and c = 2");
+    }
+
+    @Test
+    public void testDeleteWithDisabledUserTimestamps() throws Throwable
+    {
+        setGuardrail(false);
+        assertAborts("DELETE FROM %s USING TIMESTAMP 1 WHERE k=1");
+    }
+
+    @Test
+    public void testDeleteWithEnabledUserTimestamps() throws Throwable
+    {
+        setGuardrail(true);
+        assertValid("DELETE FROM %s USING TIMESTAMP 1 WHERE k=1");
+    }
+
+    @Test
+    public void testBatchWithDisabledUserTimestamps() throws Throwable
+    {
+        setGuardrail(false);
+        assertValid("BEGIN BATCH USING TIMESTAMP 1 " +
+                    "INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') " +
+                    "APPLY BATCH");
+        assertAborts("BEGIN BATCH " +
+                     "INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') USING TIMESTAMP 1 " +
+                     "APPLY BATCH");
+    }
+
+    @Test
+    public void testBatchWithEnabledUserTimestamps() throws Throwable
+    {
+        setGuardrail(true);
+        assertValid("BEGIN BATCH USING TIMESTAMP 1 " +
+                    "INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') " +
+                    "APPLY BATCH");
+        assertValid("BEGIN BATCH " +
+                    "INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') USING TIMESTAMP 1 " +
+                    "APPLY BATCH");
+    }
+
+    @Test
+    public void testExcludedUsers() throws Throwable
+    {
+        for (boolean userTimestampsEnabled : new boolean[]{ false, true })
+        {
+            setGuardrail(userTimestampsEnabled);
+            testExcludedUsers(() -> "INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') USING TIMESTAMP 1",
+                              () -> "UPDATE %s USING TIMESTAMP 1 SET v = 'val2' WHERE k = 1 and c = 2",
+                              () -> "DELETE FROM %s USING TIMESTAMP 1 WHERE k=1",
+                              () -> "BEGIN BATCH USING TIMESTAMP 1 INSERT INTO %s (k, c, v) VALUES (1, 2, 'v'); APPLY BATCH",
+                              () -> "BEGIN BATCH INSERT INTO %s (k, c, v) VALUES (1, 2, 'v') USING TIMESTAMP 1; APPLY BATCH");
+        }
+    }
+
+    private void assertAborts(String query) throws Throwable
+    {
+        assertAborts("User provided timestamps (USING TIMESTAMP) is not allowed", query);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailViewsPerTableTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailViewsPerTableTest.java
new file mode 100644
index 0000000..628e244
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailViewsPerTableTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.config.DatabaseDescriptor;
+
+import static java.lang.String.format;
+
+/**
+ * Tests the guardrail for the number of secondary indexes in a table, {@link Guardrails#secondaryIndexesPerTable}.
+ */
+public class GuardrailViewsPerTableTest extends ThresholdTester
+{
+    private static final int VIEWS_PER_TABLE_WARN_THRESHOLD = 1;
+    private static final int VIEWS_PER_TABLE_ABORT_THRESHOLD = 3;
+
+    private static final String CREATE_TABLE = "CREATE TABLE %s (k int PRIMARY KEY, v int)";
+    private static final String CREATE_VIEW = "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s " +
+                                              "WHERE k IS NOT NULL AND v IS NOT NULL PRIMARY KEY (v, k)";
+
+    public GuardrailViewsPerTableTest()
+    {
+        super(VIEWS_PER_TABLE_WARN_THRESHOLD,
+              VIEWS_PER_TABLE_ABORT_THRESHOLD,
+              DatabaseDescriptor.getGuardrailsConfig().getMaterializedViewsPerTable(),
+              Guardrails::setMaterializedViewsPerTableThreshold,
+              Guardrails::getMaterializedViewsPerTableWarnThreshold,
+              Guardrails::getMaterializedViewsPerTableAbortThreshold);
+    }
+
+    @Override
+    protected long currentValue()
+    {
+        return getCurrentColumnFamilyStore().viewManager.size();
+    }
+
+    @Before
+    public void before()
+    {
+        super.before();
+        createTable(CREATE_TABLE);
+    }
+
+    @Test
+    public void testCreateView() throws Throwable
+    {
+        String view1 = assertCreateViewSucceeds();
+        assertCurrentValue(1);
+
+        assertCreateViewWarns();
+        assertCreateViewWarns();
+        assertCreateViewAborts();
+        assertCurrentValue(3);
+
+        // drop the first view, we should be able to create new MV again
+        dropView(view1);
+        assertCurrentValue(2);
+        assertCreateViewWarns();
+        assertCreateViewAborts();
+        assertCurrentValue(3);
+
+        // previous guardrail should not apply to another base table
+        createTable("CREATE TABLE %s (k int primary key, v int)");
+        assertCreateViewSucceeds();
+        assertCreateViewWarns();
+        assertCreateViewWarns();
+        assertCreateViewAborts();
+        assertCurrentValue(3);
+    }
+
+    @Test
+    public void testExcludedUsers() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int)");
+
+        testExcludedUsers(() -> format("CREATE MATERIALIZED VIEW %s AS SELECT * FROM %s " +
+                                       "  WHERE k IS NOT NULL AND v1 IS NOT NULL PRIMARY KEY (v1, k)",
+                                       createViewName(), currentTable()),
+                          () -> format("CREATE MATERIALIZED VIEW %s AS SELECT * FROM %s " +
+                                       "  WHERE k IS NOT NULL AND v2 IS NOT NULL PRIMARY KEY (v2, k)",
+                                       createViewName(), currentTable()));
+    }
+
+    private String assertCreateViewSucceeds() throws Throwable
+    {
+        String viewName = createViewName();
+        assertThresholdValid(format(CREATE_VIEW, viewName));
+        return viewName;
+    }
+
+    private void assertCreateViewWarns() throws Throwable
+    {
+        String viewName = createViewName();
+        assertThresholdWarns(format("Creating materialized view %s on table %s, current number of views %s exceeds warning threshold of %s.",
+                                    viewName, currentTable(), currentValue() + 1, guardrails().getMaterializedViewsPerTableWarnThreshold()),
+                             format(CREATE_VIEW, viewName));
+    }
+
+    private void assertCreateViewAborts() throws Throwable
+    {
+        String viewName = createViewName();
+        assertThresholdAborts(format("aborting the creation of materialized view %s on table %s",
+                                     viewName, currentTable()),
+                              format(CREATE_VIEW, viewName));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailsConfigProviderTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailsConfigProviderTest.java
new file mode 100644
index 0000000..20e1aea
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailsConfigProviderTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.config.GuardrailsOptions;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.service.ClientState;
+import org.assertj.core.api.Assertions;
+
+import static java.lang.String.format;
+
+public class GuardrailsConfigProviderTest extends GuardrailTester
+{
+    @Test
+    public void testBuildCustom() throws Throwable
+    {
+        String name = getClass().getCanonicalName() + '$' + CustomProvider.class.getSimpleName();
+        GuardrailsConfigProvider provider = GuardrailsConfigProvider.build(name);
+        Threshold guard = new Threshold(state -> provider.getOrCreate(state).getTables(),
+                                        (isWarn, what, v, t) -> format("%s: for %s, %s > %s",
+                                                                       isWarn ? "Warning" : "Aborting", what, v, t));
+
+        assertValid(() -> guard.guard(5, "Z", userClientState));
+        assertWarns(() -> guard.guard(25, "A", userClientState), "Warning: for A, 25 > 10");
+        assertWarns(() -> guard.guard(100, "B", userClientState), "Warning: for B, 100 > 10");
+        assertAborts(() -> guard.guard(101, "X", userClientState), "Aborting: for X, 101 > 100");
+        assertAborts(() -> guard.guard(200, "Y", userClientState), "Aborting: for Y, 200 > 100");
+        assertValid(() -> guard.guard(5, "Z", userClientState));
+
+        Assertions.assertThatThrownBy(() -> GuardrailsConfigProvider.build("unexistent_class"))
+                  .isInstanceOf(ConfigurationException.class)
+                  .hasMessageContaining("Unable to find custom guardrails config provider class 'unexistent_class'");
+    }
+
+    /**
+     * Custom {@link GuardrailsConfigProvider} implementation that simply duplicates the threshold values.
+     */
+    public static class CustomProvider extends GuardrailsConfigProvider.Default
+    {
+        public GuardrailsConfig getOrCreate(ClientState state)
+        {
+            return new CustomConfig();
+        }
+    }
+
+    public static class CustomConfig extends GuardrailsOptions
+    {
+        private final IntThreshold tables = new IntThreshold();
+
+        public CustomConfig()
+        {
+            tables.setThresholds(10, 100);
+        }
+
+        @Override
+        public IntThreshold getTables()
+        {
+            return tables;
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailsTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailsTest.java
new file mode 100644
index 0000000..13d9ee3
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailsTest.java
@@ -0,0 +1,300 @@
+/*
+ * 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.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import static java.lang.String.format;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class GuardrailsTest extends GuardrailTester
+{
+    @Test
+    public void testDisabledThreshold() throws Throwable
+    {
+        Threshold.ErrorMessageProvider errorMessageProvider = (isWarn, what, v, t) -> "Should never trigger";
+        testDisabledThreshold(new Threshold(state -> new ThresholdConfig(), errorMessageProvider));
+    }
+
+    private void testDisabledThreshold(Threshold guard) throws Throwable
+    {
+        assertFalse(guard.enabled(userClientState));
+
+        assertValid(() -> guard.guard(5, "Z", null));
+        assertValid(() -> guard.guard(25, "A", userClientState));
+        assertValid(() -> guard.guard(100, "B", userClientState));
+        assertValid(() -> guard.guard(101, "X", userClientState));
+        assertValid(() -> guard.guard(200, "Y", userClientState));
+    }
+
+    @Test
+    public void testThreshold() throws Throwable
+    {
+        Threshold guard = new Threshold(state -> new ThresholdConfig(10, 100),
+                                        (isWarn, what, v, t) -> format("%s: for %s, %s > %s",
+                                                                       isWarn ? "Warning" : "Aborting", what, v, t));
+
+        assertTrue(guard.enabled(userClientState));
+
+        assertValid(() -> guard.guard(5, "Z", userClientState));
+        assertWarns(() -> guard.guard(25, "A", userClientState), "Warning: for A, 25 > 10");
+        assertWarns(() -> guard.guard(100, "B", userClientState), "Warning: for B, 100 > 10");
+        assertAborts(() -> guard.guard(101, "X", userClientState), "Aborting: for X, 101 > 100");
+        assertAborts(() -> guard.guard(200, "Y", userClientState), "Aborting: for Y, 200 > 100");
+        assertValid(() -> guard.guard(5, "Z", userClientState));
+    }
+
+    @Test
+    public void testWarnOnlyThreshold() throws Throwable
+    {
+        Threshold guard = new Threshold(state -> new ThresholdConfig(10, ThresholdConfig.DISABLED),
+                                        (isWarn, what, v, t) -> format("%s: for %s, %s > %s",
+                                                                       isWarn ? "Warning" : "Aborting", what, v, t));
+
+        assertTrue(guard.enabled(userClientState));
+
+        assertValid(() -> guard.guard(5, "Z", userClientState));
+        assertWarns(() -> guard.guard(11, "A", userClientState), "Warning: for A, 11 > 10");
+    }
+
+    @Test
+    public void testAbortOnlyThreshold() throws Throwable
+    {
+        Threshold guard = new Threshold(state -> new ThresholdConfig(ThresholdConfig.DISABLED, 10),
+                                        (isWarn, what, v, t) -> format("%s: for %s, %s > %s",
+                                                                       isWarn ? "Warning" : "Aborting", what, v, t));
+
+        assertTrue(guard.enabled(userClientState));
+
+        assertValid(() -> guard.guard(5, "Z", userClientState));
+        assertAborts(() -> guard.guard(11, "A", userClientState), "Aborting: for A, 11 > 10");
+    }
+
+    @Test
+    public void testThresholdUsers() throws Throwable
+    {
+        Threshold guard = new Threshold(state -> new ThresholdConfig(10, 100),
+                                        (isWarn, what, v, t) -> format("%s: for %s, %s > %s",
+                                                                       isWarn ? "Warning" : "Aborting", what, v, t));
+
+        // value under both thresholds
+        assertValid(() -> guard.guard(5, "x", null));
+        assertValid(() -> guard.guard(5, "x", userClientState));
+        assertValid(() -> guard.guard(5, "x", systemClientState));
+        assertValid(() -> guard.guard(5, "x", superClientState));
+
+        // value over warning threshold
+        assertWarns(() -> guard.guard(100, "y", null), "Warning: for y, 100 > 10");
+        assertWarns(() -> guard.guard(100, "y", userClientState), "Warning: for y, 100 > 10");
+        assertValid(() -> guard.guard(100, "y", systemClientState));
+        assertValid(() -> guard.guard(100, "y", superClientState));
+
+        // value over abort threshold
+        assertAborts(() -> guard.guard(101, "z", null), "Aborting: for z, 101 > 100");
+        assertAborts(() -> guard.guard(101, "z", userClientState), "Aborting: for z, 101 > 100");
+        assertValid(() -> guard.guard(101, "z", systemClientState));
+        assertValid(() -> guard.guard(101, "z", superClientState));
+    }
+
+    @Test
+    public void testDisableFlag() throws Throwable
+    {
+        assertAborts(() -> new DisableFlag(state -> true, "X").ensureEnabled(userClientState), "X is not allowed");
+        assertValid(() -> new DisableFlag(state -> false, "X").ensureEnabled(userClientState));
+
+        assertAborts(() -> new DisableFlag(state -> true, "X").ensureEnabled("Y", userClientState), "Y is not allowed");
+        assertValid(() -> new DisableFlag(state -> false, "X").ensureEnabled("Y", userClientState));
+    }
+
+    @Test
+    public void testDisableFlagUsers() throws Throwable
+    {
+        DisableFlag enabled = new DisableFlag(state -> false, "X");
+        assertValid(() -> enabled.ensureEnabled(null));
+        assertValid(() -> enabled.ensureEnabled(userClientState));
+        assertValid(() -> enabled.ensureEnabled(systemClientState));
+        assertValid(() -> enabled.ensureEnabled(superClientState));
+
+        DisableFlag disabled = new DisableFlag(state -> true, "X");
+        assertAborts(() -> disabled.ensureEnabled(null), "X is not allowed");
+        assertAborts(() -> disabled.ensureEnabled(userClientState), "X is not allowed");
+        assertValid(() -> disabled.ensureEnabled(systemClientState));
+        assertValid(() -> disabled.ensureEnabled(superClientState));
+    }
+
+    @Test
+    public void testDisallowedValues() throws Throwable
+    {
+        // Using a sorted set below to ensure the order in the error message checked below are not random
+        Values<Integer> disallowed = new Values<>(state -> new ValuesConfig(Collections.emptySet(), insertionOrderedSet(4, 6, 20)),
+                                                  "integer");
+
+        Consumer<Integer> action = i -> Assert.fail("The ignore action shouldn't have been triggered");
+        assertValid(() -> disallowed.guard(set(3), action, userClientState));
+        assertAborts(() -> 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));
+        assertAborts(() -> 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));
+
+        assertAborts(() -> disallowed.guard(set(4, 6), action, null),
+                     "Provided values [4, 6] are not allowed for integer (disallowed values are: [4, 6, 20])");
+        assertAborts(() -> 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 testDisallowedValuesUsers() throws Throwable
+    {
+        Values<Integer> disallowed = new Values<>(state -> new ValuesConfig(Collections.emptySet(), Collections.singleton(2)),
+                                                  "integer");
+
+        Consumer<Integer> 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])";
+        assertAborts(() -> disallowed.guard(set(2), action, null), message);
+        assertAborts(() -> disallowed.guard(set(2), action, userClientState), message);
+        assertValid(() -> disallowed.guard(set(2), action, systemClientState));
+        assertValid(() -> disallowed.guard(set(2), action, superClientState));
+
+        Set<Integer> 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<Integer> disallowedValues = set(2);
+        message = "Provided values [2] are not allowed for integer (disallowed values are: [2])";
+        assertAborts(() -> disallowed.guard(disallowedValues, action, null), message);
+        assertAborts(() -> disallowed.guard(disallowedValues, action, userClientState), message);
+        assertValid(() -> disallowed.guard(disallowedValues, action, systemClientState));
+        assertValid(() -> disallowed.guard(disallowedValues, action, superClientState));
+    }
+
+    @Test
+    public void testIgnoredValues() throws Throwable
+    {
+        // Using a sorted set below to ensure the order in the error message checked below are not random
+        Values<Integer> ignored = new Values<>(state -> new ValuesConfig(insertionOrderedSet(4, 6, 20), Collections.emptySet()),
+                                               "integer");
+
+        Set<Integer> triggeredOn = set();
+        assertValid(() -> ignored.guard(set(3), triggeredOn::add, userClientState));
+        assertEquals(set(), triggeredOn);
+
+        assertWarns(() -> ignored.guard(set(4), triggeredOn::add, userClientState),
+                    "Ignoring provided values [4] as they are not supported for integer (ignored values are: [4, 6, 20])");
+        assertEquals(set(4), triggeredOn);
+        triggeredOn.clear();
+
+        assertWarns(() -> ignored.guard(set(4, 6), triggeredOn::add, null),
+                    "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();
+
+        assertWarns(() -> ignored.guard(set(4, 5, 6, 7), triggeredOn::add, null),
+                    "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();
+    }
+
+    private static Set<Integer> set(Integer value)
+    {
+        return Collections.singleton(value);
+    }
+
+    private static Set<Integer> set(Integer... values)
+    {
+        return new HashSet<>(Arrays.asList(values));
+    }
+
+    @SafeVarargs
+    private static <T> Set<T> insertionOrderedSet(T... values)
+    {
+        return new LinkedHashSet<>(Arrays.asList(values));
+    }
+
+    private static class ThresholdConfig implements Threshold.Config
+    {
+        public static final int DISABLED = -1;
+
+        private final int warn;
+        private final int abort;
+
+        public ThresholdConfig()
+        {
+            this.warn = DISABLED;
+            this.abort = DISABLED;
+        }
+
+        public ThresholdConfig(int warn, int abort)
+        {
+            this.warn = warn;
+            this.abort = abort;
+        }
+
+        public long getWarnThreshold()
+        {
+            return warn;
+        }
+
+        public long getAbortThreshold()
+        {
+            return abort;
+        }
+    }
+
+    private static class ValuesConfig implements Values.Config<Integer>
+    {
+        private final Set<Integer> ignored;
+        private final Set<Integer> disallowed;
+
+        public ValuesConfig(Set<Integer> ignored, Set<Integer> disallowed)
+        {
+            this.ignored = ignored;
+            this.disallowed = disallowed;
+        }
+
+        public Set<Integer> getIgnored()
+        {
+            return ignored;
+        }
+
+        public Set<Integer> getDisallowed()
+        {
+            return disallowed;
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/ThresholdTester.java b/test/unit/org/apache/cassandra/db/guardrails/ThresholdTester.java
new file mode 100644
index 0000000..fdaa50b
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/ThresholdTester.java
@@ -0,0 +1,188 @@
+/*
+ * 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.function.BiConsumer;
+import java.util.function.ToIntFunction;
+import java.util.function.ToLongFunction;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.config.GuardrailsOptions;
+import org.assertj.core.api.Assertions;
+
+import static java.lang.String.format;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * Utility class for testing a {@link Threshold} guardrail.
+ */
+public abstract class ThresholdTester extends GuardrailTester
+{
+    private final long warnThreshold;
+    private final long abortThreshold;
+    private final GuardrailsOptions.Threshold config;
+    private final TriConsumer<Guardrails, Long, Long> setter;
+    private final ToLongFunction<Guardrails> warnGetter;
+    private final ToLongFunction<Guardrails> abortGetter;
+
+    protected ThresholdTester(long warnThreshold,
+                              long abortThreshold,
+                              GuardrailsOptions.Threshold config,
+                              TriConsumer<Guardrails, Long, Long> setter,
+                              ToLongFunction<Guardrails> warnGetter,
+                              ToLongFunction<Guardrails> abortGetter)
+    {
+        this.warnThreshold = warnThreshold;
+        this.abortThreshold = abortThreshold;
+        this.config = config;
+        this.setter = setter;
+        this.warnGetter = warnGetter;
+        this.abortGetter = abortGetter;
+    }
+
+    protected ThresholdTester(int warnThreshold,
+                              int abortThreshold,
+                              GuardrailsOptions.IntThreshold config,
+                              TriConsumer<Guardrails, Integer, Integer> setter,
+                              ToIntFunction<Guardrails> warnGetter,
+                              ToIntFunction<Guardrails> abortGetter)
+    {
+        this(warnThreshold,
+             abortThreshold,
+             (GuardrailsOptions.Threshold) config,
+             (g, w, a) -> setter.accept(g, w.intValue(), a.intValue()),
+             g -> (long) warnGetter.applyAsInt(g),
+             g -> (long) abortGetter.applyAsInt(g));
+    }
+
+    protected abstract long currentValue();
+
+    protected void assertCurrentValue(int count)
+    {
+        assertEquals(count, currentValue());
+    }
+
+    @Before
+    public void before()
+    {
+        setter.accept(guardrails(), warnThreshold, abortThreshold);
+    }
+
+    @Test
+    public void testConfigValidation()
+    {
+        testValidationOfThresholdProperties("warn threshold", "abort threshold");
+    }
+
+    protected void testValidationOfThresholdProperties(String warnName, String abortName)
+    {
+        setter.accept(guardrails(), -1L, -1L);
+
+        testValidationOfStrictlyPositiveProperty((g, a) -> setter.accept(g, -1L, a), abortName);
+        testValidationOfStrictlyPositiveProperty((g, w) -> setter.accept(g, w, -1L), warnName);
+
+        setter.accept(guardrails(), -1L, -1L);
+        Assertions.assertThatThrownBy(() -> setter.accept(guardrails(), 2L, 1L))
+                  .hasMessageContaining("The warn threshold 2 should be lower than the abort threshold 1");
+    }
+
+    protected void assertThresholdValid(String query) throws Throwable
+    {
+        assertValid(query);
+
+        Assertions.assertThat(currentValue())
+                  .isLessThanOrEqualTo(warnGetter.applyAsLong(guardrails()))
+                  .isLessThanOrEqualTo(abortGetter.applyAsLong(guardrails()));
+    }
+
+    protected void assertThresholdWarns(String message, String query) throws Throwable
+    {
+        assertWarns(message, query);
+
+        Assertions.assertThat(currentValue())
+                  .isGreaterThan(warnGetter.applyAsLong(guardrails()))
+                  .isLessThanOrEqualTo(abortGetter.applyAsLong(guardrails()));
+    }
+
+    protected void assertThresholdAborts(String message, String query) throws Throwable
+    {
+        assertAborts(message, query);
+
+        Assertions.assertThat(currentValue())
+                  .isGreaterThanOrEqualTo(warnGetter.applyAsLong(guardrails()))
+                  .isEqualTo(abortGetter.applyAsLong(guardrails()));
+    }
+
+    private void assertInvalidPositiveProperty(BiConsumer<Guardrails, Long> setter,
+                                               long value,
+                                               long maxValue,
+                                               boolean allowZero,
+                                               String name)
+    {
+        try
+        {
+            assertValidProperty(setter, value);
+            fail(format("Expected exception for guardrails.%s value: %d", name, value));
+        }
+        catch (IllegalArgumentException e)
+        {
+            String expectedMessage = null;
+
+            if (value > maxValue)
+                expectedMessage = format("Invalid value %d for %s: maximum allowed value is %d",
+                                         value, name, maxValue);
+            if (value == 0 && !allowZero)
+                expectedMessage = format("Invalid value for %s: 0 is not allowed; if attempting to disable use %s",
+                                         name, GuardrailsOptions.Threshold.DISABLED);
+
+            if (value < GuardrailsOptions.Threshold.DISABLED)
+                expectedMessage = format("Invalid value %d for %s: negative values are not "
+                                         + "allowed, outside of %s which disables the guardrail",
+                                         value, name, GuardrailsOptions.Threshold.DISABLED);
+
+            assertEquals(format("Exception message '%s' does not contain '%s'", e.getMessage(), expectedMessage),
+                         expectedMessage, e.getMessage());
+        }
+    }
+
+    private void assertInvalidStrictlyPositiveProperty(BiConsumer<Guardrails, Long> setter, long value, String name)
+    {
+        assertInvalidPositiveProperty(setter, value, config.maxValue(), config.allowZero(), name);
+    }
+
+    protected void testValidationOfStrictlyPositiveProperty(BiConsumer<Guardrails, Long> setter, String name)
+    {
+        assertInvalidStrictlyPositiveProperty(setter, Integer.MIN_VALUE, name);
+        assertInvalidStrictlyPositiveProperty(setter, -2, name);
+        assertValidProperty(setter, GuardrailsOptions.Threshold.DISABLED); // disabled
+        assertInvalidStrictlyPositiveProperty(setter, 0, name);
+        assertValidProperty(setter, 1L);
+        assertValidProperty(setter, 2L);
+        assertValidProperty(setter, config.maxValue());
+    }
+
+    @FunctionalInterface
+    public interface TriConsumer<T, U, V>
+    {
+        void accept(T t, U u, V v);
+    }
+}

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org