{"thread":{"id":"28237","subject":"[PATCH] Add a credential-helper for KDE","startedAt":"2011-08-27T19:54:02Z","lastAt":"2011-09-30T10:21:11Z","messageCount":5,"participants":["Lukas Sandström","Jeff King"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"174393","messageId":"4E594B5A.6070902@gmail.com","threadId":"28237","inReplyTo":null,"subject":"[PATCH] Add a credential-helper for KDE","fromName":"Lukas Sandström","fromEmail":"luksan@gmail.com","sentAt":"2011-08-27T19:54:02Z","receivedAt":"2011-08-27T19:54:02Z","isPatch":true,"sender":{"key":"luksan@gmail.com","avatar":"https://avatars.githubusercontent.com/u/152281?v=4"},"body":"This Python script plugs into the credentials API\nof Git to ask the user for passwords with a nice\nKDE password dialog.\n\nThe password is saved in the KWallet.\n\nSigned-off-by: Lukas Sandström <luksan@gmail.com>\n---\n\nHere is a credentials-helper for KDE. You need to have PyKDE installed to use it.\n\nSee Documentation/gitcredentials.txt for more info.\n\n .../git-kde-credentials-helper.py                  |  122 ++++++++++++++++++++\n 1 files changed, 122 insertions(+), 0 deletions(-)\n create mode 100755 contrib/kde-credetials-helper/git-kde-credentials-helper.py\n\ndiff --git a/contrib/kde-credetials-helper/git-kde-credentials-helper.py b/contrib/kde-credetials-helper/git-kde-credentials-helper.py\nnew file mode 100755\nindex 0000000..8d3be4d\n--- /dev/null\n+++ b/contrib/kde-credetials-helper/git-kde-credentials-helper.py\n@@ -0,0 +1,122 @@\n+#!/usr/bin/env python\n+# encoding=utf-8\n+#\n+# Copyright 2011, Lukas Sandström\n+#\n+# Licensed under the GPL version 2.\n+\n+import sys, commands\n+from PyQt4.QtCore import QString\n+from PyKDE4.kdecore import i18n, ki18n, KAboutData, KCmdLineArgs, KCmdLineOptions\n+from PyKDE4.kdeui import KApplication, KWallet, KPasswordDialog\n+\n+appName     = \"git-kde-credentials-helper\"\n+catalog     = \"\"\n+programName = ki18n (\"Git KDE credentials helper\")\n+version     = \"0.1\"\n+description = ki18n (\"Credentials storage helper for Git\")\n+license     = KAboutData.License_GPL_V2\n+copyright   = ki18n (\"(c) 2011 Lukas Sandström\")\n+text        = ki18n (\"none\")\n+homePage    = \"http://www.git-scm.com\"\n+bugEmail    = \"luksan@gmail.com\"\n+\n+aboutData   = KAboutData (appName, catalog, programName, version, description,\n+                          license, copyright, text, homePage, bugEmail)\n+\n+class CredentialHelper(KApplication):\n+    def __init__(self, token, username = None, desc = None, reject = False):\n+        super(CredentialHelper, self).__init__()\n+        self.password = None\n+        self.username = username\n+        self.save_password = False\n+        self.token = token\n+        self.desc = desc\n+\n+        if not self.token:\n+            return\n+\n+        self.open_wallet()\n+\n+        if reject:\n+            self.wallet.removeEntry(QString(token))\n+            return\n+\n+        if not self.check_wallet():\n+            self.ask_password_dialog()\n+        \n+        if self.save_password:\n+            self.store_password()\n+\n+        self.output_credentials()\n+\n+    def output_credentials(self):\n+        if self.username:\n+            print \"username=\" + self.username\n+        if self.password:\n+            print \"password=\" + self.password\n+\n+    def store_password(self):\n+        self.wallet.writeMap(QString(self.token),\n+            {QString(\"username\") : QString(self.username),\n+             QString(\"password\") : QString(self.password)})\n+\n+    def open_wallet(self):\n+        self.wallet = KWallet.Wallet.openWallet(\n+            KWallet.Wallet.LocalWallet(), 0, KWallet.Wallet.Synchronous)\n+        if not self.wallet.isOpen():\n+            return None\n+        if not self.wallet.hasFolder(\"GitCredentials\"):\n+            self.wallet.createFolder(\"GitCredentials\")\n+        self.wallet.setFolder(\"GitCredentials\")\n+\n+    def check_wallet(self):\n+        (res, data) = self.wallet.readMap(self.token)\n+        if res != 0:\n+            return None\n+        try:\n+            self.username = data[QString(\"username\")]\n+            self.password = data[QString(\"password\")]\n+        except KeyError:\n+            return None\n+        return self.username and self.password\n+\n+    def ask_password_dialog(self):\n+        dlg = KPasswordDialog(None,\n+            KPasswordDialog.KPasswordDialogFlag(\n+                KPasswordDialog.ShowKeepPassword |\n+                KPasswordDialog.ShowUsernameLine))\n+        if self.desc:\n+            desc = self.desc\n+        else:\n+            desc = self.token\n+        dlg.setPrompt(i18n(\"Please enter username and password for %s\" % (desc)))\n+        dlg.setUsername(self.username)\n+        dlg.setKeepPassword(True)\n+        if not dlg.exec_():\n+            return\n+        self.username = dlg.username()\n+        self.password = dlg.password()\n+        self.save_password = dlg.keepPassword()\n+\n+def main():    \n+    KCmdLineArgs.init(sys.argv, aboutData)\n+    \n+    options = KCmdLineOptions()\n+    options.add(\"unique <token>\", ki18n(\"Unique token identifying the credential\"))\n+    options.add(\"description <desc>\", ki18n(\"Human readable description of the credential\"))\n+    options.add(\"username <username>\", ki18n(\"Requested username\"))\n+    options.add(\"reject\", ki18n(\"Purge credential\"))    \n+    \n+    KCmdLineArgs.addCmdLineOptions(options)\n+    args = KCmdLineArgs.parsedArgs();\n+\n+    username = args.getOption(\"username\")\n+    token = args.getOption(\"unique\")\n+    desc = args.getOption(\"description\")\n+    reject = args.isSet(\"reject\")\n+\n+    app = CredentialHelper(token, username, desc, reject)\n+\n+if __name__ == \"__main__\":\n+    main()\n-- \n1.7.6.1\n"},{"id":"174592","messageId":"20110831014237.GA2519@sigill.intra.peff.net","threadId":"28237","inReplyTo":"4E594B5A.6070902@gmail.com","subject":"Re: [PATCH] Add a credential-helper for KDE","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2011-08-31T01:42:37Z","receivedAt":"2011-08-31T01:42:37Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sat, Aug 27, 2011 at 09:54:02PM +0200, Lukas Sandström wrote:\n\n> This Python script plugs into the credentials API\n> of Git to ask the user for passwords with a nice\n> KDE password dialog.\n\nThanks for working on this.\n\n>  .../git-kde-credentials-helper.py                  |  122 ++++++++++++++++++++\n\nCan we call it git-credential-kdewallet or similar? Then users can just\ndo:\n\n  git config credential.helper kdewallet\n\n(where \"kdewallet\" can be whatever you think is most appropriate; the\nkey is naming it git-credential-*).\n\n>  1 files changed, 122 insertions(+), 0 deletions(-)\n>  create mode 100755 contrib/kde-credetials-helper/git-kde-credentials-helper.py\n\nMinor typo in directory name.\n\n> +    def check_wallet(self):\n> +        (res, data) = self.wallet.readMap(self.token)\n> +        if res != 0:\n> +            return None\n> +        try:\n> +            self.username = data[QString(\"username\")]\n> +            self.password = data[QString(\"password\")]\n> +        except KeyError:\n> +            return None\n> +        return self.username and self.password\n\nIf I am reading this correctly, you look up based purely on the context\ntoken. Which means that if I do something like this:\n\n  $ git push https://host.com/repo.git\n  [enter username: user1, password: foo]\n  $ git push https://user2@host.com/other-repo.git\n\nWe will invoke the helper as:\n\n  git credential-kdewallet --unique=https:host.com --username=user2\n\nbut the helper will ignore the \"user2\" bit, and return \"user1 / foo\".\n\nThe \"cache\" helper I wrote handles this situation better, by indexing\nboth on the token and the username. I wonder if the username should\nbecome part of the token. Or if the token should really just become a\ncanonicalized URL, minus the actual path. So the first one would get:\n\n  --unique=https://host.com\n\nand the second would get:\n\n  --unique=https://user2@host.com\n\nThen helpers wouldn't need to worry about doing anything special.\n\nWhat do you think? Also, any comments in general on writing a helper?\nYou are the first one besides me to do so. Did you find anything in the\ninterface or the documentation confusing? Suggestions are very welcome,\nas nothing has been released yet and we're free to tweak as much as we\nwant.\n\n-Peff\n"},{"id":"175722","messageId":"4E7605CA.7020204@gmail.com","threadId":"28237","inReplyTo":"20110831014237.GA2519@sigill.intra.peff.net","subject":"[PATCH v2] Add a credential-helper for KDE","fromName":"Lukas Sandström","fromEmail":"luksan@gmail.com","sentAt":"2011-09-18T14:52:58Z","receivedAt":"2011-09-18T14:52:58Z","isPatch":true,"sender":{"key":"luksan@gmail.com","avatar":"https://avatars.githubusercontent.com/u/152281?v=4"},"body":"This Python script plugs into the credentials API\nof Git to ask the user for passwords with a nice\nKDE password dialog.\n\nThe password is saved in the KWallet.\n\nSigned-off-by: Lukas Sandström <luksan@gmail.com>\n---\n\nOn 2011-08-31 03:42, Jeff King wrote:\n> Can we call it git-credential-kdewallet or similar? Then users can just\n> do:\n> \n>   git config credential.helper kdewallet\n> \n> (where \"kdewallet\" can be whatever you think is most appropriate; the\n> key is naming it git-credential-*).\n\nDone.\n\n[...]\n\n> If I am reading this correctly, you look up based purely on the context\n> token. Which means that if I do something like this:\n> \n>   $ git push https://host.com/repo.git\n>   [enter username: user1, password: foo]\n>   $ git push https://user2@host.com/other-repo.git\n> \n> We will invoke the helper as:\n> \n>   git credential-kdewallet --unique=https:host.com --username=user2\n> \n> but the helper will ignore the \"user2\" bit, and return \"user1 / foo\".\n> \n> The \"cache\" helper I wrote handles this situation better, by indexing\n> both on the token and the username. I wonder if the username should\n> become part of the token. Or if the token should really just become a\n> canonicalized URL, minus the actual path. So the first one would get:\n> \n>   --unique=https://host.com\n> \n> and the second would get:\n> \n>   --unique=https://user2@host.com\n> \n> Then helpers wouldn't need to worry about doing anything special.\n> \n> What do you think? Also, any comments in general on writing a helper?\n> You are the first one besides me to do so. Did you find anything in the\n> interface or the documentation confusing? Suggestions are very welcome,\n> as nothing has been released yet and we're free to tweak as much as we\n> want.\n> \n> -Peff\n\nRight. Multiple usernames per \"unique\" context is supported in this version.\nI looked at the git-credential-storage helper when I wrote the first patch,\nwhich didn't have obvious support for multiple usernames per unique context.\n\nKeeping the username outside the token is probably a good thing, but perhaps it\nshould be clarified in the api-docs that multiple usernames has to be supported.\n\nAlso; what about rejecting credentials. This code currently deletes just a \nusername/password pair if a username is specified, and all credentials associated\nwith the token if only --unique and --reject is specified. Is this correct/expected\nbehavior?\n\nWhen I first wrote the helper I tried to immediately ask for a new password if a\ncredential was rejected, but this didn't work with the HTTP auth code, since it\ndoesn't retry the auth with the new credentials after a reject. I think it would\nbe better if we asked for a new password instead of just saying \"auth failed\" and \nhaving the user retry the fetch/pull when the stored credentials are incorrect.\n\n/Lkas\n\n .../git-credential-kdewallet.py                    |  137 ++++++++++++++++++++\n 1 files changed, 137 insertions(+), 0 deletions(-)\n create mode 100755 contrib/git-credential-kdewallet/git-credential-kdewallet.py\n\ndiff --git a/contrib/git-credential-kdewallet/git-credential-kdewallet.py b/contrib/git-credential-kdewallet/git-credential-kdewallet.py\nnew file mode 100755\nindex 0000000..29c4ae1\n--- /dev/null\n+++ b/contrib/git-credential-kdewallet/git-credential-kdewallet.py\n@@ -0,0 +1,137 @@\n+#!/usr/bin/env python\n+# encoding=utf-8\n+#\n+# Copyright 2011, Lukas Sandström\n+#\n+# Licensed under the GPL version 2.\n+\n+import sys\n+from PyQt4.QtCore import QString\n+from PyKDE4.kdecore import i18n, ki18n, KAboutData, KCmdLineArgs, KCmdLineOptions\n+from PyKDE4.kdeui import KApplication, KWallet, KPasswordDialog\n+\n+appName     = \"git-credential-kdewallet\"\n+catalog     = \"\"\n+programName = ki18n (\"Git KDE credentials helper\")\n+version     = \"0.1\"\n+description = ki18n (\"Credentials storage helper for Git\")\n+license     = KAboutData.License_GPL_V2\n+copyright   = ki18n (\"(c) 2011 Lukas Sandström\")\n+text        = ki18n (\"none\")\n+homePage    = \"http://www.git-scm.com\"\n+bugEmail    = \"luksan@gmail.com\"\n+\n+aboutData   = KAboutData (appName, catalog, programName, version, description,\n+                          license, copyright, text, homePage, bugEmail)\n+\n+class CredentialHelper(KApplication):\n+    def __init__(self, token, username = None, desc = None, reject = False):\n+        super(CredentialHelper, self).__init__()\n+        self.password = None\n+        self.username = username\n+        self.save_password = False\n+        self.token = token\n+        self.desc = desc\n+\n+        if not self.token:\n+            return\n+\n+        self.open_wallet()\n+\n+        if reject:\n+            self.reject_credential()\n+            return\n+\n+        if not self.check_wallet():\n+            self.ask_password_dialog()\n+\n+        if self.save_password:\n+            self.store_password()\n+\n+        self.output_credentials()\n+\n+    def output_credentials(self):\n+        if self.username:\n+            print \"username=\" + self.username\n+        if self.password:\n+            print \"password=\" + self.password\n+\n+    def reject_credential(self):\n+        (res, data) = self.wallet.readMap(self.token)\n+        if self.username:\n+            try:\n+                del data[QString(self.username)]\n+            except KeyError:\n+                pass\n+            self.wallet.writeMap(self.token, data)\n+        else:\n+            self.wallet.removeEntry(self.token)\n+\n+    def store_password(self):\n+        (res, data) = self.wallet.readMap(self.token)\n+        data[QString(self.username)] = QString(self.password)\n+        self.wallet.writeMap(QString(self.token), data)\n+\n+    def open_wallet(self):\n+        self.wallet = KWallet.Wallet.openWallet(\n+            KWallet.Wallet.LocalWallet(), 0, KWallet.Wallet.Synchronous)\n+        if not self.wallet.isOpen():\n+            return None\n+        if not self.wallet.hasFolder(\"GitCredentials\"):\n+            self.wallet.createFolder(\"GitCredentials\")\n+        self.wallet.setFolder(\"GitCredentials\")\n+\n+    def check_wallet(self):\n+        (res, data) = self.wallet.readMap(self.token)\n+        if res != 0:\n+            return None\n+        for u, p in data.iteritems():\n+            # Pick the first complete credential if no username is specified\n+            if not self.username and u and p:\n+                self.username = u\n+                self.password = p\n+                return True\n+            if self.username == u:\n+                self.password = p\n+                return True\n+        return None\n+\n+    def ask_password_dialog(self):\n+        dlg = KPasswordDialog(None,\n+            KPasswordDialog.KPasswordDialogFlag(\n+                KPasswordDialog.ShowKeepPassword |\n+                KPasswordDialog.ShowUsernameLine))\n+        if self.desc:\n+            desc = self.desc\n+        else:\n+            desc = self.token\n+        dlg.setPrompt(i18n(\"Please enter username and password for %s\" % (desc)))\n+        dlg.setUsername(self.username)\n+        dlg.setKeepPassword(True)\n+        if not dlg.exec_():\n+            return\n+        self.username = dlg.username()\n+        self.password = dlg.password()\n+        self.save_password = dlg.keepPassword()\n+\n+def main():\n+    KCmdLineArgs.init(sys.argv, aboutData)\n+\n+    options = KCmdLineOptions()\n+    options.add(\"unique <token>\", ki18n(\"Unique token identifying the credential\"))\n+    options.add(\"description <desc>\", ki18n(\"Human readable description of the credential\"))\n+    options.add(\"username <username>\", ki18n(\"Requested username\"))\n+    options.add(\"reject\", ki18n(\"Purge credential\"))\n+\n+    KCmdLineArgs.addCmdLineOptions(options)\n+    args = KCmdLineArgs.parsedArgs();\n+\n+    username = args.getOption(\"username\")\n+    token = args.getOption(\"unique\")\n+    desc = args.getOption(\"description\")\n+    reject = args.isSet(\"reject\")\n+\n+    app = CredentialHelper(token, username, desc, reject)\n+\n+if __name__ == \"__main__\":\n+    main()\n-- \n1.7.6.1\n"},{"id":"175728","messageId":"20110918184954.GB31176@sigill.intra.peff.net","threadId":"28237","inReplyTo":"4E7605CA.7020204@gmail.com","subject":"Re: [PATCH v2] Add a credential-helper for KDE","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2011-09-18T18:49:54Z","receivedAt":"2011-09-18T18:49:54Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sun, Sep 18, 2011 at 04:52:58PM +0200, Lukas Sandström wrote:\n\n> Right. Multiple usernames per \"unique\" context is supported in this version.\n> I looked at the git-credential-storage helper when I wrote the first patch,\n> which didn't have obvious support for multiple usernames per unique context.\n\nYeah, sorry about that. The -cache helper is much more fully fleshed out\n(though I have improved the -store helper in the past few days to handle\nmultiple usernames better).\n\n> Keeping the username outside the token is probably a good thing, but perhaps it\n> should be clarified in the api-docs that multiple usernames has to be supported.\n\nOK, I'll try to write up a clarification.\n\n> Also; what about rejecting credentials. This code currently deletes just a \n> username/password pair if a username is specified, and all credentials associated\n> with the token if only --unique and --reject is specified. Is this correct/expected\n> behavior?\n\nYes, that's what I think should happen, and what both of my helpers do.\nIn practice, I don't think it will be called that way by git, which\nwill always be rejecting a username we just tried. But I wanted to leave\nthings flexible in case a user wants to manually remove a credential\nfrom a store.\n\n> When I first wrote the helper I tried to immediately ask for a new password if a\n> credential was rejected, but this didn't work with the HTTP auth code, since it\n> doesn't retry the auth with the new credentials after a reject. I think it would\n> be better if we asked for a new password instead of just saying \"auth failed\" and \n> having the user retry the fetch/pull when the stored credentials are incorrect.\n\nYeah, I had a patch early on to retry authentication a few times before\nexiting, but I wondered how helpful it was. It's usually pretty easy to\nretry your command again via shell history, and sometimes looping on\nasking for authentication can be annoying (because things like askpass\nwill actually grab the keyboard focus).\n\nSo I dunno what is best. I don't consider it a big deal, but maybe\nothers do.\n\nEven if we did do the retry from git, the helper shouldn't ask\nimmediately for the new credential inside a --reject. It should wait to\nbe invoked again asking for the password. I know this may be an extra\nfork/exec/startup cycle, but it keeps the interface to the helper simple\nand flexible.\n\n-Peff\n"},{"id":"176562","messageId":"20110930102111.GA24507@sigill.intra.peff.net","threadId":"28237","inReplyTo":"4E7605CA.7020204@gmail.com","subject":"Re: [PATCH v2] Add a credential-helper for KDE","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2011-09-30T10:21:11Z","receivedAt":"2011-09-30T10:21:11Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sun, Sep 18, 2011 at 04:52:58PM +0200, Lukas Sandström wrote:\n\n> This Python script plugs into the credentials API\n> of Git to ask the user for passwords with a nice\n> KDE password dialog.\n> \n> The password is saved in the KWallet.\n\nSo I managed to play with this a bit tonight. Overall, it seems pretty\nnice.\n\nInitially, it seemed somewhat clumsy. It asked me to open the wallet\n(using a password) each time git ran. Which is about as annoying as just\ntyping my git password each time. :)\n\nThe magic trick was to configure kwallet to \"keep the wallet open for 10\nminutes after the last use\" instead of \"close when no applications have\nthe wallet open\". Since git runs as many small programs, kwallet has no\nreal idea of how long a git session is.\n\nThis is totally not a kwallet thing, and nothing to do with your helper.\nBut since the helper is so annoyingly useless without that config, it\nmight be worth mentioning it in a README.\n\n> Right. Multiple usernames per \"unique\" context is supported in this version.\n> I looked at the git-credential-storage helper when I wrote the first patch,\n> which didn't have obvious support for multiple usernames per unique context.\n\nThis part passed my tests just fine. Very nice.\n\n> +class CredentialHelper(KApplication):\n> +    def __init__(self, token, username = None, desc = None, reject = False):\n> +        super(CredentialHelper, self).__init__()\n> +        self.password = None\n> +        self.username = username\n> +        self.save_password = False\n> +        self.token = token\n> +        self.desc = desc\n> +\n> +        if not self.token:\n> +            return\n\nMy tests complained about doing nothing when there is no token. As I've\nmentioned elsewhere, this doesn't matter now (as git never invokes the\nhelper that way), but it would be nice to future-proof the helper by\njust ignoring the wallet, but still doing the nice password dialog.\n\n> +    def open_wallet(self):\n> +        self.wallet = KWallet.Wallet.openWallet(\n> +            KWallet.Wallet.LocalWallet(), 0, KWallet.Wallet.Synchronous)\n> +        if not self.wallet.isOpen():\n> +            return None\n> +        if not self.wallet.hasFolder(\"GitCredentials\"):\n> +            self.wallet.createFolder(\"GitCredentials\")\n> +        self.wallet.setFolder(\"GitCredentials\")\n\nI peeked around the KWallet manager. There's a \"passwords\" folder in the\nwallet, and I was surprised that the passwords didn't go there. But when\nI tried using konqueror to store a password, I found that it also made\nits own folder, and then stored a map within it for each URL.\n\nSo I'm not really sure if you're following kwallet best practices or\nnot, as I'm clearly confused about what the \"passwords\" folder is for.\n;)\n\n> +    def check_wallet(self):\n> +        (res, data) = self.wallet.readMap(self.token)\n\nSo you're just using the token as a big blob. Which is how I\nanticipated, but is the complete opposite of what OS X Keychain wants.\nWhich is leading me to think we should really just hand helpers both\nforms: the information broken down by item (e.g., --host=github.com),\nand a full URL (e.g., --url=https://github.com/). And then the helpers\ncan use whatever they like (where you would use \"url\" instead of the\ncurrent \"unique\").\n\n-Peff\n"}]}