You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by fr...@apache.org on 2025/10/03 04:02:47 UTC

(cassandra-sidecar) branch trunk updated: CASSSIDECAR-351: Code refactoring for some configuration classes and migrate RoleBasedAuthorizationIntegrationTest to integration-tests (#268)

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 ea54b140 CASSSIDECAR-351: Code refactoring for some configuration classes and migrate RoleBasedAuthorizationIntegrationTest to integration-tests (#268)
ea54b140 is described below

commit ea54b140b07c2655e8f635d383a29b1c0e04da55
Author: Francisco Guerrero <fr...@apache.org>
AuthorDate: Thu Oct 2 21:02:42 2025 -0700

    CASSSIDECAR-351: Code refactoring for some configuration classes and migrate RoleBasedAuthorizationIntegrationTest to integration-tests (#268)
    
    Patch by Francisco Guerrero; reviewed by Saranya Krishnakumar for CASSSIDECAR-351
---
 CHANGES.txt                                        |   1 +
 integration-framework/build.gradle                 |   2 +-
 .../cassandra/sidecar/testing/MtlsTestHelper.java  |  76 +-
 .../testing/SharedClusterIntegrationTestBase.java  |  69 +-
 .../testing/TemporaryCqlSessionProvider.java       |  11 +
 .../apache/cassandra/testing/DriverTestUtils.java  |  42 +
 .../org/apache/cassandra/testing/TestUtils.java    |   2 +
 .../org/apache/cassandra/testing/TlsTestUtils.java | 146 ++++
 .../acl/RoleBasedAuthorizationIntegrationTest.java | 953 +++++++++++++++++++++
 .../SidecarPeerDownDetectorIntegrationTest.java    |   6 +-
 .../LifecycleProviderIntegrationTester.java        |   3 +-
 .../SharedClusterSidecarIntegrationTestBase.java   |  64 +-
 .../yaml/AccessControlConfigurationImpl.java       | 119 ++-
 .../config/yaml/CacheConfigurationImpl.java        | 149 +++-
 .../config/yaml/PeriodicTaskConfigurationImpl.java |   2 +-
 .../yaml/SSTableImportConfigurationImpl.java       |   6 +-
 .../yaml/SSTableSnapshotConfigurationImpl.java     |   5 +-
 .../sidecar/config/yaml/SslConfigurationImpl.java  |   4 +-
 .../db/schema/SidecarRolePermissionsSchema.java    |   2 +-
 .../MutualTLSAuthenticationIntegrationTest.java    |   3 +-
 .../acl/RoleBasedAuthorizationIntegrationTest.java | 624 --------------
 .../sidecar/testing/IntegrationTestModule.java     |  28 +-
 .../MutualTLSAuthenticationHandlerTest.java        |  18 +-
 .../config/yaml/CacheConfigurationImplTest.java    |  18 +-
 .../cassandra/sidecar/utils/CacheFactoryTest.java  |   6 +-
 .../distributed/impl/CassandraCluster.java         |   2 +-
 26 files changed, 1587 insertions(+), 774 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index a9cb74a3..d431fb2a 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 0.3.0
 -----
+ * Code refactoring for some configuration classes and migrate RoleBasedAuthorizationIntegrationTest to integration-tests (CASSSIDECAR-351)
  * Fix restore job metrics and log adjustments (CASSSIDECAR-350)
  * Add observability around authN and authZ caches in Sidecar (CASSSIDECAR-348)
  * Thread race issue when adding excluded metric in FilteringMetricRegistry (CASSSIDECAR-349)
diff --git a/integration-framework/build.gradle b/integration-framework/build.gradle
index fceb7922..5edf49ea 100644
--- a/integration-framework/build.gradle
+++ b/integration-framework/build.gradle
@@ -42,7 +42,7 @@ dependencies {
     api('org.mockito:mockito-inline:4.10.0')
     api("org.assertj:assertj-core:${assertjCoreVersion}")
 
-    api('com.datastax.cassandra:cassandra-driver-core:3.11.3')
+    api('com.datastax.cassandra:cassandra-driver-core:3.11.5')
     implementation("com.google.inject:guice:${guiceVersion}")
 
     implementation("com.google.guava:guava:27.0.1-jre")
diff --git a/integration-framework/src/main/java/org/apache/cassandra/sidecar/testing/MtlsTestHelper.java b/integration-framework/src/main/java/org/apache/cassandra/sidecar/testing/MtlsTestHelper.java
index 51c0764c..d3d6e067 100644
--- a/integration-framework/src/main/java/org/apache/cassandra/sidecar/testing/MtlsTestHelper.java
+++ b/integration-framework/src/main/java/org/apache/cassandra/sidecar/testing/MtlsTestHelper.java
@@ -19,13 +19,8 @@
 package org.apache.cassandra.sidecar.testing;
 
 import java.nio.file.Path;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.Objects;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import java.util.function.Consumer;
 
 import org.apache.cassandra.testing.utils.tls.CertificateBuilder;
 import org.apache.cassandra.testing.utils.tls.CertificateBundle;
@@ -35,14 +30,14 @@ import org.apache.cassandra.testing.utils.tls.CertificateBundle;
  */
 public class MtlsTestHelper
 {
-    private static final Logger LOGGER = LoggerFactory.getLogger(MtlsTestHelper.class);
-    public static final char[] EMPTY_PASSWORD = new char[0];
-    public static final String EMPTY_PASSWORD_STRING = "";
+    public static final String PASSWORD_STRING = "cassandra";
+    public static final char[] PASSWORD = PASSWORD_STRING.toCharArray();
     /**
      * A system property that can enable / disable testing with Mutual TLS
      */
     public static final String CASSANDRA_INTEGRATION_TEST_ENABLE_MTLS = "cassandra.integration.sidecar.test.enable_mtls";
     private final boolean enableMtlsForTesting;
+    private final Path secretsPath;
     CertificateBundle certificateAuthority;
     Path truststorePath;
     Path serverKeyStorePath;
@@ -56,10 +51,11 @@ public class MtlsTestHelper
     public MtlsTestHelper(Path secretsPath, boolean enableMtlsForTesting) throws Exception
     {
         this.enableMtlsForTesting = enableMtlsForTesting;
-        maybeInitializeSecrets(Objects.requireNonNull(secretsPath, "secretsPath cannot be null"));
+        this.secretsPath = Objects.requireNonNull(secretsPath, "secretsPath cannot be null");
+        maybeInitializeSecrets();
     }
 
-    void maybeInitializeSecrets(Path secretsPath) throws Exception
+    void maybeInitializeSecrets() throws Exception
     {
         if (!enableMtlsForTesting)
         {
@@ -67,11 +63,10 @@ public class MtlsTestHelper
         }
 
         certificateAuthority =
-        new CertificateBuilder().subject("CN=Apache Cassandra Root CA, OU=Certification Authority, O=Unknown, C=Unknown")
-                                .alias("fakerootca")
+        new CertificateBuilder().subject("CN=Apache cassandra Root CA, OU=Certification Authority, O=Unknown, C=Unknown")
                                 .isCertificateAuthority(true)
                                 .buildSelfSigned();
-        truststorePath = certificateAuthority.toTempKeyStorePath(secretsPath, EMPTY_PASSWORD, EMPTY_PASSWORD);
+        truststorePath = certificateAuthority.toTempKeyStorePath(secretsPath, PASSWORD, PASSWORD);
 
         CertificateBuilder serverKeyStoreBuilder =
         new CertificateBuilder().subject("CN=Apache Cassandra, OU=mtls_test, O=Unknown, L=Unknown, ST=Unknown, C=Unknown")
@@ -79,15 +74,28 @@ public class MtlsTestHelper
         // Add SANs for every potential hostname Sidecar will serve
         for (int i = 1; i <= 20; i++)
         {
-            serverKeyStoreBuilder.addSanDnsName("localhost" + i);
+            serverKeyStoreBuilder.addSanDnsName("localhost" + i)
+                                 .addSanIpAddress("127.0.0." + i);
         }
 
         CertificateBundle serverKeyStore = serverKeyStoreBuilder.buildIssuedBy(certificateAuthority);
-        serverKeyStorePath = serverKeyStore.toTempKeyStorePath(secretsPath, EMPTY_PASSWORD, EMPTY_PASSWORD);
-        CertificateBundle clientKeyStore = new CertificateBuilder().subject("CN=Apache Cassandra, OU=mtls_test, O=Unknown, L=Unknown, ST=Unknown, C=Unknown")
-                                                                   .addSanDnsName("localhost")
-                                                                   .buildIssuedBy(certificateAuthority);
-        clientKeyStorePath = clientKeyStore.toTempKeyStorePath(secretsPath, EMPTY_PASSWORD, EMPTY_PASSWORD);
+        serverKeyStorePath = serverKeyStore.toTempKeyStorePath(secretsPath, PASSWORD, PASSWORD);
+        clientKeyStorePath = issueClientKeyStore(null);
+    }
+
+    public Path issueClientKeyStore(Consumer<CertificateBuilder> certificateBuilderConsumer) throws Exception
+    {
+        CertificateBuilder builder = new CertificateBuilder()
+                                     .subject("CN=Apache Cassandra, OU=ssl_test, O=Unknown, L=Unknown, ST=Unknown, C=Unknown")
+                                     .addSanDnsName("localhost")
+                                     .addSanIpAddress("127.0.0.1");
+
+        if (certificateBuilderConsumer != null)
+        {
+            certificateBuilderConsumer.accept(builder);
+        }
+        CertificateBundle clientKeystore = builder.buildIssuedBy(certificateAuthority);
+        return clientKeystore.toTempKeyStorePath(secretsPath, PASSWORD, PASSWORD);
     }
 
     public boolean isEnabled()
@@ -105,9 +113,14 @@ public class MtlsTestHelper
         return clientKeyStorePath.toString();
     }
 
+    public String clientKeyStorePassword()
+    {
+        return PASSWORD_STRING;
+    }
+
     public String trustStorePassword()
     {
-        return EMPTY_PASSWORD_STRING;
+        return PASSWORD_STRING;
     }
 
     public String trustStoreType()
@@ -122,30 +135,11 @@ public class MtlsTestHelper
 
     public String serverKeyStorePassword()
     {
-        return EMPTY_PASSWORD_STRING;
+        return PASSWORD_STRING;
     }
 
     public String serverKeyStoreType()
     {
         return "PKCS12";
     }
-
-    public Map<String, String> mtlOptionMap()
-    {
-        if (!isEnabled())
-        {
-            return Collections.emptyMap();
-        }
-
-        LOGGER.info("Test mTLS certificate is enabled. "
-                    + "Will use test keystore as truststore so the client will trust the server");
-        Map<String, String> optionMap = new HashMap<>();
-        optionMap.put("truststore_path", trustStorePath());
-        optionMap.put("truststore_password", EMPTY_PASSWORD_STRING);
-        optionMap.put("truststore_type", trustStoreType());
-        optionMap.put("keystore_path", clientKeyStorePath.toString());
-        optionMap.put("keystore_password", EMPTY_PASSWORD_STRING);
-        optionMap.put("keystore_type", "PKCS12");
-        return optionMap;
-    }
 }
diff --git a/integration-framework/src/main/java/org/apache/cassandra/sidecar/testing/SharedClusterIntegrationTestBase.java b/integration-framework/src/main/java/org/apache/cassandra/sidecar/testing/SharedClusterIntegrationTestBase.java
index 8d360801..f766ed3a 100644
--- a/integration-framework/src/main/java/org/apache/cassandra/sidecar/testing/SharedClusterIntegrationTestBase.java
+++ b/integration-framework/src/main/java/org/apache/cassandra/sidecar/testing/SharedClusterIntegrationTestBase.java
@@ -35,6 +35,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.StreamSupport;
@@ -114,6 +115,7 @@ import org.apache.cassandra.testing.TestVersionSupplier;
 
 import static org.apache.cassandra.sidecar.config.yaml.S3ClientConfigurationImpl.DEFAULT_API_CALL_TIMEOUT;
 import static org.apache.cassandra.sidecar.testing.MtlsTestHelper.CASSANDRA_INTEGRATION_TEST_ENABLE_MTLS;
+import static org.apache.cassandra.testing.DriverTestUtils.buildContactPoints;
 import static org.apache.cassandra.testing.utils.IInstanceUtils.tryGetIntConfig;
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -183,15 +185,15 @@ public abstract class SharedClusterIntegrationTestBase
         this.testVersion = maybeTestVersion.get();
         logger.info("Testing with version={}", testVersion);
 
+        beforeClusterProvisioning();
+
         classLoaderWrapper = new IsolatedDTestClassLoaderWrapper();
         classLoaderWrapper.initializeDTestJarClassLoader(testVersion, TestVersion.class);
-
-        beforeClusterProvisioning();
+        mtlsTestHelper = new MtlsTestHelper(secretsPath);
         cluster = provisionClusterWithRetries(this.testVersion);
         assertThat(cluster).isNotNull();
         afterClusterProvisioned();
         initializeSchemaForTest();
-        mtlsTestHelper = new MtlsTestHelper(secretsPath);
         startSidecar(cluster);
         beforeTestStart();
     }
@@ -316,16 +318,41 @@ public abstract class SharedClusterIntegrationTestBase
         createTestKeyspace(name.maybeQuotedKeyspace(), rf);
     }
 
+    protected void createTestKeyspace(Session session, QualifiedName name, Map<String, Integer> rf)
+    {
+        createTestKeyspace(session, name.maybeQuotedKeyspace(), rf);
+    }
+
     protected void createTestKeyspace(String keyspace, Map<String, Integer> rf)
     {
-        cluster.schemaChangeIgnoringStoppedInstances("CREATE KEYSPACE IF NOT EXISTS " + keyspace
-                                                     + " WITH REPLICATION = { 'class' : 'NetworkTopologyStrategy', " +
-                                                     generateRfString(rf) + " };");
+        createTestKeyspace(cluster::schemaChangeIgnoringStoppedInstances, keyspace, rf);
+    }
+
+    protected void createTestKeyspace(Session session, String keyspace, Map<String, Integer> rf)
+    {
+        createTestKeyspace(session::execute, keyspace, rf);
+    }
+
+    protected void createTestKeyspace(Consumer<String> queryExecution, String keyspace, Map<String, Integer> rf)
+    {
+        queryExecution.accept("CREATE KEYSPACE IF NOT EXISTS " + keyspace
+                              + " WITH REPLICATION = { 'class' : 'NetworkTopologyStrategy', " +
+                              generateRfString(rf) + " };");
     }
 
     protected void createTestTable(QualifiedName name, String createTableStatement)
     {
-        cluster.schemaChangeIgnoringStoppedInstances(String.format(createTableStatement, name));
+        createTestTable(cluster::schemaChangeIgnoringStoppedInstances, name, createTableStatement);
+    }
+
+    protected void createTestTable(Session session, QualifiedName name, String createTableStatement)
+    {
+        createTestTable(session::execute, name, createTableStatement);
+    }
+
+    protected void createTestTable(Consumer<String> queryExecution, QualifiedName name, String createTableStatement)
+    {
+        queryExecution.accept(String.format(createTableStatement, name));
     }
 
     /**
@@ -442,17 +469,17 @@ public abstract class SharedClusterIntegrationTestBase
         {
             return;
         }
-        closeServer(serverWrapper.server);
+        closeServer(serverWrapper);
     }
 
-    protected void closeServer(Server s) throws InterruptedException
+    protected void closeServer(ServerWrapper wrapper) throws InterruptedException
     {
-        if (s == null)
+        if (wrapper == null)
         {
             return;
         }
         CountDownLatch closeLatch = new CountDownLatch(1);
-        s.close().onSuccess(res -> closeLatch.countDown());
+        wrapper.server.close().onSuccess(res -> closeLatch.countDown());
         if (closeLatch.await(60, TimeUnit.SECONDS))
         {
             logger.info("Close event received before timeout.");
@@ -558,6 +585,11 @@ public abstract class SharedClusterIntegrationTestBase
     // Utility methods
 
     public static Cluster createDriverCluster(ICluster<? extends IInstance> dtest)
+    {
+        return createDriverCluster(dtest, null);
+    }
+
+    public static Cluster createDriverCluster(ICluster<? extends IInstance> dtest, Consumer<com.datastax.driver.core.Cluster.Builder> overrideBuilder)
     {
         dtest.stream().forEach((i) -> {
             if (!i.config().has(Feature.NATIVE_PROTOCOL) || !i.config().has(Feature.GOSSIP))
@@ -573,7 +605,10 @@ public abstract class SharedClusterIntegrationTestBase
                                                               i.config().getInt("native_transport_port"));
             builder.addContactPointsWithPorts(address);
         });
-
+        if (overrideBuilder != null)
+        {
+            overrideBuilder.accept(builder);
+        }
         return builder.build();
     }
 
@@ -653,7 +688,7 @@ public abstract class SharedClusterIntegrationTestBase
         @Singleton
         public CQLSessionProvider cqlSessionProvider()
         {
-            List<InetSocketAddress> contactPoints = buildContactPoints();
+            List<InetSocketAddress> contactPoints = buildContactPoints(instances);
             return new TemporaryCqlSessionProvider(contactPoints,
                                                    SharedExecutorNettyOptions.INSTANCE);
         }
@@ -716,14 +751,6 @@ public abstract class SharedClusterIntegrationTestBase
             return new InJvmDTestLifecycleProvider(instances);
         }
 
-        private List<InetSocketAddress> buildContactPoints()
-        {
-            return StreamSupport.stream(instances.spliterator(), false)
-                                .map(instance -> new InetSocketAddress(instance.config().broadcastAddress().getAddress(),
-                                                                       tryGetIntConfig(instance, "native_transport_port", 9042)))
-                                .collect(Collectors.toList());
-        }
-
         public static SidecarConfigurationImpl.Builder defaultConfigurationBuilder(
         MtlsTestHelper mtlsTestHelper, Function<SidecarConfigurationImpl.Builder, SidecarConfigurationImpl.Builder> configurationOverrides)
         {
diff --git a/integration-framework/src/main/java/org/apache/cassandra/sidecar/testing/TemporaryCqlSessionProvider.java b/integration-framework/src/main/java/org/apache/cassandra/sidecar/testing/TemporaryCqlSessionProvider.java
index 61fb09d5..f19f9223 100644
--- a/integration-framework/src/main/java/org/apache/cassandra/sidecar/testing/TemporaryCqlSessionProvider.java
+++ b/integration-framework/src/main/java/org/apache/cassandra/sidecar/testing/TemporaryCqlSessionProvider.java
@@ -29,6 +29,8 @@ import org.slf4j.LoggerFactory;
 
 import com.datastax.driver.core.Cluster;
 import com.datastax.driver.core.NettyOptions;
+import com.datastax.driver.core.PlainTextAuthProvider;
+import com.datastax.driver.core.SSLOptions;
 import com.datastax.driver.core.Session;
 import com.datastax.driver.core.exceptions.DriverException;
 import com.datastax.driver.core.exceptions.DriverInternalError;
@@ -50,14 +52,21 @@ public class TemporaryCqlSessionProvider implements CQLSessionProvider
     private static final Logger logger = LoggerFactory.getLogger(TemporaryCqlSessionProvider.class);
     private final List<InetSocketAddress> contactPoints;
     private Session localSession;
+    private final SSLOptions sslOptions;
     private final NettyOptions nettyOptions;
     private final ReconnectionPolicy reconnectionPolicy;
 
     public TemporaryCqlSessionProvider(List<InetSocketAddress> contactPoints, NettyOptions options)
+    {
+        this(contactPoints, options, null);
+    }
+
+    public TemporaryCqlSessionProvider(List<InetSocketAddress> contactPoints, NettyOptions options, SSLOptions sslOptions)
     {
         nettyOptions = options;
         reconnectionPolicy = new ExponentialReconnectionPolicy(100, 1000);
         this.contactPoints = contactPoints;
+        this.sslOptions = sslOptions;
     }
 
     @NotNull
@@ -77,6 +86,8 @@ public class TemporaryCqlSessionProvider implements CQLSessionProvider
                              .addContactPointsWithPorts(contactPoints)
                              .withReconnectionPolicy(reconnectionPolicy)
                              .withoutMetrics()
+                             .withSSL(sslOptions)
+                             .withAuthProvider(new PlainTextAuthProvider("cassandra", "cassandra"))
                              // tests can create a lot of these Cluster objects, to avoid creating HWTs and
                              // event thread pools for each we have the override
                              .withNettyOptions(nettyOptions)
diff --git a/integration-framework/src/main/java/org/apache/cassandra/testing/DriverTestUtils.java b/integration-framework/src/main/java/org/apache/cassandra/testing/DriverTestUtils.java
new file mode 100644
index 00000000..e259bbe9
--- /dev/null
+++ b/integration-framework/src/main/java/org/apache/cassandra/testing/DriverTestUtils.java
@@ -0,0 +1,42 @@
+/*
+ * 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.testing;
+
+import java.net.InetSocketAddress;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import org.apache.cassandra.distributed.api.IInstance;
+
+import static org.apache.cassandra.testing.utils.IInstanceUtils.tryGetIntConfig;
+
+/**
+ * Test utility class for the Cassandra driver
+ */
+public class DriverTestUtils
+{
+    public static List<InetSocketAddress> buildContactPoints(Iterable<? extends IInstance> instances)
+    {
+        return StreamSupport.stream(instances.spliterator(), false)
+                            .map(instance -> new InetSocketAddress(instance.config().broadcastAddress().getAddress(),
+                                                                   tryGetIntConfig(instance, "native_transport_port", 9042)))
+                            .collect(Collectors.toList());
+    }
+}
diff --git a/integration-framework/src/main/java/org/apache/cassandra/testing/TestUtils.java b/integration-framework/src/main/java/org/apache/cassandra/testing/TestUtils.java
index 73e84075..c1619fd6 100644
--- a/integration-framework/src/main/java/org/apache/cassandra/testing/TestUtils.java
+++ b/integration-framework/src/main/java/org/apache/cassandra/testing/TestUtils.java
@@ -149,5 +149,7 @@ public final class TestUtils
         System.setProperty("cassandra.allow_alter_rf_during_range_movement", "true");
 
         System.setProperty("cassandra.minimum_replication_factor", "1");
+        // Uncomment the line below to debug SSL Handshake issues during tests
+        //System.setProperty("javax.net.debug", "ssl:handshake");
     }
 }
