CASSANDRASC-71: Allow configuring permissions for uploaded SSTables

This commit introduces a new configuration for SSTable uploads (`file_permissions`) which allows
an operator to configure the desired file permissions used for files that are uploaded via SSTable
upload.

Patch by Francisco Guerrero; Reviewed by Dinesh Joshi, Yifan Cai for CASSANDRASC-71
diff --git a/CHANGES.txt b/CHANGES.txt
index 9c330d6..f7d0831 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 1.0.0
 -----
+ * Allow configuring permissions for uploaded SSTables (CASSANDRASC-71)
  * Refactor Sidecar configuration (CASSANDRASC-69)
  * Add Client Methods for Obtaining Sidecar and Cassandra Health (CASSANDRASC-70)
  * Publish bytes streamed and written metrics (CASSANDRASC-68)
diff --git a/src/main/dist/conf/sidecar.yaml b/src/main/dist/conf/sidecar.yaml
index 2342c87..ddd9adc 100644
--- a/src/main/dist/conf/sidecar.yaml
+++ b/src/main/dist/conf/sidecar.yaml
@@ -75,6 +75,7 @@
   sstable_upload:
     concurrent_upload_limit: 80
     min_free_space_percent: 10
+    # file_permissions: "rw-r--r--" # when not specified, the default file permissions are owner read & write, group & others read
   allowable_time_skew_in_minutes: 60
   sstable_import:
     poll_interval_millis: 100
diff --git a/src/main/java/org/apache/cassandra/sidecar/config/SSTableUploadConfiguration.java b/src/main/java/org/apache/cassandra/sidecar/config/SSTableUploadConfiguration.java
index dd8df66..f36620c 100644
--- a/src/main/java/org/apache/cassandra/sidecar/config/SSTableUploadConfiguration.java
+++ b/src/main/java/org/apache/cassandra/sidecar/config/SSTableUploadConfiguration.java
@@ -18,6 +18,8 @@
 
 package org.apache.cassandra.sidecar.config;
 
+import java.nio.file.attribute.PosixFilePermission;
+
 /**
  * Configuration for SSTable component uploads on this service
  */
@@ -32,4 +34,15 @@
      * @return the configured minimum space percentage required for an SSTable component upload
      */
     float minimumSpacePercentageRequired();
+
+    /**
+     * Returns the String representation of a set of posix file permissions used during an SSTable file upload.
+     * When an SSTable file is created the specified permissions will be used to create the file.
+     * For example, the String {@code rw-r--r--} represents the set of permissions
+     * {@link PosixFilePermission#OWNER_READ}, {@link PosixFilePermission#OWNER_WRITE},
+     * {@link PosixFilePermission#GROUP_READ}, and {@link PosixFilePermission#OTHERS_READ}.
+     *
+     * @return the String representation of a set of posix file permissions used during an SSTable file upload
+     */
+    String filePermissions();
 }
diff --git a/src/main/java/org/apache/cassandra/sidecar/config/yaml/SSTableUploadConfigurationImpl.java b/src/main/java/org/apache/cassandra/sidecar/config/yaml/SSTableUploadConfigurationImpl.java
index cea66cc..68a8a45 100644
--- a/src/main/java/org/apache/cassandra/sidecar/config/yaml/SSTableUploadConfigurationImpl.java
+++ b/src/main/java/org/apache/cassandra/sidecar/config/yaml/SSTableUploadConfigurationImpl.java
@@ -18,6 +18,8 @@
 
 package org.apache.cassandra.sidecar.config.yaml;
 
+import java.nio.file.attribute.PosixFilePermissions;
+
 import com.fasterxml.jackson.annotation.JsonProperty;
 import org.apache.cassandra.sidecar.config.SSTableUploadConfiguration;
 
