Skip to content

Commit c17bf69

Browse files
committed
Added LFS credentials caching
1 parent 9fc86f0 commit c17bf69

File tree

10 files changed

+97
-47
lines changed

10 files changed

+97
-47
lines changed

src/main/kotlin/com/jetpackduba/gitnuro/Icons.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ object AppIcons {
3535
const val KEY = "key.svg"
3636
const val LAYOUT = "layout.svg"
3737
const val LIST = "list.svg"
38+
const val LFS = "lfs.svg"
3839
const val LOCATION = "location.svg"
3940
const val LOCK = "lock.svg"
4041
const val LOGO = "logo.svg"

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

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,29 @@ private const val KEY_LENGTH = 16
1414

1515
@Singleton
1616
class CredentialsCacheRepository @Inject constructor() {
17-
private val credentialsCached = mutableListOf<CredentialsType>()
17+
private val credentialsCached = mutableListOf<CredentialsCacheType>()
1818
private val credentialsLock = Mutex(false)
1919

2020
// having a random key to encrypt the password may help in case of a memory dump attack
2121
private val encryptionKey = getRandomKey()
2222

23-
fun getCachedHttpCredentials(url: String, isLfs: Boolean): CredentialsType.HttpCredentials? {
24-
val credentials = credentialsCached.filterIsInstance<CredentialsType.HttpCredentials>().firstOrNull {
23+
fun getCachedHttpCredentials(url: String, isLfs: Boolean): CredentialsCacheType.HttpCredentialsCache? {
24+
val credentials = credentialsCached.filterIsInstance<CredentialsCacheType.HttpCredentialsCache>().firstOrNull {
2525
it.url == url && it.isLfs == isLfs
2626
}
2727

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 {
31+
fun getCachedSshCredentials(url: String): CredentialsCacheType.SshCredentialsCache? {
32+
val credentials = credentialsCached.filterIsInstance<CredentialsCacheType.SshCredentialsCache>().firstOrNull {
3333
it.url == url
3434
}
3535

3636
return credentials?.copy(password = credentials.password.cipherDecrypt())
3737
}
3838

39-
suspend fun cacheHttpCredentials(credentials: CredentialsType.HttpCredentials) {
39+
suspend fun cacheHttpCredentials(credentials: CredentialsCacheType.HttpCredentialsCache) {
4040
cacheHttpCredentials(credentials.url, credentials.userName, credentials.password, credentials.isLfs)
4141
}
4242

@@ -45,11 +45,11 @@ class CredentialsCacheRepository @Inject constructor() {
4545

4646
credentialsLock.withLock {
4747
val previouslyCached = credentialsCached.any {
48-
it is CredentialsType.HttpCredentials && it.url == url
48+
it is CredentialsCacheType.HttpCredentialsCache && it.url == url
4949
}
5050

5151
if (!previouslyCached) {
52-
val credentials = CredentialsType.HttpCredentials(url, userName, passwordEncrypted, isLfs)
52+
val credentials = CredentialsCacheType.HttpCredentialsCache(url, userName, passwordEncrypted, isLfs)
5353
credentialsCached.add(credentials)
5454
}
5555
}
@@ -60,11 +60,11 @@ class CredentialsCacheRepository @Inject constructor() {
6060

6161
credentialsLock.withLock {
6262
val previouslyCached = credentialsCached.any {
63-
it is CredentialsType.SshCredentials && it.url == url
63+
it is CredentialsCacheType.SshCredentialsCache && it.url == url
6464
}
6565

6666
if (!previouslyCached) {
67-
val credentials = CredentialsType.SshCredentials(url, passwordEncrypted)
67+
val credentials = CredentialsCacheType.SshCredentialsCache(url, passwordEncrypted)
6868
credentialsCached.add(credentials)
6969
}
7070
}
@@ -101,16 +101,16 @@ class CredentialsCacheRepository @Inject constructor() {
101101
}
102102
}
103103

104-
sealed interface CredentialsType {
105-
data class SshCredentials(
104+
sealed interface CredentialsCacheType {
105+
data class SshCredentialsCache(
106106
val url: String,
107107
val password: String,
108-
) : CredentialsType
108+
) : CredentialsCacheType
109109

110-
data class HttpCredentials(
110+
data class HttpCredentialsCache(
111111
val url: String,
112112
val userName: String,
113113
val password: String,
114114
val isLfs: Boolean,
115-
) : CredentialsType
115+
) : CredentialsCacheType
116116
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,13 @@ sealed interface CredentialsAccepted : CredentialsState {
8585
data class SshCredentialsAccepted(val password: String) : CredentialsAccepted
8686
data class GpgCredentialsAccepted(val password: String) : CredentialsAccepted
8787
data class HttpCredentialsAccepted(val user: String, val password: String) : CredentialsAccepted
88-
data class LfsCredentialsAccepted(val user: String, val password: String) : CredentialsAccepted
88+
data class LfsCredentialsAccepted(val user: String, val password: String) : CredentialsAccepted {
89+
companion object {
90+
fun fromCachedCredentials(credentials: CredentialsCacheType.HttpCredentialsCache): LfsCredentialsAccepted {
91+
return LfsCredentialsAccepted(credentials.userName, credentials.password)
92+
}
93+
}
94+
}
8995
}
9096

9197
sealed interface CredentialsRequest : CredentialsState {

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import org.eclipse.jgit.transport.CredentialsProvider
1818
import org.eclipse.jgit.transport.URIish
1919
import java.io.*
2020
import java.util.concurrent.TimeUnit
21-
import kotlin.coroutines.cancellation.CancellationException
2221

2322
private const val TIMEOUT_MIN = 1L
2423
private const val TAG = "HttpCredentialsProvider"
@@ -32,7 +31,7 @@ class HttpCredentialsProvider @AssistedInject constructor(
3231
@Assisted val git: Git?,
3332
) : CredentialsProvider(), CredentialsCache {
3433

35-
private var credentialsCached: CredentialsType.HttpCredentials? = null
34+
private var credentialsCached: CredentialsCacheType.HttpCredentialsCache? = null
3635

3736
override fun isInteractive() = true
3837

@@ -89,7 +88,7 @@ class HttpCredentialsProvider @AssistedInject constructor(
8988
passwordItem.value = credentials.password.toCharArray()
9089

9190
if (appSettingsRepository.cacheCredentialsInMemory) {
92-
credentialsCached = CredentialsType.HttpCredentials(
91+
credentialsCached = CredentialsCacheType.HttpCredentialsCache(
9392
url = uri.toString(),
9493
userName = credentials.user,
9594
password = credentials.password,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class SshCredentialsProvider @Inject constructor(
1414
private val credentialsCacheRepository: CredentialsCacheRepository,
1515
private val appSettingsRepository: AppSettingsRepository,
1616
) : CredentialsProvider(), CredentialsCache {
17-
private var credentialsCached: CredentialsType.SshCredentials? = null
17+
private var credentialsCached: CredentialsCacheType.SshCredentialsCache? = null
1818

1919
override fun isInteractive() = true
2020

@@ -39,7 +39,7 @@ class SshCredentialsProvider @Inject constructor(
3939
passwordItem.value = sshCredentials.password.toCharArray()
4040

4141
if (cacheCredentialsInMemory) {
42-
credentialsCached = CredentialsType.SshCredentials(
42+
credentialsCached = CredentialsCacheType.SshCredentialsCache(
4343
url = uri.toString(),
4444
password = sshCredentials.password,
4545
)

src/main/kotlin/com/jetpackduba/gitnuro/lfs/GLfsFactory.kt

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

33
import com.jetpackduba.gitnuro.Result
4+
import com.jetpackduba.gitnuro.credentials.CredentialsAccepted
45
import com.jetpackduba.gitnuro.credentials.CredentialsCacheRepository
56
import com.jetpackduba.gitnuro.credentials.CredentialsStateManager
67
import com.jetpackduba.gitnuro.logging.printLog
@@ -81,7 +82,7 @@ class GLfsFactory @Inject constructor(
8182
db: Repository,
8283
input: InputStream?,
8384
length: Long,
84-
attribute: Attribute?
85+
attribute: Attribute?,
8586
): LfsInputStream {
8687
return if (this.isEnabled(db, attribute)) LfsInputStream(
8788
LfsBlobFilter.cleanLfsBlob(
@@ -98,22 +99,24 @@ class GLfsFactory @Inject constructor(
9899
outputStream,
99100
null,
100101
lfsRepository,
101-
credentialsStateManager
102+
credentialsStateManager,
103+
credentialsCacheRepository,
102104
) else null
103105
}
104106

105107
@Nullable
106108
override fun getPrePushHook(
107109
repo: Repository,
108110
outputStream: PrintStream?,
109-
errorStream: PrintStream?
111+
errorStream: PrintStream?,
110112
): PrePushHook? {
111113
return if (this.isEnabled(repo)) GLfsPrePushHook(
112114
repo,
113115
outputStream,
114116
errorStream,
115117
lfsRepository,
116-
credentialsStateManager
118+
credentialsStateManager,
119+
credentialsCacheRepository,
117120
) else null
118121
}
119122

@@ -134,24 +137,13 @@ class GLfsFactory @Inject constructor(
134137
}
135138
}
136139

137-
//class GSmudgeFilter(
138-
// repository: Repository,
139-
// input: InputStream?,
140-
// output: OutputStream?,
141-
//) : SmudgeFilter(
142-
// repository,
143-
// input,
144-
// output,
145-
//) {
146-
//
147-
//}
148-
149140
class GLfsPrePushHook(
150141
repository: Repository,
151142
outputStream: PrintStream?,
152143
errorStream: PrintStream?,
153144
private val lfsRepository: LfsRepository,
154145
private val credentialsStateManager: CredentialsStateManager,
146+
private val credentialsCacheRepository: CredentialsCacheRepository,
155147
) : PrePushHook(repository, outputStream, errorStream) {
156148
private var refs: Collection<RemoteRefUpdate> = emptyList()
157149

@@ -234,6 +226,7 @@ class GLfsPrePushHook(
234226
Constants.DEFAULT_REMOTE_NAME
235227
else
236228
remoteName
229+
237230
return Constants.R_REMOTES + remoteName
238231
}
239232

@@ -252,16 +245,55 @@ class GLfsPrePushHook(
252245
objects = toPush.map { LfsObjectBatch(it.oid.name(), it.size) },
253246
)
254247

255-
when (val lfsObjects = getLfsObjects(lfsServerUrl, lfsPrepareUploadObjectBatch)) {
248+
var credentials: CredentialsAccepted.LfsCredentialsAccepted? = null
249+
var credentialsAlreadyRequested = false
250+
251+
val cachedCredentials = run {
252+
val cacheHttpCredentials = credentialsCacheRepository.getCachedHttpCredentials(lfsServerUrl, isLfs = true)
253+
254+
if (cacheHttpCredentials != null) {
255+
CredentialsAccepted.LfsCredentialsAccepted.fromCachedCredentials(cacheHttpCredentials)
256+
} else {
257+
null
258+
}
259+
}
260+
261+
val lfsObjects = getLfsObjects(lfsServerUrl, lfsPrepareUploadObjectBatch) {
262+
if (!credentialsAlreadyRequested && cachedCredentials != null) {
263+
credentialsAlreadyRequested = true
264+
265+
credentials = cachedCredentials
266+
267+
cachedCredentials
268+
} else {
269+
val newCredentials = credentialsStateManager.requestLfsCredentials()
270+
271+
credentials = newCredentials
272+
273+
newCredentials
274+
}
275+
}
276+
277+
when (lfsObjects) {
256278
is Result.Err -> {
257279
throw Exception("LFS Error ${lfsObjects.error}")
258280
}
259281

260282
is Result.Ok -> for (p in toPush) {
283+
val safeCredentials = credentials
284+
if (cachedCredentials != safeCredentials && safeCredentials != null) {
285+
credentialsCacheRepository.cacheHttpCredentials(
286+
lfsServerUrl,
287+
safeCredentials.user,
288+
safeCredentials.password,
289+
isLfs = true,
290+
)
291+
}
261292
val lfs = Lfs(repository)
262293

263294
printLog("LFS", "Requesting credentials for objects upload")
264-
val credentials = credentialsStateManager.requestLfsCredentials()
295+
// TODO Items upload could have their own credentials but it's not common
296+
// val credentials = credentialsStateManager.requestLfsCredentials()
265297

266298
lfsObjects.value.objects.forEach { obj ->
267299
val uploadUrl = obj.actions.upload?.href
@@ -272,8 +304,8 @@ class GLfsPrePushHook(
272304
p.oid.name(),
273305
lfs.getMediaFile(p.oid),
274306
p.size,
275-
credentials.user,
276-
credentials.password,
307+
credentials?.user,
308+
credentials?.password,
277309
)
278310
}
279311
}
@@ -287,6 +319,7 @@ class GLfsPrePushHook(
287319
private suspend fun getLfsObjects(
288320
lfsServerUrl: String,
289321
lfsPrepareUploadObjectBatch: LfsPrepareUploadObjectBatch,
322+
onRequestCredentials: suspend () -> CredentialsAccepted.LfsCredentialsAccepted,
290323
): Result<LfsObjects, LfsError> {
291324

292325
var lfsObjects: Result<LfsObjects, LfsError>
@@ -308,7 +341,7 @@ class GLfsPrePushHook(
308341
newLfsObjects.error.code == HttpStatusCode.Unauthorized
309342

310343
if (requiresAuth) {
311-
val credentials = credentialsStateManager.requestLfsCredentials()
344+
val credentials = onRequestCredentials()
312345
username = credentials.user
313346
password = credentials.password
314347
}

src/main/kotlin/com/jetpackduba/gitnuro/lfs/LfsNetworkDataSource.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,6 @@ class LfsNetworkDataSource @Inject constructor(
165165
while (!packet.exhausted()) {
166166
val bytes = packet.readByteArray()
167167
file.appendBytes(bytes)
168-
println("Received ${file.length()} bytes from ${response.contentLength()}")
169168
}
170169
}
171170

src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/CredentialsDialog.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.jetpackduba.gitnuro.ui.dialogs
22

33
import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.collectAsState
5+
import androidx.compose.ui.res.painterResource
6+
import com.jetpackduba.gitnuro.AppIcons
57
import com.jetpackduba.gitnuro.credentials.CredentialsAccepted
68
import com.jetpackduba.gitnuro.credentials.CredentialsRequest
79
import com.jetpackduba.gitnuro.credentials.CredentialsState
@@ -48,6 +50,9 @@ fun CredentialsDialog(tabViewModel: TabViewModel) {
4850

4951
CredentialsRequest.LfsCredentialsRequest -> {
5052
UserPasswordDialog(
53+
title = "LFS Server Credentials",
54+
subtitle = "Introduce the credentials for your LFS server",
55+
icon = painterResource(AppIcons.LFS),
5156
onReject = {
5257
tabViewModel.credentialsDenied()
5358
},

src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/UserPasswordDialog.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidx.compose.ui.focus.FocusRequester
1212
import androidx.compose.ui.focus.focusProperties
1313
import androidx.compose.ui.focus.focusRequester
1414
import androidx.compose.ui.graphics.Color
15+
import androidx.compose.ui.graphics.painter.Painter
1516
import androidx.compose.ui.input.key.onPreviewKeyEvent
1617
import androidx.compose.ui.res.painterResource
1718
import androidx.compose.ui.text.font.FontWeight
@@ -29,8 +30,11 @@ import com.jetpackduba.gitnuro.ui.components.PrimaryButton
2930

3031
@Composable
3132
fun UserPasswordDialog(
33+
title: String = "Introduce your remote server credentials",
34+
subtitle: String = "Your remote requires authentication with a\nusername and a password",
35+
icon: Painter = painterResource(AppIcons.LOCK),
3236
onReject: () -> Unit,
33-
onAccept: (user: String, password: String) -> Unit
37+
onAccept: (user: String, password: String) -> Unit,
3438
) {
3539
var userField by remember { mutableStateOf("") }
3640
var passwordField by remember { mutableStateOf("") }
@@ -49,7 +53,7 @@ fun UserPasswordDialog(
4953
verticalArrangement = Arrangement.Center,
5054
) {
5155
Icon(
52-
painterResource(AppIcons.LOCK),
56+
icon,
5357
contentDescription = null,
5458
modifier = Modifier
5559
.padding(bottom = 16.dp)
@@ -58,7 +62,7 @@ fun UserPasswordDialog(
5862
)
5963

6064
Text(
61-
text = "Introduce your remote server credentials",
65+
text = title,
6266
modifier = Modifier
6367
.padding(bottom = 8.dp),
6468
color = MaterialTheme.colors.onBackground,
@@ -67,7 +71,7 @@ fun UserPasswordDialog(
6771
)
6872

6973
Text(
70-
text = "Your remote requires authentication with a\nusername and a password",
74+
text = subtitle,
7175
modifier = Modifier
7276
.padding(bottom = 16.dp),
7377
color = MaterialTheme.colors.onBackground,

src/main/resources/lfs.svg

Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)