Skip to content
Closed
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 @@ -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
Expand Down Expand Up @@ -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<GatewayConnectionHandle?>()
Expand All @@ -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() })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -40,6 +41,15 @@ object SslContextFactory {
}

fun fromTrustedCerts(certs: List<X509Certificate>): TlsContext {
val defaultTrustManager = defaultTrustManager()
if (certs.isEmpty()) {
val sslContext = SSLContext.getInstance(SSL_PROTOCOL).apply {
init(null, arrayOf<TrustManager>(defaultTrustManager), SecureRandom())
}

return TlsContext(sslContext, defaultTrustManager)
}

val keyStore = KeyStoreUtils.createEmpty()

certs.forEachIndexed { idx, cert ->
Expand All @@ -55,12 +65,17 @@ object SslContextFactory {
)
tmf.init(keyStore)

val trustManager = tmf.trustManagers
val customTrustManager = tmf.trustManagers
.filterIsInstance<X509TrustManager>()
.first()

val trustManager = CompositeX509TrustManager(
defaultTrustManager,
customTrustManager
)

val sslContext = SSLContext.getInstance(SSL_PROTOCOL).apply {
init(null, tmf.trustManagers, SecureRandom())
init(null, arrayOf<TrustManager>(trustManager), SecureRandom())
}

return TlsContext(sslContext, trustManager)
Expand All @@ -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<X509TrustManager>()
.first()
}

private class CompositeX509TrustManager(
private val defaultTrustManager: X509TrustManager,
private val customTrustManager: X509TrustManager
) : X509TrustManager {

override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
checkTrusted(
{ defaultTrustManager.checkClientTrusted(chain, authType) },
{ customTrustManager.checkClientTrusted(chain, authType) }
)
}

override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
checkTrusted(
{ defaultTrustManager.checkServerTrusted(chain, authType) },
{ customTrustManager.checkServerTrusted(chain, authType) }
)
}

override fun getAcceptedIssuers(): Array<X509Certificate> =
defaultTrustManager.acceptedIssuers + customTrustManager.acceptedIssuers

private fun checkTrusted(defaultCheck: () -> Unit, customCheck: () -> Unit) {
try {
defaultCheck()
} catch (defaultFailure: CertificateException) {
try {
customCheck()
} catch (_: CertificateException) {
throw defaultFailure
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

@adietish adietish Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed OpenShiftClientFactory and made builders instead (were present for 1 use case before).
The new BaseClientBuilder now has the connect timeout, too:

    OkHttpClient.Builder()
            .sslSocketFactory(sslContext.socketFactory, trustManager)
==>         .connectTimeout(DEFAULT_HTTP_TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .readTimeout(DEFAULT_HTTP_TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .writeTimeout(DEFAULT_HTTP_TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .callTimeout(DEFAULT_HTTP_TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .protocols(listOf(Protocol.HTTP_1_1))
            .build()

.build()

return client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -56,4 +66,4 @@ class Projects(private val client: ApiClient) {
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -141,11 +136,6 @@ class DevSpacesServerStepView(
::onFieldChanged,
::createEnterKeyListener,
setTokenDisplay
),
RedHatSSOAuthenticationStrategy(
tfServer,
::saveKubeconfig,
sessionManager
)
)
}
Expand Down Expand Up @@ -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)
}
}
}
Expand All @@ -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)
}
}
}
Expand Down Expand Up @@ -615,4 +609,4 @@ class DevSpacesServerStepView(
service.state.server = cluster.url
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}

Expand Down