Added support for external git credentials helper [BETA]
Fixes https://github.com/JetpackDuba/Gitnuro/issues/16
This commit is contained in:
parent
c597624354
commit
7bdc2c4cf5
@ -1,19 +1,27 @@
|
|||||||
package com.jetpackduba.gitnuro.credentials
|
package com.jetpackduba.gitnuro.credentials
|
||||||
|
|
||||||
|
import com.jetpackduba.gitnuro.exceptions.NotSupportedHelper
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.eclipse.jgit.lib.Config
|
||||||
import org.eclipse.jgit.transport.CredentialItem
|
import org.eclipse.jgit.transport.CredentialItem
|
||||||
import org.eclipse.jgit.transport.CredentialsProvider
|
import org.eclipse.jgit.transport.CredentialsProvider
|
||||||
import org.eclipse.jgit.transport.URIish
|
import org.eclipse.jgit.transport.URIish
|
||||||
import javax.inject.Inject
|
import java.io.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class HttpCredentialsProvider @Inject constructor(
|
private const val TIMEOUT_MIN = 1L
|
||||||
private val credentialsStateManager: CredentialsStateManager
|
|
||||||
|
class HttpCredentialsProvider @AssistedInject constructor(
|
||||||
|
private val credentialsStateManager: CredentialsStateManager,
|
||||||
|
@Assisted val git: Git?,
|
||||||
) : CredentialsProvider() {
|
) : CredentialsProvider() {
|
||||||
override fun isInteractive(): Boolean {
|
override fun isInteractive(): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun supports(vararg items: CredentialItem?): Boolean {
|
override fun supports(vararg items: CredentialItem?): Boolean {
|
||||||
println(items)
|
|
||||||
val fields = items.map { credentialItem -> credentialItem?.promptText }
|
val fields = items.map { credentialItem -> credentialItem?.promptText }
|
||||||
return if (fields.isEmpty()) {
|
return if (fields.isEmpty()) {
|
||||||
true
|
true
|
||||||
@ -23,30 +31,199 @@ class HttpCredentialsProvider @Inject constructor(
|
|||||||
fields.contains("Password")
|
fields.contains("Password")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun get(uri: URIish?, vararg items: CredentialItem?): Boolean {
|
override fun get(uri: URIish, vararg items: CredentialItem): Boolean {
|
||||||
credentialsStateManager.updateState(CredentialsState.HttpCredentialsRequested)
|
val userItem = items.firstOrNull { it.promptText == "Username" }
|
||||||
|
val passwordItem = items.firstOrNull { it.promptText == "Password" }
|
||||||
|
|
||||||
var credentials = credentialsStateManager.currentCredentialsState
|
if (userItem !is CredentialItem.Username || passwordItem !is CredentialItem.Password) {
|
||||||
while (credentials is CredentialsState.CredentialsRequested) {
|
return false
|
||||||
credentials = credentialsStateManager.currentCredentialsState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (credentials is CredentialsState.HttpCredentialsAccepted) {
|
val externalCredentialsHelper = getExternalCredentialsHelper(uri, git)
|
||||||
val userItem = items.firstOrNull { it?.promptText == "Username" }
|
|
||||||
val passwordItem = items.firstOrNull { it?.promptText == "Password" }
|
|
||||||
|
|
||||||
if (userItem is CredentialItem.Username &&
|
if (externalCredentialsHelper == null) {
|
||||||
passwordItem is CredentialItem.Password
|
val credentials = askForCredentials()
|
||||||
) {
|
|
||||||
|
|
||||||
|
if (credentials is CredentialsState.HttpCredentialsAccepted) {
|
||||||
userItem.value = credentials.user
|
userItem.value = credentials.user
|
||||||
passwordItem.value = credentials.password.toCharArray()
|
passwordItem.value = credentials.password.toCharArray()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
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 CredentialsState.HttpCredentialsAccepted) {
|
||||||
|
userItem.value = credentials.user
|
||||||
|
passwordItem.value = credentials.password.toCharArray()
|
||||||
|
|
||||||
|
saveCredentialsInExternalHelper(uri, externalCredentialsHelper, credentials)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveCredentialsInExternalHelper(
|
||||||
|
uri: URIish,
|
||||||
|
externalCredentialsHelper: ExternalCredentialsHelper,
|
||||||
|
credentials: CredentialsState.HttpCredentialsAccepted
|
||||||
|
) {
|
||||||
|
val process = Runtime.getRuntime()
|
||||||
|
.exec(String.format("${externalCredentialsHelper.path} %s", "store"));
|
||||||
|
|
||||||
|
val output = process.outputStream // write to the input stream of the helper
|
||||||
|
val bufferedWriter = BufferedWriter(OutputStreamWriter(output))
|
||||||
|
|
||||||
|
bufferedWriter.use {
|
||||||
|
bufferedWriter.write("protocol=${uri.scheme}\n")
|
||||||
|
bufferedWriter.write("host=${uri.host}\n")
|
||||||
|
|
||||||
|
if (externalCredentialsHelper.useHttpPath) {
|
||||||
|
bufferedWriter.write("path=${uri.path}\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferedWriter.write("username=${credentials.user}\n")
|
||||||
|
bufferedWriter.write("password=${credentials.password}\n")
|
||||||
|
bufferedWriter.write("")
|
||||||
|
|
||||||
|
bufferedWriter.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun askForCredentials(): CredentialsState {
|
||||||
|
credentialsStateManager.updateState(CredentialsState.HttpCredentialsRequested)
|
||||||
|
var credentials = credentialsStateManager.currentCredentialsState
|
||||||
|
while (credentials is CredentialsState.CredentialsRequested) {
|
||||||
|
credentials = credentialsStateManager.currentCredentialsState
|
||||||
|
}
|
||||||
|
|
||||||
|
return credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleExternalCredentialHelper(
|
||||||
|
externalCredentialsHelper: ExternalCredentialsHelper,
|
||||||
|
uri: URIish,
|
||||||
|
items: Array<out CredentialItem>
|
||||||
|
): ExternalCredentialsRequestResult {
|
||||||
|
val process = Runtime.getRuntime()
|
||||||
|
.exec(String.format("${externalCredentialsHelper.path} %s", "get"))
|
||||||
|
|
||||||
|
val output = process.outputStream // write to the input stream of the helper
|
||||||
|
val input = process.inputStream // reads from the output stream of the helper
|
||||||
|
|
||||||
|
val bufferedWriter = BufferedWriter(OutputStreamWriter(output))
|
||||||
|
val bufferedReader = BufferedReader(InputStreamReader(input))
|
||||||
|
|
||||||
|
bufferedWriter.use {
|
||||||
|
bufferedWriter.write("protocol=${uri.scheme}\n")
|
||||||
|
bufferedWriter.write("host=${uri.host}\n")
|
||||||
|
|
||||||
|
if (externalCredentialsHelper.useHttpPath) {
|
||||||
|
bufferedWriter.write("path=${uri.path}\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferedWriter.write("")
|
||||||
|
|
||||||
|
bufferedWriter.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
var usernameSet = false
|
||||||
|
var passwordSet = false
|
||||||
|
|
||||||
|
process.waitFor(TIMEOUT_MIN, TimeUnit.MINUTES)
|
||||||
|
|
||||||
|
// If the process is alive after $TIMEOUT_MIN, it means that it hasn't given an answer and then finished
|
||||||
|
if(process.isAlive) {
|
||||||
|
process.destroy()
|
||||||
|
return ExternalCredentialsRequestResult.FAIL
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferedReader.use {
|
||||||
|
var line: String;
|
||||||
|
while (bufferedReader.readLine().also { line = it } != null && !(usernameSet && passwordSet)) {
|
||||||
|
if (line.startsWith("username=")) {
|
||||||
|
val split = line.split("=")
|
||||||
|
val userName = split.getOrNull(1) ?: return ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED
|
||||||
|
|
||||||
|
val userNameItem = items.firstOrNull { it.promptText == "Username" }
|
||||||
|
|
||||||
|
if (userNameItem is CredentialItem.Username) {
|
||||||
|
userNameItem.value = userName
|
||||||
|
usernameSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (line.startsWith("password=")) {
|
||||||
|
val split = line.split("=")
|
||||||
|
val password = split.getOrNull(1) ?: return ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED
|
||||||
|
|
||||||
|
val passwordItem = items.firstOrNull { it.promptText == "Password" }
|
||||||
|
|
||||||
|
if (passwordItem is CredentialItem.Password) {
|
||||||
|
passwordItem.value = password.toCharArray()
|
||||||
|
passwordSet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (usernameSet && passwordSet)
|
||||||
|
ExternalCredentialsRequestResult.SUCCESS
|
||||||
|
else
|
||||||
|
ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getExternalCredentialsHelper(uri: URIish, git: Git?): ExternalCredentialsHelper? {
|
||||||
|
val config = if (git == null) {
|
||||||
|
val homePath = System.getProperty("user.home")
|
||||||
|
val configFile = File("$homePath/.gitconfig")
|
||||||
|
|
||||||
|
Config().apply {
|
||||||
|
if (configFile.exists()) {
|
||||||
|
fromText(configFile.readText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
git.repository.config
|
||||||
|
}
|
||||||
|
|
||||||
|
val hostWithProtocol = "${uri.scheme}://${uri.host}"
|
||||||
|
|
||||||
|
val genericCredentialHelper = config.getString("credential", null, "helper")
|
||||||
|
val uriSpecificCredentialHelper = config.getString("credential", hostWithProtocol, "helper")
|
||||||
|
val credentialHelperPath = uriSpecificCredentialHelper ?: genericCredentialHelper ?: return null
|
||||||
|
|
||||||
|
if(credentialHelperPath == "cache" || credentialHelperPath == "store") {
|
||||||
|
throw NotSupportedHelper("Invalid credentials: \"$credentialHelperPath\" is not yet supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use getString instead of getBoolean as boolean has a default value by we want null if the config field is not set
|
||||||
|
val uriSpecificUseHttpHelper = config.getString("credential", hostWithProtocol, "useHttpPath")
|
||||||
|
val genericUseHttpHelper = config.getBoolean("credential", "useHttpPath", false)
|
||||||
|
|
||||||
|
val useHttpPath = uriSpecificUseHttpHelper?.toBoolean() ?: genericUseHttpHelper
|
||||||
|
|
||||||
|
return ExternalCredentialsHelper(credentialHelperPath, useHttpPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ExternalCredentialsHelper(
|
||||||
|
val path: String,
|
||||||
|
val useHttpPath: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class ExternalCredentialsRequestResult {
|
||||||
|
SUCCESS,
|
||||||
|
FAIL,
|
||||||
|
CREDENTIALS_NOT_STORED;
|
||||||
}
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.jetpackduba.gitnuro.di.factories
|
||||||
|
|
||||||
|
import com.jetpackduba.gitnuro.credentials.HttpCredentialsProvider
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface HttpCredentialsFactory {
|
||||||
|
fun create(git: Git?) : HttpCredentialsProvider
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package com.jetpackduba.gitnuro.exceptions
|
||||||
|
|
||||||
|
class NotSupportedHelper(message: String) : GitnuroException(message)
|
@ -59,7 +59,7 @@ class CloneRepositoryUseCase @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.setTransportConfigCallback { handleTransportUseCase(it) }
|
.setTransportConfigCallback { handleTransportUseCase(it, null) }
|
||||||
.call()
|
.call()
|
||||||
|
|
||||||
ensureActive()
|
ensureActive()
|
||||||
|
@ -27,7 +27,7 @@ class DeleteRemoteBranchUseCase @Inject constructor(
|
|||||||
|
|
||||||
val pushResults = git.push()
|
val pushResults = git.push()
|
||||||
.setTransportConfigCallback {
|
.setTransportConfigCallback {
|
||||||
handleTransportUseCase(it)
|
handleTransportUseCase(it, git)
|
||||||
}
|
}
|
||||||
.setRefSpecs(refSpec)
|
.setRefSpecs(refSpec)
|
||||||
.setRemote(remoteName)
|
.setRemote(remoteName)
|
||||||
|
@ -16,7 +16,7 @@ class FetchAllBranchesUseCase @Inject constructor(
|
|||||||
git.fetch()
|
git.fetch()
|
||||||
.setRemote(remote.name)
|
.setRemote(remote.name)
|
||||||
.setRefSpecs(remote.fetchRefSpecs)
|
.setRefSpecs(remote.fetchRefSpecs)
|
||||||
.setTransportConfigCallback { handleTransportUseCase(it) }
|
.setTransportConfigCallback { handleTransportUseCase(it, git) }
|
||||||
.setCredentialsProvider(CredentialsProvider.getDefault())
|
.setCredentialsProvider(CredentialsProvider.getDefault())
|
||||||
.call()
|
.call()
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ package com.jetpackduba.gitnuro.git.remote_operations
|
|||||||
|
|
||||||
import com.jetpackduba.gitnuro.credentials.GSessionManager
|
import com.jetpackduba.gitnuro.credentials.GSessionManager
|
||||||
import com.jetpackduba.gitnuro.credentials.HttpCredentialsProvider
|
import com.jetpackduba.gitnuro.credentials.HttpCredentialsProvider
|
||||||
|
import com.jetpackduba.gitnuro.di.factories.HttpCredentialsFactory
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.transport.HttpTransport
|
import org.eclipse.jgit.transport.HttpTransport
|
||||||
import org.eclipse.jgit.transport.SshTransport
|
import org.eclipse.jgit.transport.SshTransport
|
||||||
import org.eclipse.jgit.transport.Transport
|
import org.eclipse.jgit.transport.Transport
|
||||||
@ -10,13 +12,13 @@ import javax.inject.Provider
|
|||||||
|
|
||||||
class HandleTransportUseCase @Inject constructor(
|
class HandleTransportUseCase @Inject constructor(
|
||||||
private val sessionManager: GSessionManager,
|
private val sessionManager: GSessionManager,
|
||||||
private val httpCredentialsProvider: Provider<HttpCredentialsProvider>,
|
private val httpCredentialsProvider: HttpCredentialsFactory,
|
||||||
) {
|
) {
|
||||||
operator fun invoke(transport: Transport?) {
|
operator fun invoke(transport: Transport?, git: Git?) {
|
||||||
if (transport is SshTransport) {
|
if (transport is SshTransport) {
|
||||||
transport.sshSessionFactory = sessionManager.generateSshSessionFactory()
|
transport.sshSessionFactory = sessionManager.generateSshSessionFactory()
|
||||||
} else if (transport is HttpTransport) {
|
} else if (transport is HttpTransport) {
|
||||||
transport.credentialsProvider = httpCredentialsProvider.get()
|
transport.credentialsProvider = httpCredentialsProvider.create(git)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,7 +13,7 @@ class PullBranchUseCase @Inject constructor(
|
|||||||
suspend operator fun invoke(git: Git, rebase: Boolean) = withContext(Dispatchers.IO) {
|
suspend operator fun invoke(git: Git, rebase: Boolean) = withContext(Dispatchers.IO) {
|
||||||
val pullResult = git
|
val pullResult = git
|
||||||
.pull()
|
.pull()
|
||||||
.setTransportConfigCallback { handleTransportUseCase(it) }
|
.setTransportConfigCallback { handleTransportUseCase(it, git) }
|
||||||
.setRebase(rebase)
|
.setRebase(rebase)
|
||||||
.setCredentialsProvider(CredentialsProvider.getDefault())
|
.setCredentialsProvider(CredentialsProvider.getDefault())
|
||||||
.call()
|
.call()
|
||||||
|
@ -16,7 +16,7 @@ class PullFromSpecificBranchUseCase @Inject constructor(
|
|||||||
suspend operator fun invoke(git: Git, rebase: Boolean, remoteBranch: Ref) = withContext(Dispatchers.IO) {
|
suspend operator fun invoke(git: Git, rebase: Boolean, remoteBranch: Ref) = withContext(Dispatchers.IO) {
|
||||||
val pullResult = git
|
val pullResult = git
|
||||||
.pull()
|
.pull()
|
||||||
.setTransportConfigCallback { handleTransportUseCase(it) }
|
.setTransportConfigCallback { handleTransportUseCase(it, git) }
|
||||||
.setRemote(remoteBranch.remoteName)
|
.setRemote(remoteBranch.remoteName)
|
||||||
.setRemoteBranchName(remoteBranch.simpleName)
|
.setRemoteBranchName(remoteBranch.simpleName)
|
||||||
.setRebase(rebase)
|
.setRebase(rebase)
|
||||||
|
@ -22,7 +22,7 @@ class PushBranchUseCase @Inject constructor(
|
|||||||
if (pushTags)
|
if (pushTags)
|
||||||
setPushTags()
|
setPushTags()
|
||||||
}
|
}
|
||||||
.setTransportConfigCallback { handleTransportUseCase(it) }
|
.setTransportConfigCallback { handleTransportUseCase(it, git) }
|
||||||
.call()
|
.call()
|
||||||
|
|
||||||
val results =
|
val results =
|
||||||
|
@ -27,7 +27,7 @@ class PushToSpecificBranchUseCase @Inject constructor(
|
|||||||
if (pushTags)
|
if (pushTags)
|
||||||
setPushTags()
|
setPushTags()
|
||||||
}
|
}
|
||||||
.setTransportConfigCallback { handleTransportUseCase(it) }
|
.setTransportConfigCallback { handleTransportUseCase(it, git) }
|
||||||
.call()
|
.call()
|
||||||
|
|
||||||
val results =
|
val results =
|
||||||
|
Loading…
Reference in New Issue
Block a user