Skip to content

Commit fd5b12e

Browse files
committed
Refactored credentials management to support SSH caching
1 parent b28f70f commit fd5b12e

File tree

14 files changed

+210
-154
lines changed

14 files changed

+210
-154
lines changed

src/main/kotlin/com/jetpackduba/gitnuro/credentials/CredentialsCacheRepository.kt

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.jetpackduba.gitnuro.credentials
22

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

31+
fun getCachedSshCredentials(url: String): CredentialsType.SshCredentials? {
32+
val credentials = credentialsCached.filterIsInstance<CredentialsType.SshCredentials>().firstOrNull {
33+
it.url == url
34+
}
35+
36+
return credentials?.copy(password = credentials.password.cipherDecrypt())
37+
}
38+
3139
suspend fun cacheHttpCredentials(credentials: CredentialsType.HttpCredentials) {
3240
cacheHttpCredentials(credentials.url, credentials.userName, credentials.password, credentials.isLfs)
3341
}
3442

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

38-
credentialsLock.lockUse {
46+
credentialsLock.withLock {
3947
val previouslyCached = credentialsCached.any {
4048
it is CredentialsType.HttpCredentials && it.url == url
4149
}
@@ -47,14 +55,16 @@ class CredentialsCacheRepository @Inject constructor() {
4755
}
4856
}
4957

50-
suspend fun cacheSshCredentials(sshKey: String, password: String) {
51-
credentialsLock.lockUse {
58+
suspend fun cacheSshCredentials(url: String, password: String) {
59+
val passwordEncrypted = password.cipherEncrypt()
60+
61+
credentialsLock.withLock {
5262
val previouslyCached = credentialsCached.any {
53-
it is CredentialsType.SshCredentials && it.sshKey == sshKey
63+
it is CredentialsType.SshCredentials && it.url == url
5464
}
5565

5666
if (!previouslyCached) {
57-
val credentials = CredentialsType.SshCredentials(sshKey, password)
67+
val credentials = CredentialsType.SshCredentials(url, passwordEncrypted)
5868
credentialsCached.add(credentials)
5969
}
6070
}
@@ -93,7 +103,7 @@ class CredentialsCacheRepository @Inject constructor() {
93103

94104
sealed interface CredentialsType {
95105
data class SshCredentials(
96-
val sshKey: String,
106+
val url: String,
97107
val password: String,
98108
) : CredentialsType
99109

src/main/kotlin/com/jetpackduba/gitnuro/credentials/CredentialsStateManager.kt

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,77 @@ package com.jetpackduba.gitnuro.credentials
22

33
import kotlinx.coroutines.flow.MutableStateFlow
44
import kotlinx.coroutines.flow.StateFlow
5+
import kotlinx.coroutines.flow.first
6+
import kotlinx.coroutines.sync.Mutex
7+
import kotlinx.coroutines.sync.withLock
58
import javax.inject.Inject
69
import javax.inject.Singleton
10+
import kotlin.coroutines.cancellation.CancellationException
711

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

1520
val currentCredentialsState: CredentialsState
1621
get() = credentialsState.value
1722

18-
fun updateState(newCredentialsState: CredentialsState) {
19-
_credentialsState.value = newCredentialsState
23+
suspend fun requestHttpCredentials(): CredentialsAccepted.HttpCredentialsAccepted {
24+
return requestAwaitingCredentials(CredentialsRequest.HttpCredentialsRequest)
2025
}
2126

22-
fun requestCredentials(credentialsRequest: CredentialsRequest) {
23-
updateState(credentialsRequest)
27+
suspend fun requestSshCredentials(): CredentialsAccepted.SshCredentialsAccepted {
28+
return requestAwaitingCredentials(CredentialsRequest.SshCredentialsRequest)
29+
}
30+
31+
suspend fun requestGpgCredentials(isRetry: Boolean, password: String): CredentialsAccepted.GpgCredentialsAccepted {
32+
return requestAwaitingCredentials(CredentialsRequest.GpgCredentialsRequest(isRetry, password))
33+
}
34+
35+
suspend fun requestLfsCredentials(): CredentialsAccepted.LfsCredentialsAccepted {
36+
return requestAwaitingCredentials(CredentialsRequest.LfsCredentialsRequest)
37+
}
38+
39+
fun credentialsDenied() {
40+
_credentialsState.value = CredentialsState.CredentialsDenied
41+
}
42+
43+
fun httpCredentialsAccepted(user: String, password: String) {
44+
_credentialsState.value = CredentialsAccepted.HttpCredentialsAccepted(user, password)
45+
}
46+
47+
fun sshCredentialsAccepted(password: String) {
48+
_credentialsState.value = CredentialsAccepted.SshCredentialsAccepted(password)
49+
}
50+
51+
fun gpgCredentialsAccepted(password: String) {
52+
_credentialsState.value = CredentialsAccepted.GpgCredentialsAccepted(password)
53+
}
54+
55+
fun lfsCredentialsAccepted(user: String, password: String) {
56+
_credentialsState.value = CredentialsAccepted.LfsCredentialsAccepted(user, password)
57+
}
58+
59+
private suspend inline fun <reified T : CredentialsAccepted> requestAwaitingCredentials(credentialsRequest: CredentialsRequest): T {
60+
mutex.withLock {
61+
assert(this.currentCredentialsState is CredentialsState.None)
62+
63+
_credentialsState.value = credentialsRequest
64+
65+
val credentialsResult = this.credentialsState
66+
.first { it !is CredentialsRequest }
67+
68+
_credentialsState.value = CredentialsState.None
69+
70+
return when (credentialsResult) {
71+
is T -> credentialsResult
72+
is CredentialsState.CredentialsDenied -> throw CancellationException("Credentials denied")
73+
else -> throw IllegalStateException("Unexpected credentials result")
74+
}
75+
}
2476
}
2577
}
2678

src/main/kotlin/com/jetpackduba/gitnuro/credentials/GSessionManager.kt

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,29 @@ import javax.inject.Inject
1010
import javax.inject.Provider
1111

1212
class GSessionManager @Inject constructor(
13-
private val mySessionFactory: MySessionFactory,
13+
private val GSshSessionFactory: GSshSessionFactory,
1414
) {
15-
fun generateSshSessionFactory(): MySessionFactory {
16-
return mySessionFactory
15+
fun generateSshSessionFactory(): GSshSessionFactory {
16+
return GSshSessionFactory
1717
}
1818
}
1919

20-
class MySessionFactory @Inject constructor(
21-
private val sessionProvider: Provider<SshRemoteSession>
22-
) : SshSessionFactory(), CredentialsCache {
20+
class GSshSessionFactory @Inject constructor(
21+
private val sessionProvider: Provider<SshRemoteSession>,
22+
) : SshSessionFactory() {
2323
override fun getSession(
2424
uri: URIish,
25-
credentialsProvider: CredentialsProvider?,
25+
credentialsProvider: CredentialsProvider,
2626
fs: FS?,
27-
tms: Int
27+
tms: Int,
2828
): RemoteSession {
2929
val remoteSession = sessionProvider.get()
30-
remoteSession.setup(uri)
30+
remoteSession.setup(uri, credentialsProvider as SshCredentialsProvider)
3131

3232
return remoteSession
3333
}
3434

3535
override fun getType(): String {
3636
return "ssh" //TODO What should be the value of this?
3737
}
38-
39-
override suspend fun cacheCredentialsIfNeeded() {
40-
// Nothing to do until we add some kind of password cache for SSHKeys
41-
}
4238
}

src/main/kotlin/com/jetpackduba/gitnuro/credentials/GpgCredentialsProvider.kt

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.jetpackduba.gitnuro.credentials
22

3+
import kotlinx.coroutines.runBlocking
34
import org.eclipse.jgit.transport.CredentialItem
45
import org.eclipse.jgit.transport.CredentialsProvider
56
import org.eclipse.jgit.transport.URIish
@@ -47,28 +48,21 @@ class GpgCredentialsProvider @Inject constructor(
4748
}
4849

4950
// Request passphrase
50-
credentialsStateManager.updateState(
51-
CredentialsRequest.GpgCredentialsRequest(
51+
val credentials = runBlocking {
52+
credentialsStateManager.requestGpgCredentials(
5253
isRetry = isRetry,
5354
// Use previously set credentials for cases where this method is invoked again (like when the passphrase is not correct)
5455
password = credentialsSet?.second ?: ""
5556
)
56-
)
57-
58-
var credentials = credentialsStateManager.currentCredentialsState
59-
60-
while (credentials is CredentialsRequest.GpgCredentialsRequest) {
61-
credentials = credentialsStateManager.currentCredentialsState
6257
}
6358

64-
if (credentials is CredentialsAccepted.GpgCredentialsAccepted) {
65-
item.value = credentials.password.toCharArray()
6659

67-
if (promptText != null)
68-
credentialsSet = promptText to credentials.password
60+
item.value = credentials.password.toCharArray()
6961

70-
return true
71-
}
62+
if (promptText != null)
63+
credentialsSet = promptText to credentials.password
64+
65+
return true
7266
}
7367

7468
return false

src/main/kotlin/com/jetpackduba/gitnuro/credentials/HttpCredentialsProvider.kt

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.jetpackduba.gitnuro.managers.IShellManager
88
import com.jetpackduba.gitnuro.repositories.AppSettingsRepository
99
import dagger.assisted.Assisted
1010
import dagger.assisted.AssistedInject
11+
import kotlinx.coroutines.runBlocking
1112
import org.eclipse.jgit.api.Git
1213
import org.eclipse.jgit.internal.JGitText
1314
import org.eclipse.jgit.lib.Config
@@ -33,9 +34,7 @@ class HttpCredentialsProvider @AssistedInject constructor(
3334

3435
private var credentialsCached: CredentialsType.HttpCredentials? = null
3536

36-
override fun isInteractive(): Boolean {
37-
return true
38-
}
37+
override fun isInteractive() = true
3938

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

86-
if (cachedCredentials == null) {
85+
if (cachedCredentials == null || appSettingsRepository.cacheCredentialsInMemory) {
8786
val credentials = askForCredentials()
8887

89-
if (credentials is CredentialsAccepted.HttpCredentialsAccepted) {
90-
userItem.value = credentials.user
91-
passwordItem.value = credentials.password.toCharArray()
92-
93-
if (appSettingsRepository.cacheCredentialsInMemory) {
94-
credentialsCached = CredentialsType.HttpCredentials(
95-
url = uri.toString(),
96-
userName = credentials.user,
97-
password = credentials.password,
98-
isLfs = false,
99-
)
100-
}
88+
userItem.value = credentials.user
89+
passwordItem.value = credentials.password.toCharArray()
10190

102-
return true
103-
} else if (credentials is CredentialsState.CredentialsDenied) {
104-
throw CancellationException("Credentials denied")
91+
if (appSettingsRepository.cacheCredentialsInMemory) {
92+
credentialsCached = CredentialsType.HttpCredentials(
93+
url = uri.toString(),
94+
userName = credentials.user,
95+
password = credentials.password,
96+
isLfs = false,
97+
)
10598
}
99+
100+
return true
106101
} else {
107102
userItem.value = cachedCredentials.userName
108103
passwordItem.value = cachedCredentials.password.toCharArray()
109104

110105
return true
111106
}
112-
113-
return false
114-
115107
} else {
116108
when (handleExternalCredentialHelper(externalCredentialsHelper, uri, items)) {
117109
ExternalCredentialsRequestResult.SUCCESS -> return true
118110
ExternalCredentialsRequestResult.FAIL -> return false
119111
ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED -> {
120112
val credentials = askForCredentials()
121-
if (credentials is CredentialsAccepted.HttpCredentialsAccepted) {
122-
userItem.value = credentials.user
123-
passwordItem.value = credentials.password.toCharArray()
113+
userItem.value = credentials.user
114+
passwordItem.value = credentials.password.toCharArray()
124115

125-
saveCredentialsInExternalHelper(uri, externalCredentialsHelper, credentials)
116+
saveCredentialsInExternalHelper(uri, externalCredentialsHelper, credentials)
126117

127-
return true
128-
}
129-
130-
return false
118+
return true
131119
}
132120
}
133121
}
@@ -136,7 +124,7 @@ class HttpCredentialsProvider @AssistedInject constructor(
136124
private fun saveCredentialsInExternalHelper(
137125
uri: URIish,
138126
externalCredentialsHelper: ExternalCredentialsHelper,
139-
credentials: CredentialsAccepted.HttpCredentialsAccepted
127+
credentials: CredentialsAccepted.HttpCredentialsAccepted,
140128
) {
141129
val arguments = listOf("store")
142130
val process = shellManager.runCommandProcess(externalCredentialsHelper.sanitizedCommand() + arguments)
@@ -160,18 +148,12 @@ class HttpCredentialsProvider @AssistedInject constructor(
160148
}
161149
}
162150

163-
private fun askForCredentials(): CredentialsState {
164-
credentialsStateManager.updateState(CredentialsRequest.HttpCredentialsRequest)
165-
var credentials = credentialsStateManager.currentCredentialsState
166-
while (credentials is CredentialsRequest) {
167-
credentials = credentialsStateManager.currentCredentialsState
168-
}
169-
170-
return credentials
151+
private fun askForCredentials(): CredentialsAccepted.HttpCredentialsAccepted = runBlocking {
152+
credentialsStateManager.requestHttpCredentials()
171153
}
172154

173155
private fun handleExternalCredentialHelper(
174-
externalCredentialsHelper: ExternalCredentialsHelper, uri: URIish, items: Array<out CredentialItem>
156+
externalCredentialsHelper: ExternalCredentialsHelper, uri: URIish, items: Array<out CredentialItem>,
175157
): ExternalCredentialsRequestResult {
176158
// auth git-credential
177159
val arguments = listOf("get")

0 commit comments

Comments
 (0)