Skip to content

Commit

Permalink
Refactored credentials management to support SSH caching
Browse files Browse the repository at this point in the history
  • Loading branch information
JetpackDuba committed Feb 15, 2025
1 parent b28f70f commit fd5b12e
Show file tree
Hide file tree
Showing 14 changed files with 210 additions and 154 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.jetpackduba.gitnuro.credentials

import com.jetpackduba.gitnuro.extensions.lockUse
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.eclipse.jgit.util.Base64
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
Expand All @@ -28,14 +28,22 @@ class CredentialsCacheRepository @Inject constructor() {
return credentials?.copy(password = credentials.password.cipherDecrypt())
}

fun getCachedSshCredentials(url: String): CredentialsType.SshCredentials? {
val credentials = credentialsCached.filterIsInstance<CredentialsType.SshCredentials>().firstOrNull {
it.url == url
}

return credentials?.copy(password = credentials.password.cipherDecrypt())
}

suspend fun cacheHttpCredentials(credentials: CredentialsType.HttpCredentials) {
cacheHttpCredentials(credentials.url, credentials.userName, credentials.password, credentials.isLfs)
}

suspend fun cacheHttpCredentials(url: String, userName: String, password: String, isLfs: Boolean) {
val passwordEncrypted = password.cipherEncrypt()

credentialsLock.lockUse {
credentialsLock.withLock {
val previouslyCached = credentialsCached.any {
it is CredentialsType.HttpCredentials && it.url == url
}
Expand All @@ -47,14 +55,16 @@ class CredentialsCacheRepository @Inject constructor() {
}
}

suspend fun cacheSshCredentials(sshKey: String, password: String) {
credentialsLock.lockUse {
suspend fun cacheSshCredentials(url: String, password: String) {
val passwordEncrypted = password.cipherEncrypt()

credentialsLock.withLock {
val previouslyCached = credentialsCached.any {
it is CredentialsType.SshCredentials && it.sshKey == sshKey
it is CredentialsType.SshCredentials && it.url == url
}

if (!previouslyCached) {
val credentials = CredentialsType.SshCredentials(sshKey, password)
val credentials = CredentialsType.SshCredentials(url, passwordEncrypted)
credentialsCached.add(credentials)
}
}
Expand Down Expand Up @@ -93,7 +103,7 @@ class CredentialsCacheRepository @Inject constructor() {

sealed interface CredentialsType {
data class SshCredentials(
val sshKey: String,
val url: String,
val password: String,
) : CredentialsType

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,77 @@ package com.jetpackduba.gitnuro.credentials

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.cancellation.CancellationException

// TODO Being a singleton, we may have problems if multiple tabs request credentials at the same time
@Singleton
class CredentialsStateManager @Inject constructor() {
private val mutex = Mutex()
private val _credentialsState = MutableStateFlow<CredentialsState>(CredentialsState.None)
val credentialsState: StateFlow<CredentialsState>
get() = _credentialsState

val currentCredentialsState: CredentialsState
get() = credentialsState.value

fun updateState(newCredentialsState: CredentialsState) {
_credentialsState.value = newCredentialsState
suspend fun requestHttpCredentials(): CredentialsAccepted.HttpCredentialsAccepted {
return requestAwaitingCredentials(CredentialsRequest.HttpCredentialsRequest)
}

fun requestCredentials(credentialsRequest: CredentialsRequest) {
updateState(credentialsRequest)
suspend fun requestSshCredentials(): CredentialsAccepted.SshCredentialsAccepted {
return requestAwaitingCredentials(CredentialsRequest.SshCredentialsRequest)
}

suspend fun requestGpgCredentials(isRetry: Boolean, password: String): CredentialsAccepted.GpgCredentialsAccepted {
return requestAwaitingCredentials(CredentialsRequest.GpgCredentialsRequest(isRetry, password))
}

suspend fun requestLfsCredentials(): CredentialsAccepted.LfsCredentialsAccepted {
return requestAwaitingCredentials(CredentialsRequest.LfsCredentialsRequest)
}

fun credentialsDenied() {
_credentialsState.value = CredentialsState.CredentialsDenied
}

fun httpCredentialsAccepted(user: String, password: String) {
_credentialsState.value = CredentialsAccepted.HttpCredentialsAccepted(user, password)
}

fun sshCredentialsAccepted(password: String) {
_credentialsState.value = CredentialsAccepted.SshCredentialsAccepted(password)
}

fun gpgCredentialsAccepted(password: String) {
_credentialsState.value = CredentialsAccepted.GpgCredentialsAccepted(password)
}

fun lfsCredentialsAccepted(user: String, password: String) {
_credentialsState.value = CredentialsAccepted.LfsCredentialsAccepted(user, password)
}

private suspend inline fun <reified T : CredentialsAccepted> requestAwaitingCredentials(credentialsRequest: CredentialsRequest): T {
mutex.withLock {
assert(this.currentCredentialsState is CredentialsState.None)

_credentialsState.value = credentialsRequest

val credentialsResult = this.credentialsState
.first { it !is CredentialsRequest }

_credentialsState.value = CredentialsState.None

return when (credentialsResult) {
is T -> credentialsResult
is CredentialsState.CredentialsDenied -> throw CancellationException("Credentials denied")
else -> throw IllegalStateException("Unexpected credentials result")
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,29 @@ import javax.inject.Inject
import javax.inject.Provider

class GSessionManager @Inject constructor(
private val mySessionFactory: MySessionFactory,
private val GSshSessionFactory: GSshSessionFactory,
) {
fun generateSshSessionFactory(): MySessionFactory {
return mySessionFactory
fun generateSshSessionFactory(): GSshSessionFactory {
return GSshSessionFactory
}
}

class MySessionFactory @Inject constructor(
private val sessionProvider: Provider<SshRemoteSession>
) : SshSessionFactory(), CredentialsCache {
class GSshSessionFactory @Inject constructor(
private val sessionProvider: Provider<SshRemoteSession>,
) : SshSessionFactory() {
override fun getSession(
uri: URIish,
credentialsProvider: CredentialsProvider?,
credentialsProvider: CredentialsProvider,
fs: FS?,
tms: Int
tms: Int,
): RemoteSession {
val remoteSession = sessionProvider.get()
remoteSession.setup(uri)
remoteSession.setup(uri, credentialsProvider as SshCredentialsProvider)

return remoteSession
}

override fun getType(): String {
return "ssh" //TODO What should be the value of this?
}

override suspend fun cacheCredentialsIfNeeded() {
// Nothing to do until we add some kind of password cache for SSHKeys
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.jetpackduba.gitnuro.credentials

import kotlinx.coroutines.runBlocking
import org.eclipse.jgit.transport.CredentialItem
import org.eclipse.jgit.transport.CredentialsProvider
import org.eclipse.jgit.transport.URIish
Expand Down Expand Up @@ -47,28 +48,21 @@ class GpgCredentialsProvider @Inject constructor(
}

// Request passphrase
credentialsStateManager.updateState(
CredentialsRequest.GpgCredentialsRequest(
val credentials = runBlocking {
credentialsStateManager.requestGpgCredentials(
isRetry = isRetry,
// Use previously set credentials for cases where this method is invoked again (like when the passphrase is not correct)
password = credentialsSet?.second ?: ""
)
)

var credentials = credentialsStateManager.currentCredentialsState

while (credentials is CredentialsRequest.GpgCredentialsRequest) {
credentials = credentialsStateManager.currentCredentialsState
}

if (credentials is CredentialsAccepted.GpgCredentialsAccepted) {
item.value = credentials.password.toCharArray()

if (promptText != null)
credentialsSet = promptText to credentials.password
item.value = credentials.password.toCharArray()

return true
}
if (promptText != null)
credentialsSet = promptText to credentials.password

return true
}

return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.jetpackduba.gitnuro.managers.IShellManager
import com.jetpackduba.gitnuro.repositories.AppSettingsRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.runBlocking
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.internal.JGitText
import org.eclipse.jgit.lib.Config
Expand All @@ -33,9 +34,7 @@ class HttpCredentialsProvider @AssistedInject constructor(

private var credentialsCached: CredentialsType.HttpCredentials? = null

override fun isInteractive(): Boolean {
return true
}
override fun isInteractive() = true

override fun supports(vararg items: CredentialItem?): Boolean {
val fields = items.map { credentialItem -> credentialItem?.promptText }
Expand Down Expand Up @@ -83,51 +82,40 @@ class HttpCredentialsProvider @AssistedInject constructor(
isLfs = false,
)

if (cachedCredentials == null) {
if (cachedCredentials == null || appSettingsRepository.cacheCredentialsInMemory) {
val credentials = askForCredentials()

if (credentials is CredentialsAccepted.HttpCredentialsAccepted) {
userItem.value = credentials.user
passwordItem.value = credentials.password.toCharArray()

if (appSettingsRepository.cacheCredentialsInMemory) {
credentialsCached = CredentialsType.HttpCredentials(
url = uri.toString(),
userName = credentials.user,
password = credentials.password,
isLfs = false,
)
}
userItem.value = credentials.user
passwordItem.value = credentials.password.toCharArray()

return true
} else if (credentials is CredentialsState.CredentialsDenied) {
throw CancellationException("Credentials denied")
if (appSettingsRepository.cacheCredentialsInMemory) {
credentialsCached = CredentialsType.HttpCredentials(
url = uri.toString(),
userName = credentials.user,
password = credentials.password,
isLfs = false,
)
}

return true
} else {
userItem.value = cachedCredentials.userName
passwordItem.value = cachedCredentials.password.toCharArray()

return true
}

return false

} else {
when (handleExternalCredentialHelper(externalCredentialsHelper, uri, items)) {
ExternalCredentialsRequestResult.SUCCESS -> return true
ExternalCredentialsRequestResult.FAIL -> return false
ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED -> {
val credentials = askForCredentials()
if (credentials is CredentialsAccepted.HttpCredentialsAccepted) {
userItem.value = credentials.user
passwordItem.value = credentials.password.toCharArray()
userItem.value = credentials.user
passwordItem.value = credentials.password.toCharArray()

saveCredentialsInExternalHelper(uri, externalCredentialsHelper, credentials)
saveCredentialsInExternalHelper(uri, externalCredentialsHelper, credentials)

return true
}

return false
return true
}
}
}
Expand All @@ -136,7 +124,7 @@ class HttpCredentialsProvider @AssistedInject constructor(
private fun saveCredentialsInExternalHelper(
uri: URIish,
externalCredentialsHelper: ExternalCredentialsHelper,
credentials: CredentialsAccepted.HttpCredentialsAccepted
credentials: CredentialsAccepted.HttpCredentialsAccepted,
) {
val arguments = listOf("store")
val process = shellManager.runCommandProcess(externalCredentialsHelper.sanitizedCommand() + arguments)
Expand All @@ -160,18 +148,12 @@ class HttpCredentialsProvider @AssistedInject constructor(
}
}

private fun askForCredentials(): CredentialsState {
credentialsStateManager.updateState(CredentialsRequest.HttpCredentialsRequest)
var credentials = credentialsStateManager.currentCredentialsState
while (credentials is CredentialsRequest) {
credentials = credentialsStateManager.currentCredentialsState
}

return credentials
private fun askForCredentials(): CredentialsAccepted.HttpCredentialsAccepted = runBlocking {
credentialsStateManager.requestHttpCredentials()
}

private fun handleExternalCredentialHelper(
externalCredentialsHelper: ExternalCredentialsHelper, uri: URIish, items: Array<out CredentialItem>
externalCredentialsHelper: ExternalCredentialsHelper, uri: URIish, items: Array<out CredentialItem>,
): ExternalCredentialsRequestResult {
// auth git-credential
val arguments = listOf("get")
Expand Down
Loading

0 comments on commit fd5b12e

Please sign in to comment.