@@ -28,39 +30,52 @@
 {
     public static final String CONCURRENT_UPLOAD_LIMIT_PROPERTY = "concurrent_upload_limit";
     public static final int DEFAULT_CONCURRENT_UPLOAD_LIMIT = 80;
+
     public static final String MIN_FREE_SPACE_PERCENT_PROPERTY = "min_free_space_percent";
     public static final float DEFAULT_MIN_FREE_SPACE_PERCENT = 10;
 
+    public static final String FILE_PERMISSIONS_PROPERTY = "file_permissions";
+    public static final String DEFAULT_FILE_PERMISSIONS = "rw-r--r--";
+
     @JsonProperty(value = CONCURRENT_UPLOAD_LIMIT_PROPERTY, defaultValue = DEFAULT_CONCURRENT_UPLOAD_LIMIT + "")
     protected final int concurrentUploadsLimit;
 
     @JsonProperty(value = MIN_FREE_SPACE_PERCENT_PROPERTY, defaultValue = DEFAULT_MIN_FREE_SPACE_PERCENT + "")
     protected final float minimumSpacePercentageRequired;
 
+    protected String filePermissions;
+
     public SSTableUploadConfigurationImpl()
     {
-        this(DEFAULT_CONCURRENT_UPLOAD_LIMIT, DEFAULT_MIN_FREE_SPACE_PERCENT);
+        this(DEFAULT_CONCURRENT_UPLOAD_LIMIT, DEFAULT_MIN_FREE_SPACE_PERCENT, DEFAULT_FILE_PERMISSIONS);
     }
 
     public SSTableUploadConfigurationImpl(int concurrentUploadsLimit)
     {
-        this(concurrentUploadsLimit, DEFAULT_MIN_FREE_SPACE_PERCENT);
+        this(concurrentUploadsLimit, DEFAULT_MIN_FREE_SPACE_PERCENT, DEFAULT_FILE_PERMISSIONS);
     }
 
     public SSTableUploadConfigurationImpl(float minimumSpacePercentageRequired)
     {
-        this(DEFAULT_CONCURRENT_UPLOAD_LIMIT, minimumSpacePercentageRequired);
+        this(DEFAULT_CONCURRENT_UPLOAD_LIMIT, minimumSpacePercentageRequired, DEFAULT_FILE_PERMISSIONS);
+    }
+
+    public SSTableUploadConfigurationImpl(String filePermissions)
+    {
+        this(DEFAULT_CONCURRENT_UPLOAD_LIMIT, DEFAULT_MIN_FREE_SPACE_PERCENT, filePermissions);
     }
 
     public SSTableUploadConfigurationImpl(int concurrentUploadsLimit,
-                                          float minimumSpacePercentageRequired)
+                                          float minimumSpacePercentageRequired,
+                                          String filePermissions)
     {
         this.concurrentUploadsLimit = concurrentUploadsLimit;
         this.minimumSpacePercentageRequired = minimumSpacePercentageRequired;
+        setFilePermissions(filePermissions);
     }
 
     /**
-     * @return the maximum number of concurrent SSTable component uploads allowed for this service
+     * {@inheritDoc}
      */
     @Override
     @JsonProperty(value = CONCURRENT_UPLOAD_LIMIT_PROPERTY, defaultValue = DEFAULT_CONCURRENT_UPLOAD_LIMIT + "")
@@ -70,7 +85,7 @@
     }
 
     /**
-     * @return the configured minimum space percentage required for an SSTable component upload
+     * {@inheritDoc}
      */
     @Override
     @JsonProperty(value = MIN_FREE_SPACE_PERCENT_PROPERTY, defaultValue = DEFAULT_MIN_FREE_SPACE_PERCENT + "")
@@ -78,4 +93,37 @@
     {
         return minimumSpacePercentageRequired;
     }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    @JsonProperty(value = FILE_PERMISSIONS_PROPERTY, defaultValue = DEFAULT_FILE_PERMISSIONS)
+    public String filePermissions()
+    {
+        return filePermissions;
+    }
+
+    @JsonProperty(value = FILE_PERMISSIONS_PROPERTY, defaultValue = DEFAULT_FILE_PERMISSIONS)
+    public void setFilePermissions(String filePermissions)
+    {
+        if (filePermissions != null)
+        {
+            try
+            {
+                // forces a validation of the input
+                this.filePermissions = PosixFilePermissions.toString(PosixFilePermissions.fromString(filePermissions));
+            }
+            catch (IllegalArgumentException exception)
+            {
+                String errorMessage = String.format("Invalid file_permissions configuration=\"%s\"", filePermissions);
+                throw new IllegalArgumentException(errorMessage);
+            }
+        }
+        else
+        {
+            this.filePermissions = null;
+        }
+    }
 }
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandler.java b/src/main/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandler.java
index d14d5b1..b81f622 100644
--- a/src/main/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandler.java
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandler.java
@@ -123,8 +123,11 @@
         .compose(validRequest -> uploadPathBuilder.resolveStagingDirectory(host))
         .compose(this::ensureSufficientSpaceAvailable)
         .compose(v -> uploadPathBuilder.build(host, request))