diff --git a/integration-framework/src/main/java/org/apache/cassandra/testing/TlsTestUtils.java b/integration-framework/src/main/java/org/apache/cassandra/testing/TlsTestUtils.java
new file mode 100644
index 00000000..cd669dd0
--- /dev/null
+++ b/integration-framework/src/main/java/org/apache/cassandra/testing/TlsTestUtils.java
@@ -0,0 +1,146 @@
+/*
+ * 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.testing;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.TrustManagerFactory;
+
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.PlainTextAuthProvider;
+import com.datastax.driver.core.RemoteEndpointAwareNettySSLOptions;
+import com.datastax.driver.core.SSLOptions;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.policies.DCAwareRoundRobinPolicy;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import org.apache.cassandra.distributed.api.IInstance;
+
+import static org.apache.cassandra.testing.utils.IInstanceUtils.tryGetIntConfig;
+
+/**
+ * Test utility class for all TLS-things related
+ */
+public class TlsTestUtils
+{
+    public static SSLOptions getSSLOptions(String keystorePath, String keystorePassword,
+                                           String trustStorePath, String trustStorePassword) throws RuntimeException
+    {
+        try
+        {
+            SslContext context = buildSSLContext(keystorePath, keystorePassword, trustStorePath, trustStorePassword);
+            return new RemoteEndpointAwareNettySSLOptions(context);
+        }
+        catch (GeneralSecurityException | IOException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static void withAuthenticatedSession(IInstance instance,
+                                                String username,
+                                                String password,
+                                                Consumer<Session> consumer,
+                                                SSLOptions sslOptions)
+    {
+        withAuthenticatedSession(instance, username, password, (builder) -> builder, consumer, sslOptions);
+    }
+
+    public static void withAuthenticatedSession(IInstance instance,
+                                                String username,
+                                                String password,
+                                                Function<Cluster.Builder, Cluster.Builder> builderCustomizer,
+                                                Consumer<Session> consumer,
+                                                SSLOptions sslOptions)
+    {
+        InetSocketAddress nativeInetSocketAddress = new InetSocketAddress(instance.config().broadcastAddress().getAddress(),
+                                                                          tryGetIntConfig(instance.config(), "native_transport_port", 9042));
+        InetAddress address = nativeInetSocketAddress.getAddress();
+
+        com.datastax.driver.core.Cluster.Builder builder = com.datastax.driver.core.Cluster.builder()
+                                                                                           .withLoadBalancingPolicy(new DCAwareRoundRobinPolicy.Builder().build())
+                                                                                           .withSSL(sslOptions)
+                                                                                           .withoutJMXReporting()
+                                                                                           .withAuthProvider(new PlainTextAuthProvider(username, password))
+                                                                                           .addContactPoint(address.getHostAddress())
+                                                                                           .withPort(nativeInetSocketAddress.getPort());
+
+        if (builderCustomizer != null)
+        {
+            builder = builderCustomizer.apply(builder);
+        }
+
+        try (com.datastax.driver.core.Cluster c = builder.build(); Session session = c.connect())
+        {
+            consumer.accept(session);
+        }
+    }
+
+    public static SslContext buildSSLContext(String keystorePath, String keystorePassword,
+                                             String trustStorePath, String trustStorePassword) throws GeneralSecurityException, IOException
+    {
+
+        SslContextBuilder sslContextBuilder;
+        try
+        {
+            sslContextBuilder = SslContextBuilder.forClient()
+                                                 .protocols(List.of("TLSv1.2", "TLSv1.3"));
+
+            if (keystorePath != null && keystorePassword != null)
+            {
+                KeyStore keyStore = createKeystore(keystorePath, keystorePassword);
+                KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+                kmf.init(keyStore, keystorePassword.toCharArray());
+                sslContextBuilder.keyManager(kmf);
+            }
+
+            // We set the truststore only if it is configured. For an SSL connection, if the truststore is required
+            // but the user has not provided one, the default Java truststore is used.
+            KeyStore truststore = createKeystore(trustStorePath, trustStorePassword);
+            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            tmf.init(truststore);
+            sslContextBuilder.trustManager(tmf);
+            return sslContextBuilder.build();
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException("Error creating SsLContext for Cassandra connections", e);
+        }
+    }
+
+    static KeyStore createKeystore(String path, String password) throws GeneralSecurityException, IOException
+    {
+        KeyStore keystore = KeyStore.getInstance("PKCS12");
+        try (InputStream inputStream = Files.newInputStream(Paths.get(path)))
+        {
+            keystore.load(inputStream, password.toCharArray());
+        }
+        return keystore;
+    }
+}
diff --git a/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/acl/RoleBasedAuthorizationIntegrationTest.java b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/acl/RoleBasedAuthorizationIntegrationTest.java
new file mode 100644
index 00000000..7e51a16e
--- /dev/null
+++ b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/acl/RoleBasedAuthorizationIntegrationTest.java
@@ -0,0 +1,953 @@
+/*
+ * 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.acl;
+
+import java.net.InetSocketAddress;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.datastax.driver.core.SSLOptions;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.exceptions.AuthenticationException;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.multibindings.ProvidesIntoMap;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.ext.web.client.HttpResponse;
+import io.vertx.ext.web.client.WebClient;
+import org.apache.cassandra.distributed.api.ICluster;
+import org.apache.cassandra.distributed.api.IInstance;
+import org.apache.cassandra.distributed.api.IInstanceConfig;
+import org.apache.cassandra.distributed.shared.Uninterruptibles;
+import org.apache.cassandra.sidecar.common.response.ListSnapshotFilesResponse;
+import org.apache.cassandra.sidecar.common.server.CQLSessionProvider;
+import org.apache.cassandra.sidecar.common.server.utils.DurationSpec;
+import org.apache.cassandra.sidecar.common.server.utils.MillisecondBoundConfiguration;
+import org.apache.cassandra.sidecar.common.server.utils.SecondBoundConfiguration;
+import org.apache.cassandra.sidecar.config.AccessControlConfiguration;
+import org.apache.cassandra.sidecar.config.CacheConfiguration;
+import org.apache.cassandra.sidecar.config.KeyStoreConfiguration;
+import org.apache.cassandra.sidecar.config.ParameterizedClassConfiguration;
+import org.apache.cassandra.sidecar.config.PeriodicTaskConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.config.yaml.AccessControlConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.CacheConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.KeyStoreConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.ParameterizedClassConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.PeriodicTaskConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.SslConfigurationImpl;
+import org.apache.cassandra.sidecar.coordination.ClusterLease;
+import org.apache.cassandra.sidecar.db.schema.SidecarSchema;
+import org.apache.cassandra.sidecar.db.schema.SidecarSchemaInitializer;
+import org.apache.cassandra.sidecar.metrics.SidecarMetrics;
+import org.apache.cassandra.sidecar.modules.multibindings.ClassKey;
+import org.apache.cassandra.sidecar.modules.multibindings.KeyClassMapKey;
+import org.apache.cassandra.sidecar.modules.multibindings.MultiBindingTypeResolver;
+import org.apache.cassandra.sidecar.modules.multibindings.PeriodicTaskMapKeys;
+import org.apache.cassandra.sidecar.tasks.PeriodicTask;
+import org.apache.cassandra.sidecar.testing.MtlsTestHelper;
+import org.apache.cassandra.sidecar.testing.QualifiedName;
+import org.apache.cassandra.sidecar.testing.SharedClusterSidecarIntegrationTestBase;
+import org.apache.cassandra.sidecar.testing.SharedExecutorNettyOptions;
+import org.apache.cassandra.sidecar.testing.TemporaryCqlSessionProvider;
+import org.apache.cassandra.sidecar.utils.SimpleCassandraVersion;
+import org.apache.cassandra.testing.ClusterBuilderConfiguration;
+
+import static org.apache.cassandra.sidecar.db.schema.SidecarRolePermissionsSchema.ROLE_PERMISSIONS_TABLE;
+import static org.apache.cassandra.testing.DriverTestUtils.buildContactPoints;
+import static org.apache.cassandra.testing.TestUtils.DC1_RF1;
+import static org.apache.cassandra.testing.TlsTestUtils.getSSLOptions;
+import static org.apache.cassandra.testing.TlsTestUtils.withAuthenticatedSession;
+import static org.apache.cassandra.testing.utils.AssertionUtils.getBlocking;
+import static org.apache.cassandra.testing.utils.AssertionUtils.loopAssert;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+/**
+ * Test for role based access control in Sidecar
+ * Note:
+ * - Do not add new test cases in this class. Add them into test method, example refer to testForAdmin.
+ * - Create a new keyspace or test role for each test method as required to prevent permissions overlapping
+ */
+class RoleBasedAuthorizationIntegrationTest extends SharedClusterSidecarIntegrationTestBase
+{
+    protected static final int MIN_VERSION_WITH_MTLS = 5;
+    private static final String ADMIN_IDENTITY = "spiffe://cassandra/sidecar/admin";
+    // CASSANDRA_IDENTITY is only used to configure schemas for test setup, do not use this identity for anything else
+    private static final String CASSANDRA_IDENTITY = "spiffe://cassandra/sidecar/cassandra_role";
+    // An identity for the Sidecar role
+    private static final String SIDECAR_ROLE_IDENTITY = "spiffe://cassandra/sidecar/sidecar_role";
+    private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE IF NOT EXISTS %s (a int, b text, PRIMARY KEY (a))";
+    private static final Logger LOGGER = LoggerFactory.getLogger(RoleBasedAuthorizationIntegrationTest.class);
+
+    public static final RoleWithIdentityTestScenario SUPERUSER =
+    new RoleWithIdentityTestScenario("test_keyspace", "super_user_test_role", "spiffe://cassandra/sidecar/super_user_test_user")
+    .superuser();
+    public static final RoleWithIdentityTestScenario NON_SUPERUSER_ROLE_WITH_TRANSITIVE_SUPERUSER_ROLE =
+    new RoleWithIdentityTestScenario("test_keyspace", "non_super_user_with_transitive_super_user_role",
+                                     "spiffe://cassandra/sidecar/non_super_user_with_transitive_super_user");
+    public static final RoleWithIdentityTestScenario NON_ADMIN_READ_TEST_KEYSPACE_ROLE =
+    new RoleWithIdentityTestScenario("non_admin_test_keyspace", "non_admin_test_role", "spiffe://cassandra/sidecar/non_admin_test_user")
+    .addPermission("data/non_admin_test_keyspace", "SCHEMA:READ");
+    public static final RoleWithIdentityTestScenario NON_ADMIN_CREATE_SNAPSHOT_TEST_KEYSPACE_ROLE =
+    new RoleWithIdentityTestScenario("grant_table_test_keyspace", "non_admin_test_role", "spiffe://cassandra/sidecar/non_admin_test_user")
+    .addPermission("data/grant_table_test_keyspace/test_table", "SNAPSHOT:CREATE");
+    public static final RoleWithIdentityTestScenario NON_ADMIN_CREATE_SNAPSHOT_KEYSPACE_LEVEL_ROLE =
+    new RoleWithIdentityTestScenario("grant_keyspace_test_keyspace", "non_admin_test_role", "spiffe://cassandra/sidecar/non_admin_test_user")
+    .addPermission("data/grant_keyspace_test_keyspace", "SNAPSHOT:CREATE");
+    public static final RoleWithIdentityTestScenario NON_ADMIN_CREATE_SNAPSHOT_ALL_TABLES_ROLE =
+    new RoleWithIdentityTestScenario("grant_tables_except_keyspace_test_keyspace", "non_admin_test_role", "spiffe://cassandra/sidecar/non_admin_test_user")
+    .addPermission("data/grant_tables_except_keyspace_test_keyspace/*", "SNAPSHOT:CREATE");
+    public static final RoleWithIdentityTestScenario NON_ADMIN_MULTIPLE_PERMISSIONS_ROLE =
+    new RoleWithIdentityTestScenario("multiple_permissions_required_test_keyspace", "non_admin_test_role", "spiffe://cassandra/sidecar/non_admin_test_user")
+    .addPermission("data/multiple_permissions_required_test_keyspace/test_table", "SNAPSHOT:CREATE");
+    public static final RoleWithIdentityTestScenario NON_ADMIN_GRANT_DATA_LEVEL_ROLE =
+    new RoleWithIdentityTestScenario("test_keyspace", "grant_data_test_role", "spiffe://cassandra/sidecar/grant_data_test_user")
+    .addPermission("data", "SNAPSHOT:CREATE");
+    public static final RoleWithIdentityTestScenario BULK_READ_PERMISSION_ROLE =
+    new RoleWithIdentityTestScenario("grant_bulk_read_test_keyspace", "bulk_read_test_role", "spiffe://cassandra/sidecar/bulk_read_test_user")
+    .addPermission("data/grant_bulk_read_across_tables_test_keyspace", "ANALYTICS:READ_DIRECT")
+    .addPermission("data/grant_bulk_read_test_keyspace/test_table", "ANALYTICS:READ_DIRECT");
+    public static final RoleWithIdentityTestScenario BULK_READ_PERMISSIONS_TEST_TABLE_ROLE =
+    new RoleWithIdentityTestScenario("grant_bulk_read_across_tables_test_keyspace", "bulk_read_across_data_test_role",
+                                     "spiffe://cassandra/sidecar/bulk_read_across_data_test_user")
+    .addPermission("data", "ANALYTICS:READ_DIRECT");
+    public static final RoleWithIdentityTestScenario BULK_READ_PERMISSIONS_TEST_TABLE_2_ROLE =
+    new RoleWithIdentityTestScenario("grant_bulk_read_across_tables_test_keyspace", "bulk_read_across_data_test_role",
+                                     "spiffe://cassandra/sidecar/bulk_read_across_data_test_user")
+    .table("test_table2");
+    public static final RoleWithIdentityTestScenario BULK_WRITE_PERMISSION_ROLE =
+    new RoleWithIdentityTestScenario("grant_bulk_write_test_keyspace", "bulk_write_test_role", "spiffe://cassandra/sidecar/bulk_write_test_user")
+    .addPermission("data/grant_bulk_write_test_keyspace/test_table", "ANALYTICS:WRITE_DIRECT");
+    public static final RoleWithIdentityTestScenario CDC_PERMISSION_ROLE =
+    new RoleWithIdentityTestScenario(null, "cdc_test_role", "spiffe://cassandra/sidecar/cdc_test_user")
+    .addPermission("cluster", "CDC");
+    public static final RoleWithIdentityTestScenario ALL_ANALYTICS_PERMISSION_ROLE =
+    new RoleWithIdentityTestScenario("all_analytics_permission_test_keyspace", "all_analytics_permission_test_role",
+                                     "spiffe://cassandra/sidecar/all_analytics_permission_test_user")
+    .addPermission("data/all_analytics_permission_test_keyspace/test_table", "ANALYTICS:*");
+    public static final RoleWithIdentityTestScenario BULK_READ_WRITE_PERMISSION_ROLE =
+    new RoleWithIdentityTestScenario("grant_bulk_read_write_test_keyspace", "bulk_read_write_test_role", "spiffe://cassandra/sidecar/bulk_read_write_test_user")
+    .addPermission("data/grant_bulk_read_write_test_keyspace/test_table", "ANALYTICS:READ_DIRECT,WRITE_DIRECT");
+    public static final RoleWithIdentityTestScenario WILDCARD_ACROSS_TARGETS_ROLE =
+    new RoleWithIdentityTestScenario(null, "wildcard_across_targets_test_role", "spiffe://cassandra/sidecar/wildcard_across_targets_test_user");
+    public static final RoleWithIdentityTestScenario WILDCARD_SUBPARTS_ROLE =
+    new RoleWithIdentityTestScenario(null, "wildcard_with_subparts_test_role", "spiffe://cassandra/sidecar/wildcard_with_subparts_test_user")
+    .addPermission("cluster", "GOSSIP,SCHEMA:READ");
+
+    // Describe all test scenarios
+    static final List<RoleWithIdentityTestScenario> ROLE_WITH_IDENTITY_TEST_SCENARIOS = List.of(SUPERUSER,
+                                                                                                NON_SUPERUSER_ROLE_WITH_TRANSITIVE_SUPERUSER_ROLE,
+                                                                                                NON_ADMIN_READ_TEST_KEYSPACE_ROLE,
+                                                                                                NON_ADMIN_CREATE_SNAPSHOT_TEST_KEYSPACE_ROLE,
+                                                                                                NON_ADMIN_CREATE_SNAPSHOT_KEYSPACE_LEVEL_ROLE,
+                                                                                                NON_ADMIN_CREATE_SNAPSHOT_ALL_TABLES_ROLE,
+                                                                                                NON_ADMIN_MULTIPLE_PERMISSIONS_ROLE,
+                                                                                                NON_ADMIN_GRANT_DATA_LEVEL_ROLE,
+                                                                                                BULK_READ_PERMISSION_ROLE,
+                                                                                                BULK_READ_PERMISSIONS_TEST_TABLE_ROLE,
+                                                                                                BULK_READ_PERMISSIONS_TEST_TABLE_2_ROLE,
+                                                                                                BULK_WRITE_PERMISSION_ROLE,
+                                                                                                BULK_READ_WRITE_PERMISSION_ROLE,
+                                                                                                ALL_ANALYTICS_PERMISSION_ROLE,
+                                                                                                CDC_PERMISSION_ROLE,
+                                                                                                WILDCARD_ACROSS_TARGETS_ROLE,
+                                                                                                WILDCARD_SUBPARTS_ROLE
+    );
+
+    private Path nonAdminClientKeystorePath;
+
+    @Override
+    protected void beforeClusterProvisioning()
+    {
+        // mTLS authentication was added in Cassandra starting 5.0 version
+        assumeThat(SimpleCassandraVersion.create(testVersion.version()).major)
+        .as("mTLS authentication is not supported in 4.0 and 4.1 Cassandra versions")
+        .isGreaterThanOrEqualTo(MIN_VERSION_WITH_MTLS);
+    }
+
+    @Override
+    protected ClusterBuilderConfiguration testClusterConfiguration()
+    {
+        return super.testClusterConfiguration()
+                    .additionalInstanceConfig(Map.of("authenticator", "org.apache.cassandra.auth.PasswordAuthenticator"));
+    }
+
+    @Override
+    protected void afterClusterProvisioned()
+    {
+        IInstance instance = cluster.getFirstRunningInstance();
+
+        // Configure an admin identity so we can perform CQL queries
+        // This is a hack for Cassandra 5.0, which doesn't truly support
+        // MutualTlsWithPasswordFallbackAuthenticator
+        configureAdminAndSidecarIdentity(instance);
+
+        // Stop the instance and update the configuration to start using MutualTlsAuthenticator
+        cluster.stopUnchecked(instance);
+
+        // Update the cluster configuration
+        IInstanceConfig instanceConfig = instance.config();
+        instanceConfig.set("authenticator.class_name", "org.apache.cassandra.auth.MutualTlsAuthenticator");
+        instanceConfig.set("authenticator.parameters", Map.of("validator_class_name",
+                                                              "org.apache.cassandra.auth.SpiffeCertificateValidator"));
+
+        instanceConfig.set("role_manager", "CassandraRoleManager");
+        instanceConfig.set("authorizer", "CassandraAuthorizer");
+        instanceConfig.set("client_encryption_options.enabled", "true");
+        instanceConfig.set("client_encryption_options.optional", "true");
+        instanceConfig.set("client_encryption_options.require_client_auth", "true");
+        instanceConfig.set("client_encryption_options.require_endpoint_verification", "false");
+        instanceConfig.set("client_encryption_options.keystore", mtlsTestHelper.serverKeyStorePath());
+        instanceConfig.set("client_encryption_options.keystore_password", mtlsTestHelper.serverKeyStorePassword());
+        instanceConfig.set("client_encryption_options.truststore", mtlsTestHelper.trustStorePath());
+        instanceConfig.set("client_encryption_options.truststore_password", mtlsTestHelper.trustStorePassword());
+        instanceConfig.set("credentials_update_interval", "50ms");
+
+        // Start the instance again
+        instance.startup();
+    }
+
+    /**
+     * @return the configuration with overrides for access control configuration
+     */
+    @Override
+    protected Function<SidecarConfigurationImpl.Builder, SidecarConfigurationImpl.Builder> configurationOverrides()
+    {
+        return builder -> {
+            Map<String, String> params = Map.of("certificate_validator", "io.vertx.ext.auth.mtls.impl.CertificateValidatorImpl",
+                                                "certificate_identity_extractor", "org.apache.cassandra.sidecar.acl.authentication.CassandraIdentityExtractor");
+            ParameterizedClassConfiguration mTLSConfig
+            = new ParameterizedClassConfigurationImpl("org.apache.cassandra.sidecar.acl.authentication.MutualTlsAuthenticationHandlerFactory",
+                                                      params);
+            ParameterizedClassConfiguration rbacConfig
+            = new ParameterizedClassConfigurationImpl("org.apache.cassandra.sidecar.acl.authorization.RoleBasedAuthorizationProvider",
+                                                      Map.of());
+
+            CacheConfiguration permissionCacheConfiguration = CacheConfigurationImpl.builder()
+                                                                                    .expireAfterAccess(MillisecondBoundConfiguration.parse("100ms"))
+                                                                                    .build();
+
+            AccessControlConfiguration accessControlConfiguration = AccessControlConfigurationImpl.builder()
+                                                                                                  .enabled(true)
+                                                                                                  .authenticatorsConfiguration(List.of(mTLSConfig))
+                                                                                                  .authorizerConfiguration(rbacConfig)
+                                                                                                  .adminIdentities(Set.of(ADMIN_IDENTITY))
+                                                                                                  .permissionCacheConfiguration(permissionCacheConfiguration)
+                                                                                                  .build();
+
+            KeyStoreConfiguration truststoreConfiguration = new KeyStoreConfigurationImpl(mtlsTestHelper.trustStorePath(),
+                                                                                          mtlsTestHelper.trustStorePassword(),
+                                                                                          mtlsTestHelper.trustStoreType(),
+                                                                                          SecondBoundConfiguration.parse("1d"));
+
+            KeyStoreConfiguration keyStoreConfiguration = new KeyStoreConfigurationImpl(mtlsTestHelper.serverKeyStorePath(),
+                                                                                        mtlsTestHelper.serverKeyStorePassword(),
+                                                                                        mtlsTestHelper.serverKeyStoreType(),
+                                                                                        SecondBoundConfiguration.parse("1d"));
+
+            SslConfigurationImpl sslConfiguration = SslConfigurationImpl.builder()
+                                                                        .enabled(true)
+                                                                        .clientAuth("REQUEST")
+                                                                        .keystore(keyStoreConfiguration)
+                                                                        .truststore(truststoreConfiguration)
+                                                                        .build();
+
+            PeriodicTaskConfiguration healthCheckConfiguration = PeriodicTaskConfigurationImpl.Builder
+                                                                 .builder()
+                                                                 .enabled(true)
+                                                                 .initialDelay(MillisecondBoundConfiguration.ONE)
+                                                                 .executeInterval(MillisecondBoundConfiguration.ONE)
+                                                                 .build();
+
+            return builder.accessControlConfiguration(accessControlConfiguration)
+                          .healthCheckConfiguration(healthCheckConfiguration)
+                          .sslConfiguration(sslConfiguration);
+        };
+    }
+
+    @Override
+    protected void startSidecar(ICluster<? extends IInstance> cluster) throws InterruptedException
+    {
+        serverWrapper = startSidecarWithInstances(cluster, new TestModule(mtlsTestHelper, cluster));
+    }
+
+    @Override
+    protected void beforeTestStart()
+    {
+        long startNanos = System.nanoTime();
+        // wait for the schema initialization
+        waitForSchemaReady(10, TimeUnit.SECONDS);
+        LOGGER.info("Waited {} nanos for schema initialization", System.nanoTime() - startNanos);
+    }
+
+    @Test
+    void testForAdmin() throws Exception
+    {
+        String keyspaceSchemaRoute = String.format("/api/v1/keyspaces/%s/schema", "test_keyspace");
+        // Uses client keystore with admin identity. Configured admin identities bypass authorization checks
+        Path clientKeystorePath = mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                                                     certificateBuilder.addSanUriName(ADMIN_IDENTITY));
+
+        verifyAccess(HttpMethod.GET, keyspaceSchemaRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+    }
+
+    @Test
+    void testForSuperUser() throws Exception
+    {
+        String keyspaceSchemaRoute = String.format("/api/v1/keyspaces/%s/schema", "test_keyspace");
+        // uses client keystore with superuser identity
+        Path clientKeystorePath = mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                                                     certificateBuilder.addSanUriName("spiffe://cassandra/sidecar/super_user_test_user"));
+
+        verifyAccess(HttpMethod.GET, keyspaceSchemaRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+    }
+
+    @Test
+    void testForTransitiveSuperUser() throws Exception
+    {
+        String keyspaceSchemaRoute = String.format("/api/v1/keyspaces/%s/schema", "test_keyspace");
+        // uses client keystore with superuser identity
+        Path clientKeystorePath =
+        mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                           certificateBuilder.addSanUriName("spiffe://cassandra/sidecar/non_super_user_with_transitive_super_user"));
+
+        verifyAccess(HttpMethod.GET, keyspaceSchemaRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+    }
+
+    @Test
+    void testForNonAdmin()
+    {
+        String keyspaceSchemaRoute = String.format("/api/v1/keyspaces/%s/schema", "non_admin_test_keyspace");
+
+        verifyAccess(HttpMethod.GET, keyspaceSchemaRoute, nonAdminClientKeystorePath, assertStatus(HttpResponseStatus.OK));
+    }
+
+    @Test
+    void testGrantingForTable()
+    {
+        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
+                                                   "grant_table_test_keyspace", "test_table");
+
+        // SNAPSHOT:CREATE permission granted for data/grant_table_test_keyspace/test_table
+        verifyAccess(HttpMethod.PUT, createSnapshotRoute, nonAdminClientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        // SNAPSHOT:DELETE permission not granted for data/grant_table_test_keyspace/test_table
+        verifyAccess(HttpMethod.DELETE, createSnapshotRoute, nonAdminClientKeystorePath, assertStatus(HttpResponseStatus.FORBIDDEN));
+    }
+
+    @Test
+    void testGrantingForKeyspace()
+    {
+        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
+                                                   "grant_keyspace_test_keyspace", "test_table");
+
+        // SNAPSHOT:CREATE permission granted for data/grant_keyspace_test_keyspace/test_table with
+        // data/grant_tables_test_keyspace grant
+        verifyAccess(HttpMethod.PUT, createSnapshotRoute, nonAdminClientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        // access not granted for different keyspace
+        String notAllowedSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
+                                                       "not_allowed_keyspace", "test_table");
+        verifyAccess(HttpMethod.PUT, notAllowedSnapshotRoute, nonAdminClientKeystorePath, assertStatus(HttpResponseStatus.FORBIDDEN));
+
+        // SNAPSHOT:DELETE permission not granted for data/grant_keyspace_test_keyspace/test_table
+        verifyAccess(HttpMethod.DELETE, createSnapshotRoute, nonAdminClientKeystorePath, assertStatus(HttpResponseStatus.FORBIDDEN));
+    }
+
+    @Test
+    void testGrantingAllTablesExceptKeyspace()
+    {
+        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
+                                                   "grant_tables_except_keyspace_test_keyspace", "test_table");
+
+        // SNAPSHOT:CREATE permission granted for data/grant_tables_except_keyspace_test_keyspace/test_table
+        // with data/grant_tables_except_keyspace_test_keyspace grant
+        verifyAccess(HttpMethod.PUT, createSnapshotRoute, nonAdminClientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        // SCHEMA:READ is not granted since it expects permissions at keyspace level
+        String keyspaceSchemaRoute = String.format("/api/v1/keyspaces/%s/schema", "grant_tables_except_keyspace_test_keyspace");
+        verifyAccess(HttpMethod.GET, keyspaceSchemaRoute, nonAdminClientKeystorePath, assertStatus(HttpResponseStatus.FORBIDDEN));
+    }
+
+    @Test
+    void testGrantingAtDataLevel() throws Exception
+    {
+        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
+                                                   "test_keyspace", "test_table");
+        Path clientKeystorePath = mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                                                     certificateBuilder.addSanUriName("spiffe://cassandra/sidecar/grant_data_test_user"));
+
+        // SNAPSHOT:CREATE permission granted for data/test_keyspace/test_table with data resource grant
+        verifyAccess(HttpMethod.PUT, createSnapshotRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        // SNAPSHOT:DELETE permission not granted for data/test_keyspace/test_table not granted
+        verifyAccess(HttpMethod.DELETE, createSnapshotRoute, clientKeystorePath, assertStatus(HttpResponseStatus.FORBIDDEN));
+    }
+
+    @Test
+    void testGrantingWithWildcardSubparts() throws Exception
+    {
+        String schemaRoute = "/api/v1/cassandra/schema";
+        String gossipRoute = "/api/v1/cassandra/gossip";
+        String ringRoute = "/api/v1/cassandra/ring";
+        Path clientKeystorePath =
+        mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                           certificateBuilder.addSanUriName("spiffe://cassandra/sidecar/wildcard_with_subparts_test_user"));
+
+        // SCHEMA:READ permission granted for cluster with GOSSIP,SCHEMA:READ
+        verifyAccess(HttpMethod.GET, schemaRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        // GOSSIP:READ permission granted for cluster with GOSSIP,SCHEMA:READ
+        verifyAccess(HttpMethod.GET, gossipRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        // RING:READ permission not granted with GOSSIP,SCHEMA:READ
+        verifyAccess(HttpMethod.GET, ringRoute, clientKeystorePath, assertStatus(HttpResponseStatus.FORBIDDEN));
+    }
+
+    @Test
+    void testEndpointRequiringMultipleActions()
+    {
+        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
+                                                   "multiple_permissions_required_test_keyspace", "test_table");
+
+        String listSnapshotRoute
+        = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/%s",
+                        "multiple_permissions_required_test_keyspace", "test_table", "my-snapshot");
+
+        // SNAPSHOT:CREATE permission granted for data/multiple_permissions_required_test_keyspace/test_table
+        verifyAccess(HttpMethod.PUT, createSnapshotRoute, nonAdminClientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        String[] componentDownloadUrl = new String[1];
+        Path clientKeystorePath = cassandraIdentityClientKeyStore();
+        SSLOptions sslOptions = getSSLOptions(clientKeystorePath.toString(),
+                                              mtlsTestHelper.clientKeyStorePassword(),
+                                              mtlsTestHelper.trustStorePath(),
+                                              mtlsTestHelper.trustStorePassword());
+        withAuthenticatedSession(cluster.get(1), "cassandra", "cassandra", session -> {
+            // grant sidecar permission for streaming
+            updateSidecarPermission(session,
+                                    "non_admin_test_role",
+                                    "data/multiple_permissions_required_test_keyspace/test_table",
+                                    "SNAPSHOT:READ");
+
+            // wait for cache refresh
+            loopAssert(2, 100, () -> {
+                verifyAccess(HttpMethod.GET, listSnapshotRoute, nonAdminClientKeystorePath, response -> {
+                    assertThat(response).isNotNull();
+                    assertThat(response.statusCode()).isEqualTo(HttpResponseStatus.OK.code());
+                    ListSnapshotFilesResponse snapshotFiles = response.bodyAsJson(ListSnapshotFilesResponse.class);
+                    List<ListSnapshotFilesResponse.FileInfo> filesToStream =
+                    snapshotFiles.snapshotFilesInfo()
+                                 .stream()
+                                 .filter(info -> info.fileName.endsWith("-Data.db"))
+                                 .sorted(Comparator.comparing(o -> o.fileName))
+                                 .collect(Collectors.toList());
+                    assertThat(filesToStream).isNotNull().isNotEmpty();
+                    componentDownloadUrl[0] = filesToStream.get(0).componentDownloadUrl();
+                });
+            });
+
+            // grant sidecar permission for streaming
+            updateSidecarPermission(session,
+                                    "non_admin_test_role",
+                                    "data/multiple_permissions_required_test_keyspace/test_table",
+                                    "SNAPSHOT:STREAM");
+
+            // STREAM SSTable request requires both Sidecar SNAPSHOT:STREAM permission and Cassandra's SELECT
+            // permission on a table it accesses data.
+            loopAssert(2, 100, () -> {
+                // request denied without SELECT permission
+                verifyAccess(HttpMethod.GET, componentDownloadUrl[0], nonAdminClientKeystorePath, assertStatus(HttpResponseStatus.FORBIDDEN));
+            });
+
+            // grant SELECT permission to non_admin_test_role
+            grantTablePermission(session, "multiple_permissions_required_test_keyspace", "test_table", "non_admin_test_role");
+        }, sslOptions);
+
+        loopAssert(2, 100, () -> {
+            // request denied without SELECT permission
+            verifyAccess(HttpMethod.GET, componentDownloadUrl[0], nonAdminClientKeystorePath, assertStatus(HttpResponseStatus.OK));
+        });
+    }
+
+    @Test
+    void testGrantingBulkReadFeaturePermission() throws Exception
+    {
+        Path clientKeystorePath = mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                                                     certificateBuilder.addSanUriName("spiffe://cassandra/sidecar/bulk_read_test_user"));
+
+        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
+                                                   "grant_bulk_read_test_keyspace", "test_table");
+
+        // SNAPSHOT:CREATE permission granted for data/grant_bulk_read_test_keyspace/test_table with ANALYTICS:READ_DIRECT
+        verifyAccess(HttpMethod.PUT, createSnapshotRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        String keyspaceSchemaRoute = String.format("/api/v1/keyspaces/%s/schema", "grant_bulk_read_test_keyspace");
+
+        // SCHEMA:READ permission granted for data/grant_bulk_read_test_keyspace/test_table with ANALYTICS:READ_DIRECT
+        verifyAccess(HttpMethod.GET, keyspaceSchemaRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        String topologyRoute = String.format("/api/v1/keyspaces/%s/token-range-replicas", "grant_bulk_read_test_keyspace");
+        // TOPOLOGY:READ permission not granted with ANALYTICS:READ_DIRECT
+        verifyAccess(HttpMethod.GET, topologyRoute, clientKeystorePath, assertStatus(HttpResponseStatus.FORBIDDEN));
+
+        String tableStatsRoute = String.format("/api/v1/cassandra/keyspaces/%s/tables/%s/stats",
+                                               "grant_bulk_read_test_keyspace", "test_table");
+        // STATS permission granted with ANALYTICS:READ_DIRECT
+        verifyAccess(HttpMethod.GET, tableStatsRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        String clusterStatsRoute = "/api/v1/cassandra/stats/streams";
+        // STATS permission at cluster scope not granted with ANALYTICS:READ_DIRECT
+        verifyAccess(HttpMethod.GET, clusterStatsRoute, clientKeystorePath, assertStatus(HttpResponseStatus.FORBIDDEN));
+    }
+
+    @Test
+    void testGrantingBulkReadFeaturePermissionAcrossTables() throws Exception
+    {
+        Path clientKeystorePath = mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                                                     certificateBuilder.addSanUriName("spiffe://cassandra/sidecar/bulk_read_test_user"));
+
+        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
+                                                   "grant_bulk_read_across_tables_test_keyspace", "test_table");
+
+        // SNAPSHOT:CREATE permission granted for data/grant_bulk_read_across_tables_test_keyspace/test_table with ANALYTICS:READ_DIRECT
+        verifyAccess(HttpMethod.PUT, createSnapshotRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        String createSnapshotRouteTable2 = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
+                                                         "grant_bulk_read_across_tables_test_keyspace", "test_table2");
+
+        // SNAPSHOT:CREATE for different table also granted with keyspace scoped ANALYTICS:READ_DIRECT permission
+        verifyAccess(HttpMethod.PUT, createSnapshotRouteTable2, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        String createSnapshotRouteKeyspace2 = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
+                                                            "test_keyspace", "test_table2");
+
+        // SNAPSHOT:CREATE for different keyspace not granted with keyspace scoped ANALYTICS:READ_DIRECT permission
+        verifyAccess(HttpMethod.PUT, createSnapshotRouteKeyspace2, clientKeystorePath, assertStatus(HttpResponseStatus.FORBIDDEN));
+
+        String tableStatsRoute = String.format("/api/v1/cassandra/keyspaces/%s/tables/%s/stats",
+                                               "grant_bulk_read_across_tables_test_keyspace", "test_table");
+        // STATS permission granted with ANALYTICS:READ_DIRECT
+        verifyAccess(HttpMethod.GET, tableStatsRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+    }
+
+    @Test
+    void testGrantingBulkReadFeaturePermissionAcrossData() throws Exception
+    {
+        Path clientKeystorePath =
+        mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                           certificateBuilder.addSanUriName("spiffe://cassandra/sidecar/bulk_read_across_data_test_user"));
+
+        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot-2",
+                                                   "test_keyspace", "test_table");
+
+        // SNAPSHOT:CREATE permission granted for data/test_keyspace/test_table with ANALYTICS:READ_DIRECT permission
+        verifyAccess(HttpMethod.PUT, createSnapshotRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        String createSnapshotRouteKeyspace2 = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot-3",
+                                                            "non_admin_test_keyspace", "test_table");
+
+        // SNAPSHOT:CREATE for different keyspace also granted with data scoped ANALYTICS:READ_DIRECT permission
+        verifyAccess(HttpMethod.PUT, createSnapshotRouteKeyspace2, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+    }
+
+    @Test
+    void testGrantingBulkWriteFeaturePermission() throws Exception
+    {
+        Path clientKeystorePath = mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                                                     certificateBuilder.addSanUriName("spiffe://cassandra/sidecar/bulk_write_test_user"));
+
+        String topologyRoute = String.format("/api/v1/keyspaces/%s/token-range-replicas", "grant_bulk_write_test_keyspace");
+        // TOPOLOGY:READ permission granted with ANALYTICS:WRITE_DIRECT
+        verifyAccess(HttpMethod.GET, topologyRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        String keyspaceRingRoute = String.format("/api/v1/cassandra/ring/keyspaces/%s", "grant_bulk_write_test_keyspace");
+
+        // RING:READ permission not granted for data/grant_bulk_write_test_keyspace with ANALYTICS:WRITE_DIRECT
+        verifyAccess(HttpMethod.GET, keyspaceRingRoute, clientKeystorePath, assertStatus(HttpResponseStatus.FORBIDDEN));
+    }
+
+    @Test
+    void testGrantingBothBulkReadAndWriteFeaturePermission() throws Exception
+    {
+        Path clientKeystorePath = mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                                                     certificateBuilder.addSanUriName("spiffe://cassandra/sidecar/bulk_read_write_test_user"));
+
+        String topologyRoute = String.format("/api/v1/keyspaces/%s/token-range-replicas", "grant_bulk_read_write_test_keyspace");
+        // TOPOLOGY:READ permission granted with ANALYTICS:READ_DIRECT,WRITE_DIRECT
+        verifyAccess(HttpMethod.GET, topologyRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        String keyspaceRingRoute = String.format("/api/v1/cassandra/ring/keyspaces/%s", "grant_bulk_read_write_test_keyspace");
+
+        // RING:READ permission granted for data/grant_bulk_read_write_test_keyspace with ANALYTICS:READ_DIRECT,WRITE_DIRECT
+        verifyAccess(HttpMethod.GET, keyspaceRingRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
+                                                   "grant_bulk_read_write_test_keyspace", "test_table");
+
+        // SNAPSHOT:CREATE permission granted for data/grant_bulk_read_write_test_keyspace/test_table with ANALYTICS:READ_DIRECT,WRITE_DIRECT
+        verifyAccess(HttpMethod.PUT, createSnapshotRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+    }
+
+    @Test
+    void testGrantingAllAnalyticsRelatedPermissions() throws Exception
+    {
+        Path clientKeystorePath =
+        mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                           certificateBuilder.addSanUriName("spiffe://cassandra/sidecar/all_analytics_permission_test_user"));
+
+        String topologyRoute = String.format("/api/v1/keyspaces/%s/token-range-replicas", "all_analytics_permission_test_keyspace");
+        // TOPOLOGY:READ permission under ANALYTICS:WRITE_DIRECT granted with ANALYTICS:*
+        verifyAccess(HttpMethod.GET, topologyRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+
+        String keyspaceRingRoute = String.format("/api/v1/cassandra/ring/keyspaces/%s", "all_analytics_permission_test_keyspace");
+
+        // RING:READ permission under ANALYTICS:READ_DIRECT granted with ANALYTICS:*
+        verifyAccess(HttpMethod.GET, keyspaceRingRoute, clientKeystorePath, assertStatus(HttpResponseStatus.OK));
+    }
+
+    @Test
+    void testGrantingCdcFeaturePermission() throws Exception
+    {
+        String listCdcPath = "/api/v1/cdc/segments";
+        Path clientKeystorePath = mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                                                     certificateBuilder.addSanUriName("spiffe://cassandra/sidecar/cdc_test_user"));
+        // CDC permission granted with CDC
+        WebClient client = trustedClient(clientKeystorePath.toString(), mtlsTestHelper.clientKeyStorePassword(),
+                                         mtlsTestHelper.trustStorePath(), mtlsTestHelper.trustStorePassword());
+        HttpResponse<Buffer> listResp = createRequest(client, HttpMethod.GET, listCdcPath);
+
+        // CDC permission granted
+        // CDC is not turned on for cluster, hence 500 or 503 expected
+        assertThat(listResp.statusCode()).isIn(HttpResponseStatus.SERVICE_UNAVAILABLE.code(),
+                                               HttpResponseStatus.INTERNAL_SERVER_ERROR.code());
+    }
+
+    @Override
+    protected void initializeSchemaForTest()
+    {
+        Path clientKeystorePath = cassandraIdentityClientKeyStore();
+
+        createRequiredKeystores();
+        SSLOptions sslOptions = getSSLOptions(clientKeystorePath.toString(),
+                                              mtlsTestHelper.clientKeyStorePassword(),
+                                              mtlsTestHelper.trustStorePath(),
+                                              mtlsTestHelper.trustStorePassword());
+        withAuthenticatedSession(cluster.get(1), "cassandra", "cassandra", session -> {
+            // Required for authentication of sidecar requests to Cassandra. Only superusers can grant permissions
+            createTestKeyspace(session, "sidecar_internal", DC1_RF1);
+            createSidecarRolesPermissionsTable(session);
+            createRequiredKeyspaceTables(session);
+            createRequiredRoles(session);
+        }, sslOptions);
+    }
+
+    private void createSidecarRolesPermissionsTable(Session session)
+    {
+        String statement = String.format("CREATE TABLE IF NOT EXISTS sidecar_internal.%s ("
+                                         + "role text,"
+                                         + "resource text,"
+                                         + "permissions set<text>,"
+                                         + "PRIMARY KEY(role, resource))",
+                                         ROLE_PERMISSIONS_TABLE);
+        session.execute(statement);
+    }
+
+    private void createRequiredKeyspaceTables(Session session)
+    {
+        for (RoleWithIdentityTestScenario scenario : ROLE_WITH_IDENTITY_TEST_SCENARIOS)
+        {
+            if (scenario.keyspace == null || scenario.table == null)
+                continue;
+
+            QualifiedName table = new QualifiedName(scenario.keyspace, scenario.table);
+            createTestKeyspace(session, table, DC1_RF1);
+            createTestTable(session, table, CREATE_TABLE_STATEMENT);
+            session.execute("INSERT INTO " + table + " (a, b) VALUES (1, 'text');");
+        }
+    }
+
+    private void createRequiredRoles(Session session)
+    {
+        for (RoleWithIdentityTestScenario mapping : ROLE_WITH_IDENTITY_TEST_SCENARIOS)
+        {
+            session.execute("CREATE ROLE IF NOT EXISTS \"" + mapping.role + "\" " +
+                            "WITH SUPERUSER = " + mapping.superuser + " " +
+                            "AND LOGIN = true");
+            session.execute(String.format("ADD IDENTITY IF NOT EXISTS '%s' TO ROLE '%s'", mapping.identity, mapping.role));
+
+            for (RoleWithIdentityTestScenario.Permission permission : mapping.permissions)
+            {
+                session.execute(String.format("INSERT INTO sidecar_internal.role_permissions_v1 (role, resource, permissions) " +
+                                              "VALUES ('%s', '%s', {'%s'})", mapping.role, permission.resource, permission.permission));
+            }
+        }
+
+        // grant a superuser role transitively to the non_super_user_with_transitive_super_user_role role
+        session.execute("GRANT super_user_test_role TO non_super_user_with_transitive_super_user_role");
+    }
+
+    private void createRequiredKeystores()
+    {
+        try
+        {
+            nonAdminClientKeystorePath = mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                                                            certificateBuilder.addSanUriName("spiffe://cassandra/sidecar/non_admin_test_user"));
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException("Failed to create required keystores", e);
+        }
+    }
+
+    private void grantTablePermission(Session session, String keyspace, String table, String role)
+    {
+        session.execute("GRANT ALL PERMISSIONS ON " + keyspace + "." + table + " TO " + role);
+    }
+
+    private void updateSidecarPermission(Session session, String role, String resource, String permission)
+    {
+        session.execute(String.format("UPDATE sidecar_internal.role_permissions_v1 SET permissions = permissions + {'%s'} " +
+                                      "where role = '%s' and resource = '%s'", permission, role, resource));
+    }
+
+    private void verifyAccess(HttpMethod method, String testRoute, Path clientKeystorePath, Verifier<HttpResponse<Buffer>> assertions)
+    {
+        verifyAccess(method, testRoute, clientKeystorePath.toString(), assertions);
+    }
+
+    private void verifyAccess(HttpMethod method, String testRoute, String clientKeystorePath, Verifier<HttpResponse<Buffer>> assertions)
+    {
+        WebClient client = trustedClient(clientKeystorePath, mtlsTestHelper.clientKeyStorePassword(),
+                                         mtlsTestHelper.trustStorePath(), mtlsTestHelper.trustStorePassword());
+        try
+        {
+            HttpResponse<Buffer> response = getBlocking(client.request(method, serverWrapper.serverPort, "127.0.0.1", testRoute).send());
+            assertions.accept(response);
+        }
+        finally
+        {
+            client.close();
+        }
+    }
+
+    private HttpResponse<Buffer> createRequest(WebClient client, HttpMethod method, String route)
+    {
+        return getBlocking(client.request(method, serverWrapper.serverPort, "127.0.0.1", route).send());
+    }
+
+    private void configureAdminAndSidecarIdentity(IInstance instance)
+    {
+        for (int i = 0; i < 60; i++)
+        {
+            try
+            {
+                withAuthenticatedSession(instance, "cassandra", "cassandra", session -> {
+
+                    // TODO: it would be a good idea to scope down Sidecar role to permissions it needs
+                    session.execute("CREATE ROLE IF NOT EXISTS \"" + "sidecar_role" + "\" " +
+                                    "WITH SUPERUSER = " + true + " " +
+                                    "AND LOGIN = true");
+
+                    String statement1 = String.format("ADD IDENTITY IF NOT EXISTS '%s' TO ROLE '%s'", CASSANDRA_IDENTITY, "cassandra");
+                    session.execute(statement1);
+                    String statement = String.format("ADD IDENTITY IF NOT EXISTS '%s' TO ROLE '%s'", SIDECAR_ROLE_IDENTITY, "sidecar_role");
+                    session.execute(statement);
+                }, null);
+                return;
+            }
+            catch (AuthenticationException exception)
+            {
+                if (exception.getMessage() != null &&
+                    exception.getMessage().contains("Provided username cassandra and/or password are incorrect"))
+                {
+                    // Wait until the default username/password is created
+                    Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
+                }
+                else
+                {
+                    throw exception;
+                }
+            }
+        }
+    }
+
+    private Path cassandraIdentityClientKeyStore()
+    {
+        Path clientKeystorePath;
+        try
+        {
+            clientKeystorePath = mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                                                    certificateBuilder.addSanUriName(CASSANDRA_IDENTITY));
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException(e);
+        }
+        return clientKeystorePath;
+    }
+
+    /**
+     * Encapsulates information needed to configure a test as well as how an identity maps to a role
+     */
+    static class RoleWithIdentityTestScenario
+    {
+        private final String keyspace;
+        private String table;
+        private final String role;
+        private final String identity;
+        private boolean superuser;
+        private final List<Permission> permissions = new ArrayList<>();
+
+        RoleWithIdentityTestScenario(String keyspace, String role, String identity)
+        {
+            this.keyspace = keyspace;
+            this.table = "test_table";
+            this.role = role;
+            this.identity = identity;
+        }
+
+        RoleWithIdentityTestScenario superuser()
+        {
+            this.superuser = true;
+            return this;
+        }
+
+        RoleWithIdentityTestScenario table(String table)
+        {
+            this.table = table;
+            return this;
+        }
+
+        RoleWithIdentityTestScenario addPermission(String resource, String permission)
+        {
+            permissions.add(new Permission(resource, permission));
+            return this;
+        }
+
+        static class Permission
+        {
+            private final String resource;
+            private final String permission;
+
+            Permission(String resource, String permission)
+            {
+                this.resource = resource;
+                this.permission = permission;
+            }
+        }
+    }
+
+    static class TestModule extends AbstractModule
+    {
+        private final MtlsTestHelper mtlsTestHelper;
+        private final ICluster<? extends IInstance> cluster;
+
+        TestModule(MtlsTestHelper mtlsTestHelper, ICluster<? extends IInstance> cluster)
+        {
+            this.mtlsTestHelper = mtlsTestHelper;
+            this.cluster = cluster;
+        }
+
+        @Provides
+        @Singleton
+        public CQLSessionProvider cqlSessionProvider()
+        {
+            List<InetSocketAddress> contactPoints = buildContactPoints(cluster);
+
+            // Issue a certificate for the Sidecar Role to talk to the Cassandra database
+            Path clientKeystoreForSidecarToCassandraConnections;
+            try
+            {
+                clientKeystoreForSidecarToCassandraConnections = mtlsTestHelper.issueClientKeyStore(certificateBuilder ->
+                                                                                                    certificateBuilder.addSanUriName(SIDECAR_ROLE_IDENTITY));
+            }
+            catch (Exception e)
+            {
+                throw new RuntimeException(e);
+            }
+
+            SSLOptions sslOptions = getSSLOptions(clientKeystoreForSidecarToCassandraConnections.toString(),
+                                                  mtlsTestHelper.clientKeyStorePassword(),
+                                                  mtlsTestHelper.trustStorePath(),
+                                                  mtlsTestHelper.trustStorePassword());
+            return new TemporaryCqlSessionProvider(contactPoints,
+                                                   SharedExecutorNettyOptions.INSTANCE,
+                                                   sslOptions);
+        }
+
+        /**
+         * This is an example of replacing a bound object. In the resolver below, the SidecarSchemaInitializer
+         * from production code is removed and replaced by the SidecarSchemaInitializer provided in this module.
+         * See {@link #sidecarSchemaInitializer(SidecarConfiguration, CQLSessionProvider, SidecarMetrics, SidecarSchema, ClusterLease)}
+         */
+        @Provides
+        @Singleton
+        MultiBindingTypeResolver<PeriodicTask> periodicTaskTypeResolver(Map<Class<? extends ClassKey>, PeriodicTask> periodicTaskMap)
+        {
+            return () -> {
+                Map<Class<? extends ClassKey>, PeriodicTask> map = new HashMap<>(periodicTaskMap);
+                map.remove(PeriodicTaskMapKeys.SidecarSchemaInitializerTaskKey.class);
+                return map;
+            };
+        }
+
+        // @formatter:off
+        static class TestSidecarSchemaInitializerTaskKey implements ClassKey {}
+        // @formatter:on
+        @ProvidesIntoMap
+        @KeyClassMapKey(TestSidecarSchemaInitializerTaskKey.class)
+        PeriodicTask sidecarSchemaInitializer(SidecarConfiguration configuration,
+                                              CQLSessionProvider cqlSessionProvider,
+                                              SidecarMetrics sidecarMetrics,
+                                              SidecarSchema sidecarSchema,
+                                              ClusterLease clusterLease)
+        {
+            return new SidecarSchemaInitializer(configuration,
+                                                cqlSessionProvider,
+                                                sidecarSchema.sidecarInternalKeyspace(),
+                                                sidecarMetrics.server().schema(),
+                                                clusterLease)
+            {
+                @Override
+                public DurationSpec delay()
+                {
+                    return MillisecondBoundConfiguration.ONE;
+                }
+            };
+        }
+    }
+}
diff --git a/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/health/SidecarPeerDownDetectorIntegrationTest.java b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/health/SidecarPeerDownDetectorIntegrationTest.java
index 2c1d02bd..6adb51b0 100644
--- a/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/health/SidecarPeerDownDetectorIntegrationTest.java
+++ b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/health/SidecarPeerDownDetectorIntegrationTest.java
@@ -64,7 +64,7 @@ import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
 import org.apache.cassandra.testing.ClusterBuilderConfiguration;
 
 import static org.apache.cassandra.sidecar.testing.MtlsTestHelper.CASSANDRA_INTEGRATION_TEST_ENABLE_MTLS;
