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]

