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