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]


Reply via email to