diff --git a/airbyte-cdk/bulk/toolkits/extract-jdbc/build.gradle b/airbyte-cdk/bulk/toolkits/extract-jdbc/build.gradle index ec56ca6ca647..fdfdb68a2608 100644 --- a/airbyte-cdk/bulk/toolkits/extract-jdbc/build.gradle +++ b/airbyte-cdk/bulk/toolkits/extract-jdbc/build.gradle @@ -1,6 +1,11 @@ dependencies { implementation project(':airbyte-cdk:bulk:core:bulk-cdk-core-base') implementation project(':airbyte-cdk:bulk:core:bulk-cdk-core-extract') + api 'org.bouncycastle:bcpkix-jdk18on:1.77' + api 'org.bouncycastle:bcprov-jdk18on:1.77' + api 'org.bouncycastle:bctls-jdk18on:1.77' + api 'org.bouncycastle:bcpkix-jdk18on:1.77' + api 'org.apache.httpcomponents:httpcore:4.4' testFixturesApi testFixtures(project(':airbyte-cdk:bulk:core:bulk-cdk-core-base')) testFixturesApi testFixtures(project(':airbyte-cdk:bulk:core:bulk-cdk-core-extract')) diff --git a/airbyte-cdk/bulk/toolkits/extract-jdbc/src/main/kotlin/io/airbyte/cdk/jdbc/SSLCertificateUtils.kt b/airbyte-cdk/bulk/toolkits/extract-jdbc/src/main/kotlin/io/airbyte/cdk/jdbc/SSLCertificateUtils.kt new file mode 100644 index 000000000000..dd17f4ddfe78 --- /dev/null +++ b/airbyte-cdk/bulk/toolkits/extract-jdbc/src/main/kotlin/io/airbyte/cdk/jdbc/SSLCertificateUtils.kt @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2024 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.cdk.jdbc + +import io.github.oshai.kotlinlogging.KotlinLogging +import java.io.BufferedInputStream +import java.io.ByteArrayInputStream +import java.io.FileReader +import java.io.IOException +import java.net.URI +import java.nio.charset.StandardCharsets +import java.nio.file.FileSystem +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Path +import java.security.KeyFactory +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.NoSuchAlgorithmException +import java.security.PrivateKey +import java.security.SecureRandom +import java.security.Security +import java.security.cert.Certificate +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.spec.InvalidKeySpecException +import java.security.spec.PKCS8EncodedKeySpec +import java.util.* +import javax.net.ssl.SSLContext +import kotlin.text.Charsets.UTF_8 +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openssl.PEMEncryptedKeyPair +import org.bouncycastle.openssl.PEMKeyPair +import org.bouncycastle.openssl.PEMParser +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter +import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder + +private val log = KotlinLogging.logger {} + +/** + * General SSL utilities used for certificate and keystore operations related to secured db + * connections. + */ +object SSLCertificateUtils { + + private const val PKCS_12 = "PKCS12" + private const val X509 = "X.509" + private val RANDOM: Random = SecureRandom() + + // #17000: postgres driver is hardcoded to only load an entry alias "user" + const val KEYSTORE_ENTRY_PREFIX: String = "user" + const val KEYSTORE_FILE_NAME: String = KEYSTORE_ENTRY_PREFIX + "keystore_" + const val KEYSTORE_FILE_TYPE: String = ".p12" + + private fun saveKeyStoreToFile( + keyStore: KeyStore, + keyStorePassword: String, + filesystem: FileSystem, + directory: String + ): URI { + val pathToStore: Path = filesystem.getPath(directory) + val pathToFile = + pathToStore.resolve(KEYSTORE_FILE_NAME + RANDOM.nextInt() + KEYSTORE_FILE_TYPE) + val os = Files.newOutputStream(pathToFile) + keyStore.store(os, keyStorePassword.toCharArray()) + return pathToFile.toUri() + } + + private fun fromPEMString(certString: String): Certificate { + val cf = CertificateFactory.getInstance(X509) + val byteArrayInputStream = + ByteArrayInputStream(certString.toByteArray(StandardCharsets.UTF_8)) + val bufferedInputStream = BufferedInputStream(byteArrayInputStream) + return cf.generateCertificate(bufferedInputStream) + } + + fun keyStoreFromCertificate( + cert: Certificate, + keyStorePassword: String, + filesystem: FileSystem, + directory: String + ): URI { + val keyStore = KeyStore.getInstance(PKCS_12) + keyStore.load(null) + keyStore.setCertificateEntry(KEYSTORE_ENTRY_PREFIX + "1", cert) + return saveKeyStoreToFile(keyStore, keyStorePassword, filesystem, directory) + } + + fun keyStoreFromCertificate( + certString: String, + keyStorePassword: String, + filesystem: FileSystem, + directory: String + ): URI { + return keyStoreFromCertificate( + fromPEMString(certString), + keyStorePassword, + filesystem, + directory, + ) + } + + fun keyStoreFromCertificate(certString: String, keyStorePassword: String): URI { + return keyStoreFromCertificate( + fromPEMString(certString), + keyStorePassword, + FileSystems.getDefault(), + "" + ) + } + + fun keyStoreFromCertificate( + certString: String, + keyStorePassword: String, + directory: String + ): URI { + return keyStoreFromCertificate( + certString, + keyStorePassword, + FileSystems.getDefault(), + directory, + ) + } + + fun keyStoreFromClientCertificate( + cert: Certificate, + key: PrivateKey, + keyStorePassword: String, + filesystem: FileSystem, + directory: String + ): URI { + val keyStore = KeyStore.getInstance(PKCS_12) + keyStore.load(null) + keyStore.setKeyEntry( + KEYSTORE_ENTRY_PREFIX, + key, + keyStorePassword.toCharArray(), + arrayOf(cert), + ) + return saveKeyStoreToFile(keyStore, keyStorePassword, filesystem, directory) + } + + // Utility function to detect the key algorithm (RSA, DSA, EC) from the key bytes + fun detectKeyAlgorithm(keyBytes: ByteArray): KeyFactory { + return when { + isRsaKey(keyBytes) -> KeyFactory.getInstance("RSA", "BC") + isDsaKey(keyBytes) -> KeyFactory.getInstance("DSA", "BC") + isEcKey(keyBytes) -> KeyFactory.getInstance("EC", "BC") + else -> throw IllegalArgumentException("Unknown or unsupported key type") + } + } + + // Example heuristics for detecting the key type (you can adjust as needed) + fun isRsaKey(keyBytes: ByteArray): Boolean { + return keyBytes.size > 100 && keyBytes[0].toInt() == 0x30 // ASN.1 structure for RSA keys + } + + fun isDsaKey(keyBytes: ByteArray): Boolean { + return keyBytes.size > 50 && + keyBytes[0].toInt() == 0x30 // Adjust based on DSA key specifics + } + + fun isEcKey(keyBytes: ByteArray): Boolean { + return keyBytes.size > 50 && keyBytes[0].toInt() == 0x30 // ASN.1 structure for EC keys + } + + @JvmStatic + fun convertPKCS1ToPKCS8(pkcs1KeyPath: Path, pkcs8KeyPath: Path, keyStorePassword: String?) { + Security.addProvider(BouncyCastleProvider()) + FileReader(pkcs1KeyPath.toFile(), UTF_8).use { reader -> + val pemParser = PEMParser(reader) + val pemObject = pemParser.readObject() + // Convert PEM to a PrivateKey (JcaPEMKeyConverter handles different types like RSA, + // DSA, EC) + val converter = JcaPEMKeyConverter().setProvider("BC") + val privateKey = + when (pemObject) { + is PEMEncryptedKeyPair -> { + // Handle encrypted key (if it was encrypted with a password) + val decryptorProvider = + JcePEMDecryptorProviderBuilder().build(keyStorePassword?.toCharArray()) + val keyPair = pemObject.decryptKeyPair(decryptorProvider) + converter.getPrivateKey(keyPair.privateKeyInfo) + } + is PEMKeyPair -> { + // Handle non-encrypted key + converter.getPrivateKey(pemObject.privateKeyInfo) + } + else -> throw IllegalArgumentException("Unsupported key format") + } + + // Convert the private key to PKCS#8 format + val pkcs8EncodedKey = convertToPkcs8(privateKey) + + // Write the PKCS#8 encoded key in DER format to the output path + Files.write(pkcs8KeyPath, pkcs8EncodedKey) + } + } + + fun convertToPkcs8(privateKey: PrivateKey): ByteArray { + // Convert the private key to PKCS#8 format using PrivateKeyInfo + val privateKeyInfo = PrivateKeyInfo.getInstance(privateKey.encoded) + return privateKeyInfo.encoded + } + + @Throws( + IOException::class, + InterruptedException::class, + NoSuchAlgorithmException::class, + InvalidKeySpecException::class, + CertificateException::class, + KeyStoreException::class, + ) + fun keyStoreFromClientCertificate( + certString: String, + keyString: String, + keyStorePassword: String, + filesystem: FileSystem, + directory: String + ): URI { + // Convert RSA key (PKCS#1) to PKCS#8 key + // Note: java.security doesn't have a built-in support of PKCS#1 format. Hence we need a + // conversion using BouncyCastle. + + val tmpDir = Files.createTempDirectory(null) + val pkcs1Key = Files.createTempFile(tmpDir, null, null) + val pkcs8Key = Files.createTempFile(tmpDir, null, null) + pkcs1Key.toFile().deleteOnExit() + pkcs8Key.toFile().deleteOnExit() + + Files.write(pkcs1Key, keyString.toByteArray(StandardCharsets.UTF_8)) + convertPKCS1ToPKCS8(pkcs1Key.toAbsolutePath(), pkcs8Key.toAbsolutePath(), keyStorePassword) + val spec = PKCS8EncodedKeySpec(Files.readAllBytes(pkcs8Key)) + var privateKey = + try { + KeyFactory.getInstance("RSA").generatePrivate(spec) + } catch (ex1: InvalidKeySpecException) { + try { + KeyFactory.getInstance("DSA").generatePrivate(spec) + } catch (ex2: InvalidKeySpecException) { + KeyFactory.getInstance("EC").generatePrivate(spec) + } + } + + return keyStoreFromClientCertificate( + fromPEMString(certString), + privateKey, + keyStorePassword, + filesystem, + directory, + ) + } + + fun keyStoreFromClientCertificate( + certString: String, + keyString: String, + keyStorePassword: String, + directory: String + ): URI { + return keyStoreFromClientCertificate( + certString, + keyString, + keyStorePassword, + FileSystems.getDefault(), + directory, + ) + } + + fun createContextFromCaCert(caCertificate: String): SSLContext { + try { + val factory = CertificateFactory.getInstance(X509) + val trustedCa = + factory.generateCertificate( + ByteArrayInputStream(caCertificate.toByteArray(StandardCharsets.UTF_8)), + ) + val trustStore = KeyStore.getInstance(PKCS_12) + trustStore.load(null, null) + trustStore.setCertificateEntry("ca", trustedCa) + val sslContextBuilder = + org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(trustStore, null) + return sslContextBuilder.build() + } catch (e: Exception) { + throw RuntimeException(e) + } + } +} diff --git a/airbyte-cdk/bulk/toolkits/extract-jdbc/src/test/kotlin/io/airbyte/cdk/jdbc/SSLCertificateUtilsTest.kt b/airbyte-cdk/bulk/toolkits/extract-jdbc/src/test/kotlin/io/airbyte/cdk/jdbc/SSLCertificateUtilsTest.kt new file mode 100644 index 000000000000..9eb00a3bfffd --- /dev/null +++ b/airbyte-cdk/bulk/toolkits/extract-jdbc/src/test/kotlin/io/airbyte/cdk/jdbc/SSLCertificateUtilsTest.kt @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2023 Airbyte, Inc., all rights reserved. + */ +package io.airbyte.cdk.jdbc + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import java.io.IOException +import java.nio.file.FileSystem +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Path +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.NoSuchAlgorithmException +import java.security.cert.CertificateException +import java.security.spec.InvalidKeySpecException +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class SSLCertificateUtilsTest { + @Throws( + CertificateException::class, + IOException::class, + KeyStoreException::class, + NoSuchAlgorithmException::class + ) + fun testkeyStoreFromCertificateInternal( + certString: String, + pwd: String, + fs: FileSystem, + directory: String + ) { + val ksUri = SSLCertificateUtils.keyStoreFromCertificate(certString, pwd, fs, directory) + + val ks = KeyStore.getInstance("PKCS12") + val inputStream = Files.newInputStream(Path.of(ksUri)) + ks.load(inputStream, pwd.toCharArray()) + Assertions.assertEquals(1, ks.size()) + Files.delete(Path.of(ksUri)) + } + + @Test + @Throws( + CertificateException::class, + IOException::class, + KeyStoreException::class, + NoSuchAlgorithmException::class + ) + fun testkeyStoreFromCertificate() { + testkeyStoreFromCertificateInternal( + caPem, + KEY_STORE_PASSWORD, + FileSystems.getDefault(), + SLASH_TMP + ) + + val exception: Exception = + Assertions.assertThrows(CertificateException::class.java) { + testkeyStoreFromCertificateInternal( + caPem_Bad, + KEY_STORE_PASSWORD, + FileSystems.getDefault(), + SLASH_TMP + ) + } + Assertions.assertNotNull(exception) + } + + @Test + @Throws( + CertificateException::class, + IOException::class, + KeyStoreException::class, + NoSuchAlgorithmException::class + ) + fun testkeyStoreFromCertificateInMemory() { + testkeyStoreFromCertificateInternal(caPem, KEY_STORE_PASSWORD, FileSystems.getDefault(), "") + + val exception: Exception = + Assertions.assertThrows(CertificateException::class.java) { + testkeyStoreFromCertificateInternal( + caPem_Bad, + KEY_STORE_PASSWORD, + FileSystems.getDefault(), + "" + ) + } + Assertions.assertNotNull(exception) + } + + @SuppressFBWarnings("HARD_CODE_PASSWORD") + @Throws( + KeyStoreException::class, + IOException::class, + CertificateException::class, + NoSuchAlgorithmException::class, + InvalidKeySpecException::class, + InterruptedException::class + ) + fun testKeyStoreFromClientCertificateInternal( + certString: String, + keyString: String, + keyStorePassword: String, + filesystem: FileSystem, + directory: String + ) { + val ksUri = + SSLCertificateUtils.keyStoreFromClientCertificate( + certString, + keyString, + keyStorePassword, + filesystem, + directory + ) + val ks = KeyStore.getInstance("PKCS12") + val inputStream = Files.newInputStream(Path.of(ksUri)) + ks.load(inputStream, KEY_STORE_PASSWORD.toCharArray()) + Assertions.assertTrue(ks.isKeyEntry(SSLCertificateUtils.KEYSTORE_ENTRY_PREFIX)) + Assertions.assertFalse(ks.isKeyEntry("cd_")) + Assertions.assertEquals(1, ks.size()) + Files.delete(Path.of(ksUri)) + } + + @Test + @Throws( + CertificateException::class, + IOException::class, + NoSuchAlgorithmException::class, + InvalidKeySpecException::class, + KeyStoreException::class, + InterruptedException::class + ) + fun testKeyStoreFromClientCertificate() { + testKeyStoreFromClientCertificateInternal( + clientPem, + clientKey, + KEY_STORE_PASSWORD, + FileSystems.getDefault(), + SLASH_TMP + ) + + val exceptionKey: Exception = + Assertions.assertThrows(IllegalArgumentException::class.java) { + testKeyStoreFromClientCertificateInternal( + clientPem, + clientKey_wrong_format, + KEY_STORE_PASSWORD, + FileSystems.getDefault(), + SLASH_TMP + ) + } + Assertions.assertNotNull(exceptionKey) + + val exceptionCert: Exception = + Assertions.assertThrows(CertificateException::class.java) { + testKeyStoreFromClientCertificateInternal( + caPem_Bad, + clientKey, + KEY_STORE_PASSWORD, + FileSystems.getDefault(), + SLASH_TMP + ) + } + Assertions.assertNotNull(exceptionCert) + } + + @Test + @Throws( + CertificateException::class, + IOException::class, + NoSuchAlgorithmException::class, + InvalidKeySpecException::class, + KeyStoreException::class, + InterruptedException::class + ) + fun testKeyStoreFromClientCertificateInMemory() { + testKeyStoreFromClientCertificateInternal( + clientPem, + clientKey, + KEY_STORE_PASSWORD, + FileSystems.getDefault(), + "" + ) + + val exceptionKey: Exception = + Assertions.assertThrows(IllegalArgumentException::class.java) { + testKeyStoreFromClientCertificateInternal( + clientPem, + clientKey_wrong_format, + KEY_STORE_PASSWORD, + FileSystems.getDefault(), + "" + ) + } + Assertions.assertNotNull(exceptionKey) + + val exceptionCert: Exception = + Assertions.assertThrows(CertificateException::class.java) { + testKeyStoreFromClientCertificateInternal( + caPem_Bad, + clientKey, + KEY_STORE_PASSWORD, + FileSystems.getDefault(), + "" + ) + } + Assertions.assertNotNull(exceptionCert) + } + + companion object { + private const val SLASH_TMP = "/tmp" + private const val KEY_STORE_PASSWORD = "123456" + private const val KEY_STORE_PASSWORD2 = "78910" + val caPem: String = + (""" + -----BEGIN CERTIFICATE----- + MIIDAzCCAeugAwIBAgIBATANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR + TF9TZXJ2ZXJfOC4wLjMwX0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X + DTIyMDgwODA1NDMwOFoXDTMyMDgwNTA1NDMwOFowPDE6MDgGA1UEAwwxTXlTUUxf + U2VydmVyXzguMC4zMF9BdXRvX0dlbmVyYXRlZF9DQV9DZXJ0aWZpY2F0ZTCCASIw + DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKb2tDaE4TO/4xKRZ0QqpB4ho3cy + daw85Sn8VNLa42EJgZVpSr0WCFl11Go7r0O2TMvceaWsnJU7FLhYHSR+Dlm62yVO + 0DsnMOC0kUoDnjSE/PmponWnoC79fgXV7AwKxSW4LLxYlPHQb4Kb7rv+UJ3KbxZz + zB7JEm9WQCJ/byn1/jxQtoPGvWL2csX3RFr9QNh8UgpOBQsbebeLWNgxdYda2sz3 + kJcwk754Vj1mx6iszjLP0oHZu+RuoM+xIrpDmpPNMW/0rQl6q+vCymNxaxX8+MuW + czRJ1hjh4cVjArp8YhJCEMVaLajVkhbzYaPRsdW1NGjh+C3eZnOm5fRi35kCAwEA + AaMQMA4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAWKlbtUosXVy7 + LbFEuL3c2Igs023v0mQNvtZVBl5Qpsxpc3+ybmQfksEQoPxPKmWpsnWv5Bsvt335 + /NHv1wSajHEpoyDBtF1QT2rR/kjezFpiH9AY3xwtBdZhTDlc5UBrpyv+Issn1CZF + edcIk54Gzxifn+Et5WP8b6HV/ehdE0qQPtHDmendEaIHXg12/NE+hj3DocSVm8w/ + LUNeYd9wXefwMrEWwDn0DZSsShZmgJoppA15qOnq+FVW/bhZwRv5L4l3AJv0SGoA + o7DXxD0VGHDA6aC4tJssZbrnoDCBPzYmt9s9GwVupuEroJHZ0Wks4pt4Wx50DUgA + KC3v0Mo/gg== + -----END CERTIFICATE-----""".trimIndent()) + + val caPem_Bad: String = + (""" + -----BEGIN CERTIFICATE----- + MIIDAzCCAeugAwIBAgIBATANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR + TF9TZXJ2ZXJfOC4wLjMwX0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X + DTIyMDgwODA1NDMwOFoXDTMyMDgwNTA1NDMwOFowPDE6MDgGA1UEAwwxTXlTUUxf + U2VydmVyXzguMC4zMF9BdXRvX0dlbmVyYXRlZF9DQV9DZXJ0aWZpY2F0ZTCCASIw + DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKb2tDaE4TO/4xKRZ0QqpB4ho3cy + daw85Sn8VNLa42EJgZVpSr0WCFl11Go7r0O2TMvceaWsnJU7FLhYHSR+Dlm62yVO + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + /NHv1wSajHEpoyDBtF1QT2rR/kjezFpiH9AY3xwtBdZhTDlc5UBrpyv+Issn1CZF + edcIk54Gzxifn+Et5WP8b6HV/ehdE0qQPtHDmendEaIHXg12/NE+hj3DocSVm8w/ + LUNeYd9wXefwMrEWwDn0DZSsShZmgJoppA15qOnq+FVW/bhZwRv5L4l3AJv0SGoA + o7DXxD0VGHDA6aC4tJssZbrnoDCBPzYmt9s9GwVupuEroJHZ0Wks4pt4Wx50DUgA + KC3v0Mo/gg== + -----END CERTIFICATE-----""".trimIndent()) + + val clientPem: String = + (""" + -----BEGIN CERTIFICATE----- + MIIDBDCCAeygAwIBAgIBAzANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR + TF9TZXJ2ZXJfOC4wLjMwX0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X + DTIyMDgwODA1NDMwOFoXDTMyMDgwNTA1NDMwOFowQDE+MDwGA1UEAww1TXlTUUxf + U2VydmVyXzguMC4zMF9BdXRvX0dlbmVyYXRlZF9DbGllbnRfQ2VydGlmaWNhdGUw + ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV/eRPDZmrPP8d2pKsFizU + JQkGOYDKXOilLibR1TQwN/8MToop8+mvtMi7zr/cWBDR0qTObbduWFQdK82vGppS + ZgrRG3QWVpe8NNI9AhriVZiOmcEQqgAhbgos57Tkjy3qghNbUN1KGb3I0DnNOtvF + RIdATbE+LxOTgCzz/Cw6DVReunQvVo9T4EC4PBBUelMWlAJLo61AQVLM3ufx4ug2 + 1wbV6D/aSRooNhkwWcwk+2vabxKnOzFAQzNU7dIZlBpo6coHFwZDUxtdM2DtuLHn + /r9CsMw8p4wtdIRXrTDmiF/xTXKnABGM8kEqPovZ6eh7He1jrzLTVANUfNQc5b8F + AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAGDJ6XgLBzat + rpLDfGHR/tZ4eFzt1Nhjzl4CyFjpUcr2e2K5XmuveJAecaQSHff2zXwfGpg/BIen + WcPm2daIzcfN/wWg8ENMB/JE3dMq44pOmWs2g4FPDQuaH81IV0hGX4klk2XZpskJ + moWXyGY43Xr3bbNBjyOxgBsQc4kD96ODMUKfzNMH4p9hXKAMrF9DqHwQUho5uM6M + RnU7Uzr745xw7LKJglCgO20t4302wzsUAEPuCTcB9wJy1/cRbMmoLAdUdn6XhFb4 + pR3UDNJvXGc8by6VWrXOeB0BFeB3beMxezlTHDOWoWeJwvEfAAD/dpwHXwp5dm9L + VjtlERcTfH8= + -----END CERTIFICATE-----""".trimIndent()) + + val clientKey: String = + (""" + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEAlf3kTw2Zqzz/HdqSrBYs1CUJBjmAylzopS4m0dU0MDf/DE6K + KfPpr7TIu86/3FgQ0dKkzm23blhUHSvNrxqaUmYK0Rt0FlaXvDTSPQIa4lWYjpnB + EKoAIW4KLOe05I8t6oITW1DdShm9yNA5zTrbxUSHQE2xPi8Tk4As8/wsOg1UXrp0 + L1aPU+BAuDwQVHpTFpQCS6OtQEFSzN7n8eLoNtcG1eg/2kkaKDYZMFnMJPtr2m8S + pzsxQEMzVO3SGZQaaOnKBxcGQ1MbXTNg7bix5/6/QrDMPKeMLXSEV60w5ohf8U1y + pwARjPJBKj6L2enoex3tY68y01QDVHzUHOW/BQIDAQABAoIBAHk/CHyC4PKUVyHZ + 2vCy6EABRB89AogSvJkyCn1anFpSGaDoKDWrjv7S4+U1RtCme8oxPboE5N+VFUGT + dCwVFCSBikLor1mTXAruo/hfKD5HtQ+o6HFBCuP7IMyV7RtJRnOn/F+3qXpJ/qlC + 8UaeSqNXNwHbC+jZgzibxzrfYRz3BqnBYZsSP7/piN+rk6vAGs7WeawO1adqsLS6 + Hr9GilEe+bW/CtXsah3AYVwxDnwo/c03JYBdzYkRRqLgJ9dDG/5o/88FeeKbVb+U + ZrGV9adwa+KGwsuMTYi7pkXUosm+43hLkmYUykxFv0vfkGz8EnDh4MBtY66QMkUJ + cQgWl6ECgYEAxVJNsxpJjEa+d737iK7ylza0GhcTI3+uNPN92u0oucictMzLIm7N + HAUhrHoO71NDYQYJlox7BG8mjze7l6fkwGfpg2u/KsN0vIqc+F+gIQeC7kmpRxQk + l96pxMW25VhibZJFBaDx9UeBkR9RBnI1AF3jD3+wOdua+C9CMahdTDkCgYEAwph4 + FY2gcOSpA0Xz1cOFPNuwQhy9Lh3MJsb1kt20hlTcmpp3GpBrzyggiyIlpBtBHDrP + 6FcjZtV58bv8ckKB8jklvooJkyjmowBx+L7mHZ6/7QFPDQkp/dY9dQPtWjgrPyo+ + rLIN+SoVmyKdyXXaauyjyEPAexsuxzUKq0MMIS0CgYEAirvJQYnT+DqtJAeBWKKY + kdS2YDmlDSpyU2x3KnvgTG9OLphmojkBIRhCir/uzDngf9D84Mq4m2+CzuNCk+hJ + nzXwKqSQ7gIqi31xy/d/4Hklh2BnEkCJUfYNqvnQFARGf/99Y+268Ndrs5svHrch + qLZaNMV0I9nRZXnksoFLx5ECgYBJ8LFAT041V005Jy1dfit0Um2I0W64xS27VkId + igx8NmaUgDjdaR7t2etzsofm8UwuM9KoD+QtwNPTHIDx0X+a0EgdPEojFpl8OkEU + KUU64AVBQwwMgfzorK0xd0qKy2jzWVPzPry8flczWVXnJNbXZg9dmxDaNhvyKZ9i + L9m+CQKBgG3kkQTtsT7k1kQt/6cqjAaBq9Koi0gbS8hWjTioqPKHVQAAEjqVkmqa + uuD/3Knh1gCgxW4jAUokRwfM7IgVA/plQQDQaKBzcFUl94Hl+t6VuvdvtA02MboE + 7TicEc38QKFoLN2hti0Bmm1eJCionsSPiuyDYH5XnhSz7TDjV9sM + -----END RSA PRIVATE KEY-----""".trimIndent()) + + const val clientKey_wrong_format: String = + ("""MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDBmUvDIVGZ5HsRgnXKns2fTf26pfKND45xu + NOEWVetpvo3lGc28vVMvtPiNH/kuELxo5NesC89iotxfbOTl4I9BbjFVg3eO1nNhwmToU2f1kJJ5QFRjFw+xacIMsfBT5xy/v9U7ohZXdEk6txYkOpvhfja + JcLDutT+NtzRdBsttgyItp5fODnk02G4bLsJ68jVH1/CXkDRvxktLR0/NctbtPVuACwA1QG9MsVbH3cE7SymIrzgI8JHwud63dQUb5iQWZ0iIDBqmF95wvg + ox9O4QjnZCkHxo3kuYxBPaxAuMMVTohLBH/oAvo0FJt+0XF453sLPO8x3zOUnJJLhn4VHAgMBAAECggEBALQ4UB7F1YC9ARO7rouAaUnzAE/QS4qlAKU8uS + prQQOWfTdgHvU4FsHqorPgy23PWgI3k+iBenh/kG+F5LVwRP0pZmfNQ/uspFx/aJrVfb1dZzgCxsdzMiv9MxCetPVvduRWHLqjkqoee6MyPwzzWkmXHaF1p + WkvczdzOvyAaQyS3UPsnQzS0kt4mELGZs/E24K9vD9KfSrdRXxkk3fsLFbLrrau/mEhQ/CKX7Xl4MBchiH+lF8kHvpAc27fevrnDPToZp2cbfSc1oeeKjIM + VmYFKytTCi5IXCNG6S0H31rNpX+5VbdZc1iJLPH7Ch6J+dRzX36R+5zSmp7OIl5gAoECgYEA5f1p/umqMW91HQ+amZoIg6gldFfGglFM5IVCrn0RRB/BrgH + Rnpo0jo3JaOUyQMfyDz69lkpKEgejYTPGDkz3kJmpA54rBwmFitB13ZaqhzM63VzYE3hPdCqpy1VTLxW2+T5nEbLuiR4rC2Y7z+CRBmYdQUNxSq90rCpveg + XIq4sCgYEA135M0fmeBAjTiz3f2pRt7ne64WzY4jJ0SRe6BrVA6PnI9V5+wwtRzyhee9A0awzal6t0OvAdxmnAZg3PsP1fbOPeVwXbvBKtZ4rM7adv6UdYy + 6oxjd9eULK92YnVOcZPf595WmoK28L37EHlxjP8p6lnMBk/BF9Y3N3rz2xyNLUCgYAZ8qdczTwYa7zI1JPatJg1Umk3YRfSaB3GwootaYrjJroRSb8+p6M6 + WiDZJtKuoGBc+/Uj2anVsurp8o9r2Z8sv0lkURoFpztb1/0UTQVcT5lalDkEqVQ9hPq3KB9Edqy4HiQ+yPNEoRS2KoihAXMbR7YRQOytQnJlYjxFhhWH1QK + BgQCNFv97FyETaSgAacGQHlCfqrqr75VM/FXQqX09+RyHrUubA4ShdV7Z8Id0L0yyrlbMqRBPqnkEOKck6nQKYMpCxCsF9Sr6R4xLV8B29YK7TOBhcIxDZH + UfBvhwXuNBkYrpd2OABCAZ5NxoTnj/vXf12l9aSZ1N4pOPAKntRAa+ZQKBgQDCPgJQfZePJGOvSIkW/TkXcHpGsexb5p900Si23BLjnMtCNMSkHuIWb60xq + I3vLFKhrLiYzYVQ5n3C6PYLcdfiDYwruYU3zmtr/gpg/QzcsvTe5CW/hxTAkzsZsFBOquJyuyCRBGN59tH6N6ietu8zzvCc8EeJJX7N7AX0ezF7lQ==""") + } +} diff --git a/airbyte-integrations/connectors/source-mysql-v2/build.gradle b/airbyte-integrations/connectors/source-mysql-v2/build.gradle index 2b8bc919c6e8..300b5b424606 100644 --- a/airbyte-integrations/connectors/source-mysql-v2/build.gradle +++ b/airbyte-integrations/connectors/source-mysql-v2/build.gradle @@ -14,9 +14,6 @@ airbyteBulkConnector { dependencies { implementation 'mysql:mysql-connector-java:8.0.30' - implementation 'org.bouncycastle:bcpkix-jdk18on:1.77' - implementation 'org.bouncycastle:bcprov-jdk18on:1.77' - implementation 'org.bouncycastle:bctls-jdk18on:1.77' testImplementation platform('org.testcontainers:testcontainers-bom:1.19.8') testImplementation 'org.testcontainers:mysql' diff --git a/airbyte-integrations/connectors/source-mysql-v2/metadata.yaml b/airbyte-integrations/connectors/source-mysql-v2/metadata.yaml index 9fbd0662b4fc..389cae03ccdd 100644 --- a/airbyte-integrations/connectors/source-mysql-v2/metadata.yaml +++ b/airbyte-integrations/connectors/source-mysql-v2/metadata.yaml @@ -9,7 +9,7 @@ data: connectorSubtype: database connectorType: source definitionId: 561393ed-7e3a-4d0d-8b8b-90ded371754c - dockerImageTag: 0.0.3 + dockerImageTag: 0.0.4 dockerRepository: airbyte/source-mysql-v2 documentationUrl: https://docs.airbyte.com/integrations/sources/mysql githubIssueLabel: source-mysql-v2 diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcEncryption.kt b/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcEncryption.kt new file mode 100644 index 000000000000..ab21dbc88774 --- /dev/null +++ b/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcEncryption.kt @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2024 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.integrations.source.mysql + +import io.airbyte.cdk.ConfigErrorException +import io.airbyte.cdk.SystemErrorException +import io.airbyte.cdk.jdbc.SSLCertificateUtils +import io.github.oshai.kotlinlogging.KotlinLogging +import java.net.MalformedURLException +import java.net.URI +import java.nio.file.FileSystems +import java.util.* + +private val log = KotlinLogging.logger {} + +class MysqlJdbcEncryption( + val sslMode: SSLMode = SSLMode.PREFERRED, + val caCertificate: String? = null, + val clientCertificate: String? = null, + val clientKey: String? = null, + val clientKeyPassword: String? = null, +) { + companion object { + const val TRUST_KEY_STORE_URL: String = "trustCertificateKeyStoreUrl" + const val TRUST_KEY_STORE_PASS: String = "trustCertificateKeyStorePassword" + const val CLIENT_KEY_STORE_URL: String = "clientCertificateKeyStoreUrl" + const val CLIENT_KEY_STORE_PASS: String = "clientCertificateKeyStorePassword" + const val CLIENT_KEY_STORE_TYPE: String = "clientCertificateKeyStoreType" + const val TRUST_KEY_STORE_TYPE: String = "trustCertificateKeyStoreType" + const val KEY_STORE_TYPE_PKCS12: String = "PKCS12" + const val SSL_MODE: String = "sslMode" + } + + private fun getOrGeneratePassword(): String { + if (!clientKeyPassword.isNullOrEmpty()) { + return clientKeyPassword + } else { + return UUID.randomUUID().toString() + } + } + + private fun prepareCACertificateKeyStore(): Pair? { + // if config is not available - done + // if has CA cert - make keystore with given password or generate a new password. + var caCertKeyStorePair: Pair? = null + + if (caCertificate.isNullOrEmpty()) { + return caCertKeyStorePair + } + val clientKeyPassword = getOrGeneratePassword() + try { + val caCertKeyStoreUri = + SSLCertificateUtils.keyStoreFromCertificate( + caCertificate, + clientKeyPassword, + FileSystems.getDefault(), + "" + ) + return Pair(caCertKeyStoreUri, clientKeyPassword) + } catch (ex: Exception) { + throw ConfigErrorException("Failed to create keystore for CA certificate.", ex) + } + } + + private fun prepareClientCertificateKeyStore(): Pair? { + var clientCertKeyStorePair: Pair? = null + + if (!clientCertificate.isNullOrEmpty() && !clientKey.isNullOrEmpty()) { + val clientKeyPassword = getOrGeneratePassword() + try { + val clientCertKeyStoreUri = + SSLCertificateUtils.keyStoreFromClientCertificate( + clientCertificate, + clientKey, + clientKeyPassword, + "" + ) + clientCertKeyStorePair = Pair(clientCertKeyStoreUri, clientKeyPassword) + } catch (ex: Exception) { + throw RuntimeException("Failed to create keystore for Client certificate", ex) + } + } + return clientCertKeyStorePair + } + + fun parseSSLConfig(): Map { + var caCertKeyStorePair: Pair? + var clientCertKeyStorePair: Pair? + val additionalParameters: MutableMap = mutableMapOf() + + additionalParameters[SSL_MODE] = sslMode.name.lowercase() + + caCertKeyStorePair = prepareCACertificateKeyStore() + + if (null != caCertKeyStorePair) { + log.debug { "uri for ca cert keystore: ${caCertKeyStorePair.first}" } + try { + additionalParameters.putAll( + mapOf( + TRUST_KEY_STORE_URL to caCertKeyStorePair.first.toURL().toString(), + TRUST_KEY_STORE_PASS to caCertKeyStorePair.second, + TRUST_KEY_STORE_TYPE to KEY_STORE_TYPE_PKCS12 + ) + ) + } catch (e: MalformedURLException) { + throw ConfigErrorException("Unable to get a URL for trust key store") + } + + clientCertKeyStorePair = prepareClientCertificateKeyStore() + + if (null != clientCertKeyStorePair) { + log.debug { + "uri for client cert keystore: ${clientCertKeyStorePair.first} / ${clientCertKeyStorePair.second}" + } + try { + additionalParameters.putAll( + mapOf( + CLIENT_KEY_STORE_URL to clientCertKeyStorePair.first.toURL().toString(), + CLIENT_KEY_STORE_PASS to clientCertKeyStorePair.second, + CLIENT_KEY_STORE_TYPE to KEY_STORE_TYPE_PKCS12 + ) + ) + } catch (e: MalformedURLException) { + throw ConfigErrorException("Unable to get a URL for client key store") + } + } + } + return additionalParameters + } +} + +/** + * Enum representing the SSL mode for MySQL connections. The actual jdbc property name is the lower + * case of the enum name. + */ +enum class SSLMode() { + PREFERRED, + REQUIRED, + VERIFY_CA, + VERIFY_IDENTITY; + + companion object { + + fun fromJdbcPropertyName(jdbcPropertyName: String): SSLMode { + return SSLMode.values().find { it.name.equals(jdbcPropertyName, ignoreCase = true) } + ?: throw SystemErrorException("Unknown SSL mode: $jdbcPropertyName") + } + } +} diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfiguration.kt b/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfiguration.kt index 70f05317b8ce..f6917ef23ea0 100644 --- a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfiguration.kt +++ b/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfiguration.kt @@ -63,9 +63,31 @@ class MysqlSourceConfigurationFactory : } // Determine protocol and configure encryption. val encryption: Encryption = pojo.getEncryptionValue() - if (encryption is SslVerifyCertificate) { - // TODO: reuse JdbcSSLCOnnectionUtils; parse the input into properties - } + val sslMode = SSLMode.fromJdbcPropertyName(pojo.encryption.encryptionMethod) + val jdbcEncryption = + when (encryption) { + is EncryptionPreferred, + is EncryptionRequired -> MysqlJdbcEncryption(sslMode = sslMode) + is SslVerifyCertificate -> + MysqlJdbcEncryption( + sslMode = sslMode, + caCertificate = encryption.sslCertificate, + clientCertificate = encryption.sslClientCertificate, + clientKey = encryption.sslClientKey, + clientKeyPassword = encryption.sslClientPassword + ) + is SslVerifyIdentity -> + MysqlJdbcEncryption( + sslMode = sslMode, + caCertificate = encryption.sslCertificate, + clientCertificate = encryption.sslClientCertificate, + clientKey = encryption.sslClientKey, + clientKeyPassword = encryption.sslClientPassword + ) + } + val sslJdbcParameters = jdbcEncryption.parseSSLConfig() + jdbcProperties.putAll(sslJdbcParameters) + // Build JDBC URL val address = "%s:%d" val jdbcUrlFmt = "jdbc:mysql://${address}" diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationJsonObject.kt b/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationJsonObject.kt index f19f04fcb713..dbd02fb1ae90 100644 --- a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationJsonObject.kt +++ b/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationJsonObject.kt @@ -199,8 +199,8 @@ class MysqlSourceConfigurationJsonObject : ConfigurationJsonObjectBase() { @JsonSubTypes( JsonSubTypes.Type(value = EncryptionPreferred::class, name = "preferred"), JsonSubTypes.Type(value = EncryptionRequired::class, name = "required"), - JsonSubTypes.Type(value = SslVerifyCertificate::class, name = "Verify CA"), - JsonSubTypes.Type(value = SslVerifyCertificate::class, name = "Verify Identity"), + JsonSubTypes.Type(value = SslVerifyCertificate::class, name = "verify_ca"), + JsonSubTypes.Type(value = SslVerifyIdentity::class, name = "verify_identity"), ) @JsonSchemaTitle("Encryption") @JsonSchemaDescription("The encryption method which is used when communicating with the database.") @@ -218,7 +218,7 @@ data object EncryptionPreferred : Encryption ) data object EncryptionRequired : Encryption -@JsonSchemaTitle("Verify CA") +@JsonSchemaTitle("verify_ca") @JsonSchemaDescription( "To always require encryption and verify that the source has a valid SSL certificate." ) @@ -232,13 +232,13 @@ class SslVerifyCertificate : Encryption { @JsonSchemaInject(json = """{"airbyte_secret":true,"multiline":true}""") lateinit var sslCertificate: String - @JsonProperty("ssl_client_certificate") + @JsonProperty("ssl_client_certificate", required = false) @JsonSchemaTitle("Client certificate File") @JsonPropertyDescription( "Client certificate (this is not a required field, but if you want to use it, you will need to add the Client key as well)", ) @JsonSchemaInject(json = """{"airbyte_secret":true,"multiline":true}""") - lateinit var sslClientCertificate: String + var sslClientCertificate: String? = null @JsonProperty("ssl_client_key") @JsonSchemaTitle("Client Key") @@ -246,7 +246,7 @@ class SslVerifyCertificate : Encryption { "Client key (this is not a required field, but if you want to use it, you will need to add the Client certificate as well)", ) @JsonSchemaInject(json = """{"airbyte_secret":true,"multiline":true}""") - lateinit var sslClientKey: String + var sslClientKey: String? = null @JsonProperty("ssl_client_key_password") @JsonSchemaTitle("Client key password") @@ -254,7 +254,46 @@ class SslVerifyCertificate : Encryption { "Password for keystorage. This field is optional. If you do not add it - the password will be generated automatically.", ) @JsonSchemaInject(json = """{"airbyte_secret":true,"multiline":true}""") - lateinit var sslClientPassword: String + var sslClientPassword: String? = null +} + +@JsonSchemaTitle("verify_identity") +@JsonSchemaDescription( + "To always require encryption and verify that the source has a valid SSL certificate." +) +@SuppressFBWarnings(value = ["NP_NONNULL_RETURN_VIOLATION"], justification = "Micronaut DI") +class SslVerifyIdentity : Encryption { + @JsonProperty("ssl_certificate", required = true) + @JsonSchemaTitle("CA certificate") + @JsonPropertyDescription( + "CA certificate", + ) + @JsonSchemaInject(json = """{"airbyte_secret":true,"multiline":true}""") + lateinit var sslCertificate: String + + @JsonProperty("ssl_client_certificate", required = false) + @JsonSchemaTitle("Client certificate File") + @JsonPropertyDescription( + "Client certificate (this is not a required field, but if you want to use it, you will need to add the Client key as well)", + ) + @JsonSchemaInject(json = """{"airbyte_secret":true,"multiline":true}""") + var sslClientCertificate: String? = null + + @JsonProperty("ssl_client_key") + @JsonSchemaTitle("Client Key") + @JsonPropertyDescription( + "Client key (this is not a required field, but if you want to use it, you will need to add the Client certificate as well)", + ) + @JsonSchemaInject(json = """{"airbyte_secret":true,"multiline":true}""") + var sslClientKey: String? = null + + @JsonProperty("ssl_client_key_password") + @JsonSchemaTitle("Client key password") + @JsonPropertyDescription( + "Password for keystorage. This field is optional. If you do not add it - the password will be generated automatically.", + ) + @JsonSchemaInject(json = """{"airbyte_secret":true,"multiline":true}""") + var sslClientPassword: String? = null } @ConfigurationProperties("$CONNECTOR_CONFIG_PREFIX.encryption") @@ -268,8 +307,7 @@ class MicronautPropertiesFriendlyEncryption { "preferred" -> EncryptionPreferred "required" -> EncryptionRequired "verify_ca" -> SslVerifyCertificate().also { it.sslCertificate = sslCertificate!! } - "verify_identity" -> - SslVerifyCertificate().also { it.sslCertificate = sslCertificate!! } + "verify_identity" -> SslVerifyIdentity().also { it.sslCertificate = sslCertificate!! } else -> throw ConfigErrorException("invalid value $encryptionMethod") } } diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceDatatypeIntegrationTest.kt b/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceDatatypeIntegrationTest.kt index 1cc28d67a4e8..8ce1085414fa 100644 --- a/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceDatatypeIntegrationTest.kt +++ b/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceDatatypeIntegrationTest.kt @@ -125,7 +125,7 @@ class MysqlSourceDatatypeIntegrationTest { actualStream!!.supportedSyncModes.contains(SyncMode.INCREMENTAL) val jsonSchema: JsonNode = actualStream.jsonSchema?.get("properties")!! if (streamName == testCase.tableName) { - val actualSchema: JsonNode? = jsonSchema[testCase.columnName] + val actualSchema: JsonNode = jsonSchema[testCase.columnName] Assertions.assertNotNull(actualSchema) val expectedSchema: JsonNode = testCase.airbyteType.asJsonSchema() Assertions.assertEquals(expectedSchema, actualSchema) diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/test/resources/expected-spec.json b/airbyte-integrations/connectors/source-mysql-v2/src/test/resources/expected-spec.json index 4e6c6f5e195d..58ff301326ba 100644 --- a/airbyte-integrations/connectors/source-mysql-v2/src/test/resources/expected-spec.json +++ b/airbyte-integrations/connectors/source-mysql-v2/src/test/resources/expected-spec.json @@ -123,14 +123,8 @@ }, { "type": "object", - "title": "Verify CA", - "required": [ - "encryption_method", - "ssl_certificate", - "ssl_client_certificate", - "ssl_client_key", - "ssl_client_key_password" - ], + "title": "verify_ca", + "required": ["encryption_method", "ssl_certificate"], "properties": { "ssl_client_key": { "type": "string", @@ -147,9 +141,9 @@ "airbyte_secret": true }, "encryption_method": { - "enum": ["Verify CA"], + "enum": ["verify_ca"], "type": "string", - "default": "Verify CA" + "default": "verify_ca" }, "ssl_client_certificate": { "type": "string", @@ -171,14 +165,8 @@ }, { "type": "object", - "title": "Verify CA", - "required": [ - "encryption_method", - "ssl_certificate", - "ssl_client_certificate", - "ssl_client_key", - "ssl_client_key_password" - ], + "title": "verify_identity", + "required": ["encryption_method", "ssl_certificate"], "properties": { "ssl_client_key": { "type": "string", @@ -195,9 +183,9 @@ "airbyte_secret": true }, "encryption_method": { - "enum": ["Verify CA"], + "enum": ["verify_identity"], "type": "string", - "default": "Verify CA" + "default": "verify_identity" }, "ssl_client_certificate": { "type": "string",