Skip to content

Commit

Permalink
1
Browse files Browse the repository at this point in the history
Tool: gitpod/catfood.gitpod.cloud
  • Loading branch information
mustard-mh committed Jan 24, 2025
1 parent 81338c2 commit d489776
Show file tree
Hide file tree
Showing 16 changed files with 445 additions and 410 deletions.
2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions components/ide/jetbrains/toolbox/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dependencies {
// com.connectrpc https://mvnrepository.com/artifact/com.connectrpc
// connect rpc dependencies
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
implementation("com.connectrpc:connect-kotlin-okhttp:0.6.0")
implementation("com.connectrpc:connect-kotlin:0.6.0")
// Java specific dependencies.
Expand Down Expand Up @@ -83,10 +84,7 @@ tasks.shadowJar {
"com.jetbrains.toolbox.gateway",
"com.jetbrains",
"org.jetbrains",
// "com.squareup.okhttp3",
// "org.slf4j",
"org.jetbrains.intellij",
// "com.squareup.okio",
"kotlin."
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) 2025 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package io.gitpod.toolbox.auth

import com.jetbrains.toolbox.api.core.auth.Account
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

@Serializable
class GitpodAccount : Account {
private val userID: String
private val name: String
private val host: String
private val scopes: List<String>

constructor(id: String, name: String, host: String, scopes: List<String>) {
this.userID = id
this.name = name
this.host = host
this.scopes = scopes
}

override fun getId() = userID
override fun getFullName() = name

fun getGitpodHost() = host

fun encode(): String {
return Json.encodeToString(this)
}

companion object {
fun decode(str: String): GitpodAccount {
return Json.decodeFromString<GitpodAccount>(str)
}
}
}

class GitpodLoginConfiguration(val hostUrl: String)

val authScopesJetBrainsToolbox = listOf(
"function:getGitpodTokenScopes",
"function:getLoggedInUser",
"function:getOwnerToken",
"function:getWorkspace",
"function:getWorkspaces",
"function:listenForWorkspaceInstanceUpdates",
"function:startWorkspace",
"function:stopWorkspace",
"function:deleteWorkspace",
"function:getToken",
"resource:default",
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,27 @@

package io.gitpod.toolbox.auth

import com.connectrpc.Code
import com.connectrpc.ConnectException
import com.jetbrains.toolbox.api.core.auth.*
import io.gitpod.publicapi.experimental.v1.UserServiceClient
import io.gitpod.toolbox.service.GitpodPublicApiManager
import io.gitpod.toolbox.service.GitpodPublicApi
import io.gitpod.toolbox.service.Utils
import kotlinx.coroutines.future.future
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import java.net.URI
import java.util.*
import java.util.concurrent.Future

// TODO(hw): Validate Scopes
val authScopesJetBrainsToolbox = listOf(
"function:getGitpodTokenScopes",
"function:getLoggedInUser",
"function:getOwnerToken",
"function:getWorkspace",
"function:getWorkspaces",
"function:listenForWorkspaceInstanceUpdates",
"function:startWorkspace",
"function:stopWorkspace",
"function:deleteWorkspace",
"function:getToken",
"resource:default",
)

class GitpodAuthManager {
private val manager: PluginAuthManager<GitpodAccount, GitpodLoginConfiguration>
val manager: PluginAuthManager<GitpodAccount, GitpodLoginConfiguration>
private var loginListeners: MutableList<() -> Unit> = mutableListOf()
private var logoutListeners: MutableList<() -> Unit> = mutableListOf()


init {
manager = Utils.sharedServiceLocator.getAuthManager(
"gitpod",
GitpodAccount::class.java,
{ it.encode() },
{ GitpodAccount.decode(it) },
{ oauthToken, authCfg -> getAuthenticatedUser(URI.create(authCfg.baseUrl).host, oauthToken) },
{ oauthToken, gpAccount -> getAuthenticatedUser(gpAccount.getHost(), oauthToken) },
{ oauthToken, gpAccount -> getAuthenticatedUser(gpAccount.getGitpodHost(), oauthToken) },
{ gpLoginCfg ->
val authParams = mapOf(
"response_type" to "code",
Expand Down Expand Up @@ -90,43 +67,32 @@ class GitpodAuthManager {

private fun resetCurrentAccount(accountId: String) {
val account = manager.accountsWithStatus.find { it.account.id == accountId }?.account ?: return
Utils.logger.debug("reset settings for ${account.getHost()}")
Utils.gitpodSettings.resetSettings(account.getHost())
Utils.logger.debug("reset settings for ${account.getGitpodHost()}")
Utils.gitpodSettings.resetSettings(account.getGitpodHost())
}

fun logoutCurrentAccount() {
getCurrentAccount()?.let { manager.logout(it.id) }
}

fun getCurrentAccount(): GitpodAccount? {
return manager.accountsWithStatus.find { it.account.getHost() == Utils.gitpodSettings.gitpodHost }?.account
return manager.accountsWithStatus.find { it.account.getGitpodHost() == Utils.gitpodSettings.gitpodHost }?.account
}

suspend fun loginWithHost(host: String): Boolean {
val currentAccount = getCurrentAccount()
if (currentAccount?.getHost() == host) {
if (currentAccount.isValidate()) {
return true
} else {
manager.logout(currentAccount.id)
Utils.openUrl(this.getOAuthLoginUrl(host))
return false
}
}
val account = manager.accountsWithStatus.find { it.account.getHost() == host }?.account
if (account != null) {
if (account.isValidate()) {
Utils.gitpodSettings.gitpodHost = host
loginListeners.forEach { it() }
return true
} else {
manager.logout(account.id)
Utils.openUrl(this.getOAuthLoginUrl(host))
fun loginWithHost(host: String): Boolean {
val account = manager.accountsWithStatus.find {
if (it.expired) {
manager.logout(it.account.id)
return false
}
}
Utils.openUrl(this.getOAuthLoginUrl(host))
return false
}
return it.account.getGitpodHost() == host
}?.account

fun logout() {
getCurrentAccount()?.let { manager.logout(it.id) }
if (account == null) {
Utils.openUrl(getOAuthLoginUrl(host))
return false
}
return true // getFromCache
}

fun getOAuthLoginUrl(gitpodHost: String): String {
Expand All @@ -153,81 +119,9 @@ class GitpodAuthManager {

private fun getAuthenticatedUser(gitpodHost: String, oAuthToken: OAuthToken): Future<GitpodAccount> {
return Utils.coroutineScope.future {
val bearerToken = getBearerToken(oAuthToken)
val client = GitpodPublicApiManager.createClient(gitpodHost, bearerToken)
val user = GitpodPublicApiManager.tryGetAuthenticatedUser(UserServiceClient(client))
GitpodAccount(bearerToken, user.id, user.name, gitpodHost, authScopesJetBrainsToolbox)
}
}

private fun getBearerToken(oAuthToken: OAuthToken): String {
val parts = oAuthToken.authorizationHeader.replace("Bearer ", "").split(".")
// We don't validate jwt token
if (parts.size != 3) {
throw IllegalArgumentException("Invalid JWT")
}
val decoded = String(Base64.getUrlDecoder().decode(parts[1].toByteArray()))
val jsonElement = Json.parseToJsonElement(decoded)
val payloadMap = jsonElement.jsonObject.mapValues {
it.value.jsonPrimitive.content
}
return payloadMap["jti"] ?: throw IllegalArgumentException("Failed to parse JWT token")
}

}

class GitpodLoginConfiguration(val hostUrl: String)

@Serializable
class GitpodAccount : Account {
private val credentials: String
private val id: String
private val name: String
private val host: String
private val scopes: List<String>

constructor(credentials: String, id: String, name: String, host: String, scopes: List<String>) {
this.credentials = credentials
this.id = id
this.name = name
this.host = host
this.scopes = scopes
}

override fun getId() = id
override fun getFullName() = name
fun getCredentials() = credentials
fun getHost() = host
fun getScopes() = scopes

fun encode(): String {
return Json.encodeToString(this)
}

suspend fun isValidate(): Boolean {
val hostUrl = "https://$host"
val client = GitpodPublicApiManager.createClient(URI(hostUrl).host, credentials)
Utils.logger.debug("validating account $hostUrl")
try {
GitpodPublicApiManager.tryGetAuthenticatedUser(UserServiceClient(client))
// TODO: Verify scopes
return true
} catch (e: ConnectException) {
// TODO(hw): Server close jsonrpc so papi server respond internal error
if (e.code == Code.UNAUTHENTICATED || (e.code == Code.INTERNAL_ERROR && e.message != null && e.message!!.contains(
"jsonrpc2: connection is closed"
))
) {
Utils.logger.error("account $hostUrl is not valid")
return false
}
return true
}
}

companion object {
fun decode(str: String): GitpodAccount {
return Json.decodeFromString<GitpodAccount>(str)
val apiClient = GitpodPublicApi(gitpodHost, { Utils.coroutineScope.future { oAuthToken } }) {_, _ -> }
val user = apiClient.getAuthenticatedUser()
GitpodAccount(user.id, user.name, gitpodHost, authScopesJetBrainsToolbox)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,9 @@
package io.gitpod.toolbox.components

import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon
import io.gitpod.toolbox.gateway.GitpodGatewayExtension

@Suppress("FunctionName")
fun GitpodIconGray(): SvgIcon {
return SvgIcon(GitpodGatewayExtension::class.java.getResourceAsStream("/icon-gray.svg")?.readAllBytes() ?: byteArrayOf())
}
import io.gitpod.toolbox.gateway.GitpodRemoteDevExtension

@Suppress("FunctionName")
fun GitpodIcon(): SvgIcon {
return SvgIcon(GitpodGatewayExtension::class.java.getResourceAsStream("/icon.svg")?.readAllBytes() ?: byteArrayOf())
return SvgIcon(GitpodRemoteDevExtension::class.java.getResourceAsStream("/icon.svg")?.readAllBytes() ?: byteArrayOf())
}

Loading

0 comments on commit d489776

Please sign in to comment.