Skip to content

use IDEA certificate manager when connecting to the cluster (#600) #607

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ open class AllContexts(
private val contextFactory: (ClientAdapter<out KubernetesClient>, IResourceModelObservable) -> IActiveContext<out HasMetadata, out KubernetesClient>? =
IActiveContext.Factory::create,
private val modelChange: IResourceModelObservable,
private val clientFactory: (String?, String?) -> ClientAdapter<out KubernetesClient> = ClientAdapter.Factory::create
private val clientFactory: (
namespace: String?,
context: String?
) -> ClientAdapter<out KubernetesClient>
= { namespace, context -> ClientAdapter.Factory.create(namespace, context) }
) : IAllContexts {

init {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<out KubernetesClient> = ClientAdapter.Factory::create) {
open class ProcessWatches(
private val clientFactory: (String?, String?) -> ClientAdapter<out KubernetesClient>
= { namespace: String?, context: String? -> ClientAdapter.Factory.create(namespace, context) }
) {

@Suppress("UNCHECKED_CAST")
protected open val operators: Map<ResourceKind<out HasMetadata>, OperatorSpecs> = mapOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<OpenShiftClient>(client) {
Expand All @@ -49,16 +54,32 @@ open class KubeClientAdapter(client: KubernetesClient) :
}
}

abstract class ClientAdapter<C: KubernetesClient>(private val fabric8Client: C) {
abstract class ClientAdapter<C : KubernetesClient>(private val fabric8Client: C) {

companion object Factory {
fun create(namespace: String? = null, context: String? = null): ClientAdapter<out KubernetesClient> {
return create(namespace, Config.autoConfigure(context))
fun create(
namespace: String? = null,
context: String? = null,
trustManagerProvider: ((toIntegrate: Array<out X509ExtendedTrustManager>) -> X509TrustManager)
= IDEATrustManager()::configure
): ClientAdapter<out KubernetesClient> {
val config = Config.autoConfigure(context)
return create(namespace, config, trustManagerProvider)
}

fun create(namespace: String? = null, config: Config): ClientAdapter<out KubernetesClient> {
fun create(
namespace: String? = null,
config: Config,
externalTrustManagerProvider: (toIntegrate: Array<out X509ExtendedTrustManager>) -> X509TrustManager
= IDEATrustManager()::configure
): ClientAdapter<out KubernetesClient> {
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) {
Expand All @@ -68,6 +89,18 @@ abstract class ClientAdapter<C: KubernetesClient>(private val fabric8Client: C)
}
}

private fun setSslContext(
builder: HttpClient.Builder,
config: Config,
externalTrustManagerProvider: (toIntegrate: Array<out X509ExtendedTrustManager>) -> X509TrustManager
) {
val clientTrustManagers = SSLUtils.trustManagers(config)
.filterIsInstance<X509ExtendedTrustManager>()
.toTypedArray()
val externalTrustManager = externalTrustManagerProvider.invoke(clientTrustManagers)
builder.sslContext(SSLUtils.keyManagers(config), arrayOf(externalTrustManager))
}

private fun isOpenShift(osClient: NamespacedOpenShiftClient): Boolean {
return try {
osClient.isSupported
Expand Down
Original file line number Diff line number Diff line change
@@ -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>): X509ExtendedTrustManager() {

companion object {
private const val CERTIFICATE_EXCEPTION_MESSAGE = "None of the TrustManagers trust this certificate chain"
}

val innerTrustManagers: List<X509ExtendedTrustManager>
private val acceptedIssuers: Array<X509Certificate>

init {
innerTrustManagers = Collections.unmodifiableList(trustManagers)
acceptedIssuers = trustManagers
.map { manager: X509ExtendedTrustManager -> manager.acceptedIssuers }
.flatMap { acceptedIssuers: Array<X509Certificate>? ->
acceptedIssuers?.asList() ?: emptyList()
}
.toTypedArray()
}

override fun getAcceptedIssuers(): Array<X509Certificate> {
return Arrays.copyOf(acceptedIssuers, acceptedIssuers.size)
}

@Throws(CertificateException::class)
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
checkTrusted { trustManager: X509ExtendedTrustManager ->
trustManager.checkClientTrusted(
chain,
authType
)
}
}

@Throws(CertificateException::class)
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String, socket: Socket) {
checkTrusted { trustManager: X509ExtendedTrustManager ->
trustManager.checkClientTrusted(
chain,
authType,
socket
)
}
}

@Throws(CertificateException::class)
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String, sslEngine: SSLEngine) {
checkTrusted { trustManager: X509ExtendedTrustManager ->
trustManager.checkClientTrusted(
chain,
authType,
sslEngine
)
}
}

@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
checkTrusted{ trustManager: X509ExtendedTrustManager ->
trustManager.checkServerTrusted(
chain,
authType
)
}
}

@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String, socket: Socket) {
checkTrusted{ trustManager: X509ExtendedTrustManager ->
trustManager.checkServerTrusted(
chain,
authType,
socket
)
}
}

@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<X509Certificate>, 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<CertificateException> = 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
}
}
Original file line number Diff line number Diff line change
@@ -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<out X509ExtendedTrustManager>): X509TrustManager {
try {
if (hasSystemManagerField()) {
// < IC-2022.2
setCompositeManager(toAdd, trustManager)
} else {
// >= IC-2022.2
addCompositeManager(toAdd, trustManager)
}
} catch (e: RuntimeException) {
logger<IDEATrustManager>().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<out X509ExtendedTrustManager>,
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<out X509ExtendedTrustManager>
): 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<out X509ExtendedTrustManager>,
destination: X509TrustManager
): Boolean {
val systemManagersField = FieldUtils.getDeclaredField(
destination::class.java,
"mySystemManagers",
true
) ?: return false
val managers = systemManagersField.get(destination) as? MutableList<X509TrustManager> ?: return false
val nonCompositeManagers = managers.filterNot { it is CompositeX509ExtendedTrustManager }
val clientTrustManager = CompositeX509ExtendedTrustManager(trustManagers.asList())
managers.clear()
managers.addAll(nonCompositeManagers)
managers.add(clientTrustManager)
return true
}
}
Loading