-        .compose(uploadDirectory -> uploader.uploadComponent(httpRequest, uploadDirectory, request.component(),
-                                                             request.expectedChecksum()))
+        .compose(uploadDirectory -> uploader.uploadComponent(httpRequest,
+                                                             uploadDirectory,
+                                                             request.component(),
+                                                             request.expectedChecksum(),
+                                                             configuration.filePermissions()))
         .compose(fs::props)
         .onSuccess(fileProps -> {
             long serviceTimeMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeInNanos);
diff --git a/src/main/java/org/apache/cassandra/sidecar/utils/SSTableUploader.java b/src/main/java/org/apache/cassandra/sidecar/utils/SSTableUploader.java
index 6a5b271..0f21a47 100644
--- a/src/main/java/org/apache/cassandra/sidecar/utils/SSTableUploader.java
+++ b/src/main/java/org/apache/cassandra/sidecar/utils/SSTableUploader.java
@@ -67,19 +67,21 @@
      * @param uploadDirectory   the absolute path to the upload directory in the target {@code fs}
      * @param componentFileName the file name of the component
      * @param expectedChecksum  for verifying upload integrity, passed in through request
+     * @param filePermissions   specifies the posix file permissions used to create the SSTable file
      * @return path of SSTable component to which data was uploaded
      */
     public Future<String> uploadComponent(ReadStream<Buffer> readStream,
                                           String uploadDirectory,
                                           String componentFileName,
-                                          String expectedChecksum)
+                                          String expectedChecksum,
+                                          String filePermissions)
     {
 
         String targetPath = StringUtils.removeEnd(uploadDirectory, File.separator)
                             + File.separatorChar + componentFileName;
 
         return fs.mkdirs(uploadDirectory) // ensure the parent directory is created
-                 .compose(v -> createTempFile(uploadDirectory, componentFileName)) // create a temporary file
+                 .compose(v -> createTempFile(uploadDirectory, componentFileName, filePermissions))
                  .compose(tempFilePath -> streamAndVerify(readStream, tempFilePath, expectedChecksum))
                  .compose(verifiedTempFilePath -> moveAtomicallyWithFallBack(verifiedTempFilePath, targetPath));
     }
@@ -102,11 +104,12 @@
                  }); // stream to file
     }
 
-    private Future<String> createTempFile(String uploadDirectory, String componentFileName)
+    private Future<String> createTempFile(String uploadDirectory, String componentFileName, String permissions)
     {
-        LOGGER.debug("Creating temp file in directory={} with name={}{}",
-                     uploadDirectory, componentFileName, DEFAULT_TEMP_SUFFIX);
-        return fs.createTempFile(uploadDirectory, componentFileName, DEFAULT_TEMP_SUFFIX, /* perms */ (String) null);
+        LOGGER.debug("Creating temp file in directory={} with name={}{}, permissions={}",
+                     uploadDirectory, componentFileName, DEFAULT_TEMP_SUFFIX, permissions);
+
+        return fs.createTempFile(uploadDirectory, componentFileName, DEFAULT_TEMP_SUFFIX, permissions);
     }
 
     private Future<String> moveAtomicallyWithFallBack(String source, String target)
