ICS — require token for GitHub, user/pass is not allowed anymore

This commit is contained in:
Vladimir Krivosheev
2016-08-19 10:39:09 +02:00
parent 2edcf05544
commit 7c0b081b0c
16 changed files with 192 additions and 240 deletions
@@ -16,6 +16,8 @@
package com.intellij.credentialStore
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.nullize
import java.security.MessageDigest
import java.util.*
@@ -32,4 +34,60 @@ internal fun getRawKey(key: String, requestor: Class<*>?) = if (requestor == nul
internal fun toOldKey(hash: ByteArray) = "old-hashed-key|" + Base64.getEncoder().encodeToString(hash)
internal fun toOldKey(newKey: String) = toOldKey(MessageDigest.getInstance("SHA-256").digest(newKey.toByteArray()))
internal fun toOldKey(newKey: String) = toOldKey(MessageDigest.getInstance("SHA-256").digest(newKey.toByteArray()))
fun joinData(user: String?, password: String?) = "${StringUtil.escapeChars(user.orEmpty(), '\\', '@')}@$password"
fun splitData(data: String): Credentials? {
if (data.isEmpty()) {
return null
}
val list = parseString(data, '@')
val result = Credentials(list.getOrNull(0), list.getOrNull(1))
return if (result.isFulfilled()) result else null
}
private const val ESCAPING_CHAR = '\\'
private fun parseString(data: String, delimiter: Char): List<String> {
val part = StringBuilder()
val result = ArrayList<String>(2)
var i = 0
var c: Char?
do {
c = data.getOrNull(i++)
if (c != null && c != delimiter) {
if (c == ESCAPING_CHAR) {
c = data.getOrNull(i++)
}
if (c != null) {
part.append(c)
continue
}
}
result.add(part.toString())
part.setLength(0)
}
while (c != null)
return result
}
class Credentials(user: String?, password: String?) {
val user = user.nullize()
val password = password.nullize()
override fun equals(other: Any?): Boolean {
if (other !is Credentials) return false
return user == other.user && password == other.password
}
override fun hashCode() = (user?.hashCode() ?: 0) * 37 + (password?.hashCode() ?: 0)
override fun toString() = joinData(user, password)
}
fun Credentials?.isFulfilled() = this != null && user != null && password != null
@@ -0,0 +1,20 @@
package com.intellij.credentialStore
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class CredentialTest {
@Test
fun join() {
test("foo", "pass", "foo@pass")
test("foo@", "pass", "foo\\@@pass")
test("\\foo@", "pass", "\\\\foo\\@@pass")
test("\\foo\\", "pass", "\\\\foo\\\\@pass")
test("", "pass", "@pass")
test("foo", "", "foo@")
}
private fun test(u: String, p: String, joined: String) {
assertThat(joinData(u, p)).isEqualTo(joined)
assertThat(splitData(joined)).isEqualTo(Credentials(u, p))
}
}
@@ -17,13 +17,17 @@ package com.intellij.openapi.ui
import javax.swing.JComponent
fun dialog(title: String, centerPanel: JComponent, resizable: Boolean = true): DialogBuilder {
fun dialog(title: String, centerPanel: JComponent, resizable: Boolean = true, preferedFocusComponent: JComponent? = null, okActionEnabled: Boolean = true): DialogBuilder {
val builder = DialogBuilder()
builder
.title(title)
.centerPanel(centerPanel)
.setPreferredFocusComponent(preferedFocusComponent)
if (!resizable) {
builder.resizable(false)
}
if (!okActionEnabled) {
builder.okActionEnabled(false)
}
return builder
}
@@ -63,7 +63,7 @@ enum class CCFlags {
*/
span,
grow, push, pushY, pushX, right
grow, push, pushY, pushX, right, skip
}
inline fun panel(vararg layoutConstraints: LCFlags, init: Panel.() -> Unit): JPanel {
@@ -209,6 +209,7 @@ fun CC.apply(flags: Array<out CCFlags>): CC {
CCFlags.pushY -> pushY()
CCFlags.span -> span()
CCFlags.skip -> skip()
}
}
return this
@@ -17,6 +17,6 @@ package com.intellij.util
import com.intellij.openapi.util.text.StringUtil
fun String?.nullize(): String? = StringUtil.nullize(this)
fun String?.nullize(nullizeSpaces: Boolean = false): String? = StringUtil.nullize(this, nullizeSpaces)
fun String.trimMiddle(maxLength: Int): String? = StringUtil.trimMiddle(this, maxLength)
@@ -32,7 +32,7 @@ settings.update.on.start=Update repository from upstream on start
sync.repositories.panel.title=Sync Repositories
login.github.note=Strongly recommended to use an <a href="https://help.github.com/articles/creating-an-access-token-for-command-line-use">access token</a>.
login.github.note=<a href="https://help.github.com/articles/creating-an-access-token-for-command-line-use">How to create an access token</a>.
login.other.git.provider.note=Consider to configure <a href="https://help.github.com/articles/caching-your-github-password-in-git">git credentials helper</a>.
settings.upstream.url=Upstream URL\:
@@ -24,5 +24,6 @@
<orderEntry type="module" module-name="settings-repository" />
<orderEntry type="library" scope="TEST" name="assertJ" level="project" />
<orderEntry type="library" scope="TEST" name="memoryfilesystem" level="project" />
<orderEntry type="module" module-name="credential-store" scope="TEST" />
</component>
</module>
@@ -15,39 +15,110 @@
*/
package org.jetbrains.settingsRepository
import com.intellij.credentialStore.Credentials
import com.intellij.layout.*
import com.intellij.layout.CCFlags.*
import com.intellij.layout.LCFlags.*
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.ui.dialog
import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vcs.changes.issueLinks.LinkMouseListenerBase
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.JBColor
import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.SimpleTextAttributes
import com.intellij.util.PathUtilRt
import com.intellij.util.nullize
import com.intellij.util.trimMiddle
import com.intellij.util.ui.UIUtil
import org.jetbrains.keychain.Credentials
import java.util.regex.Pattern
import javax.swing.JPasswordField
import javax.swing.JTextField
import javax.swing.event.DocumentEvent
private val HREF_PATTERN = Pattern.compile("<a(?:\\s+href\\s*=\\s*[\"']([^\"']*)[\"'])?\\s*>([^<]*)</a>")
private val LINK_TEXT_ATTRIBUTES = SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, JBColor.blue)
private val SMALL_TEXT_ATTRIBUTES = SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, null)
fun showAuthenticationForm(credentials: Credentials?, uri: String, host: String?, path: String?, sshKeyFile: String?): Credentials? {
if (ApplicationManager.getApplication()?.isUnitTestMode === true) {
throw AssertionError("showAuthenticationForm called from tests")
}
return UIUtil.invokeAndWaitIfNeeded(Computable {
val note = if (sshKeyFile == null) icsMessage(if (host == "github.com") "login.github.note" else "login.other.git.provider.note") else null
var username = credentials?.id
if (username == null && host == "github.com" && path != null && sshKeyFile == null) {
val isGitHub = host == "github.com"
val note = if (sshKeyFile == null) icsMessage(if (isGitHub) "login.github.note" else "login.other.git.provider.note") else null
var username = credentials?.user
if (username == null && isGitHub && path != null && sshKeyFile == null) {
val firstSlashIndex = path.indexOf('/', 1)
username = path.substring(1, if (firstSlashIndex == -1) path.length else firstSlashIndex)
}
val authenticationForm = RepositoryAuthenticationForm(if (sshKeyFile == null) {
icsMessage("log.in.to", StringUtil.trimMiddle(uri, 50))
val message = if (sshKeyFile == null) icsMessage("log.in.to", uri.trimMiddle(50)) else icsMessage("enter.your.password.for.ssh.key", PathUtilRt.getFileName(sshKeyFile))
return UIUtil.invokeAndWaitIfNeeded(Computable {
val userField = JTextField(username)
val passwordField = JPasswordField(credentials?.password)
val centerPanel = panel(fillX) {
label(message, wrap, span, bold = true, gapBottom = 10)
if (sshKeyFile == null && !isGitHub) {
label("Username:")
userField(grow, wrap)
}
label(if (sshKeyFile == null && isGitHub) "Token:" else "Password:")
passwordField(grow, wrap)
note?.let { noteComponent(it)(skip) }
}
else {
icsMessage("enter.your.password.for.ssh.key", PathUtilRt.getFileName(sshKeyFile))
}, username, credentials?.token, note, sshKeyFile != null)
val authenticationForm = dialog(
title = "Settings Repository",
resizable = false,
centerPanel = centerPanel,
preferedFocusComponent = userField,
okActionEnabled = false)
passwordField.document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
authenticationForm.okActionEnabled(e.document.length != 0)
}
})
authenticationForm.okActionEnabled(false)
if (authenticationForm.showAndGet()) {
username = sshKeyFile ?: authenticationForm.username
val passwordChars = authenticationForm.password
Credentials(username, if (passwordChars == null) (if (username == null) null else "x-oauth-basic") else String(passwordChars))
username = sshKeyFile ?: userField.text.nullize(true)
val passwordChars = passwordField.password
Credentials(username, if (passwordChars == null || passwordChars.isEmpty()) (if (username == null) null else "x-oauth-basic") else String(passwordChars))
}
else {
null
}
})
}
private fun noteComponent(note: String): SimpleColoredComponent {
val noteComponent = SimpleColoredComponent()
val matcher = HREF_PATTERN.matcher(note)
var prev = 0
if (matcher.find()) {
do {
if (matcher.start() != prev) {
noteComponent.append(note.substring(prev, matcher.start()), SMALL_TEXT_ATTRIBUTES)
}
noteComponent.append(matcher.group(2), LINK_TEXT_ATTRIBUTES, SimpleColoredComponent.BrowserLauncherTag(matcher.group(1)))
prev = matcher.end()
}
while (matcher.find())
LinkMouseListenerBase.installSingleTagOn(noteComponent)
}
if (prev < note.length) {
noteComponent.append(note.substring(prev), SMALL_TEXT_ATTRIBUTES)
}
return noteComponent
}
@@ -15,6 +15,8 @@
*/
package org.jetbrains.settingsRepository.git
import com.intellij.credentialStore.Credentials
import com.intellij.credentialStore.isFulfilled
import com.intellij.credentialStore.macOs.isMacOsCredentialStoreSupported
import com.intellij.openapi.ui.MessageDialogBuilder
import com.intellij.openapi.ui.Messages
@@ -25,9 +27,7 @@ import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.transport.CredentialItem
import org.eclipse.jgit.transport.CredentialsProvider
import org.eclipse.jgit.transport.URIish
import org.jetbrains.keychain.Credentials
import org.jetbrains.keychain.CredentialsStore
import org.jetbrains.keychain.isFulfilled
import org.jetbrains.settingsRepository.LOG
import org.jetbrains.settingsRepository.showAuthenticationForm
@@ -116,10 +116,10 @@ class JGitCredentialsProvider(private val credentialsStore: NotNullLazyValue<Cre
if (userFromUri != null) {
// username is in url - read password only if it is for the same user
if (userFromUri != credentials?.id) {
if (userFromUri != credentials?.user) {
credentials = Credentials(userFromUri, passwordFromUri)
}
else if (passwordFromUri != null && passwordFromUri != credentials?.token) {
else if (passwordFromUri != null && passwordFromUri != credentials?.password) {
credentials = Credentials(userFromUri, passwordFromUri)
}
}
@@ -134,13 +134,13 @@ class JGitCredentialsProvider(private val credentialsStore: NotNullLazyValue<Cre
credentialsStore.value.save(uri.host, credentials!!, sshKeyFile)
}
userNameItem?.value = credentials?.id
userNameItem?.value = credentials?.user
if (passwordItem != null) {
if (passwordItem is CredentialItem.Password) {
passwordItem.value = credentials?.token?.toCharArray()
passwordItem.value = credentials?.password?.toCharArray()
}
else {
(passwordItem as CredentialItem.StringType).value = credentials?.token
(passwordItem as CredentialItem.StringType).value = credentials?.password
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2015 JetBrains s.r.o.
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,12 +15,12 @@
*/
package org.jetbrains.settingsRepository.git
import com.intellij.credentialStore.Credentials
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessNotCreatedException
import com.intellij.openapi.util.text.StringUtil
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.transport.URIish
import org.jetbrains.keychain.Credentials
import org.jetbrains.settingsRepository.LOG
private var canUseGitExe = true
@@ -15,26 +15,11 @@
*/
package org.jetbrains.keychain
import com.intellij.credentialStore.Credentials
import com.intellij.openapi.diagnostic.Logger
val LOG: Logger = Logger.getInstance(CredentialsStore::class.java)
class Credentials(id: String?, token: String?) {
val id: String? = if (id.isNullOrEmpty()) null else id
val token: String? = if (token.isNullOrEmpty()) null else token
override fun equals(other: Any?): Boolean {
if (other !is Credentials) return false
return id == other.id && token == other.token
}
override fun hashCode(): Int {
return (id?.hashCode() ?: 0) * 37 + (token?.hashCode() ?: 0)
}
}
fun Credentials?.isFulfilled(): Boolean = this != null && id != null && token != null
interface CredentialsStore {
fun get(host: String?, sshKeyFile: String? = null): Credentials?
@@ -15,8 +15,9 @@
*/
package org.jetbrains.keychain
import com.intellij.credentialStore.Credentials
import com.intellij.credentialStore.splitData
import com.intellij.ide.passwordSafe.PasswordSafe
import com.intellij.openapi.util.PasswordUtil
class FileCredentialsStore() : CredentialsStore {
override fun get(host: String?, sshKeyFile: String?): Credentials? {
@@ -24,22 +25,13 @@ class FileCredentialsStore() : CredentialsStore {
return null
}
val accountName = sshKeyFile ?: host
val data = PasswordSafe.getInstance().getPassword("ics-" + accountName) ?: return null
val data = PasswordSafe.getInstance().getPassword("ics-" + (sshKeyFile ?: host)) ?: return null
if (sshKeyFile == null) {
val separatorIndex = data.indexOf('@')
if (separatorIndex > 0) {
val username = PasswordUtil.decodePassword(data.substring(0, separatorIndex))
val password = PasswordUtil.decodePassword(data.substring(separatorIndex + 1))
return Credentials(username, password)
}
return splitData(data)
}
else {
return Credentials(sshKeyFile, data)
}
return null
}
override fun reset(host: String) {
@@ -48,6 +40,6 @@ class FileCredentialsStore() : CredentialsStore {
override fun save(host: String?, credentials: Credentials, sshKeyFile: String?) {
val accountName: String = sshKeyFile ?: host!!
PasswordSafe.getInstance().setPassword("ics-" + accountName, if (sshKeyFile == null) "${PasswordUtil.encodePassword(credentials.id)}@${PasswordUtil.encodePassword(credentials.token)}" else credentials.token!!)
PasswordSafe.getInstance().setPassword("ics-" + accountName, if (sshKeyFile == null) credentials.toString() else credentials.password!!)
}
}
@@ -15,6 +15,7 @@
*/
package org.jetbrains.keychain
import com.intellij.credentialStore.Credentials
import com.intellij.credentialStore.macOs.deleteGenericPassword
import com.intellij.credentialStore.macOs.findGenericPassword
import com.intellij.credentialStore.macOs.saveGenericPassword
@@ -74,7 +75,7 @@ class OsXCredentialsStore(serviceName: String) : CredentialsStore {
return
}
val data = if (sshKeyFile == null) "${PasswordUtil.encodePassword(credentials.id)}@${PasswordUtil.encodePassword(credentials.token)}" else credentials.token!!
val data = if (sshKeyFile == null) credentials.toString() else credentials.password!!
saveGenericPassword(getServiceName(sshKeyFile), accountName, data.toByteArray())
}
@@ -1,70 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.settingsRepository.RepositoryAuthenticationForm">
<grid id="27dc6" binding="panel" layout-manager="GridLayoutManager" row-count="5" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<vspacer id="2584d">
<constraints>
<grid row="4" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<component id="c57ee" class="javax.swing.JTextField" binding="tokenField">
<constraints>
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<component id="5dcc7" class="javax.swing.JLabel" binding="tokenLabel">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<labelFor value="c57ee"/>
<text value="Username:"/>
</properties>
</component>
<component id="f995c" class="javax.swing.JLabel">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<labelFor value="4cb05"/>
<text value="Password:"/>
</properties>
</component>
<component id="4cb05" class="javax.swing.JPasswordField" binding="passwordField">
<constraints>
<grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<component id="6db40" class="com.intellij.ui.SimpleColoredComponent" binding="noteComponent">
<constraints>
<grid row="3" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<ipad top="5" left="2" bottom="1" right="2"/>
</properties>
</component>
<component id="7ab68" class="javax.swing.JLabel" binding="messageLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="2" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<enabled value="true"/>
<font style="1"/>
<text value="Message"/>
</properties>
</component>
</children>
</grid>
</form>
@@ -1,111 +0,0 @@
package org.jetbrains.settingsRepository;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.changes.issueLinks.LinkMouseListenerBase;
import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.JBColor;
import com.intellij.ui.SimpleColoredComponent;
import com.intellij.ui.SimpleTextAttributes;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.DocumentEvent;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
final class RepositoryAuthenticationForm extends DialogWrapper {
private static final Pattern HREF_PATTERN = Pattern.compile("<a(?:\\s+href\\s*=\\s*[\"']([^\"']*)[\"'])?\\s*>([^<]*)</a>");
private static final SimpleTextAttributes LINK_TEXT_ATTRIBUTES = new SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, JBColor.blue);
private static final SimpleTextAttributes SMALL_TEXT_ATTRIBUTES = new SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, null);
private JTextField tokenField;
private SimpleColoredComponent noteComponent;
private JPasswordField passwordField;
private JPanel panel;
private JLabel tokenLabel;
private JLabel messageLabel;
private final JComponent initialFocusedComponent;
public RepositoryAuthenticationForm(@NotNull String message, @Nullable String token, @Nullable String password, @Nullable String note, boolean onlyPassword) {
super(false);
setTitle("Settings Repository");
setResizable(false);
messageLabel.setText(message);
messageLabel.setBorder(new EmptyBorder(0, 0, 10, 0));
if (onlyPassword) {
tokenLabel.setVisible(false);
tokenField.setVisible(false);
passwordField.getDocument().addDocumentListener(new DocumentAdapter() {
@Override
protected void textChanged(DocumentEvent e) {
setOKActionEnabled(e.getDocument().getLength() != 0);
}
});
initialFocusedComponent = passwordField;
setOKActionEnabled(false);
}
else {
tokenField.setText(token);
passwordField.setText(password);
initialFocusedComponent = StringUtil.isEmpty(token) ? tokenField : passwordField;
}
if (note == null) {
noteComponent.setVisible(false);
}
else {
Matcher matcher = HREF_PATTERN.matcher(note);
int prev = 0;
if (matcher.find()) {
do {
if (matcher.start() != prev) {
noteComponent.append(note.substring(prev, matcher.start()), SMALL_TEXT_ATTRIBUTES);
}
noteComponent.append(matcher.group(2), LINK_TEXT_ATTRIBUTES, new SimpleColoredComponent.BrowserLauncherTag(matcher.group(1)));
prev = matcher.end();
}
while (matcher.find());
LinkMouseListenerBase.installSingleTagOn(noteComponent);
}
if (prev < note.length()) {
noteComponent.append(note.substring(prev), SMALL_TEXT_ATTRIBUTES);
}
}
init();
}
@Nullable
@Override
public JComponent getPreferredFocusedComponent() {
return initialFocusedComponent;
}
@Override
protected JComponent createCenterPanel() {
return panel;
}
@Nullable
public String getUsername() {
return StringUtil.nullize(tokenField.getText(), true);
}
@Nullable
public char[] getPassword() {
char[] chars = passwordField.getPassword();
return chars == null || chars.length == 0 ? null : chars;
}
}
@@ -1,12 +1,12 @@
package org.jetbrains.settingsRepository.test
import com.intellij.credentialStore.Credentials
import com.intellij.openapi.util.NotNullLazyValue
import com.intellij.testFramework.ApplicationRule
import org.assertj.core.api.Assertions.assertThat
import org.eclipse.jgit.storage.file.FileRepositoryBuilder
import org.eclipse.jgit.transport.CredentialItem
import org.eclipse.jgit.transport.URIish
import org.jetbrains.keychain.Credentials
import org.jetbrains.keychain.CredentialsStore
import org.jetbrains.keychain.FileCredentialsStore
import org.jetbrains.settingsRepository.git.JGitCredentialsProvider