-import static org.apache.cassandra.sidecar.testing.MtlsTestHelper.EMPTY_PASSWORD_STRING;
+import static org.apache.cassandra.sidecar.testing.MtlsTestHelper.PASSWORD_STRING;
 import static org.apache.cassandra.sidecar.testing.SharedClusterIntegrationTestBase.IntegrationTestModule.cassandraInstanceHostname;
 import static org.apache.cassandra.sidecar.testing.SharedClusterIntegrationTestBase.IntegrationTestModule.defaultConfigurationBuilder;
 import static org.apache.cassandra.testing.TestUtils.DC1_RF3;
@@ -123,7 +123,7 @@ class SidecarPeerDownDetectorIntegrationTest extends SharedClusterSidecarIntegra
         sidecarServerMap.values().forEach(serverWrapper -> {
             try
             {
-                closeServer(serverWrapper.server);
+                closeServer(serverWrapper);
             }
             catch (Exception e)
             {
@@ -245,7 +245,7 @@ class SidecarPeerDownDetectorIntegrationTest extends SharedClusterSidecarIntegra
 
                     KeyStoreConfiguration keyStoreConfiguration =
                     new KeyStoreConfigurationImpl(mtlsTestHelper.clientKeyStorePath(),
-                                                  EMPTY_PASSWORD_STRING,
+                                                  PASSWORD_STRING,
                                                   mtlsTestHelper.serverKeyStoreType(), // server and client keystore types are the same
                                                   SecondBoundConfiguration.parse("60s"));
 
diff --git a/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/lifecycle/LifecycleProviderIntegrationTester.java b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/lifecycle/LifecycleProviderIntegrationTester.java
index c65db08e..7431cbf2 100644
--- a/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/lifecycle/LifecycleProviderIntegrationTester.java
+++ b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/lifecycle/LifecycleProviderIntegrationTester.java
@@ -89,7 +89,8 @@ public class LifecycleProviderIntegrationTester
 
         // Simulate node crashing by directly calling the lifecycle provider's stop method
         cassandraNodeCrasher.run();
-        waitForLastUpdateToConverge(String.format("Instance %s has unexpectedly diverged from the desired state RUNNING to STOPPED.", sidecarHost), TIMEOUT_SECONDS);
+        waitForLastUpdateToConverge(String.format("Instance %s has unexpectedly diverged from the desired state RUNNING to STOPPED.", sidecarHost),
+                                    TIMEOUT_SECONDS);
 
         // CQL status should be down since instance is crashed
         waitForCqlStatus("NOT_OK", TIMEOUT_SECONDS);
diff --git a/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/testing/SharedClusterSidecarIntegrationTestBase.java b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/testing/SharedClusterSidecarIntegrationTestBase.java
index b250ef11..30791650 100644
--- a/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/testing/SharedClusterSidecarIntegrationTestBase.java
+++ b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/testing/SharedClusterSidecarIntegrationTestBase.java
@@ -18,12 +18,18 @@
 
 package org.apache.cassandra.sidecar.testing;
 
+import java.util.Objects;
+
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.buffer.Buffer;
 import io.vertx.core.net.JksOptions;
+import io.vertx.ext.web.client.HttpResponse;
 import io.vertx.ext.web.client.WebClient;
 import io.vertx.ext.web.client.WebClientOptions;
 
 import static io.vertx.core.Vertx.vertx;
-import static org.apache.cassandra.sidecar.testing.MtlsTestHelper.EMPTY_PASSWORD_STRING;
+import static org.apache.cassandra.sidecar.testing.MtlsTestHelper.PASSWORD_STRING;
+import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * Builds on top of {@link SharedClusterIntegrationTestBase} and adds functionality to interact
@@ -34,6 +40,35 @@ public abstract class SharedClusterSidecarIntegrationTestBase extends SharedClus
     private WebClient trustedClient;
     private WebClient noAuthClient;
 
+    /**
+     * A utility functional interface to perform verifications for a test
+     *
+     * @param <T> the type accepted by the verifier
+     */
+    @FunctionalInterface
+    public interface Verifier<T>
+    {
+        void accept(T v) throws AssertionError;
+
+        default Verifier<T> andThen(Verifier<T> after)
+        {
+            Objects.requireNonNull(after);
+
+            return v -> {
+                accept(v);
+                after.accept(v);
+            };
+        }
+    }
+
+    protected static Verifier<HttpResponse<Buffer>> assertStatus(HttpResponseStatus expectedStatus)
+    {
+        return response -> {
+            assertThat(response).isNotNull();
+            assertThat(response.statusCode()).isEqualTo(expectedStatus.code());
+        };
+    }
+
     @Override
     protected void beforeClusterShutdown()
     {
@@ -55,21 +90,32 @@ public abstract class SharedClusterSidecarIntegrationTestBase extends SharedClus
      */
     public WebClient trustedClient()
     {
-        if (trustedClient != null)
+        if (trustedClient == null)
         {
-            return trustedClient;
+            trustedClient = trustedClient(mtlsTestHelper.clientKeyStorePath(), PASSWORD_STRING,
+                                          mtlsTestHelper.trustStorePath(), PASSWORD_STRING);
         }
+        return trustedClient;
+    }
 
+    /**
+     * @param clientKeyStorePath     the path to the client keyStore
+     * @param clientKeyStorePassword the password for the client keyStore
+     * @param trustStorePath         the path to the trustStore
+     * @param trustStorePassword     the password for the trustStore
+     * @return a client that configures the truststore and the client keystore
+     */
+    public WebClient trustedClient(String clientKeyStorePath, String clientKeyStorePassword, String trustStorePath, String trustStorePassword)
+    {
         WebClientOptions clientOptions = new WebClientOptions()
                                          .setKeyStoreOptions(new JksOptions()
-                                                             .setPath(mtlsTestHelper.clientKeyStorePath())
-                                                             .setPassword(EMPTY_PASSWORD_STRING))
+                                                             .setPath(clientKeyStorePath)
+                                                             .setPassword(clientKeyStorePassword))
                                          .setTrustStoreOptions(new JksOptions()
-                                                               .setPath(mtlsTestHelper.trustStorePath())
-                                                               .setPassword(EMPTY_PASSWORD_STRING))
+                                                               .setPath(trustStorePath)
+                                                               .setPassword(trustStorePassword))
                                          .setSsl(true);
-        trustedClient = WebClient.create(vertx(), clientOptions);
-        return trustedClient;
+        return WebClient.create(vertx(), clientOptions);
     }
 
     /**
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/AccessControlConfigurationImpl.java b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/AccessControlConfigurationImpl.java
index bca14fca..644e42eb 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/AccessControlConfigurationImpl.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/AccessControlConfigurationImpl.java
@@ -24,6 +24,7 @@ import java.util.Set;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import org.apache.cassandra.sidecar.acl.authorization.AllowAllAuthorizationProvider;
+import org.apache.cassandra.sidecar.common.DataObjectBuilder;
 import org.apache.cassandra.sidecar.common.server.utils.MillisecondBoundConfiguration;
 import org.apache.cassandra.sidecar.config.AccessControlConfiguration;
 import org.apache.cassandra.sidecar.config.CacheConfiguration;
@@ -39,8 +40,10 @@ public class AccessControlConfigurationImpl implements AccessControlConfiguratio
     private static final ParameterizedClassConfiguration DEFAULT_AUTHORIZER_CONFIGURATION
     = new ParameterizedClassConfigurationImpl(AllowAllAuthorizationProvider.class.getName(), Collections.emptyMap());
     private static final Set<String> DEFAULT_ADMIN_IDENTITIES = Collections.emptySet();
-    private static final CacheConfiguration DEFAULT_PERMISSION_CACHE_CONFIGURATION =
-    new CacheConfigurationImpl(MillisecondBoundConfiguration.parse("2h"), 1_000);
+    private static final CacheConfiguration DEFAULT_PERMISSION_CACHE_CONFIGURATION = CacheConfigurationImpl.builder()
+                                                                                                           .expireAfterAccess(MillisecondBoundConfiguration.parse("2h"))
+                                                                                                           .maximumSize(1_000)
+                                                                                                           .build();
 
     @JsonProperty(value = "enabled")
     protected final boolean enabled;
@@ -59,21 +62,16 @@ public class AccessControlConfigurationImpl implements AccessControlConfiguratio
 
     public AccessControlConfigurationImpl()
     {
-        this(DEFAULT_ENABLED, DEFAULT_AUTHENTICATORS_CONFIGURATION, DEFAULT_AUTHORIZER_CONFIGURATION,
-             DEFAULT_ADMIN_IDENTITIES, DEFAULT_PERMISSION_CACHE_CONFIGURATION);
+        this(builder());
     }
 
-    public AccessControlConfigurationImpl(boolean enabled,
-                                          List<ParameterizedClassConfiguration> authenticatorsConfiguration,
-                                          ParameterizedClassConfiguration authorizerConfiguration,
-                                          Set<String> adminIdentities,
-                                          CacheConfiguration permissionCacheConfiguration)
+    protected AccessControlConfigurationImpl(Builder builder)
     {
-        this.enabled = enabled;
-        this.authenticatorsConfiguration = authenticatorsConfiguration;
-        this.authorizerConfiguration = authorizerConfiguration;
-        this.adminIdentities = adminIdentities;
-        this.permissionCacheConfiguration = permissionCacheConfiguration;
+        enabled = builder.enabled;
+        authenticatorsConfiguration = builder.authenticatorsConfiguration;
+        authorizerConfiguration = builder.authorizerConfiguration;
+        adminIdentities = builder.adminIdentities;
+        permissionCacheConfiguration = builder.permissionCacheConfiguration;
     }
 
     /**
@@ -125,4 +123,97 @@ public class AccessControlConfigurationImpl implements AccessControlConfiguratio
     {
         return permissionCacheConfiguration;
     }
+
+    public static Builder builder()
+    {
+        return new Builder();
+    }
+
+    /**
+     * {@code AccessControlConfigurationImpl} builder static inner class.
+     */
+    public static class Builder implements DataObjectBuilder<Builder, AccessControlConfigurationImpl>
+    {
+        private boolean enabled = DEFAULT_ENABLED;
+        private List<ParameterizedClassConfiguration> authenticatorsConfiguration = DEFAULT_AUTHENTICATORS_CONFIGURATION;
+        private ParameterizedClassConfiguration authorizerConfiguration = DEFAULT_AUTHORIZER_CONFIGURATION;
+        private Set<String> adminIdentities = DEFAULT_ADMIN_IDENTITIES;
+        private CacheConfiguration permissionCacheConfiguration = DEFAULT_PERMISSION_CACHE_CONFIGURATION;
+
+        private Builder()
+        {
+        }
+
+        @Override
+        public Builder self()
+        {
+            return this;
+        }
+
+        /**
+         * Sets the {@code enabled} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param enabled the {@code enabled} to set
+         * @return a reference to this Builder
+         */
+        public Builder enabled(boolean enabled)
+        {
+            return update(b -> b.enabled = enabled);
+        }
+
+        /**
+         * Sets the {@code authenticatorsConfiguration} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param authenticatorsConfiguration the {@code authenticatorsConfiguration} to set
+         * @return a reference to this Builder
+         */
+        public Builder authenticatorsConfiguration(List<ParameterizedClassConfiguration> authenticatorsConfiguration)
+        {
+            return update(b -> b.authenticatorsConfiguration = authenticatorsConfiguration);
+        }
+
+        /**
+         * Sets the {@code authorizerConfiguration} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param authorizerConfiguration the {@code authorizerConfiguration} to set
+         * @return a reference to this Builder
+         */
+        public Builder authorizerConfiguration(ParameterizedClassConfiguration authorizerConfiguration)
+        {
+            return update(b -> b.authorizerConfiguration = authorizerConfiguration);
+        }
+
+        /**
+         * Sets the {@code adminIdentities} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param adminIdentities the {@code adminIdentities} to set
+         * @return a reference to this Builder
+         */
+        public Builder adminIdentities(Set<String> adminIdentities)
+        {
+            return update(b -> b.adminIdentities = adminIdentities);
+        }
+
+        /**
+         * Sets the {@code permissionCacheConfiguration} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param permissionCacheConfiguration the {@code permissionCacheConfiguration} to set
+         * @return a reference to this Builder
+         */
+        public Builder permissionCacheConfiguration(CacheConfiguration permissionCacheConfiguration)
+        {
+            return update(b -> b.permissionCacheConfiguration = permissionCacheConfiguration);
+        }
+
+        /**
+         * Returns a {@code AccessControlConfigurationImpl} built from the parameters previously set.
+         *
+         * @return a {@code AccessControlConfigurationImpl} built with parameters of this {@code AccessControlConfigurationImpl.Builder}
+         */
+        @Override
+        public AccessControlConfigurationImpl build()
+        {
+            return new AccessControlConfigurationImpl(this);
+        }
+    }
 }
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImpl.java b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImpl.java
index 150864a4..830e37bb 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImpl.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImpl.java
@@ -24,9 +24,9 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.cassandra.sidecar.common.DataObjectBuilder;
 import org.apache.cassandra.sidecar.common.server.utils.MillisecondBoundConfiguration;
 import org.apache.cassandra.sidecar.config.CacheConfiguration;
-import org.jetbrains.annotations.VisibleForTesting;
 
 /**
  * Configuration class that encapsulates parameters needed for Caches
@@ -34,10 +34,15 @@ import org.jetbrains.annotations.VisibleForTesting;
 public class CacheConfigurationImpl implements CacheConfiguration
 {
     private static final Logger LOGGER = LoggerFactory.getLogger(CacheConfigurationImpl.class);
+    private static final long DEFAULT_MAXIMUM_SIZE = 100;
+    private static final boolean DEFAULT_ENABLED = true;
+    private static final int DEFAULT_WARMUP_RETRIES = 5;
+    private static final MillisecondBoundConfiguration DEFAULT_WARMUP_RETRY_INTERVAL = MillisecondBoundConfiguration.parse("1s");
 
     protected MillisecondBoundConfiguration expireAfterAccess;
 
-    protected MillisecondBoundConfiguration refreshAfterWrite;
+    @JsonProperty("refresh_after_write")
+    protected final MillisecondBoundConfiguration refreshAfterWrite;
 
     @JsonProperty("maximum_size")
     protected final long maximumSize;
@@ -52,28 +57,17 @@ public class CacheConfigurationImpl implements CacheConfiguration
 
     public CacheConfigurationImpl()
     {
-        this(null, null, 100, true, 5, MillisecondBoundConfiguration.parse("1s"));
-    }
-
-    @VisibleForTesting
-    public CacheConfigurationImpl(MillisecondBoundConfiguration expireAfterAccess, long maximumSize)
-    {
-        this(expireAfterAccess, MillisecondBoundConfiguration.parse("1h"), maximumSize, true, 5, MillisecondBoundConfiguration.parse("1s"));
+        this(builder());
     }
 
-    public CacheConfigurationImpl(MillisecondBoundConfiguration expireAfterAccess,
-                                  MillisecondBoundConfiguration refreshAfterWrite,
-                                  long maximumSize,
-                                  boolean enabled,
-                                  int warmupRetries,
-                                  MillisecondBoundConfiguration warmupRetryInterval)
+    protected CacheConfigurationImpl(Builder builder)
     {
-        this.expireAfterAccess = expireAfterAccess;
-        this.refreshAfterWrite = refreshAfterWrite;
-        this.maximumSize = maximumSize;
-        this.enabled = enabled;
-        this.warmupRetries = warmupRetries;
-        this.warmupRetryInterval = warmupRetryInterval;
+        setExpireAfterAccess(builder.expireAfterAccess);
+        maximumSize = builder.maximumSize;
+        enabled = builder.enabled;
+        warmupRetries = builder.warmupRetries;
+        setWarmupRetryInterval(builder.warmupRetryInterval);
+        refreshAfterWrite = builder.refreshAfterWrite;
     }
 
     @Override
@@ -96,12 +90,6 @@ public class CacheConfigurationImpl implements CacheConfiguration
         return refreshAfterWrite;
     }
 
-    @JsonProperty("refresh_after_write")
-    public void setRefreshAfterWrite(MillisecondBoundConfiguration refreshAfterWrite)
-    {
-        this.refreshAfterWrite = refreshAfterWrite;
-    }
-
     /**
      * Legacy property {@code expire_after_access_millis}
      *
@@ -130,12 +118,14 @@ public class CacheConfigurationImpl implements CacheConfiguration
         return enabled;
     }
 
+    @Override
     @JsonProperty(value = "warmup_retries")
     public int warmupRetries()
     {
         return warmupRetries;
     }
 
+    @Override
     @JsonProperty(value = "warmup_retry_interval")
     public MillisecondBoundConfiguration warmupRetryInterval()
     {
@@ -161,4 +151,109 @@ public class CacheConfigurationImpl implements CacheConfiguration
         LOGGER.warn("'warmup_retry_interval_millis' is deprecated, use 'warmup_retry_interval' instead");
         setWarmupRetryInterval(new MillisecondBoundConfiguration(warmupRetryIntervalMillis, TimeUnit.MILLISECONDS));
     }
+
+    public static Builder builder()
+    {
+        return new Builder();
+    }
+
+    /**
+     * {@code CacheConfigurationImpl} builder static inner class.
+     */
+    public static class Builder implements DataObjectBuilder<Builder, CacheConfigurationImpl>
+    {
+        private MillisecondBoundConfiguration expireAfterAccess;
+        private MillisecondBoundConfiguration refreshAfterWrite;
+        private long maximumSize = DEFAULT_MAXIMUM_SIZE;
+        private boolean enabled = DEFAULT_ENABLED;
+        private int warmupRetries = DEFAULT_WARMUP_RETRIES;
+        private MillisecondBoundConfiguration warmupRetryInterval = DEFAULT_WARMUP_RETRY_INTERVAL;
+
+        private Builder()
+        {
+        }
+
+        @Override
+        public Builder self()
+        {
+            return this;
+        }
+
+        /**
+         * Sets the {@code expireAfterAccess} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param expireAfterAccess the {@code expireAfterAccess} to set
+         * @return a reference to this Builder
+         */
+        public Builder expireAfterAccess(MillisecondBoundConfiguration expireAfterAccess)
+        {
+            return update(b -> b.expireAfterAccess = expireAfterAccess);
+        }
+
+        /**
+         * Sets the {@code refreshAfterWrite} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param refreshAfterWrite the {@code refreshAfterWrite} to set
+         * @return a reference to this Builder
+         */
+        public Builder refreshAfterWrite(MillisecondBoundConfiguration refreshAfterWrite)
+        {
+            return update(b -> b.refreshAfterWrite = refreshAfterWrite);
+        }
+
+        /**
+         * Sets the {@code maximumSize} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param maximumSize the {@code maximumSize} to set
+         * @return a reference to this Builder
+         */
+        public Builder maximumSize(long maximumSize)
+        {
+            return update(b -> b.maximumSize = maximumSize);
+        }
+
+        /**
+         * Sets the {@code enabled} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param enabled the {@code enabled} to set
+         * @return a reference to this Builder
+         */
+        public Builder enabled(boolean enabled)
+        {
+            return update(b -> b.enabled = enabled);
+        }
+
+        /**
+         * Sets the {@code warmupRetries} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param warmupRetries the {@code warmupRetries} to set
+         * @return a reference to this Builder
+         */
+        public Builder warmupRetries(int warmupRetries)
+        {
+            return update(b -> b.warmupRetries = warmupRetries);
+        }
+
+        /**
+         * Sets the {@code warmupRetryInterval} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param warmupRetryInterval the {@code warmupRetryInterval} to set
+         * @return a reference to this Builder
+         */
+        public Builder warmupRetryInterval(MillisecondBoundConfiguration warmupRetryInterval)
+        {
+            return update(b -> b.warmupRetryInterval = warmupRetryInterval);
+        }
+
+        /**
+         * Returns a {@code CacheConfigurationImpl} built from the parameters previously set.
+         *
+         * @return a {@code CacheConfigurationImpl} built with parameters of this {@code CacheConfigurationImpl.Builder}
+         */
+        @Override
+        public CacheConfigurationImpl build()
+        {
+            return new CacheConfigurationImpl(this);
+        }
+    }
 }
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/PeriodicTaskConfigurationImpl.java b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/PeriodicTaskConfigurationImpl.java
index a70c5ff4..cf1f273a 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/PeriodicTaskConfigurationImpl.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/PeriodicTaskConfigurationImpl.java
@@ -172,7 +172,7 @@ public class PeriodicTaskConfigurationImpl implements PeriodicTaskConfiguration
      */
     public static class Builder implements DataObjectBuilder<Builder, PeriodicTaskConfigurationImpl>
     {
-        private boolean enabled;
+        private boolean enabled = DEFAULT_ENABLED;
         private MillisecondBoundConfiguration initialDelay;
         private MillisecondBoundConfiguration executeInterval;
 
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/SSTableImportConfigurationImpl.java b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/SSTableImportConfigurationImpl.java
index 01421a49..f032295c 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/SSTableImportConfigurationImpl.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/SSTableImportConfigurationImpl.java
@@ -32,8 +32,10 @@ public class SSTableImportConfigurationImpl extends PeriodicTaskConfigurationImp
 {
     private static final MillisecondBoundConfiguration DEFAULT_IMPORT_EXECUTE_INTERVAL = MillisecondBoundConfiguration.parse("100ms");
     public static final String CACHE_PROPERTY = "cache";
-    protected static final CacheConfiguration DEFAULT_CACHE_CONFIGURATION =
-    new CacheConfigurationImpl(MillisecondBoundConfiguration.parse("2h"), 10_000);
+    protected static final CacheConfiguration DEFAULT_CACHE_CONFIGURATION = CacheConfigurationImpl.builder()
+                                                                                                  .expireAfterAccess(MillisecondBoundConfiguration.parse("2h"))
+                                                                                                  .maximumSize(10_000)
+                                                                                                  .build();
 
     @JsonProperty(value = CACHE_PROPERTY)
     protected final CacheConfiguration cacheConfiguration;
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/SSTableSnapshotConfigurationImpl.java b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/SSTableSnapshotConfigurationImpl.java
index 323b5e00..1462222d 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/SSTableSnapshotConfigurationImpl.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/SSTableSnapshotConfigurationImpl.java
@@ -29,7 +29,10 @@ import org.apache.cassandra.sidecar.config.SSTableSnapshotConfiguration;
 public class SSTableSnapshotConfigurationImpl implements SSTableSnapshotConfiguration
 {
     protected static final CacheConfiguration DEFAULT_SNAPSHOT_LIST_CACHE_CONFIGURATION =
-    new CacheConfigurationImpl(MillisecondBoundConfiguration.parse("2h"), 10_000);
+    CacheConfigurationImpl.builder()
+                          .expireAfterAccess(MillisecondBoundConfiguration.parse("2h"))
+                          .maximumSize(10_000)
+                          .build();
 
     @JsonProperty(value = "snapshot_list_cache")
     protected CacheConfiguration snapshotListCacheConfiguration;
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/SslConfigurationImpl.java b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/SslConfigurationImpl.java
index 5b333377..8e5d189c 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/SslConfigurationImpl.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/SslConfigurationImpl.java
@@ -45,9 +45,7 @@ public class SslConfigurationImpl implements SslConfiguration
     public static final boolean DEFAULT_USE_OPEN_SSL = true;
     public static final SecondBoundConfiguration DEFAULT_HANDSHAKE_TIMEOUT = SecondBoundConfiguration.parse("10s");
     public static final String DEFAULT_CLIENT_AUTH = "NONE";
-    public static final List<String> DEFAULT_SECURE_TRANSPORT_PROTOCOLS
-    = Collections.unmodifiableList(Arrays.asList("TLSv1.2", "TLSv1.3"));
-
+    public static final List<String> DEFAULT_SECURE_TRANSPORT_PROTOCOLS = List.of("TLSv1.2", "TLSv1.3");
 
     @JsonProperty("enabled")
     protected final boolean enabled;
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/db/schema/SidecarRolePermissionsSchema.java b/server/src/main/java/org/apache/cassandra/sidecar/db/schema/SidecarRolePermissionsSchema.java
index f88e25d3..d0cbfc10 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/db/schema/SidecarRolePermissionsSchema.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/db/schema/SidecarRolePermissionsSchema.java
@@ -30,7 +30,7 @@ import org.jetbrains.annotations.NotNull;
  */
 public class SidecarRolePermissionsSchema extends TableSchema
 {
-    private static final String ROLE_PERMISSIONS_TABLE = "role_permissions_v1";
+    public static final String ROLE_PERMISSIONS_TABLE = "role_permissions_v1";
 
     private final SchemaKeyspaceConfiguration keyspaceConfig;
 
diff --git a/server/src/test/integration/org/apache/cassandra/sidecar/acl/MutualTLSAuthenticationIntegrationTest.java b/server/src/test/integration/org/apache/cassandra/sidecar/acl/MutualTLSAuthenticationIntegrationTest.java
index 52fc0e8e..db67b2d6 100644
--- a/server/src/test/integration/org/apache/cassandra/sidecar/acl/MutualTLSAuthenticationIntegrationTest.java
+++ b/server/src/test/integration/org/apache/cassandra/sidecar/acl/MutualTLSAuthenticationIntegrationTest.java
@@ -39,7 +39,6 @@ import org.apache.cassandra.testing.AuthMode;
 import org.apache.cassandra.testing.CassandraIntegrationTest;
 import org.apache.cassandra.testing.CassandraTestContext;
 
-import static org.apache.cassandra.sidecar.acl.RoleBasedAuthorizationIntegrationTest.MIN_VERSION_WITH_MTLS;
 import static org.apache.cassandra.sidecar.common.http.SidecarHttpHeaderNames.AUTH_ROLE;
 import static org.apache.cassandra.sidecar.testing.IntegrationTestModule.ADMIN_IDENTITY;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -48,6 +47,8 @@ import static org.assertj.core.api.Assumptions.assumeThat;
 @ExtendWith(VertxExtension.class)
 class MutualTLSAuthenticationIntegrationTest extends IntegrationTestBase
 {
+    protected static final int MIN_VERSION_WITH_MTLS = 5;
+
     @CassandraIntegrationTest()
     void testAuthenticatedAdminRequest(VertxTestContext context) throws Exception
     {
diff --git a/server/src/test/integration/org/apache/cassandra/sidecar/acl/RoleBasedAuthorizationIntegrationTest.java b/server/src/test/integration/org/apache/cassandra/sidecar/acl/RoleBasedAuthorizationIntegrationTest.java
deleted file mode 100644
index eaab0782..00000000
--- a/server/src/test/integration/org/apache/cassandra/sidecar/acl/RoleBasedAuthorizationIntegrationTest.java
+++ /dev/null
@@ -1,624 +0,0 @@
-/*
- * 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.acl;
-
-import java.nio.file.Path;
-import java.util.Comparator;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-
-import org.junit.jupiter.api.extension.ExtendWith;
-
-import com.datastax.driver.core.Session;
-import io.netty.handler.codec.http.HttpResponseStatus;
-import io.vertx.core.Future;
-import io.vertx.core.Promise;
-import io.vertx.core.buffer.Buffer;
-import io.vertx.core.http.HttpMethod;
-import io.vertx.ext.web.client.HttpResponse;
-import io.vertx.ext.web.client.WebClient;
-import io.vertx.junit5.VertxExtension;
-import io.vertx.junit5.VertxTestContext;
-import org.apache.cassandra.sidecar.common.response.ListSnapshotFilesResponse;
-import org.apache.cassandra.sidecar.testing.IntegrationTestBase;
-import org.apache.cassandra.testing.AuthMode;
-import org.apache.cassandra.testing.CassandraIntegrationTest;
-import org.apache.cassandra.testing.CassandraTestContext;
-
-import static org.apache.cassandra.sidecar.testing.IntegrationTestModule.ADMIN_IDENTITY;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assumptions.assumeThat;
-
-/**
- * Test for role based access control in Sidecar
- * Note:
- * - Do not add new test cases in this class. Add them into test method, example refer to testForAdmin.
- * - Create a new keyspace or test role for each test method as required to prevent permissions overlapping
- */
-@ExtendWith(VertxExtension.class)
-class RoleBasedAuthorizationIntegrationTest extends IntegrationTestBase
-{
-    protected static final int MIN_VERSION_WITH_MTLS = 5;
-
-    private Path nonAdminClientKeystorePath;
-    private CountDownLatch testCompleteLatch;
-
-    @CassandraIntegrationTest(authMode = AuthMode.MUTUAL_TLS)
-    void testAuthorizationScenarios(VertxTestContext context, CassandraTestContext cassandraContext) throws Exception
-    {
-        prepareForTest(cassandraContext);
-
-        // wait for cache refreshes
-        Thread.sleep(3000);
-
-        testCompleteLatch = new CountDownLatch(36);
-
-        // permissions for test cases below are granted during prepareForTest to save cache refresh time. Please
-        // refer to grantRequiredPermissions to check permissions granted for a test to understand verifications done in
-        // test
-        testForAdmin(context);
-        testForSuperUser(context);
-        testForTransitiveSuperUser(context);
-        testForNonAdmin(context);
-        testGrantingForTable(context);
-        testGrantingForKeyspace(context);
-        testGrantingAllTablesExceptKeyspace(context);
-        testGrantingAtDataLevel(context);
-        testGrantingWithWildcardSubparts(context);
-        testEndpointRequiringMultipleActions(context);
-        testGrantingBulkReadFeaturePermission(context);
-        testGrantingBulkReadFeaturePermissionAcrossTables(context);
-        testGrantingBulkReadFeaturePermissionAcrossData(context);
-        testGrantingBulkWriteFeaturePermission(context);
-        testGrantingBothBulkReadAndWriteFeaturePermission(context);
-        testGrantingAllAnalyticsRelatedPermissions(context);
-        testGrantingCdcFeaturePermission(context);
-
-        assertThat(testCompleteLatch.await(4, TimeUnit.MINUTES)).isTrue();
-        context.completeNow();
-    }
-
-    void testForAdmin(VertxTestContext context)
-    {
-        String keyspaceSchemaRoute = String.format("/api/v1/keyspaces/%s/schema", "test_keyspace");
-        // uses client keystore with admin identity. Admins bypass authorization checks
-
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, keyspaceSchemaRoute, clientKeystorePath, false);
-    }
-
-    void testForSuperUser(VertxTestContext context) throws Exception
-    {
-        String keyspaceSchemaRoute = String.format("/api/v1/keyspaces/%s/schema", "test_keyspace");
-        // uses client keystore with superuser identity
-        Path clientKeystorePath = clientKeystorePath("spiffe://cassandra/sidecar/super_user_test_user");
-
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, keyspaceSchemaRoute, clientKeystorePath, false);
-    }
-
-    void testForTransitiveSuperUser(VertxTestContext context) throws Exception
-    {
-        String keyspaceSchemaRoute = String.format("/api/v1/keyspaces/%s/schema", "test_keyspace");
-        // uses client keystore with superuser identity
-        Path clientKeystorePath = clientKeystorePath("spiffe://cassandra/sidecar/non_super_user_with_transitive_super_user");
-
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, keyspaceSchemaRoute, clientKeystorePath, false);
-    }
-
-    void testForNonAdmin(VertxTestContext context)
-    {
-        String keyspaceSchemaRoute = String.format("/api/v1/keyspaces/%s/schema", "non_admin_test_keyspace");
-
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, keyspaceSchemaRoute, nonAdminClientKeystorePath, false);
-    }
-
-    void testGrantingForTable(VertxTestContext context)
-    {
-        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
-                                                   "grant_table_test_keyspace", "test_table");
-
-        // SNAPSHOT:CREATE permission granted for data/grant_table_test_keyspace/test_table
-        verifyAccess(context, testCompleteLatch, HttpMethod.PUT, createSnapshotRoute, nonAdminClientKeystorePath, false);
-
-        // SNAPSHOT:DELETE permission not granted for data/grant_table_test_keyspace/test_table
-        verifyAccess(context, testCompleteLatch, HttpMethod.DELETE, createSnapshotRoute, nonAdminClientKeystorePath, true);
-    }
-
-    void testGrantingForKeyspace(VertxTestContext context)
-    {
-        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
-                                                   "grant_keyspace_test_keyspace", "test_table");
-
-        // SNAPSHOT:CREATE permission granted for data/grant_keyspace_test_keyspace/test_table with
-        // data/grant_tables_test_keyspace grant
-        verifyAccess(context, testCompleteLatch, HttpMethod.PUT, createSnapshotRoute, nonAdminClientKeystorePath, false);
-
-        // access not granted for different keyspace
-        String notAllowedSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
-                                                       "not_allowed_keyspace", "test_table");
-        verifyAccess(context, testCompleteLatch, HttpMethod.PUT, notAllowedSnapshotRoute, nonAdminClientKeystorePath, true);
-
-        // SNAPSHOT:DELETE permission not granted for data/grant_keyspace_test_keyspace/test_table
-        verifyAccess(context, testCompleteLatch, HttpMethod.DELETE, createSnapshotRoute, nonAdminClientKeystorePath, true);
-    }
-
-    void testGrantingAllTablesExceptKeyspace(VertxTestContext context)
-    {
-        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
-                                                   "grant_tables_except_keyspace_test_keyspace", "test_table");
-
-        // SNAPSHOT:CREATE permission granted for data/grant_tables_except_keyspace_test_keyspace/test_table
-        // with data/grant_tables_except_keyspace_test_keyspace grant
-        verifyAccess(context, testCompleteLatch, HttpMethod.PUT, createSnapshotRoute, nonAdminClientKeystorePath, false);
-
-        // SCHEMA:READ is not granted since it expects permissions at keyspace level
-        String keyspaceSchemaRoute = String.format("/api/v1/keyspaces/%s/schema", "grant_tables_except_keyspace_test_keyspace");
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, keyspaceSchemaRoute, nonAdminClientKeystorePath, true);
-    }
-
-    void testGrantingAtDataLevel(VertxTestContext context) throws Exception
-    {
-        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
-                                                   "test_keyspace", "test_table");
-        Path clientKeystorePath = clientKeystorePath("spiffe://cassandra/sidecar/grant_data_test_user");
-
-        // SNAPSHOT:CREATE permission granted for data/test_keyspace/test_table with data resource grant
-        verifyAccess(context, testCompleteLatch, HttpMethod.PUT, createSnapshotRoute, clientKeystorePath, false);
-
-        // SNAPSHOT:DELETE permission not granted for data/test_keyspace/test_table not granted
-        verifyAccess(context, testCompleteLatch, HttpMethod.DELETE, createSnapshotRoute, clientKeystorePath, true);
-    }
-
-    void testGrantingWithWildcardSubparts(VertxTestContext context) throws Exception
-    {
-        String schemaRoute = "/api/v1/cassandra/schema";
-        String gossipRoute = "/api/v1/cassandra/gossip";
-        String ringRoute = "/api/v1/cassandra/ring";
-        Path clientKeystorePath = clientKeystorePath("spiffe://cassandra/sidecar/wildcard_with_subparts_test_user");
-
-        // SCHEMA:READ permission granted for cluster with GOSSIP,SCHEMA:READ
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, schemaRoute, clientKeystorePath, false);
-
-        // GOSSIP:READ permission granted for cluster with GOSSIP,SCHEMA:READ
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, gossipRoute, clientKeystorePath, false);
-
-        // RING:READ permission not granted with GOSSIP,SCHEMA:READ
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, ringRoute, clientKeystorePath, true);
-    }
-
-    void testEndpointRequiringMultipleActions(VertxTestContext context)
-    {
-        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
-                                                   "multiple_permissions_required_test_keyspace", "test_table");
-
-        String listSnapshotRoute
-        = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/%s",
-                        "multiple_permissions_required_test_keyspace", "test_table", "my-snapshot");
-
-        // SNAPSHOT:CREATE permission granted for data/multiple_permissions_required_test_keyspace/test_table
-        WebClient client = createClient(nonAdminClientKeystorePath, truststorePath);
-
-        createReq(client, HttpMethod.PUT, createSnapshotRoute)
-        .compose(createResp -> {
-            // grant sidecar permission for streaming
-            updateSidecarPermission("non_admin_test_role",
-                                    "data/multiple_permissions_required_test_keyspace/test_table",
-                                    "SNAPSHOT:READ");
-
-            // wait for cache refresh
-            return waitForCacheRefresh(2000)
-                   .compose(v -> createReq(client, HttpMethod.GET, listSnapshotRoute));
-        })
-        .compose(listSnapshotResp -> {
-            ListSnapshotFilesResponse snapshotFiles = listSnapshotResp.bodyAsJson(ListSnapshotFilesResponse.class);
-            List<ListSnapshotFilesResponse.FileInfo> filesToStream =
-            snapshotFiles.snapshotFilesInfo()
-                         .stream()
-                         .filter(info -> info.fileName.endsWith("-Data.db"))
-                         .sorted(Comparator.comparing(o -> o.fileName))
-                         .collect(Collectors.toList());
-
-            // grant sidecar permission for streaming
-            updateSidecarPermission("non_admin_test_role",
-                                    "data/multiple_permissions_required_test_keyspace/test_table",
-                                    "SNAPSHOT:STREAM");
-
-            return waitForCacheRefresh(2000)
-                   // STREAM SSTable request requires both Sidecar SNAPSHOT:STREAM permission and Cassandra's SELECT
-                   // permission on a table it accesses data.
-                   .compose(v -> createReq(client, HttpMethod.GET, filesToStream.get(0).componentDownloadUrl()))
-                   .compose(deniedStreamResp -> {
-                       // request denied without SELECT permission
-                       assertThat(deniedStreamResp.statusCode()).isEqualTo(HttpResponseStatus.FORBIDDEN.code());
-
-                       // grant SELECT permission with cassandra role
-                       grantTablePermission("multiple_permissions_required_test_keyspace", "test_table", "non_admin_test_role");
-
-                       return waitForCacheRefresh(2000);
-                   }).compose(v -> createReq(client, HttpMethod.GET, filesToStream.get(0).componentDownloadUrl()));
-        })
-        .onSuccess(acceptedStreamResp ->  {
-            // stream request goes through with both SNAPSHOT:STREAM and SELECT permissions
-            context.verify(() -> assertThat(acceptedStreamResp.statusCode()).isEqualTo(HttpResponseStatus.OK.code()));
-            testCompleteLatch.countDown();
-        })
-        .onFailure(context::failNow);
-    }
-
-    void testGrantingBulkReadFeaturePermission(VertxTestContext context) throws Exception
-    {
-        Path clientKeystorePath = clientKeystorePath("spiffe://cassandra/sidecar/bulk_read_test_user");
-
-        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
-                                                   "grant_bulk_read_test_keyspace", "test_table");
-
-        // SNAPSHOT:CREATE permission granted for data/grant_bulk_read_test_keyspace/test_table with ANALYTICS:READ_DIRECT
-        verifyAccess(context, testCompleteLatch, HttpMethod.PUT, createSnapshotRoute, clientKeystorePath, false);
-
-        String keyspaceSchemaRoute = String.format("/api/v1/keyspaces/%s/schema", "grant_bulk_read_test_keyspace");
-
-        // SCHEMA:READ permission granted for data/grant_bulk_read_test_keyspace/test_table with ANALYTICS:READ_DIRECT
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, keyspaceSchemaRoute, clientKeystorePath, false);
-
-        String topologyRoute = String.format("/api/v1/keyspaces/%s/token-range-replicas", "grant_bulk_read_test_keyspace");
-        // TOPOLOGY:READ permission not granted with ANALYTICS:READ_DIRECT
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, topologyRoute, clientKeystorePath, true);
-
-        String tableStatsRoute = String.format("/api/v1/cassandra/keyspaces/%s/tables/%s/stats",
-                                               "grant_bulk_read_test_keyspace", "test_table");
-        // STATS permission granted with ANALYTICS:READ_DIRECT
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, tableStatsRoute, clientKeystorePath, false);
-
-        String clusterStatsRoute = "/api/v1/cassandra/stats/streams";
-        // STATS permission at cluster scope not granted with ANALYTICS:READ_DIRECT
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, clusterStatsRoute, clientKeystorePath, true);
-    }
-
-    void testGrantingBulkReadFeaturePermissionAcrossTables(VertxTestContext context) throws Exception
-    {
-        Path clientKeystorePath = clientKeystorePath("spiffe://cassandra/sidecar/bulk_read_test_user");
-
-        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
-                                                   "grant_bulk_read_across_tables_test_keyspace", "test_table");
-
-        // SNAPSHOT:CREATE permission granted for data/grant_bulk_read_across_tables_test_keyspace/test_table with ANALYTICS:READ_DIRECT
-        verifyAccess(context, testCompleteLatch, HttpMethod.PUT, createSnapshotRoute, clientKeystorePath, false);
-
-        String createSnapshotRouteTable2 = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
-                                                         "grant_bulk_read_across_tables_test_keyspace", "test_table2");
-
-        // SNAPSHOT:CREATE for different table also granted with keyspace scoped ANALYTICS:READ_DIRECT permission
-        verifyAccess(context, testCompleteLatch, HttpMethod.PUT, createSnapshotRouteTable2, clientKeystorePath, false);
-
-        String createSnapshotRouteKeyspace2 = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
-                                                            "test_keyspace", "test_table2");
-
-        // SNAPSHOT:CREATE for different keyspace not granted with keyspace scoped ANALYTICS:READ_DIRECT permission
-        verifyAccess(context, testCompleteLatch, HttpMethod.PUT, createSnapshotRouteKeyspace2, clientKeystorePath, true);
-
-        String tableStatsRoute = String.format("/api/v1/cassandra/keyspaces/%s/tables/%s/stats",
-                                               "grant_bulk_read_across_tables_test_keyspace", "test_table");
-        // STATS permission granted with ANALYTICS:READ_DIRECT
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, tableStatsRoute, clientKeystorePath, false);
-    }
-
-    void testGrantingBulkReadFeaturePermissionAcrossData(VertxTestContext context) throws Exception
-    {
-        Path clientKeystorePath = clientKeystorePath("spiffe://cassandra/sidecar/bulk_read_across_data_test_user");
-
-        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot-2",
-                                                   "test_keyspace", "test_table");
-
-        // SNAPSHOT:CREATE permission granted for data/test_keyspace/test_table with ANALYTICS:READ_DIRECT permission
-        verifyAccess(context, testCompleteLatch, HttpMethod.PUT, createSnapshotRoute, clientKeystorePath, false);
-
-        String createSnapshotRouteKeyspace2 = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot-3",
-                                                            "non_admin_test_keyspace", "test_table");
-
-        // SNAPSHOT:CREATE for different keyspace also granted with data scoped ANALYTICS:READ_DIRECT permission
-        verifyAccess(context, testCompleteLatch, HttpMethod.PUT, createSnapshotRouteKeyspace2, clientKeystorePath, false);
-    }
-
-    void testGrantingBulkWriteFeaturePermission(VertxTestContext context) throws Exception
-    {
-        Path clientKeystorePath = clientKeystorePath("spiffe://cassandra/sidecar/bulk_write_test_user");
-
-        String topologyRoute = String.format("/api/v1/keyspaces/%s/token-range-replicas", "grant_bulk_write_test_keyspace");
-        // TOPOLOGY:READ permission granted with ANALYTICS:WRITE_DIRECT
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, topologyRoute, clientKeystorePath, false);
-
-        String keyspaceRingRoute = String.format("/api/v1/cassandra/ring/keyspaces/%s", "grant_bulk_write_test_keyspace");
-
-        // RING:READ permission not granted for data/grant_bulk_write_test_keyspace with ANALYTICS:WRITE_DIRECT
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, keyspaceRingRoute, clientKeystorePath, true);
-    }
-
-    void testGrantingBothBulkReadAndWriteFeaturePermission(VertxTestContext context) throws Exception
-    {
-        Path clientKeystorePath = clientKeystorePath("spiffe://cassandra/sidecar/bulk_read_write_test_user");
-
-        String topologyRoute = String.format("/api/v1/keyspaces/%s/token-range-replicas", "grant_bulk_read_write_test_keyspace");
-        // TOPOLOGY:READ permission granted with ANALYTICS:READ_DIRECT,WRITE_DIRECT
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, topologyRoute, clientKeystorePath, false);
-
-        String keyspaceRingRoute = String.format("/api/v1/cassandra/ring/keyspaces/%s", "grant_bulk_read_write_test_keyspace");
-
-        // RING:READ permission granted for data/grant_bulk_read_write_test_keyspace with ANALYTICS:READ_DIRECT,WRITE_DIRECT
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, keyspaceRingRoute, clientKeystorePath, false);
-
-        String createSnapshotRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
-                                                   "grant_bulk_read_write_test_keyspace", "test_table");
-
-        // SNAPSHOT:CREATE permission granted for data/grant_bulk_read_write_test_keyspace/test_table with ANALYTICS:READ_DIRECT,WRITE_DIRECT
-        verifyAccess(context, testCompleteLatch, HttpMethod.PUT, createSnapshotRoute, clientKeystorePath, false);
-    }
-
-    void testGrantingAllAnalyticsRelatedPermissions(VertxTestContext context) throws Exception
-    {
-        Path clientKeystorePath = clientKeystorePath("spiffe://cassandra/sidecar/all_analytics_permission_test_user");
-
-        String topologyRoute = String.format("/api/v1/keyspaces/%s/token-range-replicas", "all_analytics_permission_test_keyspace");
-        // TOPOLOGY:READ permission under ANALYTICS:WRITE_DIRECT granted with ANALYTICS:*
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, topologyRoute, clientKeystorePath, false);
-
-        String keyspaceRingRoute = String.format("/api/v1/cassandra/ring/keyspaces/%s", "all_analytics_permission_test_keyspace");
-
-        // RING:READ permission under ANALYTICS:READ_DIRECT granted with ANALYTICS:*
-        verifyAccess(context, testCompleteLatch, HttpMethod.GET, keyspaceRingRoute, clientKeystorePath, false);
-    }
-
-    void testGrantingCdcFeaturePermission(VertxTestContext context) throws Exception
-    {
-        String listCdcPath = "/api/v1/cdc/segments";
-        Path clientKeystorePath = clientKeystorePath("spiffe://cassandra/sidecar/cdc_test_user");
-        // CDC permission granted with CDC
-        WebClient client = createClient(clientKeystorePath, truststorePath);
-        createReq(client, HttpMethod.GET, listCdcPath)
-        .onFailure(context::failNow)
-        .onSuccess(listResp -> {
-            // CDC permission granted
-            // CDC is not turned on for cluster, hence 500 or 503 expected
-            context.verify(() -> assertThat(listResp.statusCode()).isIn(HttpResponseStatus.SERVICE_UNAVAILABLE.code(),
-                                                                        HttpResponseStatus.INTERNAL_SERVER_ERROR.code()));
-            testCompleteLatch.countDown();
-        });
-    }
-
-    private void prepareForTest(CassandraTestContext cassandraContext) throws Exception
-    {
-        // mTLS authentication was added in Cassandra starting 5.0 version
-        assumeThat(cassandraContext.version.major)
-        .withFailMessage("mTLS authentication is not supported in 4.0 Cassandra version")
-        .isGreaterThanOrEqualTo(MIN_VERSION_WITH_MTLS);
-
-        // required for authentication of sidecar requests to Cassandra. Only superusers can grant permissions
-        insertIdentityRole(cassandraContext, ADMIN_IDENTITY, "cassandra");
-
-        waitForSchemaReady(30, TimeUnit.SECONDS);
-        createRequiredKeyspaceTables();
-        createRequiredRoles(cassandraContext);
-        grantRequiredPermissions();
-        createRequiredKeystores();
-    }
-
-    private void insertIdentityRole(CassandraTestContext cassandraContext, String identity, String role)
-    {
-        String statement = String.format("INSERT INTO system_auth.identity_to_role (identity, role) VALUES ('%s','%s')",
-                                         identity, role);
-        cassandraContext.cluster().schemaChangeIgnoringStoppedInstances(statement);
-    }
-
-    private void createRequiredKeyspaceTables()
-    {
-        createKeyspace("test_keyspace");
-        createKeyspace("non_admin_test_keyspace");
-        createKeyspace("grant_table_test_keyspace");
-        createKeyspace("grant_keyspace_test_keyspace");
-        createKeyspace("grant_tables_except_keyspace_test_keyspace");
-        createKeyspace("multiple_permissions_required_test_keyspace");
-        createKeyspace("grant_bulk_read_test_keyspace");
-        createKeyspace("grant_bulk_read_across_tables_test_keyspace");
-        createKeyspace("grant_bulk_write_test_keyspace");
-        createKeyspace("grant_bulk_read_write_test_keyspace");
-        createKeyspace("all_analytics_permission_test_keyspace");
-        createTable("test_keyspace", "test_table");
-        createTable("non_admin_test_keyspace", "test_table");
-        createTable("grant_table_test_keyspace", "test_table");
-        createTable("grant_keyspace_test_keyspace", "test_table");
-        createTable("grant_tables_except_keyspace_test_keyspace", "test_table");
-        createTable("multiple_permissions_required_test_keyspace", "test_table");
-        createTable("grant_bulk_read_test_keyspace", "test_table");
-        createTable("grant_bulk_read_across_tables_test_keyspace", "test_table");
-        createTable("grant_bulk_read_across_tables_test_keyspace", "test_table2");
-        createTable("grant_bulk_write_test_keyspace", "test_table");
-        createTable("grant_bulk_read_write_test_keyspace", "test_table");
-        createTable("all_analytics_permission_test_keyspace", "test_table");
-    }
-
-    private void createRequiredRoles(CassandraTestContext cassandraContext)
-    {
-        createRole("super_user_test_role", true);
-        insertIdentityRole(cassandraContext, "spiffe://cassandra/sidecar/super_user_test_user", "super_user_test_role");
-
-        createRole("non_super_user_with_transitive_super_user_role", false);
-        grantRole("non_super_user_with_transitive_super_user_role", "super_user_test_role");
-        insertIdentityRole(cassandraContext, "spiffe://cassandra/sidecar/non_super_user_with_transitive_super_user", "non_super_user_with_transitive_super_user_role");
-
-        createRole("non_admin_test_role", false);
-        insertIdentityRole(cassandraContext, "spiffe://cassandra/sidecar/non_admin_test_user", "non_admin_test_role");
-
-        createRole("grant_data_test_role", false);
-        insertIdentityRole(cassandraContext, "spiffe://cassandra/sidecar/grant_data_test_user", "grant_data_test_role");
-
-        createRole("wildcard_across_targets_test_role", false);
-        insertIdentityRole(cassandraContext, "spiffe://cassandra/sidecar/wildcard_across_targets_test_user", "wildcard_across_targets_test_role");
-
-        createRole("wildcard_with_subparts_test_role", false);
-        insertIdentityRole(cassandraContext, "spiffe://cassandra/sidecar/wildcard_with_subparts_test_user", "wildcard_with_subparts_test_role");
-
-        createRole("bulk_read_test_role", false);
-        insertIdentityRole(cassandraContext, "spiffe://cassandra/sidecar/bulk_read_test_user", "bulk_read_test_role");
-
-        createRole("bulk_read_across_data_test_role", false);
-        insertIdentityRole(cassandraContext, "spiffe://cassandra/sidecar/bulk_read_across_data_test_user", "bulk_read_across_data_test_role");
-
-        createRole("bulk_write_test_role", false);
-        insertIdentityRole(cassandraContext, "spiffe://cassandra/sidecar/bulk_write_test_user", "bulk_write_test_role");
-
-        createRole("bulk_read_write_test_role", false);
-        insertIdentityRole(cassandraContext, "spiffe://cassandra/sidecar/bulk_read_write_test_user", "bulk_read_write_test_role");
-
-        createRole("all_analytics_permission_test_role", false);
-        insertIdentityRole(cassandraContext, "spiffe://cassandra/sidecar/all_analytics_permission_test_user", "all_analytics_permission_test_role");
-
-        createRole("cdc_test_role", false);
-        insertIdentityRole(cassandraContext, "spiffe://cassandra/sidecar/cdc_test_user", "cdc_test_role");
-    }
-
-    private void grantRequiredPermissions()
-    {
-        // permission for testForNonAdmin
-        grantSidecarPermission("non_admin_test_role", "data/non_admin_test_keyspace", "SCHEMA:READ");
-
-        // permission for testGrantingForTable
-        grantSidecarPermission("non_admin_test_role", "data/grant_table_test_keyspace/test_table", "SNAPSHOT:CREATE");
-
-        // permission for testGrantingForKeyspace
-        grantSidecarPermission("non_admin_test_role", "data/grant_keyspace_test_keyspace", "SNAPSHOT:CREATE");
-
-        // permission for testGrantingAllTablesExceptKeyspace
-        grantSidecarPermission("non_admin_test_role", "data/grant_tables_except_keyspace_test_keyspace/*", "SNAPSHOT:CREATE");
-
-        // permission for testGrantingAtDataLevel
-        grantSidecarPermission("grant_data_test_role", "data", "SNAPSHOT:CREATE");
-
-        // permission for testGrantingWithWildcardSubparts
-        grantSidecarPermission("wildcard_with_subparts_test_role", "cluster", "GOSSIP,SCHEMA:READ");
-
-        // permission for testEndpointRequiringMultipleActions
-        grantSidecarPermission("non_admin_test_role",
-                               "data/multiple_permissions_required_test_keyspace/test_table",
-                               "SNAPSHOT:CREATE");
-
-        // permission for testGrantingBulkReadFeaturePermission
-        grantSidecarPermission("bulk_read_test_role", "data/grant_bulk_read_test_keyspace/test_table", "ANALYTICS:READ_DIRECT");
-
-        // permission for testGrantingBulkReadFeaturePermissionAcrossTables
-        grantSidecarPermission("bulk_read_test_role", "data/grant_bulk_read_across_tables_test_keyspace", "ANALYTICS:READ_DIRECT");
-
-        // permission for testGrantingBulkReadFeaturePermissionAcrossData
-        grantSidecarPermission("bulk_read_across_data_test_role", "data", "ANALYTICS:READ_DIRECT");
-
-        // permission for testGrantingBulkWriteFeaturePermission
-        grantSidecarPermission("bulk_write_test_role", "data/grant_bulk_write_test_keyspace/test_table", "ANALYTICS:WRITE_DIRECT");
-
-        // permission for testGrantingBothBulkReadAndWriteFeaturePermission
-        grantSidecarPermission("bulk_read_write_test_role", "data/grant_bulk_read_write_test_keyspace/test_table", "ANALYTICS:READ_DIRECT,WRITE_DIRECT");
-
-        // permission for testGrantingAllAnalyticsRelatedPermissions
-        grantSidecarPermission("all_analytics_permission_test_role", "data/all_analytics_permission_test_keyspace/test_table", "ANALYTICS:*");
-
-        // permission for testGrantingCdcFeaturePermission
-        grantSidecarPermission("cdc_test_role", "cluster", "CDC");
-    }
-
-    private void createRequiredKeystores() throws Exception
-    {
-        nonAdminClientKeystorePath = clientKeystorePath("spiffe://cassandra/sidecar/non_admin_test_user");
-    }
-
-    private void createKeyspace(String keyspace)
-    {
-        Session session = maybeGetSession();
-        session.execute("CREATE KEYSPACE IF NOT EXISTS " + keyspace + " WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':'3'}");
-    }
-
-    private void createTable(String keyspace, String table)
-    {
-        Session session = maybeGetSession();
-        session.execute(String.format("CREATE TABLE %s.%s (a int, b text, PRIMARY KEY (a));", keyspace, table));
-        session.execute("INSERT INTO " + keyspace + "." + table + " (a, b) VALUES (1, 'text');");
-    }
-
-    private void grantTablePermission(String keyspace, String table, String role)
-    {
-        Session session = maybeGetSession();
-        session.execute("GRANT ALL PERMISSIONS ON " + keyspace + "." + table + " TO " + role);
-    }
-
-    private void grantSidecarPermission(String role, String resource, String permission)
-    {
-        Session session = maybeGetSession();
-        session.execute(String.format("INSERT INTO sidecar_internal.role_permissions_v1 (role, resource, permissions) " +
-                                      "VALUES ('%s', '%s', {'%s'})", role, resource, permission));
-    }
-
-    private void updateSidecarPermission(String role, String resource, String permission)
-    {
-        Session session = maybeGetSession();
-        session.execute(String.format("UPDATE sidecar_internal.role_permissions_v1 SET permissions = permissions + {'%s'} " +
-                                      "where role = '%s' and resource = '%s'", permission, role, resource));
-    }
-
-    private void verifyAccess(VertxTestContext context, CountDownLatch countDownLatch, HttpMethod method,
-                              String testRoute, Path clientKeystorePath, boolean expectForbidden)
-    {
-        WebClient client = createClient(clientKeystorePath, truststorePath);
-        client.request(method, server.actualPort(), "127.0.0.1", testRoute)
-              .send(response -> {
-                  if (response.cause() != null)
-                  {
-                      context.failNow(response.cause());
-                      return;
-                  }
-
-                  if (expectForbidden)
-                  {
-                      if (response.result().statusCode() != HttpResponseStatus.FORBIDDEN.code())
-                      {
-                          context.failNow(HttpResponseStatus.FORBIDDEN.code() + " expected but got " + response.result().statusCode());
-                          return;
-                      }
-                  }
-                  else
-                  {
-                      if (response.result().statusCode() != HttpResponseStatus.OK.code())
-                      {
-                          context.failNow(HttpResponseStatus.OK.code() + " expected but got " + response.result().statusCode());
-                          return;
-                      }
-                  }
-                  countDownLatch.countDown();
-              });
-    }
-
-    private Future<HttpResponse<Buffer>> createReq(WebClient client, HttpMethod method, String route)
-    {
-        return client.request(method, server.actualPort(), "127.0.0.1", route).send();
-    }
-
-    // Helper method to wait for cache refresh
-    private Future<Void> waitForCacheRefresh(long durationMillis)
-    {
-        Promise<Void> promise = Promise.promise();
-        vertx.setTimer(durationMillis, id -> promise.complete());
-        return promise.future();
-    }
-}
diff --git a/server/src/test/integration/org/apache/cassandra/sidecar/testing/IntegrationTestModule.java b/server/src/test/integration/org/apache/cassandra/sidecar/testing/IntegrationTestModule.java
index 8bc70252..e6152c17 100644
--- a/server/src/test/integration/org/apache/cassandra/sidecar/testing/IntegrationTestModule.java
+++ b/server/src/test/integration/org/apache/cassandra/sidecar/testing/IntegrationTestModule.java
@@ -23,6 +23,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import com.datastax.driver.core.Session;
 import com.google.inject.AbstractModule;
