Implement a guardrail for client driver versions

patch by Stefan Miklosovic; reviewed by Andy Tolbert, Bohdan Siryk for CASSANDRA-21146
This commit is contained in:
Stefan Miklosovic 2026-03-31 12:50:53 +02:00
parent 281c1dc92c
commit e7fa71efaa
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
12 changed files with 781 additions and 5 deletions

View File

@ -1,4 +1,5 @@
6.0-alpha2
* Implement a guardrail for client driver versions (CASSANDRA-21146)
* Enable IAuthenticator to declare supported and alterable role options (CASSANDRA-20834)
* Avoid capturing lambda allocation in UnfilteredSerializer.deserializeRowBody (CASSANDRA-21289)
* Avoid Cell iterator allocation for alive rows in MetricsRecording transformation of ReadCommand (CASSANDRA-21288)

View File

@ -2682,6 +2682,27 @@ max_security_label_length: 48
# compressed by dictionary compressor and training_min_frequency is set to 0m (the default when unset).
#unset_training_min_frequency_enabled: true
# Minimum client driver versions. Connections from drivers whose version is below
# the configured minimum will be warned or rejected. Connections that do not report
# a driver name or version are considered valid unless 'unset' config entry is specified.
# The map key is the driver name as reported in the native protocol STARTUP message.
# The value is the minimum version string, which has to be a valid semver.
# An operator can use special "unknown" and "unset" entries for both guardrails with a placeholder version.
# A connection will fail / will be warned when a driver id is not among known entries when "unknown" entry is specified.
# A connection will fail / will be warned when a driver id is not set at all when "unset" entry is specified.
#minimum_client_driver_versions_warned:
# DataStax Java Driver: "4.0.0"
# DataStax Python Driver: "3.0.0"
# github.com/apache/cassandra-gocql-driver: "2.0.0"
# unknown: "0.0.0"
# unset: "0.0.0"
#minimum_client_driver_versions_disallowed:
# DataStax Java Driver: "4.0.0"
# DataStax Python Driver: "3.0.0"
# github.com/apache/cassandra-gocql-driver: "2.0.0"
# unknown: "0.0.0"
# unset: "0.0.0"
# Startup Checks are executed as part of Cassandra startup process, not all of them
# are configurable (so you can disable them) but these which are enumerated bellow.
# Uncomment the startup checks and configure them appropriately to cover your needs.

View File

@ -2446,6 +2446,27 @@ default_secondary_index_enabled: true
# compressed by dictionary compressor and training_min_frequency is set to 0m (the default when unset).
#unset_training_min_frequency_enabled: true
# Minimum client driver versions. Connections from drivers whose version is below
# the configured minimum will be warned or rejected. Connections that do not report
# a driver name or version are considered valid unless 'unset' config entry is specified.
# The map key is the driver name as reported in the native protocol STARTUP message.
# The value is the minimum version string, which has to be a valid semver.
# An operator can use special "unknown" and "unset" entries for both guardrails with a placeholder version.
# A connection will fail / will be warned when a driver id is not among known entries when "unknown" entry is specified.
# A connection will fail / will be warned when a driver id is not set at all when "unset" entry is specified.
#minimum_client_driver_versions_warned:
# DataStax Java Driver: "4.0.0"
# DataStax Python Driver: "3.0.0"
# github.com/apache/cassandra-gocql-driver: "2.0.0"
# unknown: "0.0.0"
# unset: "0.0.0"
#minimum_client_driver_versions_disallowed:
# DataStax Java Driver: "4.0.0"
# DataStax Python Driver: "3.0.0"
# github.com/apache/cassandra-gocql-driver: "2.0.0"
# unknown: "0.0.0"
# unset: "0.0.0"
# Startup Checks are executed as part of Cassandra startup process, not all of them
# are configurable (so you can disable them) but these which are enumerated bellow.
# Uncomment the startup checks and configure them appropriately to cover your needs.

View File

