sarankk commented on code in PR #165:
URL: https://github.com/apache/cassandra-sidecar/pull/165#discussion_r1902037909


##########
server/src/test/integration/org/apache/cassandra/sidecar/acl/RoleBasedAuthorizationIntegrationTest.java:
##########
@@ -0,0 +1,428 @@
+/*
+ * 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.Collections;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.util.concurrent.Uninterruptibles;
+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.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.Checkpoint;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.config.SslConfiguration;
+import org.apache.cassandra.sidecar.config.yaml.KeyStoreConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.SslConfigurationImpl;
+import org.apache.cassandra.sidecar.testing.IntegrationTestBase;
+import org.apache.cassandra.testing.CassandraIntegrationTest;
+import org.apache.cassandra.testing.ConfigurableCassandraTestContext;
+
+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
+ */
+@ExtendWith(VertxExtension.class)
+public class RoleBasedAuthorizationIntegrationTest extends IntegrationTestBase
+{
+    private static final int MIN_VERSION_WITH_MTLS = 5;
+
+    @CassandraIntegrationTest(buildCluster = false)
+    void testForAdmin(VertxTestContext context, 
ConfigurableCassandraTestContext cassandraContext) throws Exception
+    {
+        // starts cluster for 5.0 and above version test
+        startClusterWithMtlsAndAuthorizer(cassandraContext);
+
+        String keyspaceSchemaRoute = 
String.format("/api/v1/keyspaces/%s/schema", "sample_keyspace");
+        // uses client keystore with admin identity. Admins bypass 
authorization checks
+        verifyAccess(context, HttpMethod.GET, keyspaceSchemaRoute, 
clientKeystorePath);
+    }
+
+    @CassandraIntegrationTest(buildCluster = false)
+    void testForSuperUser(VertxTestContext context, 
ConfigurableCassandraTestContext cassandraContext) throws Exception
+    {
+        // starts cluster for 5.0 and above version test
+        startClusterWithMtlsAndAuthorizer(cassandraContext);
+
+        createRole("test_role", true);
+        insertIdentityRole("spiffe://cassandra/sidecar/test_user", 
"test_role");
+
+        String keyspaceSchemaRoute = 
String.format("/api/v1/keyspaces/%s/schema", "sample_keyspace");
+        // uses client keystore with superuser identity
+        Path clientKeystorePath = 
clientKeystorePath("spiffe://cassandra/sidecar/test_user");
+        verifyAccess(context, HttpMethod.GET, keyspaceSchemaRoute, 
clientKeystorePath);
+    }
+
+    @CassandraIntegrationTest(buildCluster = false)
+    void testForNonAdmin(VertxTestContext context, 
ConfigurableCassandraTestContext cassandraContext) throws Exception
+    {
+        // starts cluster for 5.0 and above version test
+        startClusterWithMtlsAndAuthorizer(cassandraContext);
+
+        createRole("test_role", false);
+        insertIdentityRole("spiffe://cassandra/sidecar/test_user", 
"test_role");
+
+        // grant permission for not non super user
+        grantKeyspacePermission("sample_keyspace", "test_role");
+        String keyspaceSchemaRoute = 
String.format("/api/v1/keyspaces/%s/schema", "sample_keyspace");
+        Path clientKeystorePath = 
clientKeystorePath("spiffe://cassandra/sidecar/test_user");
+        verifyAccess(context, HttpMethod.GET, keyspaceSchemaRoute, 
clientKeystorePath);
+    }
+
+    @CassandraIntegrationTest(buildCluster = false)
+    void testGrantingForTable(VertxTestContext context, 
ConfigurableCassandraTestContext cassandraContext)
+    throws Exception
+    {
+        // starts cluster for 5.0 and above version test
+        startClusterWithMtlsAndAuthorizer(cassandraContext);
+
+        createRole("test_role", false);
+        insertIdentityRole("spiffe://cassandra/sidecar/test_user", 
"test_role");
+
+        grantSidecarPermission("test_role", 
"data/sample_keyspace/sample_table", "CREATE:SNAPSHOT");
+
+        String createSnapshotRoute = 
String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
+                                                   "sample_keyspace", 
"sample_table");
+        Path clientKeystorePath = 
clientKeystorePath("spiffe://cassandra/sidecar/test_user");
+
+        Checkpoint checkpoint = context.checkpoint(2);
+
+        // CREATE:SNAPSHOT permission granted for 
data/sample_keyspace/sample_table
+        verifyAccess(context, checkpoint, HttpMethod.PUT, createSnapshotRoute, 
clientKeystorePath, false);
+
+        // DELETE:SNAPSHOT permission not granted for 
data/sample_keyspace/sample_table
+        verifyAccess(context, checkpoint, HttpMethod.DELETE, 
createSnapshotRoute, clientKeystorePath, true);
+    }
+
+    @CassandraIntegrationTest(buildCluster = false)
+    void testEndpointWithOrAuthorization(VertxTestContext context, 
ConfigurableCassandraTestContext cassandraContext)
+    throws Exception
+    {
+        // starts cluster for 5.0 and above version test
+        startClusterWithMtlsAndAuthorizer(cassandraContext);
+
+        createRole("test_role", false);
+        insertIdentityRole("spiffe://cassandra/sidecar/test_user", 
"test_role");
+
+        grantSidecarPermission("test_role", "data/sample_keyspace", "VIEW:*");
+        String keyspaceSchemaRoute = 
String.format("/api/v1/keyspaces/%s/schema", "sample_keyspace");
+        Path clientKeystorePath = 
clientKeystorePath("spiffe://cassandra/sidecar/test_user");
+
+        // schema endpoint for keyspaces accepts CREATE, ALTER, DROP or 
DESCRIBE cassandra permissions.
+        // cassandra permission for test_role on sample_keyspace not granted, 
sidecar permission VIEW:* is used to
+        // grant access
+        verifyAccess(context, HttpMethod.GET, keyspaceSchemaRoute, 
clientKeystorePath);
+    }
+
+    @CassandraIntegrationTest(buildCluster = false)
+    void testWildcardActionForAllTargets(VertxTestContext context, 
ConfigurableCassandraTestContext cassandraContext)
+    throws Exception
+    {
+        // starts cluster for 5.0 and above version test
+        startClusterWithMtlsAndAuthorizer(cassandraContext);
+
+        createRole("test_role", false);
+        insertIdentityRole("spiffe://cassandra/sidecar/test_user", 
"test_role");
+
+        // VIEW action allowed across targets for same resource. VIEW:* for 
cluster resource allows VIEW:SCHEMA,
+        // VIEW:CDC, VIEW:CLUSTER etc
+        grantSidecarPermission("test_role", "cluster", "VIEW:*");
+
+        String timeSkewRoute = "/api/v1/time-skew";
+        Path clientKeystorePath = 
clientKeystorePath("spiffe://cassandra/sidecar/test_user");
+
+        Checkpoint checkpoint = context.checkpoint(4);
+
+        // Uses sidecar permission VIEW:* added
+        verifyAccess(context, checkpoint, HttpMethod.GET, timeSkewRoute, 
clientKeystorePath, false);
+
+        String schemaRoute = "/api/v1/cassandra/schema";
+        verifyAccess(context, checkpoint, HttpMethod.GET, schemaRoute, 
clientKeystorePath, false);
+
+        String ringRoute = "/api/v1/cassandra/ring";
+        // Allows VIEW:CLUSTER too
+        verifyAccess(context, checkpoint, HttpMethod.GET, ringRoute, 
clientKeystorePath, false);
+
+        String keyspaceSchemaRoute = 
String.format("/api/v1/keyspaces/%s/schema", "sample_keyspace");
+        // Does not allow finding schema for keyspace, keyspace schema route 
requires access specific to
+        // data/sample_keyspace resource
+        verifyAccess(context, checkpoint, HttpMethod.GET, keyspaceSchemaRoute, 
clientKeystorePath, true);
+    }
+
+    @CassandraIntegrationTest(buildCluster = false)
+    void testResourceWideActions(VertxTestContext context, 
ConfigurableCassandraTestContext cassandraContext) throws Exception
+    {
+        // starts cluster for 5.0 and above version test
+        startClusterWithMtlsAndAuthorizer(cassandraContext);
+
+        createRole("test_role", false);
+        insertIdentityRole("spiffe://cassandra/sidecar/test_user", 
"test_role");
+
+        grantSidecarPermission("test_role", "data/sample_keyspace", "*:*");
+
+        String keyspaceSchemaRoute = 
String.format("/api/v1/keyspaces/%s/schema", "sample_keyspace");
+        Path clientKeystorePath = 
clientKeystorePath("spiffe://cassandra/sidecar/test_user");
+
+        Checkpoint checkpoint = context.checkpoint(3);
+
+        // *:* permission across resource data/test_keyspace allows all 
possible actions across all possible targets,
+        // Such as VIEW:SCHEMA, VIEW:CLUSTER etc.
+        verifyAccess(context, checkpoint, HttpMethod.GET, keyspaceSchemaRoute, 
clientKeystorePath, false);
+
+        String keyspaceRingRoute = 
String.format("/api/v1/cassandra/ring/keyspaces/%s", "sample_keyspace");
+        // VIEW:CLUSTER granted
+        verifyAccess(context, checkpoint, HttpMethod.GET, keyspaceRingRoute, 
clientKeystorePath, false);
+
+        String viewSnapshotRoute = 
String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/%s",
+                                                 "sample_keyspace", 
"sample_table", "sample_snapshot");
+        // does not allow VIEW:SNAPSHOT which requires table resource too
+        verifyAccess(context, checkpoint, HttpMethod.GET, viewSnapshotRoute, 
clientKeystorePath, true);
+    }
+
+    @CassandraIntegrationTest(buildCluster = false)
+    void testAllWildcardActionsForTarget(VertxTestContext context, 
ConfigurableCassandraTestContext cassandraContext)
+    throws Exception
+    {
+        // starts cluster for 5.0 and above version test
+        startClusterWithMtlsAndAuthorizer(cassandraContext);
+
+        createRole("test_role", false);
+        insertIdentityRole("spiffe://cassandra/sidecar/test_user", 
"test_role");
+
+        grantSidecarPermission("test_role", 
"data/sample_keyspace/sample_table", "*:SNAPSHOT");
+
+        String createSnapshotRoute = 
String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
+                                                   "sample_keyspace", 
"sample_table");
+        Path clientKeystorePath = 
clientKeystorePath("spiffe://cassandra/sidecar/test_user");
+
+        Checkpoint checkpoint = context.checkpoint(2);
+
+        // *:SNAPSHOT permission across data/sample_resource/sample_table 
allows all possible actions for SNAPSHOT target
+        // such as CREATE:SNAPSHOT, VIEW:SNAPSHOT, DELETE:SNAPSHOT. Does not 
allow STREAM:SSTABLE or other actions
+        verifyAccess(context, checkpoint, HttpMethod.PUT, createSnapshotRoute, 
clientKeystorePath, false);
+
+        String streamSSTableRoute =  
String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/%s/components/%s",
+                                                   "sample_keyspace", 
"sample_table", "my-snapshot", "nc-1-big-Data.db");
+        verifyAccess(context, checkpoint, HttpMethod.GET, streamSSTableRoute, 
clientKeystorePath, true);
+    }
+
+    @CassandraIntegrationTest(buildCluster = false)
+    void testEndpointRequiringMultipleActions(VertxTestContext context, 
ConfigurableCassandraTestContext cassandraContext)
+    throws Exception
+    {
+       // starts cluster for 5.0 and above version test
+        startClusterWithMtlsAndAuthorizer(cassandraContext);
+
+        createRole("test_role", false);
+        insertIdentityRole("spiffe://cassandra/sidecar/test_user", 
"test_role");
+
+        grantSidecarPermission("test_role", 
"data/sample_keyspace/sample_table", "CREATE:SNAPSHOT");
+
+        String createSnapshotRoute = 
String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
+                                                   "sample_keyspace", 
"sample_table");
+        Path clientKeystorePath = 
clientKeystorePath("spiffe://cassandra/sidecar/test_user");
+
+        String streamRoute
+        =  
String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/%s/components/%s",
+                         "sample_keyspace", "sample_table", "my-snapshot", 
"nc-1-big-Data.db");
+
+        // CREATE:SNAPSHOT permission granted for 
data/sample_keyspace/sample_table
+        WebClient client = createClient(clientKeystorePath, truststorePath);
+        client.put(server.actualPort(), "127.0.0.1", createSnapshotRoute)
+              .send()
+              .compose(createResp -> {
+                  
assertThat(createResp.statusCode()).isEqualTo(HttpResponseStatus.OK.code());
+
+                  // grant sidecar permission for streaming
+                  updateSidecarPermission("test_role", 
"data/sample_keyspace/sample_table", "STREAM:SSTABLE");
+
+                  // wait for cache refresh
+                  Uninterruptibles.sleepUninterruptibly(3000, 
TimeUnit.MILLISECONDS);
+
+                  // STREAM SSTable request requires both Sidecar 
STREAM:SSTABLE permission and Cassandra's SELECT
+                  // permission on a table it accesses data.
+                  return streamRequest(client, streamRoute);
+              })
+              .compose(deniedStreamResp -> {
+                  // access denied without SELECT permission
+                  
assertThat(deniedStreamResp.statusCode()).isEqualTo(HttpResponseStatus.FORBIDDEN.code());
+
+                  // grant SELECT permission with cassandra role
+                  grantTablePermission("sample_keyspace", "sample_table", 
"test_role");
+
+                  // wait for cache refresh
+                  Uninterruptibles.sleepUninterruptibly(3000, 
TimeUnit.MILLISECONDS);
+
+                  return streamRequest(client, streamRoute);
+              })
+              .onFailure(context::failNow)
+              .onComplete(acceptedStreamResp -> {
+                  if (acceptedStreamResp.cause() != null)
+                  {
+                      context.failNow(acceptedStreamResp.cause());
+                      return;
+                  }
+
+                  // request goes through with both permissions granted
+                  
assertThat(acceptedStreamResp.result().statusCode()).isEqualTo(HttpResponseStatus.OK.code());
+                  context.completeNow();
+              });
+    }
+
+    private void 
startClusterWithMtlsAndAuthorizer(ConfigurableCassandraTestContext  
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);
+
+        cassandraContext.configureAndStartCluster(builder -> {
+            builder.appendConfig(config -> 
config.set("authenticator.class_name",
+                                                      
"org.apache.cassandra.auth.MutualTlsWithPasswordFallbackAuthenticator")
+                                                 
.set("authenticator.parameters",
+                                                      
Collections.singletonMap("validator_class_name", 
"org.apache.cassandra.auth.SpiffeCertificateValidator"))
+                                                 .set("role_manager", 
"CassandraRoleManager")
+                                                 .set("authorizer", 
"CassandraAuthorizer")
+                                                 
.set("client_encryption_options.enabled", "true")
+                                                 
.set("client_encryption_options.optional", "true")
+                                                 
.set("client_encryption_options.require_client_auth", "true")
+                                                 
.set("client_encryption_options.require_endpoint_verification", "false")
+                                                 
.set("client_encryption_options.keystore", 
serverKeystorePath.toAbsolutePath().toString())
+                                                 
.set("client_encryption_options.keystore_password", serverKeystorePassword)
+                                                 
.set("client_encryption_options.truststore", 
truststorePath.toAbsolutePath().toString())
+                                                 
.set("client_encryption_options.truststore_password", truststorePassword));
+        });
+        waitForSchemaReady(30, TimeUnit.SECONDS);
+
+        // required for authentication of sidecar requests to Cassandra. Only 
superusers can grant permissions
+        insertIdentityRole(ADMIN_IDENTITY, "cassandra");
+        createKeyspaceTable();
+
+        // Add keystore for Sidecar
+        
sidecarTestContext.setSslConfiguration(sslConfigWithKeystoreTruststore());
+        Thread.sleep(2000);
+    }
+
+    private void createKeyspaceTable()
+    {
+        createKeyspace("sample_keyspace");
+        createTable("sample_keyspace", "sample_table");
+    }
+
+    private void createRole(String role, boolean superUser)
+    {
+        Session session = maybeGetSession();
+        session.execute("CREATE ROLE " + role + " WITH PASSWORD = 'password' 
AND SUPERUSER = " + superUser + " AND LOGIN = true;");
+    }
+
+    private void insertIdentityRole(String identity, String role)
+    {
+        Session session = maybeGetSession();
+        session.execute("INSERT INTO system_auth.identity_to_role (identity, 
role) VALUES (\'" + identity + "\',\'" + role + "\');");
+    }
+
+    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 grantKeyspacePermission(String keyspace, String role)
+    {
+        Session session = maybeGetSession();
+        session.execute("GRANT ALL PERMISSIONS ON KEYSPACE " + keyspace + " TO 
" + role);
+    }
+
+    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 SslConfiguration sslConfigWithKeystoreTruststore()
+    {
+        return SslConfigurationImpl
+               .builder()
+               .enabled(true)
+               .keystore(new 
KeyStoreConfigurationImpl(clientKeystorePath.toAbsolutePath().toString(), 
clientKeystorePassword, "PKCS12"))
+               .truststore(new 
KeyStoreConfigurationImpl(truststorePath.toAbsolutePath().toString(), 
truststorePassword, "PKCS12"))
+               .build();
+    }
+
+    private void verifyAccess(VertxTestContext context, HttpMethod method, 
String testRoute, Path clientKeystorePath)
+    {
+        Checkpoint checkpoint = context.checkpoint();
+        verifyAccess(context, checkpoint, method, testRoute, 
clientKeystorePath, false);
+    }
+
+    private void verifyAccess(VertxTestContext context, Checkpoint checkpoint, 
HttpMethod method,
+                              String testRoute, Path clientKeystorePath, 
boolean expectForbidden)

Review Comment:
   We use this method to check both access allowed cases and access denied 
cases. Hence named it `verifyAccess` 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscr...@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscr...@cassandra.apache.org
For additional commands, e-mail: pr-h...@cassandra.apache.org

Reply via email to