Skip to content

Commit 99e588c

Browse files
committed
use IDEA certificate manager when connecting to the cluster (#600)
Signed-off-by: Andre Dietisheim <[email protected]>
1 parent f89d106 commit 99e588c

File tree

6 files changed

+288
-13
lines changed

6 files changed

+288
-13
lines changed

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContexts.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ open class AllContexts(
7777
private val contextFactory: (ClientAdapter<out KubernetesClient>, IResourceModelObservable) -> IActiveContext<out HasMetadata, out KubernetesClient>? =
7878
IActiveContext.Factory::create,
7979
private val modelChange: IResourceModelObservable,
80-
private val clientFactory: (String?, String?) -> ClientAdapter<out KubernetesClient> = ClientAdapter.Factory::create
80+
private val clientFactory: (
81+
namespace: String?,
82+
context: String?
83+
) -> ClientAdapter<out KubernetesClient>
84+
= { namespace, context -> ClientAdapter.Factory.create(namespace, context) }
8185
) : IAllContexts {
8286

8387
init {

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ProcessWatches.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ import java.io.IOException
3535
import java.io.OutputStream
3636
import java.util.concurrent.ConcurrentHashMap
3737

38-
open class ProcessWatches(private val clientFactory: (String?, String?) -> ClientAdapter<out KubernetesClient> = ClientAdapter.Factory::create) {
38+
open class ProcessWatches(
39+
private val clientFactory: (String?, String?) -> ClientAdapter<out KubernetesClient>
40+
= { namespace: String?, context: String? -> ClientAdapter.Factory.create(namespace, context) }
41+
) {
3942

4043
@Suppress("UNCHECKED_CAST")
4144
protected open val operators: Map<ResourceKind<out HasMetadata>, OperatorSpecs> = mapOf(

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapter.kt

+32-10
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,24 @@
1010
******************************************************************************/
1111
package com.redhat.devtools.intellij.kubernetes.model.client
1212

13-
import com.intellij.util.net.ssl.CertificateManager
13+
import com.redhat.devtools.intellij.kubernetes.model.client.ssl.IDEATrustManager
1414
import com.redhat.devtools.intellij.kubernetes.model.util.isUnauthorized
1515
import io.fabric8.kubernetes.client.Client
1616
import io.fabric8.kubernetes.client.Config
1717
import io.fabric8.kubernetes.client.KubernetesClient
1818
import io.fabric8.kubernetes.client.KubernetesClientBuilder
1919
import io.fabric8.kubernetes.client.KubernetesClientException
20+
import io.fabric8.kubernetes.client.http.HttpClient
2021
import io.fabric8.kubernetes.client.impl.AppsAPIGroupClient
2122
import io.fabric8.kubernetes.client.impl.BatchAPIGroupClient
2223
import io.fabric8.kubernetes.client.impl.NetworkAPIGroupClient
2324
import io.fabric8.kubernetes.client.impl.StorageAPIGroupClient
25+
import io.fabric8.kubernetes.client.internal.SSLUtils
2426
import io.fabric8.openshift.client.NamespacedOpenShiftClient
2527
import io.fabric8.openshift.client.OpenShiftClient
2628
import java.util.concurrent.ConcurrentHashMap
29+
import javax.net.ssl.X509ExtendedTrustManager
30+
import javax.net.ssl.X509TrustManager
2731

2832
open class OSClientAdapter(client: OpenShiftClient, private val kubeClient: KubernetesClient) :
2933
ClientAdapter<OpenShiftClient>(client) {
@@ -50,19 +54,31 @@ open class KubeClientAdapter(client: KubernetesClient) :
5054
}
5155
}
5256

53-
abstract class ClientAdapter<C: KubernetesClient>(private val fabric8Client: C) {
57+
abstract class ClientAdapter<C : KubernetesClient>(private val fabric8Client: C) {
5458

5559
companion object Factory {
56-
fun create(namespace: String? = null, context: String? = null): ClientAdapter<out KubernetesClient> {
60+
fun create(
61+
namespace: String? = null,
62+
context: String? = null,
63+
trustManagerProvider: ((toIntegrate: Array<out X509ExtendedTrustManager>) -> X509TrustManager)
64+
= IDEATrustManager()::configure
65+
): ClientAdapter<out KubernetesClient> {
5766
val config = Config.autoConfigure(context)
58-
setAcceptCertificates(config)
59-
return create(namespace, config)
67+
return create(namespace, config, trustManagerProvider)
6068
}
6169

62-
fun create(namespace: String? = null, config: Config): ClientAdapter<out KubernetesClient> {
70+
fun create(
71+
namespace: String? = null,
72+
config: Config,
73+
externalTrustManagerProvider: (toIntegrate: Array<out X509ExtendedTrustManager>) -> X509TrustManager
74+
= IDEATrustManager()::configure
75+
): ClientAdapter<out KubernetesClient> {
6376
setNamespace(namespace, config)
6477
val kubeClient = KubernetesClientBuilder()
6578
.withConfig(config)
79+
.withHttpClientBuilderConsumer { builder ->
80+
setSslContext(builder, config, externalTrustManagerProvider)
81+
}
6682
.build()
6783
val osClient = kubeClient.adapt(NamespacedOpenShiftClient::class.java)
6884
val isOpenShift = isOpenShift(osClient)
@@ -73,10 +89,16 @@ abstract class ClientAdapter<C: KubernetesClient>(private val fabric8Client: C)
7389
}
7490
}
7591

76-
private fun setAcceptCertificates(config: Config) {
77-
val manager = CertificateManager.getInstance().state;
78-
config.isTrustCerts = manager.ACCEPT_AUTOMATICALLY
79-
config.isDisableHostnameVerification = manager.ACCEPT_AUTOMATICALLY
92+
private fun setSslContext(
93+
builder: HttpClient.Builder,
94+
config: Config,
95+
externalTrustManagerProvider: (toIntegrate: Array<out X509ExtendedTrustManager>) -> X509TrustManager
96+
) {
97+
val clientTrustManagers = SSLUtils.trustManagers(config)
98+
.filterIsInstance<X509ExtendedTrustManager>()
99+
.toTypedArray()
100+
val externalTrustManager = externalTrustManagerProvider.invoke(clientTrustManagers)
101+
builder.sslContext(SSLUtils.keyManagers(config), arrayOf(externalTrustManager))
80102
}
81103

82104
private fun isOpenShift(osClient: NamespacedOpenShiftClient): Boolean {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2023 Red Hat, Inc.
3+
* Distributed under license by Red Hat, Inc. All rights reserved.
4+
* This program is made available under the terms of the
5+
* Eclipse Public License v2.0 which accompanies this distribution,
6+
* and is available at http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* Contributors:
9+
* Based on nl.altindag.ssl.trustmanager.CompositeX509ExtendedTrustManager at https://github.com/Hakky54/sslcontext-kickstart
10+
* Red Hat, Inc. - initial API and implementation
11+
******************************************************************************/
12+
13+
package com.redhat.devtools.intellij.kubernetes.model.client.ssl
14+
15+
import java.net.Socket
16+
import java.security.cert.CertificateException
17+
import java.security.cert.X509Certificate
18+
import java.util.*
19+
import java.util.function.Consumer
20+
import javax.net.ssl.SSLEngine
21+
import javax.net.ssl.X509ExtendedTrustManager
22+
23+
class CompositeX509ExtendedTrustManager(trustManagers: List<X509ExtendedTrustManager>): X509ExtendedTrustManager() {
24+
25+
companion object {
26+
private const val CERTIFICATE_EXCEPTION_MESSAGE = "None of the TrustManagers trust this certificate chain"
27+
}
28+
29+
val innerTrustManagers: List<X509ExtendedTrustManager>
30+
private val acceptedIssuers: Array<X509Certificate>
31+
32+
init {
33+
innerTrustManagers = Collections.unmodifiableList(trustManagers)
34+
acceptedIssuers = trustManagers.stream()
35+
.map { obj: X509ExtendedTrustManager -> obj.acceptedIssuers }
36+
.flatMap { array: Array<X509Certificate> ->
37+
Arrays.stream(array)
38+
}
39+
.toArray { length -> arrayOfNulls<X509Certificate>(length) }
40+
}
41+
42+
override fun getAcceptedIssuers(): Array<X509Certificate> {
43+
return Arrays.copyOf(acceptedIssuers, acceptedIssuers.size)
44+
}
45+
46+
@Throws(CertificateException::class)
47+
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
48+
checkTrusted { trustManager: X509ExtendedTrustManager ->
49+
trustManager.checkClientTrusted(
50+
chain,
51+
authType
52+
)
53+
}
54+
}
55+
56+
@Throws(CertificateException::class)
57+
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String, socket: Socket) {
58+
checkTrusted { trustManager: X509ExtendedTrustManager ->
59+
trustManager.checkClientTrusted(
60+
chain,
61+
authType,
62+
socket
63+
)
64+
}
65+
}
66+
67+
@Throws(CertificateException::class)
68+
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String, sslEngine: SSLEngine) {
69+
checkTrusted { trustManager: X509ExtendedTrustManager ->
70+
trustManager.checkClientTrusted(
71+
chain,
72+
authType,
73+
sslEngine
74+
)
75+
}
76+
}
77+
78+
@Throws(CertificateException::class)
79+
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
80+
checkTrusted{ trustManager: X509ExtendedTrustManager ->
81+
trustManager.checkServerTrusted(
82+
chain,
83+
authType
84+
)
85+
}
86+
}
87+
88+
@Throws(CertificateException::class)
89+
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String, socket: Socket) {
90+
checkTrusted{ trustManager: X509ExtendedTrustManager ->
91+
trustManager.checkServerTrusted(
92+
chain,
93+
authType,
94+
socket
95+
)
96+
}
97+
}
98+
99+
@Throws(CertificateException::class)
100+
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String, sslEngine: SSLEngine) {
101+
checkTrusted { trustManager: X509ExtendedTrustManager ->
102+
trustManager.checkServerTrusted(
103+
chain,
104+
authType,
105+
sslEngine
106+
)
107+
}
108+
}
109+
110+
@Throws(CertificateException::class)
111+
private fun checkTrusted(consumer: (trustManager: X509ExtendedTrustManager) -> Unit) {
112+
val certificateExceptions: MutableList<CertificateException> = ArrayList()
113+
for (trustManager in innerTrustManagers) {
114+
try {
115+
consumer.invoke(trustManager)
116+
return
117+
} catch (e: CertificateException) {
118+
certificateExceptions.add(e)
119+
}
120+
}
121+
val certificateException = CertificateException(CERTIFICATE_EXCEPTION_MESSAGE)
122+
certificateExceptions.forEach(Consumer { exception: CertificateException? ->
123+
certificateException.addSuppressed(
124+
exception
125+
)
126+
})
127+
throw certificateException
128+
}
129+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2023 Red Hat, Inc.
3+
* Distributed under license by Red Hat, Inc. All rights reserved.
4+
* This program is made available under the terms of the
5+
* Eclipse Public License v2.0 which accompanies this distribution,
6+
* and is available at http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* Contributors:
9+
* Red Hat, Inc. - initial API and implementation
10+
******************************************************************************/
11+
package com.redhat.devtools.intellij.kubernetes.model.client.ssl
12+
13+
import com.intellij.openapi.diagnostic.logger
14+
import com.intellij.util.net.ssl.CertificateManager
15+
import com.intellij.util.net.ssl.ConfirmingTrustManager
16+
import javax.net.ssl.X509ExtendedTrustManager
17+
import javax.net.ssl.X509TrustManager
18+
import org.apache.commons.lang3.reflect.FieldUtils
19+
20+
class IDEATrustManager(private val trustManager: X509TrustManager = CertificateManager.getInstance().trustManager) {
21+
22+
fun configure(toIntegrate: Array<out X509ExtendedTrustManager>): X509TrustManager {
23+
try {
24+
if (hasSystemManagerField()) {
25+
// < IC-2022.2
26+
setCompositeManager(toIntegrate, trustManager)
27+
} else {
28+
// >= IC-2022.2
29+
addCompositeManager(toIntegrate, trustManager)
30+
}
31+
} catch (e: RuntimeException) {
32+
logger<IDEATrustManager>().warn("Could not configure IDEA trust manager.", e)
33+
}
34+
return trustManager
35+
}
36+
37+
/**
38+
* Returns `true` if [ConfirmingTrustManager] has a private field `mySystemManager`.
39+
* Returns `false` otherwise.
40+
* IDEA < IC-2022.2 manages a single [X509TrustManager] in a private field called `mySystemManager`.
41+
* IDEA >= IC-2022.2 manages a list of [X509TrustManager]s in a private list called `mySystemManagers`.
42+
*/
43+
private fun hasSystemManagerField(): Boolean {
44+
return FieldUtils.getDeclaredField(
45+
ConfirmingTrustManager::class.java, "mySystemManager", true) != null
46+
}
47+
48+
private fun setCompositeManager(
49+
clientTrustManagers: Array<out X509ExtendedTrustManager>,
50+
ideTrustManager: X509TrustManager
51+
): Boolean {
52+
val systemManagerField = FieldUtils.getDeclaredField(
53+
ConfirmingTrustManager::class.java, "mySystemManager", true) ?: return false
54+
val systemManager = systemManagerField.get(ideTrustManager) as? X509ExtendedTrustManager ?: return false
55+
val compositeTrustManager = createCompositeTrustManager(systemManager, clientTrustManagers)
56+
systemManagerField.set(ideTrustManager, compositeTrustManager)
57+
return true
58+
}
59+
60+
private fun addCompositeManager(
61+
clientTrustManagers: Array<out X509ExtendedTrustManager>,
62+
ideTrustManager: X509TrustManager
63+
) {
64+
val systemManagersField = FieldUtils.getDeclaredField(
65+
ideTrustManager::class.java,
66+
"mySystemManagers",
67+
true
68+
)
69+
val managers = systemManagersField.get(ideTrustManager) as? MutableList<X509TrustManager> ?: return
70+
val nonCompositeManagers = managers.filterNot { it is CompositeX509ExtendedTrustManager }
71+
val clientTrustManager = CompositeX509ExtendedTrustManager(clientTrustManagers.asList())
72+
managers.clear()
73+
managers.addAll(nonCompositeManagers)
74+
managers.add(clientTrustManager)
75+
}
76+
77+
private fun createCompositeTrustManager(
78+
systemManager: X509ExtendedTrustManager,
79+
clientTrustManagers: Array<out X509ExtendedTrustManager>
80+
): X509ExtendedTrustManager {
81+
val trustManagers = if (systemManager is CompositeX509ExtendedTrustManager) {
82+
// already patched CertificateManager, re-create composite manager
83+
mutableListOf(systemManager.innerTrustManagers[0])
84+
.plus(clientTrustManagers)
85+
} else {
86+
// 1st time we patch CertificateManager, create composite manager
87+
mutableListOf(systemManager)
88+
.plus(clientTrustManagers)
89+
}
90+
return CompositeX509ExtendedTrustManager(trustManagers)
91+
}
92+
93+
}

src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapterTest.kt

+25-1
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,32 @@
1010
******************************************************************************/
1111
package com.redhat.devtools.intellij.kubernetes.model.client
1212

13+
import com.nhaarman.mockitokotlin2.any
1314
import com.nhaarman.mockitokotlin2.doReturn
1415
import com.nhaarman.mockitokotlin2.mock
1516
import com.nhaarman.mockitokotlin2.verify
1617
import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.config
1718
import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.namedContext
1819
import io.fabric8.kubernetes.client.NamespacedKubernetesClient
1920
import io.fabric8.kubernetes.client.impl.AppsAPIGroupClient
21+
import java.security.cert.X509Certificate
22+
import javax.net.ssl.X509ExtendedTrustManager
23+
import javax.net.ssl.X509TrustManager
2024
import org.assertj.core.api.Assertions.assertThat
2125
import org.junit.Test
2226

2327
class ClientAdapterTest {
2428

29+
private val certificate: X509Certificate = mock {
30+
on { subjectX500Principal } doReturn mock()
31+
}
32+
private val trustManager: X509TrustManager = mock {
33+
on { acceptedIssuers } doReturn arrayOf(certificate)
34+
}
35+
private val trustManagerProvider: (toIntegrate: Array<out X509ExtendedTrustManager>) -> X509TrustManager = mock() {
36+
on { invoke(any()) } doReturn trustManager
37+
}
38+
2539
@Test
2640
fun `#isOpenShift should return true if has OpenShiftClient`() {
2741
// given
@@ -101,10 +115,20 @@ class ClientAdapterTest {
101115
val ctx2 = namedContext("Death Start", "Navy Garrison", "Empire", "Darh Vader" )
102116
val config = config(ctx1, listOf(ctx1, ctx2))
103117
// when
104-
ClientAdapter.Factory.create(namespace, config)
118+
ClientAdapter.Factory.create(namespace, config, trustManagerProvider)
105119
// then
106120
verify(config).namespace = namespace
107121
verify(config.currentContext.context).namespace = namespace
108122
}
109123

124+
@Test
125+
fun `#create should call trust manager provider`() {
126+
// given
127+
val ctx1 = namedContext("Aldeeran", "Aldera", "Republic", "Organa" )
128+
val config = config(ctx1, listOf(ctx1))
129+
// when
130+
ClientAdapter.Factory.create("namespace", config, trustManagerProvider)
131+
// then
132+
verify(trustManagerProvider).invoke(any())
133+
}
110134
}

0 commit comments

Comments
 (0)