From a1c8a1c0b8ff09d986b1e8d7bb62e28164edc695 Mon Sep 17 00:00:00 2001 From: Cristian SFERCOCI Date: Fri, 10 Jul 2026 16:23:13 +0000 Subject: [PATCH 1/3] fix: preserve default trust roots for OpenShift TLS --- .../gateway/auth/tls/SslContextFactory.kt | 65 ++++++++++++++++++- .../openshift/OpenShiftClientFactory.kt | 1 + 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/redhat/devtools/gateway/auth/tls/SslContextFactory.kt b/src/main/kotlin/com/redhat/devtools/gateway/auth/tls/SslContextFactory.kt index a72c2c6a..de311732 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/auth/tls/SslContextFactory.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/auth/tls/SslContextFactory.kt @@ -12,6 +12,7 @@ package com.redhat.devtools.gateway.auth.tls import java.security.SecureRandom +import java.security.cert.CertificateException import java.security.cert.X509Certificate import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager @@ -40,6 +41,15 @@ object SslContextFactory { } fun fromTrustedCerts(certs: List): TlsContext { + val defaultTrustManager = defaultTrustManager() + if (certs.isEmpty()) { + val sslContext = SSLContext.getInstance(SSL_PROTOCOL).apply { + init(null, arrayOf(defaultTrustManager), SecureRandom()) + } + + return TlsContext(sslContext, defaultTrustManager) + } + val keyStore = KeyStoreUtils.createEmpty() certs.forEachIndexed { idx, cert -> @@ -55,12 +65,17 @@ object SslContextFactory { ) tmf.init(keyStore) - val trustManager = tmf.trustManagers + val customTrustManager = tmf.trustManagers .filterIsInstance() .first() + val trustManager = CompositeX509TrustManager( + defaultTrustManager, + customTrustManager + ) + val sslContext = SSLContext.getInstance(SSL_PROTOCOL).apply { - init(null, tmf.trustManagers, SecureRandom()) + init(null, arrayOf(trustManager), SecureRandom()) } return TlsContext(sslContext, trustManager) @@ -81,3 +96,49 @@ object SslContextFactory { } } + +private fun defaultTrustManager(): X509TrustManager { + val tmf = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm() + ) + tmf.init(null as java.security.KeyStore?) + + return tmf.trustManagers + .filterIsInstance() + .first() +} + +private class CompositeX509TrustManager( + private val defaultTrustManager: X509TrustManager, + private val customTrustManager: X509TrustManager +) : X509TrustManager { + + override fun checkClientTrusted(chain: Array, authType: String) { + checkTrusted( + { defaultTrustManager.checkClientTrusted(chain, authType) }, + { customTrustManager.checkClientTrusted(chain, authType) } + ) + } + + override fun checkServerTrusted(chain: Array, authType: String) { + checkTrusted( + { defaultTrustManager.checkServerTrusted(chain, authType) }, + { customTrustManager.checkServerTrusted(chain, authType) } + ) + } + + override fun getAcceptedIssuers(): Array = + defaultTrustManager.acceptedIssuers + customTrustManager.acceptedIssuers + + private fun checkTrusted(defaultCheck: () -> Unit, customCheck: () -> Unit) { + try { + defaultCheck() + } catch (defaultFailure: CertificateException) { + try { + customCheck() + } catch (_: CertificateException) { + throw defaultFailure + } + } + } +} diff --git a/src/main/kotlin/com/redhat/devtools/gateway/openshift/OpenShiftClientFactory.kt b/src/main/kotlin/com/redhat/devtools/gateway/openshift/OpenShiftClientFactory.kt index e1e0092c..cbd71ea2 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/openshift/OpenShiftClientFactory.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/openshift/OpenShiftClientFactory.kt @@ -122,6 +122,7 @@ class OpenShiftClientFactory(private val configUtils: KubeConfigUtils) { val sslContext = createSSLContext(trustManager, usingClientCert, clientCert, clientKey) client.httpClient = client.httpClient.newBuilder() .sslSocketFactory(sslContext.socketFactory, trustManager) + .connectTimeout(15, TimeUnit.SECONDS) .build() return client From ad183e09159c87918e161f62a9ce940f4b6075a6 Mon Sep 17 00:00:00 2001 From: Cristian SFERCOCI Date: Fri, 10 Jul 2026 16:23:20 +0000 Subject: [PATCH 2/3] fix: unblock OpenShift OAuth authentication flow --- .../devtools/gateway/openshift/Projects.kt | 18 ++++-- .../view/steps/DevSpacesServerStepView.kt | 64 +++++++++---------- .../OpenShiftOAuthAuthenticationStrategy.kt | 50 ++++++++------- 3 files changed, 70 insertions(+), 62 deletions(-) diff --git a/src/main/kotlin/com/redhat/devtools/gateway/openshift/Projects.kt b/src/main/kotlin/com/redhat/devtools/gateway/openshift/Projects.kt index d4933291..9efb93d7 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/openshift/Projects.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/openshift/Projects.kt @@ -15,7 +15,6 @@ import io.kubernetes.client.openapi.ApiClient import io.kubernetes.client.openapi.ApiException import io.kubernetes.client.openapi.apis.CustomObjectsApi -import io.kubernetes.client.openapi.apis.CoreV1Api import io.kubernetes.client.openapi.apis.AuthorizationV1Api import io.kubernetes.client.openapi.models.V1SelfSubjectAccessReview import io.kubernetes.client.openapi.models.V1SelfSubjectAccessReviewSpec @@ -46,8 +45,19 @@ class Projects(private val client: ApiClient) { @Throws(ApiException::class) fun isAuthenticated(): Boolean { return try { - CoreV1Api(client).listNamespace().execute() - true // 200 OK - authenticated and authorized + AuthorizationV1Api(client).createSelfSubjectAccessReview( + V1SelfSubjectAccessReview() + .spec( + V1SelfSubjectAccessReviewSpec() + .resourceAttributes( + V1ResourceAttributes() + .group("project.openshift.io") + .resource("projects") + .verb("list") + ) + ) + ).execute() + true // 201 Created - authenticated; access result is not relevant here } catch (e: ApiException) { when (e.code) { 401 -> false // Unauthorized - not authenticated @@ -56,4 +66,4 @@ class Projects(private val client: ApiClient) { } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/DevSpacesServerStepView.kt b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/DevSpacesServerStepView.kt index 8397f9e3..de1cc038 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/DevSpacesServerStepView.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/DevSpacesServerStepView.kt @@ -29,7 +29,6 @@ import com.intellij.util.ui.JBFont import com.intellij.util.ui.JBUI import com.redhat.devtools.gateway.DevSpacesBundle import com.redhat.devtools.gateway.DevSpacesContext -import com.redhat.devtools.gateway.auth.session.RedHatAuthSessionManager import com.redhat.devtools.gateway.auth.tls.* import com.redhat.devtools.gateway.auth.tls.ui.UiTlsDecisionAdapter import com.redhat.devtools.gateway.kubeconfig.FileWatcher @@ -85,10 +84,6 @@ class DevSpacesServerStepView( private val saveConfig: Boolean get() = saveConfigCheckbox.isEnabled && saveConfigCheckbox.isSelected - private val sessionManager = - ApplicationManager.getApplication() - .getService(RedHatAuthSessionManager::class.java) - private var tfCertAuthority = JBTextField() .apply { document.addDocumentListener(onFieldChanged()) @@ -141,11 +136,6 @@ class DevSpacesServerStepView( ::onFieldChanged, ::createEnterKeyListener, setTokenDisplay - ), - RedHatSSOAuthenticationStrategy( - tfServer, - ::saveKubeconfig, - sessionManager ) ) } @@ -514,21 +504,22 @@ class DevSpacesServerStepView( private suspend fun saveKubeconfig(cluster: Cluster, token: String, indicator: ProgressIndicator) { if (!saveConfig || token.isBlank()) return - try { - indicator.text = "Updating Kube config..." - withContext(Dispatchers.IO) { + indicator.text = "Updating Kube config..." + val clusterName = cluster.name.trim() + val clusterUrl = cluster.url.trim() + val authToken = token.trim() + + // Kubeconfig persistence is optional; do not block the connection flow on local file I/O. + CoroutineScope(SupervisorJob() + Dispatchers.IO).launch { + try { KubeConfigUpdate .create( - cluster.name.trim(), - cluster.url.trim(), - token.trim()) + clusterName, + clusterUrl, + authToken) .apply() - } - - } catch (e: Exception) { - thisLogger().warn(e.message ?: "Could not save configuration file", e) - withContext(Dispatchers.Main) { - Dialogs.error(e.message ?: "Could not save configuration file", "Save Config Failed") + } catch (e: Exception) { + thisLogger().warn(e.message ?: "Could not save configuration file", e) } } } @@ -539,21 +530,24 @@ class DevSpacesServerStepView( || clientKeyPem.isBlank()) return - try { - indicator.text = "Updating Kube config..." - withContext(Dispatchers.IO) { + indicator.text = "Updating Kube config..." + val clusterName = cluster.name.trim() + val clusterUrl = cluster.url.trim() + val clientCert = clientCertPem.trim() + val clientKey = clientKeyPem.trim() + + // Kubeconfig persistence is optional; do not block the connection flow on local file I/O. + CoroutineScope(SupervisorJob() + Dispatchers.IO).launch { + try { KubeConfigUpdate .create( - cluster.name.trim(), - cluster.url.trim(), - clientCertPem.trim(), - clientKeyPem.trim()) + clusterName, + clusterUrl, + clientCert, + clientKey) .apply() - } - } catch (e: Exception) { - thisLogger().warn(e.message ?: "Could not save configuration file", e) - withContext(Dispatchers.Main) { - Dialogs.error(e.message ?: "Could not save configuration file", "Save Config Failed") + } catch (e: Exception) { + thisLogger().warn(e.message ?: "Could not save configuration file", e) } } } @@ -615,4 +609,4 @@ class DevSpacesServerStepView( service.state.server = cluster.url } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/OpenShiftOAuthAuthenticationStrategy.kt b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/OpenShiftOAuthAuthenticationStrategy.kt index eeab88a4..3cd96cd5 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/OpenShiftOAuthAuthenticationStrategy.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/OpenShiftOAuthAuthenticationStrategy.kt @@ -71,33 +71,37 @@ class OpenShiftOAuthAuthenticationStrategy( currentCoroutineContext().ensureActive() coroutineScope { - launchCancelWatcher(indicator) { login.cancel() } + val cancelWatcher = launchCancelWatcher(indicator) { login.cancel() } - indicator.text = "Obtaining OpenShift access..." - val osToken = login.awaitResult(AbstractAuthSessionManager.LOGIN_TIMEOUT_MS) + try { + indicator.text = "Obtaining OpenShift access..." + val osToken = login.awaitResult(AbstractAuthSessionManager.LOGIN_TIMEOUT_MS) - val finalToken = TokenModel( - accessToken = osToken.accessToken, - expiresAt = osToken.expiresAt, - accountLabel = osToken.accountLabel, - kind = AuthTokenKind.TOKEN, - clusterApiUrl = selectedCluster.url - ) + val finalToken = TokenModel( + accessToken = osToken.accessToken, + expiresAt = osToken.expiresAt, + accountLabel = osToken.accountLabel, + kind = AuthTokenKind.TOKEN, + clusterApiUrl = selectedCluster.url + ) - indicator.text = "Validating cluster access..." - val client = createValidatedApiClient( - server, - certAuthority, - finalToken.accessToken, - null, - null, - tlsContext, - "Authentication failed: token received from OpenShift Authenticator is invalid or expired." - ) + indicator.text = "Validating cluster access..." + val client = createValidatedApiClient( + server, + certAuthority, + finalToken.accessToken, + null, + null, + tlsContext, + "Authentication failed: token received from OpenShift Authenticator is invalid or expired." + ) - setTokenDisplay(finalToken.accessToken) - saveKubeconfig(selectedCluster, finalToken.accessToken, indicator) - devSpacesContext.client = client + setTokenDisplay(finalToken.accessToken) + saveKubeconfig(selectedCluster, finalToken.accessToken, indicator) + devSpacesContext.client = client + } finally { + cancelWatcher.cancel() + } } } From 653de2a71f1680f8bcd0cb6aff3be5cf7cd39f19 Mon Sep 17 00:00:00 2001 From: Cristian SFERCOCI Date: Fri, 10 Jul 2026 16:23:27 +0000 Subject: [PATCH 3/3] fix: complete connection when remote IDE is already present --- .../gateway/DevSpacesConnectionProvider.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnectionProvider.kt b/src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnectionProvider.kt index 013b93e8..ceb57e4e 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnectionProvider.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnectionProvider.kt @@ -37,6 +37,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import java.util.concurrent.CancellationException import javax.swing.JComponent import javax.swing.Timer @@ -80,6 +81,14 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider { val thinClient = handle.clientHandle ?: throw RuntimeException("Failed to obtain ThinClientHandle") + if (thinClient.clientPresent) { + indicator.text = "Workspace IDE has started successfully" + indicator.text2 = "Opening project window..." + runDelayed(1000) { if (indicator.isRunning) indicator.stop() } + cont.resume(handle) + return@runProcessWithProgressSynchronously + } + indicator.text = "Waiting for workspace IDE to start..." val ready = CompletableDeferred() @@ -100,6 +109,17 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider { cont.resumeWith(Result.failure(error)) } } + + runBlocking { + withTimeoutOrNull(60_000L) { ready.await() } ?: run { + if (ready.isActive) { + indicator.text = "Workspace IDE did not report readiness in time." + ready.completeExceptionally( + RuntimeException("Workspace IDE did not report readiness in time.") + ) + } + } + } } catch (e: ApiException) { indicator.text = "Connection failed" runDelayed(2000, { if (indicator.isRunning) indicator.stop() })