diff --git a/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java b/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java
index 8ca2bde..8d8ed00 100644
--- a/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java
@@ -24,10 +24,12 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
+import com.fasterxml.jackson.databind.JsonMappingException;
 import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
 
 import static org.apache.cassandra.sidecar.common.ResourceUtils.writeResourceToPath;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
 
 /**
  * Tests reading Sidecar {@link SidecarConfiguration} from YAML files
@@ -113,6 +115,28 @@
         validateMultipleInstancesSidecarConfiguration(config, true);
     }
 
+    @Test
+    void testFilePermissions() throws IOException
+    {
+        Path yamlPath = yaml("config/sidecar_file_permissions.yaml");
+        SidecarConfiguration config = SidecarConfigurationImpl.readYamlConfiguration(yamlPath);
+
+        assertThat(config).isNotNull();
+        assertThat(config.serviceConfiguration()).isNotNull();
+        assertThat(config.serviceConfiguration().ssTableUploadConfiguration()).isNotNull();
+        assertThat(config.serviceConfiguration().ssTableUploadConfiguration().filePermissions()).isEqualTo("rw-rw-rw-");
+    }
+
+    @Test
+    void testInvalidFilePermissions()
+    {
+        Path yamlPath = yaml("config/sidecar_invalid_file_permissions.yaml");
+        assertThatExceptionOfType(JsonMappingException.class)
+        .isThrownBy(() -> SidecarConfigurationImpl.readYamlConfiguration(yamlPath))
+        .withRootCauseInstanceOf(IllegalArgumentException.class)
+        .withMessageContaining("Invalid file_permissions configuration=\"not-valid\"");
+    }
+
     void validateSingleInstanceSidecarConfiguration(SidecarConfiguration config)
     {
         assertThat(config.cassandraInstances()).isNotNull().hasSize(1);
diff --git a/src/test/java/org/apache/cassandra/sidecar/config/yaml/SSTableUploadConfigurationImplTest.java b/src/test/java/org/apache/cassandra/sidecar/config/yaml/SSTableUploadConfigurationImplTest.java
new file mode 100644
index 0000000..6a2c604
--- /dev/null
+++ b/src/test/java/org/apache/cassandra/sidecar/config/yaml/SSTableUploadConfigurationImplTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.sidecar.config.yaml;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Unit tests to validate {@link SSTableUploadConfigurationImpl} inputs
+ */
+class SSTableUploadConfigurationImplTest
+{
+    @Test
+    void testNullFilePermissions()
+    {
+        SSTableUploadConfigurationImpl config = new SSTableUploadConfigurationImpl(null);
+        assertThat(config.filePermissions()).isNull();
+    }
+
+    @ParameterizedTest(name = "{index} => invalid permission string \"{0}\"")
+    @ValueSource(strings = { "", "aaaaaaaaa", "rwxaaaaaa", "rwx", "null" })
+    void filePermissionsFailsOnInvalidString(String value)
+    {
+        assertThatIllegalArgumentException()
+        .isThrownBy(() -> new SSTableUploadConfigurationImpl(value))
+        .withMessage("Invalid filePermissions configuration=\"" + value + "\"");
+    }
+
+    @ParameterizedTest(name = "{index} => valid permission string \"{0}\"")
+    @ValueSource(strings = { "---------", "rwx------", "rwxr--r--", "r-xr-xr-x", "r-xr-xrwx" })
+    void testValidFilePermission(String value)
+    {
+        SSTableUploadConfigurationImpl config = new SSTableUploadConfigurationImpl(value);
+        assertThat(config.filePermissions()).isEqualTo(value);
+    }
+}
diff --git a/src/test/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandlerTest.java b/src/test/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandlerTest.java
index 64f2986..3338bca 100644
--- a/src/test/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandlerTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandlerTest.java
@@ -22,6 +22,8 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -44,6 +46,15 @@
 import org.apache.cassandra.sidecar.common.http.SidecarHttpResponseStatus;
 import org.apache.cassandra.sidecar.snapshots.SnapshotUtils;
 
+import static java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE;
+import static java.nio.file.attribute.PosixFilePermission.GROUP_READ;
+import static java.nio.file.attribute.PosixFilePermission.GROUP_WRITE;
+import static java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE;
+import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ;
+import static java.nio.file.attribute.PosixFilePermission.OTHERS_WRITE;
+import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
+import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
+import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.when;
 
@@ -189,6 +200,42 @@
                                    Files.size(Paths.get(FILE_TO_BE_UPLOADED)), HttpResponseStatus.OK.code(), false);
     }
 