@@ -140,7 +141,9 @@ public class IntegrationTestModule extends AbstractModule
                                        .build();
     }
 
+    // @formatter:off
     static class TestClusterLeaseClaimTaskKey implements ClassKey {}
+    // @formatter:on
     @ProvidesIntoMap
     @KeyClassMapKey(TestClusterLeaseClaimTaskKey.class)
     public PeriodicTask clusterLeaseClaimTask(ServiceConfiguration serviceConfiguration,
@@ -258,16 +261,21 @@ public class IntegrationTestModule extends AbstractModule
         ParameterizedClassConfiguration rbacConfig
         = new ParameterizedClassConfigurationImpl("org.apache.cassandra.sidecar.acl.authorization.RoleBasedAuthorizationProvider",
                                                   Collections.emptyMap());
-        return new AccessControlConfigurationImpl(true,
-                                                  Collections.singletonList(mTLSConfig),
-                                                  rbacConfig,
-                                                  Collections.singleton(ADMIN_IDENTITY),
-                                                  new CacheConfigurationImpl(MillisecondBoundConfiguration.parse("1s"),
-                                                                             MillisecondBoundConfiguration.parse("1s"),
-                                                                             100,
-                                                                             true,
-                                                                             5,
-                                                                             MillisecondBoundConfiguration.parse("1s")));
+        CacheConfigurationImpl cacheConfiguration = CacheConfigurationImpl.builder()
+                                                                          .expireAfterAccess(MillisecondBoundConfiguration.parse("1s"))
+                                                                          .refreshAfterWrite(MillisecondBoundConfiguration.parse("1s"))
+                                                                          .maximumSize(100)
+                                                                          .enabled(true)
+                                                                          .warmupRetries(5)
+                                                                          .warmupRetryInterval(MillisecondBoundConfiguration.parse("1s"))
+                                                                          .build();
+        return AccessControlConfigurationImpl.builder()
+                                             .enabled(true)
+                                             .authenticatorsConfiguration(List.of(mTLSConfig))
+                                             .authorizerConfiguration(rbacConfig)
+                                             .adminIdentities(Set.of(ADMIN_IDENTITY))
+                                             .permissionCacheConfiguration(cacheConfiguration)
+                                             .build();
     }
 
     class WrapperInstancesMetadata implements InstancesMetadata
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
index 79d2cd2b..744c1551 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
@@ -27,6 +27,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -382,12 +383,17 @@ class MutualTLSAuthenticationHandlerTest
 
 
             String className = "org.apache.cassandra.sidecar.acl.authorization.AllowAllAuthorizationProvider";