@ -1006,6 +1006,9 @@ public class Config
public volatile boolean unset_training_min_frequency_warned = true;
public volatile boolean unset_training_min_frequency_enabled = true;
public volatile Map<String, String> minimum_client_driver_versions_warned = Collections.emptyMap();
public volatile Map<String, String> minimum_client_driver_versions_disallowed = Collections.emptyMap();
public volatile int sai_sstable_indexes_per_query_warn_threshold = 32;
public volatile int sai_sstable_indexes_per_query_fail_threshold = -1;
public volatile DataStorageSpec.LongBytesBound sai_string_term_size_warn_threshold = new DataStorageSpec.LongBytesBound("1KiB");

View File

@ -18,15 +18,20 @@
package org.apache.cassandra.config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Sets;
import com.vdurmont.semver4j.Semver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -113,6 +118,8 @@ public class GuardrailsOptions implements GuardrailsConfig
false);
validatePasswordPolicy(config.password_policy);
validateRoleNamePolicy(config.role_name_policy);
validateAndSanitizeClientDriverVersions(config.minimum_client_driver_versions_warned, "minimum_client_driver_versions_warned");
validateAndSanitizeClientDriverVersions(config.minimum_client_driver_versions_disallowed, "minimum_client_driver_versions_disallowed");
}
@Override
@ -195,7 +202,7 @@ public class GuardrailsOptions implements GuardrailsConfig
{
return config.keyspace_properties_warned;
}
public void setKeyspacePropertiesWarned(Set<String> properties)
{
updatePropertyWithLogging("keyspace_properties_warned",
@ -209,7 +216,7 @@ public class GuardrailsOptions implements GuardrailsConfig
{
return config.keyspace_properties_ignored;
}
public void setKeyspacePropertiesIgnored(Set<String> properties)
{
updatePropertyWithLogging("keyspace_properties_ignored",
@ -223,7 +230,7 @@ public class GuardrailsOptions implements GuardrailsConfig
{
return config.keyspace_properties_disallowed;
}
public void setKeyspacePropertiesDisallowed(Set<String> properties)
{
updatePropertyWithLogging("keyspace_properties_disallowed",
@ -1368,6 +1375,34 @@ public class GuardrailsOptions implements GuardrailsConfig
return config.unset_training_min_frequency_enabled;
}
@Override
public Map<String, String> getMinimumClientDriverVersionsWarned()
{
return config.minimum_client_driver_versions_warned;
}
@Override
public Map<String, String> getMinimumClientDriverVersionsDisallowed()
{
return config.minimum_client_driver_versions_disallowed;
}
public void setMinimumClientDriverVersionsWarned(Map<String, String> versions)
{
updatePropertyWithLogging("minimum_client_driver_versions_warned",
versions,
() -> config.minimum_client_driver_versions_warned,
x -> config.minimum_client_driver_versions_warned = x);
}
public void setMinimumClientDriverVersionsDisallowed(Map<String, String> versions)
{
updatePropertyWithLogging("minimum_client_driver_versions_disallowed",
versions,
() -> config.minimum_client_driver_versions_disallowed,
x -> config.minimum_client_driver_versions_disallowed = x);
}
private static <T> void updatePropertyWithLogging(String propertyName, T newValue, Supplier<T> getter, Consumer<T> setter)
{
T oldValue = getter.get();
@ -1601,4 +1636,57 @@ public class GuardrailsOptions implements GuardrailsConfig
{
ValueGenerator.getGenerator("role_name_policy", config).generate(ValueValidator.getValidator("role_name_policy", config), Map.of());
}
@VisibleForTesting
public static void validateAndSanitizeClientDriverVersions(Map<String, String> map, String guardrailName)
{
if (map == null || map.isEmpty())
return;
List<String> invalidEntries = new ArrayList<>();
for (Map.Entry<String, String> entry : map.entrySet())
{
String sanitized = sanitizeVersion(entry.getValue());
if (!isValidVersion(sanitized))
invalidEntries.add(entry.getKey());
}
if (!invalidEntries.isEmpty())
throw new IllegalArgumentException("Invalid version entries for " + guardrailName + " guardrail, they do not follow semver: " + invalidEntries);
map.replaceAll((driver, version) -> sanitizeVersion(version));
}
public static boolean isValidVersion(String version)
{
if (version == null)
return false;
// try to construct it
try
{
new Semver(version);
}
catch (Throwable t)
{
return false;
}
return true;
}
private static final Pattern VERSION_SANITATION_PATTERN = Pattern.compile("^[vV]");
public static String sanitizeVersion(String driverVersion)
{
String sanitizedVersionId = driverVersion == null ? null : driverVersion.trim();
if (sanitizedVersionId != null)
sanitizedVersionId = VERSION_SANITATION_PATTERN.matcher(sanitizedVersionId).replaceFirst("");
if (sanitizedVersionId == null || sanitizedVersionId.isBlank())
return null;
return sanitizedVersionId;
}
}

View File

@ -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.Map;
import java.util.function.Function;
import javax.annotation.Nullable;
import com.vdurmont.semver4j.Semver;
import com.vdurmont.semver4j.Semver.SemverType;
import org.apache.cassandra.config.GuardrailsOptions;
import org.apache.cassandra.service.ClientState;
/**
* A guardrail that warns or rejects client connections whose driver version
* is below a configured minimum per driver name.
* <p>
* The guardrail is configured with maps of driver name to minimum version string.
* Connections that do not report a driver name or version are considered valid
* and are not subject to this guardrail.
* <p>
* Version comparison uses semantic versioning via {@link Semver} with loose parsing
* to handle non-standard version strings reported by drivers.
* <p>
* This guadrail also reacts on "unknown: 0.0.0" and "unset: 0.0.0" entries in the configuration map.
* When either is set, when a driver does not set its driver id or driver id is not among explicitly enumerated
* in the configuration map, the connection will fail / will be warned respectively.
*/
public class ClientDriverVersionGuardrail extends Predicates<String>
{
private final Function<ClientState, Map<String, String>> warnVersions;
private final Function<ClientState, Map<String, String>> disallowVersions;
public ClientDriverVersionGuardrail(Function<ClientState, Map<String, String>> warnVersions,
Function<ClientState, Map<String, String>> disallowVersions)
{
super("minimum_client_driver_versions", null, null, null, null);
this.warnVersions = warnVersions;
this.disallowVersions = disallowVersions;
}
public void guard(@Nullable String driverName, @Nullable String driverVersion, @Nullable ClientState state)
{
if (!enabled(state))
return;
Map<String, String> disallowed = disallowVersions.apply(state);
Map<String, String> warned = warnVersions.apply(state);
String sanitizedDriverId = driverName == null ? null : driverName.trim();
if (sanitizedDriverId == null || sanitizedDriverId.isBlank())
{
logger.debug("minimum_client_driver_versions guardrail identified empty driver id to check the minimum version of");
if (disallowed != null && disallowed.containsKey("unset"))
{
fail("Connections with unset driver id's are forbidden.", state);
return;
}
if (warned != null && warned.containsKey("unset"))
{
warn("Connection with unset driver id was detected.");
}
return;
}
String sanitizedDriverVersion = GuardrailsOptions.sanitizeVersion(driverVersion);
if (sanitizedDriverVersion == null)
{
sanitizedDriverVersion = "unset";
logger.debug("minimum_client_driver_versions guardrail identified empty driver version to check the minimum version of");
}
else if (!GuardrailsOptions.isValidVersion(sanitizedDriverVersion))
{
logger.debug("minimum_client_driver_versions guardrail identified driver version which is not compliant semver version");
return;
}
if (disallowed != null && !disallowed.isEmpty())
{
String minimumVersionFail = disallowed.get(sanitizedDriverId);
if (minimumVersionFail != null && (sanitizedDriverVersion.equals("unset") || isBelowMinimum(sanitizedDriverVersion, minimumVersionFail)))
{
fail(String.format("Client driver %s, version %s is below required minimum version %s, connection rejected",
sanitizedDriverId,
sanitizedDriverVersion,
minimumVersionFail),
state);
return;
}
else if (minimumVersionFail == null && disallowed.containsKey("unknown"))
{
fail(String.format("Detected unknown driver id: %s.", sanitizedDriverId), state);
return;
}
}
if (warned != null && !warned.isEmpty())
{
String minimumVersionWarn = warned.get(sanitizedDriverId);
if (minimumVersionWarn != null && (sanitizedDriverVersion.equals("unset") || isBelowMinimum(sanitizedDriverVersion, minimumVersionWarn)))
{
warn(String.format("Client driver %s, version %s is below recommended minimum version %s",
sanitizedDriverId,
sanitizedDriverVersion,
minimumVersionWarn));
}
else if (minimumVersionWarn == null && warned.containsKey("unknown"))
{
warn(String.format("Detected unknown driver id: %s", sanitizedDriverId));
}
}
}
/**
* This method should not be used in normal circumstances as {@link #guard(String, String, ClientState)}
* should be called directly instead.
*
* @param rawDriverId the value to check, driver name and version delimited by a colon.
* @param state client state
*/
@Override
public void guard(String rawDriverId, @Nullable ClientState state)
{
if (rawDriverId == null)
{
guard(null, null, state);
}
else
{
String[] pair = rawDriverId.trim().split(":");
if (pair.length != 2)
return;
String sanitizedDriverName = pair[0].trim();
if (sanitizedDriverName.isBlank())
return;
String sanitizedDriverVersion = pair[1].trim();
if (sanitizedDriverVersion.isBlank())
return;
guard(sanitizedDriverName, sanitizedDriverVersion, state);
}
}
/**
* Checks if the driver version is below the minimum version
* specified in the config map. If driver name is not in minimum versions,
* such connection is considered to be allowed.
* <p>
*
* @param driverVersion version of a driver
* @param minimumVersion minimum allowed version
* @return true if the driver version is lower than the configured minimum
*/
static boolean isBelowMinimum(String driverVersion, String minimumVersion)
{
Semver versionToCheck = new Semver(driverVersion, SemverType.LOOSE);
Semver minimumVersionAllowed = new Semver(minimumVersion, SemverType.LOOSE);
return versionToCheck.isLowerThan(minimumVersionAllowed);
}
}

View File

@ -19,12 +19,14 @@
package org.apache.cassandra.db.guardrails;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
@ -571,6 +573,16 @@ public final class Guardrails implements GuardrailsMBean
isWarning ? "Disk usage in keyspace datacenter exceeds warning threshold"
: "Write request failed because disk usage exceeds failure threshold in keyspace datacenter.");
/**
* Guardrail on client driver versions. Warns or rejects connections from drivers
* whose version is below the configured minimum for that driver name.
* Connections that do not report a driver name or version are considered valid.
*/
public static final ClientDriverVersionGuardrail minimumClientDriverVersion =
new ClientDriverVersionGuardrail(
state -> CONFIG_PROVIDER.getOrCreate(state).getMinimumClientDriverVersionsWarned(),
state -> CONFIG_PROVIDER.getOrCreate(state).getMinimumClientDriverVersionsDisallowed());
/**
* Guardrail on passwords for CREATE / ALTER ROLE statements.
*/
@ -1853,6 +1865,62 @@ public final class Guardrails implements GuardrailsMBean
return DEFAULT_CONFIG.getUnsetTrainingMinFrequencyEnabled();
}
@Override
public String getMinimumClientDriverVersionsWarned()
{
try
{
return JsonUtils.JSON_OBJECT_MAPPER.writeValueAsString(DEFAULT_CONFIG.getMinimumClientDriverVersionsWarned());
}
catch (Throwable t)
{
throw new RuntimeException("Unable to serialize minimum_client_driver_versions_warned configuration: " + t.getMessage());
}
}
@Override
public String getMinimumClientDriverVersionsDisallowed()
{
try
{
return JsonUtils.JSON_OBJECT_MAPPER.writeValueAsString(DEFAULT_CONFIG.getMinimumClientDriverVersionsDisallowed());
}
catch (Throwable t)
{
throw new RuntimeException("Unable to serialize minimum_client_driver_versions_disallowed configuration: " + t.getMessage());
}
}
@Override
public void setMinimumClientDriverVersionsWarned(String value)
{
try
{
Map<String, String> map = JsonUtils.JSON_OBJECT_MAPPER.readValue(value, new TypeReference<>() {});
GuardrailsOptions.validateAndSanitizeClientDriverVersions(map, "minimum_client_driver_versions_warned");
DEFAULT_CONFIG.setMinimumClientDriverVersionsWarned(map);
}
catch (JsonProcessingException t)
{
throw new RuntimeException("Unable to deserialize payload for minimum_client_driver_versions_warned: " + t.getMessage());
}
}
@Override
public void setMinimumClientDriverVersionsDisallowed(String value)
{
try
{
Map<String, String> map = JsonUtils.JSON_OBJECT_MAPPER.readValue(value, new TypeReference<>() {});
GuardrailsOptions.validateAndSanitizeClientDriverVersions(map, "minimum_client_driver_versions_disallowed");
DEFAULT_CONFIG.setMinimumClientDriverVersionsDisallowed(map);
}
catch (JsonProcessingException t)
{
throw new RuntimeException("Unable to deserialize minimum_client_driver_versions_disallowed: " + t.getMessage());
}
}
private static String toCSV(Set<String> values)
{
return values == null || values.isEmpty() ? "" : String.join(",", values);

View File

@ -18,6 +18,7 @@
package org.apache.cassandra.db.guardrails;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
@ -682,4 +683,16 @@ public interface GuardrailsConfig
* dictionary compressor as frequently as needed, without any limits, false otherwise.
*/
boolean getUnsetTrainingMinFrequencyEnabled();
/**
* @return a map of driver name to minimum version that triggers a warning when the client's
* driver version is below the specified minimum.
*/
Map<String, String> getMinimumClientDriverVersionsWarned();
/**
* @return a map of driver name to minimum version that triggers a failure when the client's
* driver version is below the specified minimum.
*/
Map<String, String> getMinimumClientDriverVersionsDisallowed();
}

View File

@ -1190,4 +1190,28 @@ public interface GuardrailsMBean
* dictionary compressor as frequently as needed, without any limits, false otherwise.
*/
boolean getUnsetTrainingMinFrequencyEnabled();
/**
* @return JSON representation of the minimum client driver versions that trigger a warning.
*/
String getMinimumClientDriverVersionsWarned();
/**
* @return JSON representation of the minimum client driver versions that trigger a failure.
*/
String getMinimumClientDriverVersionsDisallowed();
/**
* Sets the minimum client driver versions that trigger a warning.
*
* @param value JSON representation of a map of driver name to minimum version string.
*/
void setMinimumClientDriverVersionsWarned(String value);
/**
* Sets the minimum client driver versions that trigger a failure.
*
* @param value JSON representation of a map of driver name to minimum version string.
*/
void setMinimumClientDriverVersionsDisallowed(String value);
}

View File

@ -365,7 +365,9 @@ public abstract class GuardrailsConfigCommand extends AbstractCommand
"SimpleStrategyEnabled", "simplestrategy_enabled",
"NonPartitionRestrictedQueryEnabled", "non_partition_restricted_index_query_enabled");
private static final Set<String> ignored = Set.of("password_policy", "role_name_policy");
private static final Set<String> ignored = Set.of("password_policy", "role_name_policy",
"minimum_client_driver_versions_warned",
"minimum_client_driver_versions_disallowed");
/**
* Set of guardrails which are flags, even though their suffix would suggest they are part of "values" which have warned, ignored, and disallowed sub-categories

View File

@ -22,6 +22,7 @@ import java.util.Map;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.CBUtil;
@ -124,12 +125,15 @@ public class StartupMessage extends Message.Request
ClientState clientState = state.getClientState();
clientState.setClientOptions(options);
String driverName = options.get(DRIVER_NAME);
String driverVersion = options.get(DRIVER_VERSION);
if (null != driverName)
{
clientState.setDriverName(driverName);
clientState.setDriverVersion(options.get(DRIVER_VERSION));
clientState.setDriverVersion(driverVersion);
}
Guardrails.minimumClientDriverVersion.guard(driverName, driverVersion, clientState);
IAuthenticator authenticator = DatabaseDescriptor.getAuthenticator();
if (authenticator.requireAuthentication())
{

View File

@ -0,0 +1,350 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.apache.cassandra.config.GuardrailsOptions.validateAndSanitizeClientDriverVersions;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class GuardrailClientDriverVersionTest extends GuardrailTester
{
private String originalWarned;
private String originalDisallowed;
@Before
public void setup()
{
originalWarned = guardrails().getMinimumClientDriverVersionsWarned();
originalDisallowed = guardrails().getMinimumClientDriverVersionsDisallowed();
}
@After
public void teardown()
{
guardrails().setMinimumClientDriverVersionsWarned(originalWarned != null ? originalWarned : "{}");
guardrails().setMinimumClientDriverVersionsDisallowed(originalDisallowed != null ? originalDisallowed : "{}");
}
@Test
public void testVersionEqual()
{
assertFalse(ClientDriverVersionGuardrail.isBelowMinimum("4.18.0", "4.18.0"));
}
@Test
public void testVersionLessThan()
{
assertTrue(ClientDriverVersionGuardrail.isBelowMinimum("4.17.0", "4.18.0"));
}
@Test
public void testVersionGreaterThan()
{
assertFalse(ClientDriverVersionGuardrail.isBelowMinimum("4.19.0", "4.18.0"));
}
@Test
public void testVersionMajorDifference()
{
assertTrue(ClientDriverVersionGuardrail.isBelowMinimum("3.11.0", "4.0.0"));
}
@Test
public void testVersionMinorDifference()
{
assertTrue(ClientDriverVersionGuardrail.isBelowMinimum("4.2.0", "4.18.0"));
}
@Test
public void testVersionWithVPrefix() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"driver\":\"v2.0.1\"}");
guardrails().setMinimumClientDriverVersionsDisallowed("{}");
// above minimum no warn
assertValid(() -> Guardrails.minimumClientDriverVersion.guard("driver", "v2.0.2", userClientState));
assertValid(() -> Guardrails.minimumClientDriverVersion.guard("driver", "2.0.2", userClientState));
// below minimum warn
assertWarns(() -> Guardrails.minimumClientDriverVersion.guard("driver", "v2.0.0", userClientState),
"Client driver driver, version 2.0.0 is below recommended minimum version 2.0.1");
assertWarns(() -> Guardrails.minimumClientDriverVersion.guard("driver", "2.0.0", userClientState),
"Client driver driver, version 2.0.0 is below recommended minimum version 2.0.1");
}
@Test
public void testGuardrailNotTriggeredWhenEmpty() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{}");
guardrails().setMinimumClientDriverVersionsDisallowed("{}");
assertValid(() -> Guardrails.minimumClientDriverVersion.guard("DataStax Java Driver", "3.0.0", userClientState));
}
@Test
public void testGuardrailNotTriggeredWhenVersionAboveMinimum() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
guardrails().setMinimumClientDriverVersionsDisallowed("{}");
assertValid(() -> Guardrails.minimumClientDriverVersion.guard("DataStax Java Driver", "4.20.0", userClientState));
}
@Test
public void testGuardrailNotTriggeredForNotEnumeratedDriver() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
guardrails().setMinimumClientDriverVersionsDisallowed("{\"DataStax Java Driver\":\"4.0.0\"}");
assertValid(() -> Guardrails.minimumClientDriverVersion.guard("gocql", "1.0.0", userClientState));
}
@Test
public void testGuardrailNotTriggeredForNullDriverName() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
assertValid(() -> Guardrails.minimumClientDriverVersion.guard(null, "4.15.0", userClientState));
}
@Test
public void testGuardrailNotTriggeredForNullDriverVersion() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
assertWarns(() -> Guardrails.minimumClientDriverVersion.guard("DataStax Java Driver", null, userClientState),
"Client driver DataStax Java Driver, version unset is below recommended minimum version 4.18.0");
}
@Test
public void testGuardrailNotTriggeredForBothNullDriverAndVersion() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
assertValid(() -> Guardrails.minimumClientDriverVersion.guard(null, null, userClientState));
}
@Test
public void testGuardrailNotTriggeredForEmptyDriverName() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
assertValid(() -> Guardrails.minimumClientDriverVersion.guard("", "4.15.0", userClientState));
assertValid(() -> Guardrails.minimumClientDriverVersion.guard(" ", "4.15.0", userClientState));
}
@Test
public void testGuardrailTriggeredForEmptyDriverVersion() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
assertWarns(() -> Guardrails.minimumClientDriverVersion.guard("DataStax Java Driver", "", userClientState),
"Client driver DataStax Java Driver, version unset is below recommended minimum version 4.18.0");
assertWarns(() -> Guardrails.minimumClientDriverVersion.guard("DataStax Java Driver", " ", userClientState),
"Client driver DataStax Java Driver, version unset is below recommended minimum version 4.18.0");
}
@Test
public void testGuardrailWarns() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
guardrails().setMinimumClientDriverVersionsDisallowed("{}");
assertWarns(() -> Guardrails.minimumClientDriverVersion.guard("DataStax Java Driver", "4.15.0", userClientState),
"Client driver DataStax Java Driver, version 4.15.0 is below recommended minimum version 4.18.0");
}
@Test
public void testGuardrailFails() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{}");
guardrails().setMinimumClientDriverVersionsDisallowed("{\"DataStax Java Driver\":\"4.0.0\"}");
assertFails(() -> Guardrails.minimumClientDriverVersion.guard("DataStax Java Driver", "3.11.0", userClientState),
"Client driver DataStax Java Driver, version 3.11.0 is below required minimum version 4.0.0, connection rejected");
}
@Test
public void testGuardrailFailTakesPrecedenceOverWarn() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
guardrails().setMinimumClientDriverVersionsDisallowed("{\"DataStax Java Driver\":\"4.0.0\"}");
assertFails(() -> Guardrails.minimumClientDriverVersion.guard("DataStax Java Driver", "3.11.0", userClientState),
"Client driver DataStax Java Driver, version 3.11.0 is below required minimum version 4.0.0, connection rejected");
}
@Test
public void testGuardrailWarnOnly() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
guardrails().setMinimumClientDriverVersionsDisallowed("{\"DataStax Java Driver\":\"4.0.0\"}");
assertWarns(() -> Guardrails.minimumClientDriverVersion.guard("DataStax Java Driver", "4.15.0", userClientState),
"Client driver DataStax Java Driver, version 4.15.0 is below recommended minimum version 4.18.0");
}
@Test
public void testGuardrailWithVPrefixVersion() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
guardrails().setMinimumClientDriverVersionsDisallowed("{}");
assertWarns(() -> Guardrails.minimumClientDriverVersion.guard("DataStax Java Driver", "v4.15.0", userClientState),
"Client driver DataStax Java Driver, version 4.15.0 is below recommended minimum version 4.18.0");
}
@Test
public void testGuardrailColonFormatWarns() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
guardrails().setMinimumClientDriverVersionsDisallowed("{}");
assertWarns(() -> Guardrails.minimumClientDriverVersion.guard("DataStax Java Driver:4.15.0", userClientState),
"Client driver DataStax Java Driver, version 4.15.0 is below recommended minimum version 4.18.0");
}
@Test
public void testGuardrailColonFormatNull() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
assertValid(() -> Guardrails.minimumClientDriverVersion.guard(null, userClientState));
}
@Test
public void testGuardrailColonFormatNoSeparator() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
assertValid(() -> Guardrails.minimumClientDriverVersion.guard("DataStax Java Driver", userClientState));
}
@Test
public void testWarnUnsetDriverId() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"unset\":\"0.0.0\"}");
assertWarns(() -> Guardrails.minimumClientDriverVersion.guard(null, null, userClientState),
"Connection with unset driver id was detected.");
}
@Test
public void testFailUnsetDriverId() throws Throwable
{
guardrails().setMinimumClientDriverVersionsDisallowed("{\"unset\":\"0.0.0\"}");
assertFails(() -> Guardrails.minimumClientDriverVersion.guard(null, null, userClientState),
"Connections with unset driver id's are forbidden.");
}
@Test
public void testWarnUnknownDriverId() throws Throwable
{
guardrails().setMinimumClientDriverVersionsWarned("{\"unknown\":\"0.0.0\"}");
assertWarns(() -> Guardrails.minimumClientDriverVersion.guard("whatever driver name", "1.2.3", userClientState),
"Detected unknown driver id: whatever driver name");
assertWarns(() -> Guardrails.minimumClientDriverVersion.guard("whatever driver name", null, userClientState),
"Detected unknown driver id: whatever driver name");
}
@Test
public void testFailUnknownDriverId() throws Throwable
{
guardrails().setMinimumClientDriverVersionsDisallowed("{\"unknown\":\"0.0.0\"}");
assertFails(() -> Guardrails.minimumClientDriverVersion.guard("whatever driver name", "1.2.3", userClientState),
"Detected unknown driver id: whatever driver name");
assertFails(() -> Guardrails.minimumClientDriverVersion.guard("whatever driver name", null, userClientState),
"Detected unknown driver id: whatever driver name");
}
@Test
public void testJmxGetSetWarned()
{
guardrails().setMinimumClientDriverVersionsWarned("{\"DataStax Java Driver\":\"4.18.0\"}");
String json = guardrails().getMinimumClientDriverVersionsWarned();
assertTrue(json.contains("DataStax Java Driver"));
assertTrue(json.contains("4.18.0"));
}
@Test
public void testJmxGetSetDisallowed()
{
guardrails().setMinimumClientDriverVersionsDisallowed("{\"DataStax Java Driver\":\"4.0.0\",\"DataStax Python Driver\":\"3.0.0\"}");
String json = guardrails().getMinimumClientDriverVersionsDisallowed();
assertTrue(json.contains("DataStax Java Driver"));
assertTrue(json.contains("DataStax Python Driver"));
}
@Test
public void testValidateValidVersions()
{
validateAndSanitizeClientDriverVersions(new HashMap<>(Map.of("driver", "4.18.0")), "test");
validateAndSanitizeClientDriverVersions(new HashMap<>(Map.of("driver", "v4.18.0")), "test");
validateAndSanitizeClientDriverVersions(new HashMap<>(Map.of("driver", "V4.18.0")), "test");
validateAndSanitizeClientDriverVersions(new HashMap<>(Map.of("a", "1.0.0", "b", "2.0.0")), "test");
}
@Test
public void testValidateNullMap()
{
validateAndSanitizeClientDriverVersions(null, "test");
}
@Test
public void testValidateEmptyMap()
{
validateAndSanitizeClientDriverVersions(Collections.emptyMap(), "test");
}
@Test
public void testValidateInvalidVersion()
{
assertThatThrownBy(() -> validateAndSanitizeClientDriverVersions(Map.of("driver", "not-a-version"), "test"))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void testValidateEmptyVersion()
{
assertThatThrownBy(() -> validateAndSanitizeClientDriverVersions(Map.of("driver", ""), "test"))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void testValidateBlankVersion()
{
assertThatThrownBy(() -> validateAndSanitizeClientDriverVersions(Map.of("driver", " "), "test"))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void testValidateMixedValidAndInvalid()
{
Map<String, String> map = new HashMap<>();
map.put("good-driver", "4.18.0");
map.put("bad-driver", "xyz");
assertThatThrownBy(() -> validateAndSanitizeClientDriverVersions(map, "test"))
.isInstanceOf(IllegalArgumentException.class);
}
}