+    @Test
+    public void testFilePermissionOnUpload(VertxTestContext context) throws IOException
+    {
+        String uploadId = UUID.randomUUID().toString();
+        when(mockSSTableUploadConfiguration.filePermissions()).thenReturn("rwxr-xr-x");
+
+        sendUploadRequestAndVerify(null, context, uploadId, "ks", "tbl", "without-md5.db", "",
+                                   Files.size(Paths.get(FILE_TO_BE_UPLOADED)), HttpResponseStatus.OK.code(),
+                                   false, response -> {
+
+            Path path = temporaryFolder.toPath()
+                                       .resolve("staging")
+                                       .resolve(uploadId)
+                                       .resolve("ks")
+                                       .resolve("tbl")
+                                       .resolve("without-md5.db");
+
+            try
+            {
+                Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path);
+                assertThat(permissions).contains(OWNER_READ,
+                                                 OWNER_WRITE,
+                                                 OWNER_EXECUTE,
+                                                 GROUP_READ,
+                                                 GROUP_EXECUTE,
+                                                 OTHERS_READ,
+                                                 OTHERS_EXECUTE);
+                assertThat(permissions).doesNotContain(GROUP_WRITE, OTHERS_WRITE);
+            }
+            catch (IOException e)
+            {
+                throw new RuntimeException(e);
+            }
+        });
+    }
+
     private void sendUploadRequestAndVerify(VertxTestContext context,
                                             UUID uploadId,
                                             String keyspace,
diff --git a/src/test/resources/config/sidecar_file_permissions.yaml b/src/test/resources/config/sidecar_file_permissions.yaml
new file mode 100644
index 0000000..d1aeabe
--- /dev/null
+++ b/src/test/resources/config/sidecar_file_permissions.yaml
@@ -0,0 +1,122 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# Cassandra SideCar configuration file
+#
+cassandra_instances:
+  - id: 1
+    host: localhost1
+    port: 9042
+    username: cassandra
+    password: cassandra
+    data_dirs:
+      - /ccm/test/node1/data0
+      - /ccm/test/node1/data1
+    staging_dir: /ccm/test/node1/sstable-staging
+    jmx_host: 127.0.0.1
+    jmx_port: 7100
+    jmx_ssl_enabled: false
+  #    jmx_role:
+  #    jmx_role_password:
+  - id: 2
+    host: localhost2
+    port: 9042
+    username: cassandra
+    password: cassandra
+    data_dirs:
+      - /ccm/test/node2/data0
+      - /ccm/test/node2/data1
+    staging_dir: /ccm/test/node2/sstable-staging
+    jmx_host: 127.0.0.1
+    jmx_port: 7200
+    jmx_ssl_enabled: false
+  #    jmx_role:
+  #    jmx_role_password:
+  - id: 3
+    host: localhost3
+    port: 9042
+    username: cassandra
+    password: cassandra
+    data_dirs:
+      - /ccm/test/node3/data0
+      - /ccm/test/node3/data1
+    staging_dir: /ccm/test/node3/sstable-staging
+    jmx_host: 127.0.0.1
+    jmx_port: 7300
+    jmx_ssl_enabled: false
+#    jmx_role:
+#    jmx_role_password:
+
+sidecar:
+  host: 0.0.0.0
+  port: 9043
+  request_idle_timeout_millis: 300000 # this field expects integer value
+  request_timeout_millis: 300000
+  throttle:
+    stream_requests_per_sec: 5000
+    delay_sec: 5
+    timeout_sec: 10
+  sstable_upload:
+    concurrent_upload_limit: 80
+    min_free_space_percent: 10
+    file_permissions: "rw-rw-rw-"
+  allowable_time_skew_in_minutes: 60
+  sstable_import:
+    poll_interval_millis: 100
+    cache:
+      expire_after_access_millis: 7200000 # 2 hours
+      maximum_size: 10000
+  worker_pools:
+    service:
+      name: "sidecar-worker-pool"
+      size: 20
+      max_execution_time_millis: 60000 # 60 seconds
+    internal:
+      name: "sidecar-internal-worker-pool"
+      size: 20
+      max_execution_time_millis: 900000 # 15 minutes
+
+#
+# Enable SSL configuration (Disabled by default)
+#
+#  ssl:
+#    enabled: true
+#    keystore:
+#      path: "path/to/keystore.p12"
+#      password: password
+#    truststore:
+#      path: "path/to/truststore.p12"
+#      password: password
+
+
+healthcheck:
+  poll_freq_millis: 30000
+
+cassandra_input_validation:
+  forbidden_keyspaces:
+    - system_schema
+    - system_traces
+    - system_distributed
+    - system
+    - system_auth
+    - system_views
+    - system_virtual_schema
+  allowed_chars_for_directory: "[a-zA-Z0-9_-]+"
+  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)"
diff --git a/src/test/resources/config/sidecar_invalid_file_permissions.yaml b/src/test/resources/config/sidecar_invalid_file_permissions.yaml
new file mode 100644
index 0000000..ce192ce
--- /dev/null
+++ b/src/test/resources/config/sidecar_invalid_file_permissions.yaml
@@ -0,0 +1,122 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# Cassandra SideCar configuration file
+#
+cassandra_instances:
+  - id: 1
+    host: localhost1
+    port: 9042
+    username: cassandra
+    password: cassandra
+    data_dirs:
+      - /ccm/test/node1/data0
+      - /ccm/test/node1/data1
+    staging_dir: /ccm/test/node1/sstable-staging
+    jmx_host: 127.0.0.1
+    jmx_port: 7100
+    jmx_ssl_enabled: false
+  #    jmx_role:
+  #    jmx_role_password:
+  - id: 2
+    host: localhost2
+    port: 9042
+    username: cassandra
+    password: cassandra
+    data_dirs:
+      - /ccm/test/node2/data0
+      - /ccm/test/node2/data1
+    staging_dir: /ccm/test/node2/sstable-staging
+    jmx_host: 127.0.0.1
+    jmx_port: 7200
+    jmx_ssl_enabled: false
+  #    jmx_role:
+  #    jmx_role_password:
+  - id: 3
+    host: localhost3
+    port: 9042
+    username: cassandra
+    password: cassandra
+    data_dirs:
+      - /ccm/test/node3/data0
+      - /ccm/test/node3/data1
+    staging_dir: /ccm/test/node3/sstable-staging
+    jmx_host: 127.0.0.1
+    jmx_port: 7300
+    jmx_ssl_enabled: false
+#    jmx_role:
+#    jmx_role_password:
+
+sidecar:
+  host: 0.0.0.0
+  port: 9043
+  request_idle_timeout_millis: 300000 # this field expects integer value
+  request_timeout_millis: 300000
+  throttle:
+    stream_requests_per_sec: 5000
+    delay_sec: 5
+    timeout_sec: 10
+  sstable_upload:
+    concurrent_upload_limit: 80
+    min_free_space_percent: 10
+    file_permissions: "not-valid"
+  allowable_time_skew_in_minutes: 60
+  sstable_import:
+    poll_interval_millis: 100
+    cache:
+      expire_after_access_millis: 7200000 # 2 hours
+      maximum_size: 10000
+  worker_pools:
+    service:
+      name: "sidecar-worker-pool"
+      size: 20
+      max_execution_time_millis: 60000 # 60 seconds
+    internal:
+      name: "sidecar-internal-worker-pool"
+      size: 20
+      max_execution_time_millis: 900000 # 15 minutes
+
+#
+# Enable SSL configuration (Disabled by default)
+#
+#  ssl:
+#    enabled: true
+#    keystore:
+#      path: "path/to/keystore.p12"
+#      password: password
+#    truststore:
+#      path: "path/to/truststore.p12"
+#      password: password
+
+
+healthcheck:
+  poll_freq_millis: 30000
+
+cassandra_input_validation:
+  forbidden_keyspaces:
+    - system_schema
+    - system_traces
+    - system_distributed
+    - system
+    - system_auth
+    - system_views
+    - system_virtual_schema
+  allowed_chars_for_directory: "[a-zA-Z0-9_-]+"
+  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)"
diff --git a/src/test/resources/config/sidecar_multiple_instances.yaml b/src/test/resources/config/sidecar_multiple_instances.yaml
index 4388cea..502b919 100644
--- a/src/test/resources/config/sidecar_multiple_instances.yaml
+++ b/src/test/resources/config/sidecar_multiple_instances.yaml
@@ -75,6 +75,7 @@
   sstable_upload:
     concurrent_upload_limit: 80
     min_free_space_percent: 10
+    # file_permissions: "rw-r--r--" # when not specified, the default file permissions are owner read & write, group & others read
   allowable_time_skew_in_minutes: 60
   sstable_import:
     poll_interval_millis: 100
@@ -89,7 +90,7 @@
     internal:
       name: "sidecar-internal-worker-pool"
       size: 20
-      max_execution_time_millis: 300000 # 5 minutes
+      max_execution_time_millis: 900000 # 15 minutes
 
 #
 # Enable SSL configuration (Disabled by default)