-            AccessControlConfiguration accessControlConfiguration
-            = new AccessControlConfigurationImpl(true,
-                                                 authenticatorsConfiguration(),
-                                                 new ParameterizedClassConfigurationImpl(className, Collections.emptyMap()),
-                                                 Collections.singleton(ADMIN_IDENTITY),
-                                                 new CacheConfigurationImpl(MillisecondBoundConfiguration.parse("30s"), 100));
+            CacheConfigurationImpl permissionCacheConfiguration = CacheConfigurationImpl.builder()
+                                                                                        .expireAfterAccess(MillisecondBoundConfiguration.parse("30s"))
+                                                                                        .maximumSize(100)
+                                                                                        .build();
+            AccessControlConfiguration accessControlConfiguration = AccessControlConfigurationImpl.builder()
+                                                                                                  .enabled(true)
+                                                                                                  .authenticatorsConfiguration(authenticatorsConfiguration())
+                                                                                                  .authorizerConfiguration(new ParameterizedClassConfigurationImpl(className, Collections.emptyMap()))
+                                                                                                  .adminIdentities(Set.of(ADMIN_IDENTITY))
+                                                                                                  .permissionCacheConfiguration(permissionCacheConfiguration)
+                                                                                                  .build();
 
             return super.abstractConfig(sslConfiguration, builder -> builder.accessControlConfiguration(accessControlConfiguration));
         }
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImplTest.java b/server/src/test/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImplTest.java
index ff16f9cd..92bde7d1 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImplTest.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImplTest.java
@@ -52,9 +52,14 @@ class CacheConfigurationImplTest
         MillisecondBoundConfiguration refreshAfterWrite = MillisecondBoundConfiguration.parse("5m");
         MillisecondBoundConfiguration warmupRetryInterval = MillisecondBoundConfiguration.parse("2s");
 
