arjunashok commented on code in PR #131:
URL: https://github.com/apache/cassandra-sidecar/pull/131#discussion_r1718970939


##########
src/test/java/org/apache/cassandra/sidecar/auth/MutualTlsAuthorizationTest.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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.auth;
+
+import java.io.File;
+import java.net.InetAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.Ticker;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.util.Modules;
+import io.vertx.core.Vertx;
+import io.vertx.core.net.JksOptions;
+import io.vertx.ext.auth.authorization.AndAuthorization;
+import io.vertx.ext.auth.authorization.OrAuthorization;
+import io.vertx.ext.auth.authorization.RoleBasedAuthorization;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.client.WebClientOptions;
+import io.vertx.ext.web.codec.BodyCodec;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.TestSslModule;
+import org.apache.cassandra.sidecar.auth.authentication.AuthenticatorConfig;
+import 
org.apache.cassandra.sidecar.auth.authentication.CertificateValidatorConfig;
+import 
org.apache.cassandra.sidecar.auth.authentication.IdentityValidatorConfig;
+import org.apache.cassandra.sidecar.auth.authorization.AuthorizerConfig;
+import org.apache.cassandra.sidecar.auth.authorization.MutualTlsPermissions;
+import org.apache.cassandra.sidecar.auth.authorization.PermissionsAccessor;
+import 
org.apache.cassandra.sidecar.auth.authorization.RequiredPermissionsProvider;
+import 
org.apache.cassandra.sidecar.auth.authorization.SystemAuthDatabaseAccessor;
+import org.apache.cassandra.sidecar.config.AuthenticatorConfiguration;
+import org.apache.cassandra.sidecar.config.AuthorizerConfiguration;
+import org.apache.cassandra.sidecar.config.CacheConfiguration;
+import org.apache.cassandra.sidecar.config.ServiceConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.config.SslConfiguration;
+import org.apache.cassandra.sidecar.config.yaml.AuthenticatorConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.AuthorizerConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.KeyStoreConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.SslConfigurationImpl;
+import org.apache.cassandra.sidecar.routes.MutualTlsAuthorizationHandler;
+import org.apache.cassandra.sidecar.server.MainModule;
+import org.apache.cassandra.sidecar.server.Server;
+import org.apache.cassandra.sidecar.utils.CacheFactory;
+import org.apache.cassandra.sidecar.utils.CertificateBuilder;
+import org.apache.cassandra.sidecar.utils.CertificateBundle;
+import org.apache.cassandra.sidecar.utils.SSTableImporter;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests MutualTLS Authorization for {@link MutualTlsAuthorizationHandler}
+ */
+@ExtendWith(VertxExtension.class)
+public class MutualTlsAuthorizationTest
+{
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(MutualTlsAuthorizationTest.class);
+
+    @TempDir
+    File tempDir;
+    TestModule testModule;
+    RequiredPermissionsProvider mockRequiredPermissionsProvider;
+    private Server server;
+    private Vertx vertx;
+    private CertificateBundle ca;
+    private CertificateBundle badCA;
+    private Path truststorePath;
+    private Path untrustedTruststorePath;
+
+    TestModule testModule() throws Exception
+    {
+        ca = new CertificateBuilder().subject("CN=Apache cassandra Root CA, 
OU=Certification Authority, O=Unknown, C=Unknown")
+                                     .alias("fakerootca")
+                                     .isCertificateAuthority(true)
+                                     .buildSelfSigned();
+
+        badCA = new CertificateBuilder().subject("CN=Untrusted CA, 
OU=Certification Authority, O=Unknown, C=Unknown")
+                                        .alias("fakerootca_bad")
+                                        .isCertificateAuthority(true)
+                                        .buildSelfSigned();
+
+        truststorePath = ca.toTempKeyStorePath(tempDir.toPath(),
+                                               "password".toCharArray(),
+                                               "password".toCharArray());
+
+        untrustedTruststorePath = badCA.toTempKeyStorePath(tempDir.toPath(),
+                                                           
"password".toCharArray(),
+                                                           
"password".toCharArray());
+
+
+        CertificateBundle keystore =
+        new CertificateBuilder().subject("CN=Apache Cassandra, OU=ssl_test, 
O=Unknown, L=Unknown, ST=Unknown, C=Unknown")
+                                
.addSanDnsName(InetAddress.getLocalHost().getCanonicalHostName())
+                                
.addSanDnsName(InetAddress.getLocalHost().getHostName())
+                                .addSanDnsName("localhost")
+                                .buildIssuedBy(ca);
+
+        Path serverKeystorePath = keystore.toTempKeyStorePath(tempDir.toPath(),
+                                                              
"password".toCharArray(),
+                                                              
"password".toCharArray());
+
+        mockRequiredPermissionsProvider = 
mock(RequiredPermissionsProvider.class);
+
+        return new TestMTLSModule(serverKeystorePath, truststorePath, 
mockRequiredPermissionsProvider);
+    }
+
+    @BeforeEach
+    void setUp() throws Exception
+    {
+        testModule = testModule();
+
+        Injector injector = Guice.createInjector(Modules.override(new 
MainModule())
+                                                        .with(testModule));
+        server = injector.getInstance(Server.class);
+        vertx = injector.getInstance(Vertx.class);
+
+        VertxTestContext context = new VertxTestContext();
+        server.start()
+              .onSuccess(s -> context.completeNow())
+              .onFailure(context::failNow);
+
+        context.awaitCompletion(5, TimeUnit.SECONDS);
+    }
+
+    @AfterEach
+    void tearDown() throws InterruptedException
+    {
+        final CountDownLatch closeLatch = new CountDownLatch(1);
+        server.close().onSuccess(res -> closeLatch.countDown());
+        if (closeLatch.await(60, TimeUnit.SECONDS))
+            LOGGER.info("Close event received before timeout.");
+        else
+            LOGGER.error("Close event timed out.");
+    }
+
+    @Test
+    void testSidecarHealthCheckReturnsOK(VertxTestContext testContext) throws 
Exception
+    {
+        AndAuthorization auth1 = AndAuthorization.create();
+        OrAuthorization auth1Or = OrAuthorization.create();
+        
auth1Or.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.DROP.name())
+                                                       .setResource("<ALL 
KEYSPACES>"));
+        
auth1Or.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.DROP.name())
+                                                       .setResource("<keyspace 
system_auth>"));
+        auth1.addAuthorization(auth1Or);
+
+        when(mockRequiredPermissionsProvider.requiredPermissions("GET 
/api/v1/__health", new HashMap<>()))
+        .thenReturn(auth1);
+
+        Path clientKeystorePath = generateClientCertificate(null, ca, 
"spiffe://identity1");
+
+        String url = "/api/v1/__health";
+
+        WebClient client = client(clientKeystorePath, truststorePath);
+
+        client.get(server.actualPort(), "localhost", url)
+              .as(BodyCodec.string())
+              .ssl(true)
+              .send(testContext.succeeding(response -> testContext.verify(() ->
+                                          {
+                                              
assertThat(response.statusCode()).isEqualTo(OK.code());
+                                              
assertThat(response.body()).isEqualTo("{\"status\":\"OK\"}");
+                                              testContext.completeNow();
+                                          })));
+    }
+
+    @Test
+    void testWorksForAllPermissions(VertxTestContext testContext) throws 
Exception
+    {
+        AndAuthorization auth2 = AndAuthorization.create();
+        OrAuthorization auth2Or = OrAuthorization.create();
+        
auth2Or.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.DROP.name())
+                                                       .setResource("<ALL 
KEYSPACES>"));
+        
auth2Or.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.DROP.name())
+                                                       .setResource("<keyspace 
system_auth>"));
+        auth2.addAuthorization(auth2Or);
+
+        when(mockRequiredPermissionsProvider.requiredPermissions("GET 
/api/v1/__health", new HashMap<>()))
+        .thenReturn(auth2);
+
+        Path clientKeystorePath = generateClientCertificate(null, ca, 
"spiffe://identity2");
+
+        String url = "/api/v1/__health";
+
+        WebClient client = client(clientKeystorePath, truststorePath);
+
+        client.get(server.actualPort(), "localhost", url)
+              .as(BodyCodec.string())
+              .ssl(true)
+              .send(testContext.succeeding(response -> testContext.verify(() ->
+                                          {
+                                              
assertThat(response.statusCode()).isEqualTo(OK.code());
+                                              
assertThat(response.body()).isEqualTo("{\"status\":\"OK\"}");
+                                              testContext.completeNow();
+                                          })));
+    }
+
+    @Test
+    void testIncorrectPermissions(VertxTestContext testContext) throws 
Exception
+    {
+        AndAuthorization auth3 = AndAuthorization.create();
+        OrAuthorization auth3Or = OrAuthorization.create();
+        
auth3Or.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.CREATE.name())
+                                                       .setResource("<ALL 
KEYSPACES>"));
+        
auth3Or.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.CREATE.name())
+                                                       .setResource("<keyspace 
system_auth>"));
+        auth3.addAuthorization(auth3Or);
+
+        when(mockRequiredPermissionsProvider.requiredPermissions("GET 
/api/v1/__health", new HashMap<>()))
+        .thenReturn(auth3);
+
+        Path clientKeystorePath = generateClientCertificate(null, ca, 
"spiffe://identity3");
+
+        String url = "/api/v1/__health";
+
+        WebClient client = client(clientKeystorePath, truststorePath);
+
+        client.get(server.actualPort(), "localhost", url)
+              .as(BodyCodec.string())
+              .ssl(true)
+              .send(testContext.succeeding(response -> testContext.verify(() ->
+                                          {
+                                              
assertThat(response.statusCode()).isEqualTo(UNAUTHORIZED.code());
+                                              assertThat(response.body())
+                                              
.isEqualTo("{\"status\":\"Unauthorized\",\"code\":401,\"message\":\"Not 
Authorized\"}");
+                                              testContext.completeNow();
+                                          })));
+    }
+
+    @Test
+    void testCorrectPermissionsWrongKeyspace(VertxTestContext testContext) 
throws Exception
+    {
+        AndAuthorization auth4 = AndAuthorization.create();
+        OrAuthorization auth4Or1 = OrAuthorization.create();
+        
auth4Or1.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.CREATE.name())
+                                                        .setResource("<ALL 
KEYSPACES>"));
+        
auth4Or1.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.CREATE.name())
+                                                        
.setResource("<keyspace system_auth>"));
+        auth4.addAuthorization(auth4Or1);
+        OrAuthorization auth4Or2 = OrAuthorization.create();
+        
auth4Or2.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.SELECT.name())
+                                                        .setResource("<ALL 
KEYSPACES>"));
+        
auth4Or2.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.SELECT.name())
+                                                        
.setResource("<keyspace system_auth>"));
+        auth4.addAuthorization(auth4Or2);
+        when(mockRequiredPermissionsProvider.requiredPermissions("GET 
/api/v1/__health", new HashMap<>()))
+        .thenReturn(auth4);
+
+        Path clientKeystorePath = generateClientCertificate(null, ca, 
"spiffe://identity4");
+
+        String url = "/api/v1/__health";
+
+        WebClient client = client(clientKeystorePath, truststorePath);
+
+        client.get(server.actualPort(), "localhost", url)
+              .as(BodyCodec.string())
+              .ssl(true)
+              .send(testContext.succeeding(response -> testContext.verify(() ->
+                                          {
+                                              
assertThat(response.statusCode()).isEqualTo(UNAUTHORIZED.code());
+                                              assertThat(response.body())
+                                              
.isEqualTo("{\"status\":\"Unauthorized\",\"code\":401,\"message\":\"Not 
Authorized\"}");
+                                              testContext.completeNow();
+                                          })));
+    }
+
+    @Test
+    void testWrongPermissionsCorrectKeyspace(VertxTestContext testContext) 
throws Exception
+    {
+        AndAuthorization auth5 = AndAuthorization.create();
+        OrAuthorization auth5Or = OrAuthorization.create();
+        
auth5Or.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.CREATE.name())
+                                                       .setResource("<ALL 
KEYSPACES>"));
+        
auth5Or.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.CREATE.name())
+                                                       .setResource("<keyspace 
system_auth>"));
+        auth5.addAuthorization(auth5Or);
+        when(mockRequiredPermissionsProvider.requiredPermissions("GET 
/api/v1/__health", new HashMap<>()))
+        .thenReturn(auth5);
+
+
+        Path clientKeystorePath = generateClientCertificate(null, ca, 
"spiffe://identity5");
+
+        String url = "/api/v1/__health";
+
+        WebClient client = client(clientKeystorePath, truststorePath);
+
+        client.get(server.actualPort(), "localhost", url)
+              .as(BodyCodec.string())
+              .ssl(true)
+              .send(testContext.succeeding(response -> testContext.verify(() ->
+                                          {
+                                              
assertThat(response.statusCode()).isEqualTo(UNAUTHORIZED.code());
+                                              assertThat(response.body())
+                                              
.isEqualTo("{\"status\":\"Unauthorized\",\"code\":401,\"message\":\"Not 
Authorized\"}");
+                                              testContext.completeNow();
+                                          })));
+    }
+
+    @Test
+    void testSuperUserStatus(VertxTestContext testContext) throws Exception
+    {
+        AndAuthorization auth6 = AndAuthorization.create();
+        OrAuthorization auth6Or = OrAuthorization.create();
+        
auth6Or.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.CREATE.name())
+                                                       .setResource("<ALL 
KEYSPACES>"));
+        
auth6Or.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.CREATE.name())
+                                                       .setResource("<keyspace 
system_auth>"));
+        auth6.addAuthorization(auth6Or);
+        when(mockRequiredPermissionsProvider.requiredPermissions("GET 
/api/v1/__health", new HashMap<>()))
+        .thenReturn(auth6);
+
+        Path clientKeystorePath = generateClientCertificate(null, ca, 
"spiffe://adminID");
+
+        String url = "/api/v1/__health";
+
+        WebClient client = client(clientKeystorePath, truststorePath);
+
+        client.get(server.actualPort(), "localhost", url)
+              .as(BodyCodec.string())
+              .ssl(true)
+              .send(testContext.succeeding(response -> testContext.verify(() ->
+                                          {
+                                              
assertThat(response.statusCode()).isEqualTo(OK.code());
+                                              
assertThat(response.body()).isEqualTo("{\"status\":\"OK\"}");
+                                              testContext.completeNow();
+                                          })));
+    }
+
+    @Test
+    void testHasSomePermissions(VertxTestContext testContext) throws Exception
+    {
+        AndAuthorization auth7 = AndAuthorization.create();
+        OrAuthorization auth7Or1 = OrAuthorization.create();
+        
auth7Or1.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.CREATE.name())
+                                                        .setResource("<ALL 
KEYSPACES>"));
+        
auth7Or1.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.CREATE.name())
+                                                        
.setResource("<keyspace system_auth>"));
+        auth7.addAuthorization(auth7Or1);
+        OrAuthorization auth7Or2 = OrAuthorization.create();
+        
auth7Or2.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.SELECT.name())
+                                                        .setResource("<ALL 
KEYSPACES>"));
+        
auth7Or2.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.SELECT.name())
+                                                        
.setResource("<keyspace system_auth>"));
+        auth7.addAuthorization(auth7Or2);
+        OrAuthorization auth7Or3 = OrAuthorization.create();
+        
auth7Or3.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.DROP.name())
+                                                        .setResource("<ALL 
KEYSPACES>"));
+        
auth7Or3.addAuthorization(RoleBasedAuthorization.create(MutualTlsPermissions.DROP.name())
+                                                        
.setResource("<keyspace system_auth>"));
+        auth7.addAuthorization(auth7Or3);
+        when(mockRequiredPermissionsProvider.requiredPermissions("GET 
/api/v1/__health", new HashMap<>()))
+        .thenReturn(auth7);
+
+        Path clientKeystorePath = generateClientCertificate(null, ca, 
"spiffe://identity6");
+
+        String url = "/api/v1/__health";
+
+        WebClient client = client(clientKeystorePath, truststorePath);
+
+        client.get(server.actualPort(), "localhost", url)
+              .as(BodyCodec.string())
+              .ssl(true)
+              .send(testContext.succeeding(response -> testContext.verify(() ->
+                                          {
+                                              
assertThat(response.statusCode()).isEqualTo(UNAUTHORIZED.code());
+                                              assertThat(response.body())
+                                              
.isEqualTo("{\"status\":\"Unauthorized\",\"code\":401,\"message\":\"Not 
Authorized\"}");
+                                              testContext.completeNow();
+                                          })));
+    }
+
+    private WebClient client(Path clientKeystorePath, Path 
clientTruststorePath)
+    {
+        return WebClient.create(vertx, webClientOptions(clientKeystorePath, 
clientTruststorePath));
+    }
+
+    private WebClientOptions webClientOptions(Path clientKeystorePath, Path 
clientTruststorePath)
+    {
+        WebClientOptions options = new WebClientOptions();
+        options.setKeyStoreOptions(new 
JksOptions().setPath(clientKeystorePath.toString())
+                                                   .setPassword("cassandra"));
+        options.setTrustStoreOptions(new 
JksOptions().setPath(clientTruststorePath.toString())
+                                                     .setPassword("password"));
+        options.setSsl(true);
+        return options;
+    }
+
+    private Path generateClientCertificate(Function<CertificateBuilder, 
CertificateBuilder> customizeCertificate,
+                                           CertificateBundle 
certificateAuthority,
+                                           String identity) throws Exception
+    {
+
+        CertificateBuilder builder =
+        new CertificateBuilder().subject("CN=Apache Cassandra, OU=ssl_test, 
O=Unknown, L=Unknown, ST=Unknown, C=Unknown")
+                                .notBefore(Instant.now().minus(1, 
ChronoUnit.DAYS))
+                                .notAfter(Instant.now().plus(1, 
ChronoUnit.DAYS))
+                                .alias("spiffecert")
+                                .addSanUriName(identity)
+                                .rsa2048Algorithm();
+        if (customizeCertificate != null)
+        {
+            builder = customizeCertificate.apply(builder);
+        }
+        CertificateBundle ssc = builder.buildIssuedBy(certificateAuthority);
+
+        return ssc.toTempKeyStorePath(tempDir.toPath(), 
"cassandra".toCharArray(), "cassandra".toCharArray());
+    }
+
+    @Test
+    void testSidecarSpecificPermissions()
+    {
+
+    }

Review Comment:
   Pending/incomplete test?



-- 
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: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to