diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContexts.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContexts.kt index 991ecdd98..99e9d64c7 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContexts.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContexts.kt @@ -77,7 +77,11 @@ open class AllContexts( private val contextFactory: (ClientAdapter, IResourceModelObservable) -> IActiveContext? = IActiveContext.Factory::create, private val modelChange: IResourceModelObservable, - private val clientFactory: (String?, String?) -> ClientAdapter = ClientAdapter.Factory::create + private val clientFactory: ( + namespace: String?, + context: String? + ) -> ClientAdapter + = { namespace, context -> ClientAdapter.Factory.create(namespace, context) } ) : IAllContexts { init { diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ProcessWatches.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ProcessWatches.kt index 86c74b030..d8a1a4f78 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ProcessWatches.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ProcessWatches.kt @@ -35,7 +35,10 @@ import java.io.IOException import java.io.OutputStream import java.util.concurrent.ConcurrentHashMap -open class ProcessWatches(private val clientFactory: (String?, String?) -> ClientAdapter = ClientAdapter.Factory::create) { +open class ProcessWatches( + private val clientFactory: (String?, String?) -> ClientAdapter + = { namespace: String?, context: String? -> ClientAdapter.Factory.create(namespace, context) } +) { @Suppress("UNCHECKED_CAST") protected open val operators: Map, OperatorSpecs> = mapOf( diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapter.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapter.kt index cf962fbbb..0dc05664a 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapter.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapter.kt @@ -10,19 +10,24 @@ ******************************************************************************/ package com.redhat.devtools.intellij.kubernetes.model.client +import com.redhat.devtools.intellij.kubernetes.model.client.ssl.IDEATrustManager import com.redhat.devtools.intellij.kubernetes.model.util.isUnauthorized import io.fabric8.kubernetes.client.Client import io.fabric8.kubernetes.client.Config import io.fabric8.kubernetes.client.KubernetesClient import io.fabric8.kubernetes.client.KubernetesClientBuilder import io.fabric8.kubernetes.client.KubernetesClientException +import io.fabric8.kubernetes.client.http.HttpClient import io.fabric8.kubernetes.client.impl.AppsAPIGroupClient import io.fabric8.kubernetes.client.impl.BatchAPIGroupClient import io.fabric8.kubernetes.client.impl.NetworkAPIGroupClient import io.fabric8.kubernetes.client.impl.StorageAPIGroupClient +import io.fabric8.kubernetes.client.internal.SSLUtils import io.fabric8.openshift.client.NamespacedOpenShiftClient import io.fabric8.openshift.client.OpenShiftClient import java.util.concurrent.ConcurrentHashMap +import javax.net.ssl.X509ExtendedTrustManager +import javax.net.ssl.X509TrustManager open class OSClientAdapter(client: OpenShiftClient, private val kubeClient: KubernetesClient) : ClientAdapter(client) { @@ -49,16 +54,32 @@ open class KubeClientAdapter(client: KubernetesClient) : } } -abstract class ClientAdapter(private val fabric8Client: C) { +abstract class ClientAdapter(private val fabric8Client: C) { companion object Factory { - fun create(namespace: String? = null, context: String? = null): ClientAdapter { - return create(namespace, Config.autoConfigure(context)) + fun create( + namespace: String? = null, + context: String? = null, + trustManagerProvider: ((toIntegrate: Array) -> X509TrustManager) + = IDEATrustManager()::configure + ): ClientAdapter { + val config = Config.autoConfigure(context) + return create(namespace, config, trustManagerProvider) } - fun create(namespace: String? = null, config: Config): ClientAdapter { + fun create( + namespace: String? = null, + config: Config, + externalTrustManagerProvider: (toIntegrate: Array) -> X509TrustManager + = IDEATrustManager()::configure + ): ClientAdapter { setNamespace(namespace, config) - val kubeClient = KubernetesClientBuilder().withConfig(config).build() + val kubeClient = KubernetesClientBuilder() + .withConfig(config) + .withHttpClientBuilderConsumer { builder -> + setSslContext(builder, config, externalTrustManagerProvider) + } + .build() val osClient = kubeClient.adapt(NamespacedOpenShiftClient::class.java) val isOpenShift = isOpenShift(osClient) return if (isOpenShift) { @@ -68,6 +89,18 @@ abstract class ClientAdapter(private val fabric8Client: C) } } + private fun setSslContext( + builder: HttpClient.Builder, + config: Config, + externalTrustManagerProvider: (toIntegrate: Array) -> X509TrustManager + ) { + val clientTrustManagers = SSLUtils.trustManagers(config) + .filterIsInstance() + .toTypedArray() + val externalTrustManager = externalTrustManagerProvider.invoke(clientTrustManagers) + builder.sslContext(SSLUtils.keyManagers(config), arrayOf(externalTrustManager)) + } + private fun isOpenShift(osClient: NamespacedOpenShiftClient): Boolean { return try { osClient.isSupported diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ssl/CompositeX509ExtendedTrustManager.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ssl/CompositeX509ExtendedTrustManager.kt new file mode 100644 index 000000000..d15bbb817 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ssl/CompositeX509ExtendedTrustManager.kt @@ -0,0 +1,129 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Based on nl.altindag.ssl.trustmanager.CompositeX509ExtendedTrustManager at https://github.com/Hakky54/sslcontext-kickstart + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ + +package com.redhat.devtools.intellij.kubernetes.model.client.ssl + +import java.net.Socket +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import java.util.* +import java.util.function.Consumer +import javax.net.ssl.SSLEngine +import javax.net.ssl.X509ExtendedTrustManager + +class CompositeX509ExtendedTrustManager(trustManagers: List): X509ExtendedTrustManager() { + + companion object { + private const val CERTIFICATE_EXCEPTION_MESSAGE = "None of the TrustManagers trust this certificate chain" + } + + val innerTrustManagers: List + private val acceptedIssuers: Array + + init { + innerTrustManagers = Collections.unmodifiableList(trustManagers) + acceptedIssuers = trustManagers + .map { manager: X509ExtendedTrustManager -> manager.acceptedIssuers } + .flatMap { acceptedIssuers: Array? -> + acceptedIssuers?.asList() ?: emptyList() + } + .toTypedArray() + } + + override fun getAcceptedIssuers(): Array { + return Arrays.copyOf(acceptedIssuers, acceptedIssuers.size) + } + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String) { + checkTrusted { trustManager: X509ExtendedTrustManager -> + trustManager.checkClientTrusted( + chain, + authType + ) + } + } + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String, socket: Socket) { + checkTrusted { trustManager: X509ExtendedTrustManager -> + trustManager.checkClientTrusted( + chain, + authType, + socket + ) + } + } + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String, sslEngine: SSLEngine) { + checkTrusted { trustManager: X509ExtendedTrustManager -> + trustManager.checkClientTrusted( + chain, + authType, + sslEngine + ) + } + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String) { + checkTrusted{ trustManager: X509ExtendedTrustManager -> + trustManager.checkServerTrusted( + chain, + authType + ) + } + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String, socket: Socket) { + checkTrusted{ trustManager: X509ExtendedTrustManager -> + trustManager.checkServerTrusted( + chain, + authType, + socket + ) + } + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String, sslEngine: SSLEngine) { + checkTrusted { trustManager: X509ExtendedTrustManager -> + trustManager.checkServerTrusted( + chain, + authType, + sslEngine + ) + } + } + + @Throws(CertificateException::class) + private fun checkTrusted(consumer: (trustManager: X509ExtendedTrustManager) -> Unit) { + val certificateExceptions: MutableList = ArrayList() + for (trustManager in innerTrustManagers) { + try { + consumer.invoke(trustManager) + return + } catch (e: CertificateException) { + certificateExceptions.add(e) + } + } + val certificateException = CertificateException(CERTIFICATE_EXCEPTION_MESSAGE) + certificateExceptions.forEach(Consumer { exception: CertificateException? -> + certificateException.addSuppressed( + exception + ) + }) + throw certificateException + } +} diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ssl/IDEATrustManager.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ssl/IDEATrustManager.kt new file mode 100644 index 000000000..8bfd5c774 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ssl/IDEATrustManager.kt @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.model.client.ssl + +import com.intellij.openapi.diagnostic.logger +import com.intellij.util.net.ssl.CertificateManager +import com.intellij.util.net.ssl.ConfirmingTrustManager +import java.lang.reflect.Field +import javax.net.ssl.X509ExtendedTrustManager +import javax.net.ssl.X509TrustManager +import org.apache.commons.lang3.reflect.FieldUtils + +class IDEATrustManager(private val trustManager: X509TrustManager = CertificateManager.getInstance().trustManager) { + + fun configure(toAdd: Array): X509TrustManager { + try { + if (hasSystemManagerField()) { + // < IC-2022.2 + setCompositeManager(toAdd, trustManager) + } else { + // >= IC-2022.2 + addCompositeManager(toAdd, trustManager) + } + } catch (e: RuntimeException) { + logger().warn("Could not configure IDEA trust manager.", e) + } + return trustManager + } + + /** + * Returns `true` if [ConfirmingTrustManager] has a private field `mySystemManager`. + * Returns `false` otherwise. + * IDEA < IC-2022.2 manages a single [X509TrustManager] in a private field called `mySystemManager`. + * IDEA >= IC-2022.2 manages a list of [X509TrustManager]s in a private list called `mySystemManagers`. + * + * @return true if com.intellij.util.net.ssl.ConfirmingTrustManager has a field mySystemManager. False otherwise. + */ + private fun hasSystemManagerField(): Boolean { + return getSystemManagerField() != null + } + + private fun getSystemManagerField(): Field? { + return FieldUtils.getDeclaredField( + trustManager::class.java, + "mySystemManager", + true + ) + } + + /** + * Sets a [CompositeX509ExtendedTrustManager] with the given [X509TrustManager]s + * to the given destination [X509TrustManager]. + * If a [CompositeX509ExtendedTrustManager] already exists, his first entry is taken and set to a new + * [CompositeX509ExtendedTrustManager] that replaces the existing one. + * + * @param trustManagers the trust managers that should be set to the destination trust manager + * @param destination the destination trust manager that should receive the trust managers + * @return true if the operation worked + */ + private fun setCompositeManager( + trustManagers: Array, + destination: X509TrustManager + ): Boolean { + val systemManagerField = getSystemManagerField() ?: return false + val systemManager = systemManagerField.get(destination) as? X509ExtendedTrustManager ?: return false + val compositeTrustManager = createCompositeTrustManager(systemManager, trustManagers) + systemManagerField.set(destination, compositeTrustManager) + return true + } + + private fun createCompositeTrustManager( + systemManager: X509ExtendedTrustManager, + clientTrustManagers: Array + ): X509ExtendedTrustManager { + val trustManagers = if (systemManager is CompositeX509ExtendedTrustManager) { + // already patched CertificateManager, take 1st entry in existing system manager + mutableListOf(systemManager.innerTrustManagers[0]) + } else { + // unpatched CertificateManager, take system manager + mutableListOf(systemManager) + } + trustManagers.addAll(clientTrustManagers) + return CompositeX509ExtendedTrustManager(trustManagers) + } + + /** + * Adds a [CompositeX509ExtendedTrustManager] to the given destination [X509TrustManager]. + * If a [CompositeX509ExtendedTrustManager] already exists, it is replaced by a new [CompositeX509ExtendedTrustManager]. + * + * @param trustManagers the trust managers that should be added to destination trust manager + * @param destination the trust manager that should receive the given trust managers + */ + private fun addCompositeManager( + trustManagers: Array, + destination: X509TrustManager + ): Boolean { + val systemManagersField = FieldUtils.getDeclaredField( + destination::class.java, + "mySystemManagers", + true + ) ?: return false + val managers = systemManagersField.get(destination) as? MutableList ?: return false + val nonCompositeManagers = managers.filterNot { it is CompositeX509ExtendedTrustManager } + val clientTrustManager = CompositeX509ExtendedTrustManager(trustManagers.asList()) + managers.clear() + managers.addAll(nonCompositeManagers) + managers.add(clientTrustManager) + return true + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapterTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapterTest.kt index 0156643c6..6069ceeb0 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapterTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapterTest.kt @@ -10,6 +10,7 @@ ******************************************************************************/ package com.redhat.devtools.intellij.kubernetes.model.client +import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify @@ -17,11 +18,24 @@ import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.config import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.namedContext import io.fabric8.kubernetes.client.NamespacedKubernetesClient import io.fabric8.kubernetes.client.impl.AppsAPIGroupClient +import java.security.cert.X509Certificate +import javax.net.ssl.X509ExtendedTrustManager +import javax.net.ssl.X509TrustManager import org.assertj.core.api.Assertions.assertThat import org.junit.Test class ClientAdapterTest { + private val certificate: X509Certificate = mock { + on { subjectX500Principal } doReturn mock() + } + private val trustManager: X509TrustManager = mock { + on { acceptedIssuers } doReturn arrayOf(certificate) + } + private val trustManagerProvider: (toIntegrate: Array) -> X509TrustManager = mock() { + on { invoke(any()) } doReturn trustManager + } + @Test fun `#isOpenShift should return true if has OpenShiftClient`() { // given @@ -101,10 +115,20 @@ class ClientAdapterTest { val ctx2 = namedContext("Death Start", "Navy Garrison", "Empire", "Darh Vader" ) val config = config(ctx1, listOf(ctx1, ctx2)) // when - ClientAdapter.Factory.create(namespace, config) + ClientAdapter.Factory.create(namespace, config, trustManagerProvider) // then verify(config).namespace = namespace verify(config.currentContext.context).namespace = namespace } + @Test + fun `#create should call trust manager provider`() { + // given + val ctx1 = namedContext("Aldeeran", "Aldera", "Republic", "Organa" ) + val config = config(ctx1, listOf(ctx1)) + // when + ClientAdapter.Factory.create("namespace", config, trustManagerProvider) + // then + verify(trustManagerProvider).invoke(any()) + } } \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ssl/IDEATrustManagerTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ssl/IDEATrustManagerTest.kt new file mode 100644 index 000000000..9b77bf6c7 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ssl/IDEATrustManagerTest.kt @@ -0,0 +1,185 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.model.client.ssl + +import com.nhaarman.mockitokotlin2.mock +import java.security.cert.X509Certificate +import javax.net.ssl.X509ExtendedTrustManager +import javax.net.ssl.X509TrustManager +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class IDEATrustManagerTest { + + @Test + fun `single system manager field - should replace existing trust manager with new composite trust manager`() { + // given + val trustManager = TrustManagerWithMySystemManagerField(mock()) + val operator = IDEATrustManager(trustManager) + assertThat(trustManager.mySystemManager) + .isNotInstanceOf(CompositeX509ExtendedTrustManager::class.java) + // when + operator.configure(emptyArray()) + // then + assertThat(trustManager.mySystemManager) + .isInstanceOf(CompositeX509ExtendedTrustManager::class.java) + } + + @Test + fun `single system manager field - should replace existing trust manager with new composite trust manager that contains given trust managers`() { + // given + val trustManager = TrustManagerWithMySystemManagerField(mock()) + val operator = IDEATrustManager(trustManager) + val newTrustManagers = arrayOf( + mock(), + mock() + ) + // when + operator.configure(newTrustManagers) + // then + assertThat(trustManager.mySystemManager) + .isInstanceOf(CompositeX509ExtendedTrustManager::class.java) + val afterConfigure = (trustManager.mySystemManager as CompositeX509ExtendedTrustManager).innerTrustManagers + assertThat(afterConfigure) + .contains(*newTrustManagers) // new instance contains list given to configure() + } + + @Test + fun `single system manager field - should replace existing trust manager with new composite trust manager that has replaced trust manager as 1st entry`() { + // given + val beforeReplace = mock() + val trustManager = TrustManagerWithMySystemManagerField(beforeReplace) + val operator = IDEATrustManager(trustManager) + // when + operator.configure( + arrayOf( + mock(), + mock() + ) + ) + // then + assertThat(trustManager.mySystemManager) + .isInstanceOf(CompositeX509ExtendedTrustManager::class.java) + val afterConfigure = (trustManager.mySystemManager as CompositeX509ExtendedTrustManager).innerTrustManagers + assertThat(afterConfigure[0]) // new instance contains 1st entry of replaced instance + .isEqualTo(beforeReplace) + } + + @Test + fun `single system manager field - should replace composite trust manager with new instance that has 1st entry of replaced composite manager`() { + // given + val toInclude = mock() + val toExclude = mock() + val compositeTrustManager = CompositeX509ExtendedTrustManager(listOf(toInclude, toExclude)) + val trustManager = TrustManagerWithMySystemManagerField(compositeTrustManager) + val manager = IDEATrustManager(trustManager) + // when + manager.configure( + arrayOf( + mock(), + mock() + ) + ) + // then + assertThat(trustManager.mySystemManager) + .isNotSameAs(compositeTrustManager) // a new instance was created + assertThat(trustManager.mySystemManager) + .isInstanceOf(CompositeX509ExtendedTrustManager::class.java) + val afterConfigure = (trustManager.mySystemManager as CompositeX509ExtendedTrustManager).innerTrustManagers + assertThat(afterConfigure[0]) // new instance contains 1st entry of replaced instance + .isEqualTo(toInclude) + } + + @Test + fun `multi system managers field - should still contain existing trust managers`() { + // given + val existing = mock() + val managers = mutableListOf(existing) + val trustManager = TrustManagerWithMySystemManagersField(managers) + val operator = IDEATrustManager(trustManager) + // when + operator.configure(emptyArray()) + // then + assertThat(trustManager.mySystemManagers) + .contains(existing) + } + + @Test + fun `multi system managers field - should add composite manager that contains new trust managers`() { + // given + val managers = mutableListOf(mock()) + val trustManager = TrustManagerWithMySystemManagersField(managers) + val operator = IDEATrustManager(trustManager) + val new = arrayOf( + mock(), + mock() + ) + // when + operator.configure(new) + // then + val composite = trustManager.mySystemManagers.find { + it is CompositeX509ExtendedTrustManager + } as CompositeX509ExtendedTrustManager + assertThat(composite.innerTrustManagers).containsExactly(*new) + } + + @Test + fun `multi system managers field - should replace existing composite manager that contains new trust managers`() { + // given + val existingTrustManager = mock() + val existingCompositeManager = CompositeX509ExtendedTrustManager(listOf(mock())) + val managers = mutableListOf(existingTrustManager, existingCompositeManager) + val trustManager = TrustManagerWithMySystemManagersField(managers) + val operator = IDEATrustManager(trustManager) + val new = arrayOf( + mock(), + mock() + ) + // when + operator.configure(new) + // then + assertThat(trustManager.mySystemManagers).doesNotContain(existingCompositeManager) + val composite = trustManager.mySystemManagers.find { + it is CompositeX509ExtendedTrustManager + } as CompositeX509ExtendedTrustManager + assertThat(composite.innerTrustManagers).containsExactly(*new) + } + + /** [com.intellij.util.net.ssl.ConfirmingTrustManager] in < IC-2022.2 */ + private class TrustManagerWithMySystemManagerField(var mySystemManager: X509TrustManager): X509TrustManager { + + override fun checkClientTrusted(chain: Array?, authType: String?) { + } + + override fun checkServerTrusted(chain: Array?, authType: String?) { + } + + override fun getAcceptedIssuers(): Array { + return emptyArray() + } + + } + + /** [com.intellij.util.net.ssl.ConfirmingTrustManager] in >= IC-2022.2 */ + private class TrustManagerWithMySystemManagersField(var mySystemManagers: MutableList = mutableListOf()): X509TrustManager { + + override fun checkClientTrusted(chain: Array?, authType: String?) { + } + + override fun checkServerTrusted(chain: Array?, authType: String?) { + } + + override fun getAcceptedIssuers(): Array { + return emptyArray() + } + + } +} \ No newline at end of file