-        CacheConfigurationImpl config
-        = new CacheConfigurationImpl(expireAfterAccess, refreshAfterWrite, 1000, false,
-                                     10, warmupRetryInterval);
+        CacheConfigurationImpl config = CacheConfigurationImpl.builder()
+                                                              .expireAfterAccess(expireAfterAccess)
+                                                              .refreshAfterWrite(refreshAfterWrite)
+                                                              .maximumSize(1000)
+                                                              .enabled(false)
+                                                              .warmupRetries(10)
+                                                              .warmupRetryInterval(warmupRetryInterval)
+                                                              .build();
 
         assertThat(config.expireAfterAccess()).isEqualTo(expireAfterAccess);
         assertThat(config.refreshAfterWrite()).isEqualTo(refreshAfterWrite);
@@ -67,10 +72,13 @@ class CacheConfigurationImplTest
     @Test
     void testBuilderWithDefaults()
     {
-        CacheConfigurationImpl config = new CacheConfigurationImpl(MillisecondBoundConfiguration.parse("30m"), 100);
+        CacheConfigurationImpl config = CacheConfigurationImpl.builder()
+                                                              .expireAfterAccess(MillisecondBoundConfiguration.parse("30m"))
+                                                              .maximumSize(100)
+                                                              .build();
 
         assertThat(config.expireAfterAccess()).isEqualTo(MillisecondBoundConfiguration.parse("30m"));
-        assertThat(config.refreshAfterWrite()).isEqualTo(MillisecondBoundConfiguration.parse("1h"));
+        assertThat(config.refreshAfterWrite()).isNull();
         assertThat(config.maximumSize()).isEqualTo(100);
         assertThat(config.enabled()).isTrue();
         assertThat(config.warmupRetries()).isEqualTo(5);
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/utils/CacheFactoryTest.java b/server/src/test/java/org/apache/cassandra/sidecar/utils/CacheFactoryTest.java
index 617ab2e1..3dacee89 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/utils/CacheFactoryTest.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/utils/CacheFactoryTest.java
@@ -56,8 +56,10 @@ class CacheFactoryTest
     {
         fakeTicker = new FakeTicker();
 
-        CacheConfiguration ssTableImportCacheConfiguration =
-        new CacheConfigurationImpl(SSTABLE_IMPORT_EXPIRE_AFTER_ACCESS, SSTABLE_IMPORT_CACHE_MAX_SIZE);
+        CacheConfiguration ssTableImportCacheConfiguration = CacheConfigurationImpl.builder()
+                                                                                   .expireAfterAccess(SSTABLE_IMPORT_EXPIRE_AFTER_ACCESS)
+                                                                                   .maximumSize(SSTABLE_IMPORT_CACHE_MAX_SIZE)
+                                                                                   .build();
 
         SSTableImportConfiguration ssTableImportConfiguration =
         new SSTableImportConfigurationImpl(ssTableImportCacheConfiguration);
diff --git a/test-common/src/testFixtures/java/org/apache/cassandra/distributed/impl/CassandraCluster.java b/test-common/src/testFixtures/java/org/apache/cassandra/distributed/impl/CassandraCluster.java
index 9fb27010..2a2f049e 100644
--- a/test-common/src/testFixtures/java/org/apache/cassandra/distributed/impl/CassandraCluster.java
+++ b/test-common/src/testFixtures/java/org/apache/cassandra/distributed/impl/CassandraCluster.java
@@ -133,7 +133,7 @@ public class CassandraCluster<I extends IInstance> implements IClusterExtension<
                             : TestTokenSupplier.evenlyDistributedTokens(nodesPerDc, newNodesPerDc, dcCount, 1);
             instanceConfigUpdater = config -> configuration.features.forEach(config::with);
         }
-        if (configuration.additionalInstanceConfig != null)
+        if (configuration.additionalInstanceConfig != null && !configuration.additionalInstanceConfig.isEmpty())
         {
             instanceConfigUpdater = instanceConfigUpdater.andThen(config -> configuration.additionalInstanceConfig.forEach(config::set));
         }


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