This is an automated email from the ASF dual-hosted git repository.
frankgh pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git
The following commit(s) were added to refs/heads/trunk by this push:
new 8eee2904 CASSSIDECAR-461: Validate snapshot name during list snapshot
(#348)
8eee2904 is described below
commit 8eee29046140406896c9e6c499948280ea75af0c
Author: Francisco Guerrero <[email protected]>
AuthorDate: Fri Jun 5 09:19:02 2026 -0700
CASSSIDECAR-461: Validate snapshot name during list snapshot (#348)
Patch by Francisco Guerrero; reviewed by Paulo Motta, Yifan Cai, Stefan
Miklosovic for CASSSIDECAR-461
---
CHANGES.txt | 4 ++
NEWS.txt | 18 ++++++
conf/sidecar.yaml | 10 ++++
.../CassandraInputValidationConfiguration.java | 5 ++
.../CassandraInputValidationConfigurationImpl.java | 19 +++++-
.../handlers/StreamSSTableComponentHandler.java | 2 +-
.../handlers/snapshots/ListSnapshotHandler.java | 2 +-
.../sidecar/utils/CassandraInputValidator.java | 43 +++++++++++++-
.../sidecar/utils/FastCassandraInputValidator.java | 69 +++++++++++++++++++++-
.../utils/RegexBasedCassandraInputValidator.java | 34 ++---------
.../sidecar/config/SidecarConfigurationTest.java | 20 ++++++-
.../snapshots/ListSnapshotHandlerTest.java | 38 ++++++++++++
.../sidecar/utils/CassandraInputValidatorTest.java | 35 +++++++++--
.../utils/FastCassandraInputValidatorTest.java | 4 +-
.../config/sidecar_validation_configuration.yaml | 40 -------------
15 files changed, 256 insertions(+), 87 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 3ef59747..7bb99b4e 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,7 @@
+0.5.0
+-----
+ * Validate snapshot name during list snapshot (CASSSIDECAR-461)
+
0.4.0
-----
* Ability to load Cassandra connection secrets from filesystem
(CASSSIDECAR-407)
diff --git a/NEWS.txt b/NEWS.txt
index af0ec649..788b4fd8 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -1,3 +1,21 @@
+0.5
+===
+
+Upgrading
+---------
+ - In CASSSIDECAR-461, the list snapshot endpoint now validates the
snapshot name, aligned with
+ the validation introduced by CASSANDRA-21389. A new
cassandra_input_validation.allowed_chars_for_snapshot_name
+ setting in sidecar.yaml controls the accepted pattern; the default is
"[a-zA-Z0-9_.+-]{1,255}".
+ Operators with snapshots whose names do not match this pattern should
override the setting
+ (for example, ".*") to preserve the prior permissive behavior. When
using the
+ FastCassandraInputValidator implementation, the validation will always
take place and will be
+ aligned with CASSANDRA-21389.
+
+New features
+------------
+ - Snapshot name validation on the list snapshot endpoint
(CASSSIDECAR-461), with a configurable
+ allowed_chars_for_snapshot_name pattern under cassandra_input_validation
in sidecar.yaml.
+
0.4
===
diff --git a/conf/sidecar.yaml b/conf/sidecar.yaml
index 3fc9d457..5c564f86 100644
--- a/conf/sidecar.yaml
+++ b/conf/sidecar.yaml
@@ -437,6 +437,16 @@ cassandra_input_validation:
allowed_chars_for_quoted_name: "[a-zA-Z_0-9]{1,48}"
allowed_chars_for_component_name:
"[a-zA-Z0-9_-]+(\\.db|\\.cql|\\.json|\\.crc32|TOC\\.txt)"
allowed_chars_for_restricted_component_name:
"[a-zA-Z0-9_-]+(\\.db|TOC\\.txt)"
+ # Pattern used to validate snapshot names. The default below matches the
snapshot name
+ # validation Cassandra adopted in CASSANDRA-21389, so that snapshots
accepted by Sidecar
+ # are also accepted by Cassandra.
+ #
+ # Prior to CASSANDRA-21389, Cassandra only rejected snapshot names
containing the path
+ # separator or a null character. To preserve that permissive behavior, set
this pattern
+ # to ".*" and use the RegexBasedCassandraInputValidator implementation; the
+ # FastCassandraInputValidator ignores this setting and always enforces the
+ # CASSANDRA-21389 rules.
+ allowed_chars_for_snapshot_name: "[a-zA-Z0-9_.+-]{1,255}"
blob_restore:
job_discovery_active_loop_delay: 5m
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/config/CassandraInputValidationConfiguration.java
b/server/src/main/java/org/apache/cassandra/sidecar/config/CassandraInputValidationConfiguration.java
index d77902d3..e8facc6a 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/config/CassandraInputValidationConfiguration.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/config/CassandraInputValidationConfiguration.java
@@ -58,4 +58,9 @@ public interface CassandraInputValidationConfiguration
* @return a regular expression to an allowed pattern for a subset of
component names
*/
String allowedPatternForRestrictedComponentName();
+
+ /**
+ * @return a regular expression to validate the name of a snapshot
+ */
+ String allowedPatternForSnapshotName();
}
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/CassandraInputValidationConfigurationImpl.java
b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/CassandraInputValidationConfigurationImpl.java
index 7fa29fca..e2776682 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/CassandraInputValidationConfigurationImpl.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/CassandraInputValidationConfigurationImpl.java
@@ -59,6 +59,8 @@ public class CassandraInputValidationConfigurationImpl
implements CassandraInput
// '+' is included in the allowed characters to support downloading SAI
files in snapshots
public static final String
DEFAULT_ALLOWED_CHARS_FOR_RESTRICTED_COMPONENT_NAME =
"[a-zA-Z0-9_+\\-]+(\\.db|TOC\\.txt)";
+ public static final String ALLOWED_CHARS_FOR_SNAPSHOT_NAME_PROPERTY =
"allowed_chars_for_snapshot_name";
+ public static final String DEFAULT_ALLOWED_CHARS_FOR_SNAPSHOT_NAME =
"[a-zA-Z0-9_.+-]{1,255}";
@JsonProperty(value = VALIDATOR_PROPERTY)
protected final ParameterizedClassConfiguration validatorConfiguration;
@@ -78,6 +80,9 @@ public class CassandraInputValidationConfigurationImpl
implements CassandraInput
@JsonProperty(value = ALLOWED_CHARS_FOR_RESTRICTED_COMPONENT_NAME_PROPERTY)
protected final String allowedPatternForRestrictedComponentName;
+ @JsonProperty(value = ALLOWED_CHARS_FOR_SNAPSHOT_NAME_PROPERTY)
+ protected final String allowedPatternForSnapshotName;
+
public CassandraInputValidationConfigurationImpl()
{
this(DEFAULT_VALIDATOR_CONFIGURATION,
@@ -85,7 +90,8 @@ public class CassandraInputValidationConfigurationImpl
implements CassandraInput
DEFAULT_ALLOWED_CHARS_FOR_NAME,
DEFAULT_ALLOWED_CHARS_FOR_QUOTED_NAME,
DEFAULT_ALLOWED_CHARS_FOR_COMPONENT_NAME,
- DEFAULT_ALLOWED_CHARS_FOR_RESTRICTED_COMPONENT_NAME);
+ DEFAULT_ALLOWED_CHARS_FOR_RESTRICTED_COMPONENT_NAME,
+ DEFAULT_ALLOWED_CHARS_FOR_SNAPSHOT_NAME);
}
public
CassandraInputValidationConfigurationImpl(ParameterizedClassConfiguration
validatorConfiguration,
@@ -93,7 +99,8 @@ public class CassandraInputValidationConfigurationImpl
implements CassandraInput
String
allowedPatternForName,
String
allowedPatternForQuotedName,
String
allowedPatternForComponentName,
- String
allowedPatternForRestrictedComponentName)
+ String
allowedPatternForRestrictedComponentName,
+ String
allowedPatternForSnapshotName)
{
this.validatorConfiguration = validatorConfiguration;
this.forbiddenKeyspaces = forbiddenKeyspaces;
@@ -101,6 +108,7 @@ public class CassandraInputValidationConfigurationImpl
implements CassandraInput
this.allowedPatternForQuotedName = allowedPatternForQuotedName;
this.allowedPatternForComponentName = allowedPatternForComponentName;
this.allowedPatternForRestrictedComponentName =
allowedPatternForRestrictedComponentName;
+ this.allowedPatternForSnapshotName = allowedPatternForSnapshotName;
}
/**
@@ -162,4 +170,11 @@ public class CassandraInputValidationConfigurationImpl
implements CassandraInput
{
return allowedPatternForRestrictedComponentName;
}
+
+ @Override
+ @JsonProperty(value = ALLOWED_CHARS_FOR_SNAPSHOT_NAME_PROPERTY)
+ public String allowedPatternForSnapshotName()
+ {
+ return allowedPatternForSnapshotName;
+ }
}
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/StreamSSTableComponentHandler.java
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/StreamSSTableComponentHandler.java
index 902b0327..e672625d 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/StreamSSTableComponentHandler.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/StreamSSTableComponentHandler.java
@@ -136,7 +136,7 @@ public class StreamSSTableComponentHandler extends
AbstractHandler<StreamSSTable
logger.error(errMsg, request, remoteAddress, host, cause);
if (cause instanceof NoSuchFileException)
{
- context.fail(wrapHttpException(HttpResponseStatus.NOT_FOUND,
cause.getMessage()));
+ context.fail(wrapHttpException(HttpResponseStatus.NOT_FOUND, "The
requested SSTable component was not found. " + request));
}
else
{
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/snapshots/ListSnapshotHandler.java
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/snapshots/ListSnapshotHandler.java
index 8a5a2548..6e2b9d43 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/snapshots/ListSnapshotHandler.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/snapshots/ListSnapshotHandler.java
@@ -173,7 +173,7 @@ public class ListSnapshotHandler extends
AbstractHandler<SnapshotRequestParam> i
return SnapshotRequestParam.builder()
.qualifiedTableName(qualifiedTableName(context))
- .snapshotName(context.pathParam("snapshot"))
+
.snapshotName(validator.validateSnapshotName(context.pathParam("snapshot")))
.includeSecondaryIndexFiles(includeSecondaryIndexFiles)
.build();
}
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/utils/CassandraInputValidator.java
b/server/src/main/java/org/apache/cassandra/sidecar/utils/CassandraInputValidator.java
index affdcc90..5166c287 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/utils/CassandraInputValidator.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/utils/CassandraInputValidator.java
@@ -18,7 +18,11 @@
package org.apache.cassandra.sidecar.utils;
+import java.io.File;
+import java.util.Objects;
+
import org.apache.cassandra.sidecar.common.server.data.Name;
+import org.apache.cassandra.sidecar.common.utils.Preconditions;
import org.apache.cassandra.sidecar.exceptions.CassandraInputException;
import org.jetbrains.annotations.NotNull;
@@ -57,7 +61,23 @@ public interface CassandraInputValidator
* @throws NullPointerException when the {@code snapshotName} is {@code
null}
* @throws CassandraInputException when the {@code snapshotName} contains
invalid characters in the name
*/
- String validateSnapshotName(@NotNull String snapshotName);
+ default String validateSnapshotName(@NotNull String snapshotName)
+ {
+ Objects.requireNonNull(snapshotName, "snapshotName must not be null");
+ Preconditions.checkArgument(!snapshotName.isEmpty(), "snapshotName
must be provided");
+
+ if (".".equals(snapshotName) || "..".equals(snapshotName))
+ throw new CassandraInputException("Snapshot name '" + snapshotName
+ "' is reserved");
+
+ // most UNIX systems only disallow file separator and null characters
for directory names
+ for (int i = 0; i < snapshotName.length(); i++)
+ {
+ char c = snapshotName.charAt(i);
+ if (c == File.separatorChar || c == '\0')
+ throw new CassandraInputException("Invalid characters in
snapshot name: " + snapshotName);
+ }
+ return snapshotName;
+ }
/**
* Validates that the {@code componentName} is not {@code null}, and it
contains allowed names for the
@@ -94,7 +114,17 @@ public interface CassandraInputValidator
*
* @param tableId the table identifier to validate
*/
- void validateTableId(String tableId);
+ default void validateTableId(String tableId)
+ {
+ Objects.requireNonNull(tableId, "tableId must not be null");
+ Preconditions.checkArgument(tableId.length() <= 32, "tableId cannot be
longer than 32 characters");
+ for (int i = 0; i < tableId.length(); i++)
+ {
+ char c = tableId.charAt(i);
+ if (!isHex(c))
+ throw new CassandraInputException("Invalid characters in table
id: " + tableId);
+ }
+ }
/**
* Validates that the {@code name} matches the name pattern
@@ -104,4 +134,13 @@ public interface CassandraInputValidator
* @throws CassandraInputException when the {@code unquotedInput} does not
match the pattern
*/
void validateNamePattern(Name name, String exceptionHint);
+
+ /**
+ * @param c the character to test
+ * @return {@code true} if the input {@code c} is valid hexadecimal,
{@code false} otherwise
+ */
+ static boolean isHex(char c)
+ {
+ return (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9') || (c >= 'A'
&& c <= 'F');
+ }
}
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/utils/FastCassandraInputValidator.java
b/server/src/main/java/org/apache/cassandra/sidecar/utils/FastCassandraInputValidator.java
index c01038c1..862d48c0 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/utils/FastCassandraInputValidator.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/utils/FastCassandraInputValidator.java
@@ -22,6 +22,9 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import org.apache.cassandra.sidecar.common.server.data.Name;
import org.apache.cassandra.sidecar.common.utils.Preconditions;
import
org.apache.cassandra.sidecar.config.CassandraInputValidationConfiguration;
@@ -31,12 +34,19 @@ import
org.apache.cassandra.sidecar.exceptions.ForbiddenCassandraInputException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.VisibleForTesting;
+import static
org.apache.cassandra.sidecar.config.yaml.CassandraInputValidationConfigurationImpl.DEFAULT_ALLOWED_CHARS_FOR_SNAPSHOT_NAME;
+
/**
* An implementation of the {@link CassandraInputValidator} that does not use
regular expressions
* for validations and uses optimized validations.
*/
-public class FastCassandraInputValidator extends
RegexBasedCassandraInputValidator
+public class FastCassandraInputValidator implements CassandraInputValidator
{
+ private static final Logger LOGGER =
LoggerFactory.getLogger(FastCassandraInputValidator.class);
+ /**
+ * Longest acceptable file name. Longer names lead to too long file name
error.
+ */
+ public static final int FILENAME_LENGTH = 255;
/**
* Longest permissible keyspace name
*/
@@ -60,6 +70,7 @@ public class FastCassandraInputValidator extends
RegexBasedCassandraInputValidat
final List<String> validTerminations;
@VisibleForTesting
final List<String> validRestrictedTerminations;
+ private final CassandraInputValidationConfiguration
validationConfiguration;
@VisibleForTesting
public FastCassandraInputValidator()
@@ -74,10 +85,18 @@ public class FastCassandraInputValidator extends
RegexBasedCassandraInputValidat
*/
public FastCassandraInputValidator(CassandraInputValidationConfiguration
validationConfiguration)
{
- super(validationConfiguration);
+ this.validationConfiguration = validationConfiguration;
Map<String, String> configMap =
validationConfiguration.validatorConfiguration().namedParameters();
validTerminations = parseConfiguredOrDefault(configMap,
"valid_terminations", DEFAULT_VALID_TERMINATIONS);
validRestrictedTerminations = parseConfiguredOrDefault(configMap,
"valid_restricted_terminations", DEFAULT_VALID_RESTRICTED_TERMINATIONS);
+
+ if
(!DEFAULT_ALLOWED_CHARS_FOR_SNAPSHOT_NAME.equals(validationConfiguration.allowedPatternForSnapshotName()))
+ {
+ LOGGER.info("The
cassandra_input_validation.allowed_chars_for_snapshot_name is configured to a
non-default " +
+ "value of '{}'. This value will not take effect when
using the FastCassandraInputValidator " +
+ "implementation and it will use the validations as
introduced in CASSANDRA-21389.",
+
validationConfiguration.allowedPatternForSnapshotName());
+ }
}
/**
@@ -108,6 +127,20 @@ public class FastCassandraInputValidator extends
RegexBasedCassandraInputValidat
return name;
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String validateSnapshotName(@NotNull String snapshotName)
+ {
+ CassandraInputValidator.super.validateSnapshotName(snapshotName);
+ Preconditions.checkArgument(snapshotName.length() <= FILENAME_LENGTH,
+ () -> String.format("snapshot name must
not be more than %d characters long (got %d characters for \"%s\")",
+ FILENAME_LENGTH,
snapshotName.length(), snapshotName));
+ validateSnapshotNamePattern(snapshotName);
+ return snapshotName;
+ }
+
/**
* {@inheritDoc}
*/
@@ -159,6 +192,29 @@ public class FastCassandraInputValidator extends
RegexBasedCassandraInputValidat
validateNamePattern(name.name(), name.maybeQuotedName(),
name.isSourceQuoted(), exceptionHint, 0);
}
+ /**
+ * Validates that the {@code name} is a valid name as introduced by {@code
CASSANDRA-21389}.
+ *
+ * @param name the name of the snapshot
+ */
+ protected void validateSnapshotNamePattern(String name)
+ {
+ char c;
+ boolean isValidCharacter;
+ for (int i = 0; i < name.length(); i++)
+ {
+ c = name.charAt(i);
+ isValidCharacter = isAlphanumeric(c)
+ || isUnderscore(c)
+ || isPeriod(c)
+ || isPlus(c)
+ || isDash(c);
+
+ if (!isValidCharacter)
+ throw new CassandraInputException("Invalid character in
snapshot name: " + name);
+ }
+ }
+
/**
* Validates that the {@code name} is a valid name in Cassandra as defined
by the grammar in
* <a
href="https://cassandra.apache.org/doc/4.1/cassandra/cql/ddl.html#common-definitions">Cassandra
CQL common
@@ -302,6 +358,15 @@ public class FastCassandraInputValidator extends
RegexBasedCassandraInputValidat
return c == '+';
}
+ /**
+ * @param c the character to test
+ * @return {@code true} if the input {@code c} is a period character,
{@code false} otherwise
+ */
+ protected boolean isPeriod(char c)
+ {
+ return c == '.';
+ }
+
/**
* @param configMap the configuration map
* @param key the key in the map
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/utils/RegexBasedCassandraInputValidator.java
b/server/src/main/java/org/apache/cassandra/sidecar/utils/RegexBasedCassandraInputValidator.java
index 4de8bf5a..93791ab7 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/utils/RegexBasedCassandraInputValidator.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/utils/RegexBasedCassandraInputValidator.java
@@ -18,7 +18,6 @@
package org.apache.cassandra.sidecar.utils;
-import java.io.File;
import java.util.Objects;
import org.apache.cassandra.sidecar.common.server.data.Name;
@@ -86,10 +85,10 @@ public class RegexBasedCassandraInputValidator implements
CassandraInputValidato
@Override
public String validateSnapshotName(@NotNull String snapshotName)
{
- Objects.requireNonNull(snapshotName, "snapshotName must not be null");
- // most UNIX systems only disallow file separator and null characters
for directory names
- if (snapshotName.contains(File.separator) ||
snapshotName.contains("\0"))
- throw new CassandraInputException("Invalid characters in snapshot
name: " + snapshotName);
+ CassandraInputValidator.super.validateSnapshotName(snapshotName);
+ if
(!snapshotName.matches(validationConfiguration.allowedPatternForSnapshotName()))
+ throw new CassandraInputException("Invalid pattern for snapshot
name: " + snapshotName +
+ ". The valid pattern is: " +
validationConfiguration.allowedPatternForSnapshotName());
return snapshotName;
}
@@ -137,22 +136,6 @@ public class RegexBasedCassandraInputValidator implements
CassandraInputValidato
return componentName;
}
- /**
- * {@inheritDoc}
- */
- @Override
- public void validateTableId(String tableId)
- {
- Objects.requireNonNull(tableId, "tableId must not be null");
- Preconditions.checkArgument(tableId.length() <= 32, "tableId cannot be
longer than 32 characters");
- for (int i = 0; i < tableId.length(); i++)
- {
- char c = tableId.charAt(i);
- if (!isHex(c))
- throw new CassandraInputException("Invalid characters in table
id: " + tableId);
- }
- }
-
/**
* {@inheritDoc}
*/
@@ -181,13 +164,4 @@ public class RegexBasedCassandraInputValidator implements
CassandraInputValidato
if (!unquotedInput.matches(pattern))
throw new CassandraInputException("Invalid characters in " +
exceptionHint + ": " + maybeQuoted);
}
-
- /**
- * @param c the character to test
- * @return {@code true} if the input {@code c} is valid hexadecimal,
{@code false} otherwise
- */
- protected boolean isHex(char c)
- {
- return (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9') || (c >= 'A'
&& c <= 'F');
- }
}
diff --git
a/server/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java
b/server/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java
index 21388278..169fc636 100644
---
a/server/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java
+++
b/server/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java
@@ -96,8 +96,22 @@ class SidecarConfigurationTest
@Test
void testReadingCassandraInputValidation() throws IOException
{
- Path yamlPath = yaml("config/sidecar_validation_configuration.yaml");
- SidecarConfiguration configuration =
SidecarConfigurationImpl.readYamlConfiguration(yamlPath);
+ String yaml = "cassandra_input_validation:\n" +
+ " validator:\n" +
+ " - class_name:
org.apache.cassandra.sidecar.utils.FastCassandraInputValidator\n" +
+ " parameters:\n" +
+ " valid_terminations: \".abc,.def\"\n" +
+ " valid_restricted_terminations: \".xml\"\n" +
+ " forbidden_keyspaces:\n" +
+ " - a\n" +
+ " - b\n" +
+ " - c\n" +
+ " allowed_chars_for_directory: \"[a-z]+\"\n" +
+ " allowed_chars_for_quoted_name: \"[A-Z]+\"\n" +
+ " allowed_chars_for_component_name:
\"(\\\\.db|\\\\.cql|\\\\.json|\\\\.crc32|TOC\\\\.txt)\"\n" +
+ " allowed_chars_for_restricted_component_name:
\"(\\\\.db|TOC\\\\.txt)\"\n" +
+ " allowed_chars_for_snapshot_name: \".*\"";
+ SidecarConfiguration configuration =
SidecarConfigurationImpl.fromYamlString(yaml);
CassandraInputValidationConfiguration validationConfiguration =
configuration.cassandraInputValidationConfiguration();
@@ -114,6 +128,8 @@ class SidecarConfigurationTest
.isEqualTo("(\\.db|\\.cql|\\.json|\\.crc32|TOC\\.txt)");
assertThat(validationConfiguration.allowedPatternForRestrictedComponentName())
.isEqualTo("(\\.db|TOC\\.txt)");
+ assertThat(validationConfiguration.allowedPatternForSnapshotName())
+ .isEqualTo(".*");
}
@Test
diff --git
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/snapshots/ListSnapshotHandlerTest.java
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/snapshots/ListSnapshotHandlerTest.java
index 79bf710b..b79d3d8e 100644
---
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/snapshots/ListSnapshotHandlerTest.java
+++
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/snapshots/ListSnapshotHandlerTest.java
@@ -25,6 +25,7 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -32,6 +33,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -61,6 +64,7 @@ import static
io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static
org.apache.cassandra.sidecar.snapshots.SnapshotUtils.mockInstancesMetadata;
+import static
org.apache.cassandra.sidecar.utils.FastCassandraInputValidator.FILENAME_LENGTH;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -203,6 +207,25 @@ class ListSnapshotHandlerTest
})));
}
+ @ParameterizedTest(name = "{index} => snapshot name={0}")
+ @MethodSource("invalidSnapshotNames")
+ void testRouteInvalidSnapshotNameCharacters(String invalidSnapshotName,
int expectedStatusCode, String expectedError) throws Throwable
+ {
+ VertxTestContext context = new VertxTestContext();
+ WebClient client = WebClient.create(vertx);
+ String testRoute =
"/api/v1/keyspaces/keyspace1/tables/table1/snapshots/" + invalidSnapshotName;
+ client.get(server.actualPort(), "localhost", testRoute)
+ .send(context.succeeding(response -> context.verify(() -> {
+
assertThat(response.statusCode()).isEqualTo(expectedStatusCode);
+ assertThat(response.bodyAsJsonObject().getString("message"))
+ .startsWith(expectedError);
+ context.completeNow();
+ })));
+ assertThat(context.awaitCompletion(30, TimeUnit.SECONDS)).isTrue();
+ if (context.failed())
+ throw context.causeOfFailure();
+ }
+
@Test
void failsWhenKeyspaceContainsInvalidCharacters(VertxTestContext context)
{
@@ -267,4 +290,19 @@ class ListSnapshotHandlerTest
return mockInstancesMetadata(vertx, canonicalTemporaryPath,
mockDelegate, mockSession1);
}
}
+
+ static Stream<Arguments> invalidSnapshotNames()
+ {
+ return Stream.of(
+ Arguments.of("..%2F..%2Fetc%2Fpasswd", BAD_REQUEST.code(), "Invalid
characters in snapshot name: ../../etc/passwd"),
+ Arguments.of("i_❤_u", BAD_REQUEST.code(), "Invalid pattern for
snapshot name: i_â¤_u"),
+ Arguments.of("important!", BAD_REQUEST.code(), "Invalid pattern for
snapshot name: important!"),
+ Arguments.of("backup*", BAD_REQUEST.code(), "Invalid pattern for
snapshot name: backup*"),
+ Arguments.of("o'snap", BAD_REQUEST.code(), "Invalid pattern for
snapshot name: o'snap"),
+ Arguments.of("snap(1)", BAD_REQUEST.code(), "Invalid pattern for
snapshot name: snap(1)"),
+ Arguments.of("a%20tag", BAD_REQUEST.code(), "Invalid pattern for
snapshot name: a tag"),
+ Arguments.of("a:tag", BAD_REQUEST.code(), "Invalid pattern for
snapshot name: a:tag"),
+ Arguments.of("a".repeat(FILENAME_LENGTH + 1), BAD_REQUEST.code(),
"Invalid pattern for snapshot name: " + "a".repeat(FILENAME_LENGTH + 1) + ".")
+ );
+ }
}
diff --git
a/server/src/test/java/org/apache/cassandra/sidecar/utils/CassandraInputValidatorTest.java
b/server/src/test/java/org/apache/cassandra/sidecar/utils/CassandraInputValidatorTest.java
index 400c77d1..1295dc39 100644
---
a/server/src/test/java/org/apache/cassandra/sidecar/utils/CassandraInputValidatorTest.java
+++
b/server/src/test/java/org/apache/cassandra/sidecar/utils/CassandraInputValidatorTest.java
@@ -29,6 +29,7 @@ import
org.apache.cassandra.sidecar.exceptions.CassandraInputException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Test validation methods.
@@ -132,14 +133,19 @@ abstract class CassandraInputValidatorTest
testCommon_testInvalidFileName("TOC.txt");
}
+ @ParameterizedTest(name = "[{0}]")
+ @ValueSource(strings = { "valid-snapshot-name", "valid.snapshot.name",
"valid_snapshot_name",
+ "valid+snapshot+name", "valid..snapshot..name",
"valid1snapshot2name",
+ "snap.2026-05-20" })
+ void testValidateSnapshotName_validSnapshotNames_expectNoException(String
name)
+ {
+ instance.validateSnapshotName(name);
+ }
+
@Test
- void testValidateSnapshotName_validSnapshotNames_expectNoException()
+ void testValidateSnapshotName_lengthLimit_expectNoException()
{
- instance.validateSnapshotName("valid-snapshot-name");
- instance.validateSnapshotName("valid\\snapshot\\name");
- instance.validateSnapshotName("valid:snapshot:name");
- instance.validateSnapshotName("valid$snapshot$name");
- instance.validateSnapshotName("valid snapshot name");
+ instance.validateSnapshotName("a".repeat(255));
}
@Test
@@ -158,6 +164,23 @@ abstract class CassandraInputValidatorTest
.withMessage("Invalid characters in snapshot name: " + testSnapName);
}
+ @ParameterizedTest(name = "[{0}]")
+ @ValueSource(strings = { ".", ".." })
+ void
testValidateSnapshotName_snapshotNameWithReservedChar_expectException(String
reserved)
+ {
+ assertThatExceptionOfType(CassandraInputException.class).isThrownBy(()
-> instance.validateSnapshotName(reserved))
+
.withMessage("Snapshot name '" + reserved + "' is reserved");
+ }
+
+ @Test
+ void testValidateSnapshotName_snapshotNameExceedsLength_expectException()
+ {
+ assertThatThrownBy(() ->
instance.validateSnapshotName("a".repeat(256)))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageMatching("snapshot name must not be more than 255
characters long \\(got 256 characters for.*|" +
+ "Invalid pattern for snapshot name: .*");
+ }
+
@Test
void testValidateTableIdIsNull()
{
diff --git
a/server/src/test/java/org/apache/cassandra/sidecar/utils/FastCassandraInputValidatorTest.java
b/server/src/test/java/org/apache/cassandra/sidecar/utils/FastCassandraInputValidatorTest.java
index 5767bdd9..a406b449 100644
---
a/server/src/test/java/org/apache/cassandra/sidecar/utils/FastCassandraInputValidatorTest.java
+++
b/server/src/test/java/org/apache/cassandra/sidecar/utils/FastCassandraInputValidatorTest.java
@@ -35,6 +35,7 @@ import static
org.apache.cassandra.sidecar.config.yaml.CassandraInputValidationC
import static
org.apache.cassandra.sidecar.config.yaml.CassandraInputValidationConfigurationImpl.DEFAULT_ALLOWED_CHARS_FOR_NAME;
import static
org.apache.cassandra.sidecar.config.yaml.CassandraInputValidationConfigurationImpl.DEFAULT_ALLOWED_CHARS_FOR_QUOTED_NAME;
import static
org.apache.cassandra.sidecar.config.yaml.CassandraInputValidationConfigurationImpl.DEFAULT_ALLOWED_CHARS_FOR_RESTRICTED_COMPONENT_NAME;
+import static
org.apache.cassandra.sidecar.config.yaml.CassandraInputValidationConfigurationImpl.DEFAULT_ALLOWED_CHARS_FOR_SNAPSHOT_NAME;
import static
org.apache.cassandra.sidecar.config.yaml.CassandraInputValidationConfigurationImpl.DEFAULT_FORBIDDEN_KEYSPACES;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -62,7 +63,8 @@ class FastCassandraInputValidatorTest extends
CassandraInputValidatorTest
DEFAULT_ALLOWED_CHARS_FOR_NAME,
DEFAULT_ALLOWED_CHARS_FOR_QUOTED_NAME,
DEFAULT_ALLOWED_CHARS_FOR_COMPONENT_NAME,
-
DEFAULT_ALLOWED_CHARS_FOR_RESTRICTED_COMPONENT_NAME);
+
DEFAULT_ALLOWED_CHARS_FOR_RESTRICTED_COMPONENT_NAME,
+
DEFAULT_ALLOWED_CHARS_FOR_SNAPSHOT_NAME);
FastCassandraInputValidator validator = new
FastCassandraInputValidator(config);
assertThat(validator.validTerminations).isEqualTo(List.of(".abc",
".def"));
assertThat(validator.validRestrictedTerminations).isEqualTo(List.of(".xml"));
diff --git
a/server/src/test/resources/config/sidecar_validation_configuration.yaml
b/server/src/test/resources/config/sidecar_validation_configuration.yaml
deleted file mode 100644
index ed556468..00000000
--- a/server/src/test/resources/config/sidecar_validation_configuration.yaml
+++ /dev/null
@@ -1,40 +0,0 @@
-#
-# Cassandra SideCar configuration file
-#
-cassandra:
- host: localhost
- port: 9042
- storage_dir: /cassandra/d1
- data_dirs: /cassandra/d1/data, /cassandra/d2/data
- jmx_host: 127.0.0.1
- jmx_port: 7199
- jmx_role: controlRole
- jmx_role_password: controlPassword
- jmx_ssl_enabled: true
-
-sidecar:
- host: 0.0.0.0
- port: 1234
- request_idle_timeout: 500s
- request_timeout: 20m
- throttle:
- stream_requests_per_sec: 80
- timeout: 21s
- allowable_time_skew: 89m
- sstable_import:
- execute_interval: 50ms
-
-cassandra_input_validation:
- validator:
- - class_name: org.apache.cassandra.sidecar.utils.FastCassandraInputValidator
- parameters:
- valid_terminations: ".abc,.def"
- valid_restricted_terminations: ".xml"
- forbidden_keyspaces:
- - a
- - b
- - c
- allowed_chars_for_directory: "[a-z]+"
- allowed_chars_for_quoted_name: "[A-Z]+"
- allowed_chars_for_component_name: "(\\.db|\\.cql|\\.json|\\.crc32|TOC\\.txt)"
- allowed_chars_for_restricted_component_name: "(\\.db|TOC\\.txt)"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]