credentials: add "store" helper
authorJeff King <peff@peff.net>
Sat, 10 Dec 2011 10:34:44 +0000 (05:34 -0500)
committerJunio C Hamano <gitster@pobox.com>
Tue, 13 Dec 2011 00:09:38 +0000 (16:09 -0800)
This is like "cache", except that we actually put the
credentials on disk. This can be terribly insecure, of
course, but we do what we can to protect them by filesystem
permissions, and we warn the user in the documentation.

This is not unlike using .netrc to store entries, but it's a
little more user-friendly. Instead of putting credentials in
place ahead of time, we transparently store them after
prompting the user for them once.

Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
.gitignore
Documentation/git-credential-store.txt [new file with mode: 0644]
Documentation/gitcredentials.txt
Makefile
credential-store.c [new file with mode: 0644]
t/t0302-credential-store.sh [new file with mode: 0755]

index a6b0bd4035dac461943d42486b8eb01fcdd06bea..2b7a3f98764cda0e65398694786d4822da2e8f38 100644 (file)
@@ -32,6 +32,7 @@
 /git-count-objects
 /git-credential-cache
 /git-credential-cache--daemon
+/git-credential-store
 /git-cvsexportcommit
 /git-cvsimport
 /git-cvsserver
diff --git a/Documentation/git-credential-store.txt b/Documentation/git-credential-store.txt
new file mode 100644 (file)
index 0000000..3109346
--- /dev/null
@@ -0,0 +1,75 @@
+git-credential-store(1)
+=======================
+
+NAME
+----
+git-credential-store - helper to store credentials on disk
+
+SYNOPSIS
+--------
+-------------------
+git config credential.helper 'store [options]'
+-------------------
+
+DESCRIPTION
+-----------
+
+NOTE: Using this helper will store your passwords unencrypted on disk,
+protected only by filesystem permissions. If this is not an acceptable
+security tradeoff, try linkgit:git-credential-cache[1], or find a helper
+that integrates with secure storage provided by your operating system.
+
+This command stores credentials indefinitely on disk for use by future
+git programs.
+
+You probably don't want to invoke this command directly; it is meant to
+be used as a credential helper by other parts of git. See
+linkgit:gitcredentials[7] or `EXAMPLES` below.
+
+OPTIONS
+-------
+
+--store=<path>::
+
+       Use `<path>` to store credentials. The file will have its
+       filesystem permissions set to prevent other users on the system
+       from reading it, but will not be encrypted or otherwise
+       protected. Defaults to `~/.git-credentials`.
+
+EXAMPLES
+--------
+
+The point of this helper is to reduce the number of times you must type
+your username or password. For example:
+
+------------------------------------------
+$ git config credential.helper store
+$ git push http://example.com/repo.git
+Username: <type your username>
+Password: <type your password>
+
+[several days later]
+$ git push http://example.com/repo.git
+[your credentials are used automatically]
+------------------------------------------
+
+STORAGE FORMAT
+--------------
+
+The `.git-credentials` file is stored in plaintext. Each credential is
+stored on its own line as a URL like:
+
+------------------------------
+https://user:pass@example.com
+------------------------------
+
+When git needs authentication for a particular URL context,
+credential-store will consider that context a pattern to match against
+each entry in the credentials file.  If the protocol, hostname, and
+username (if we already have one) match, then the password is returned
+to git. See the discussion of configuration in linkgit:gitcredentials[7]
+for more information.
+
+GIT
+---
+Part of the linkgit:git[1] suite
index 4e3f860ea9dcef5df3ba9642786d0c8acd206d62..066f825f2e116becb90404246c0bdadebc72c1f7 100644 (file)
@@ -71,6 +71,11 @@ cache::
        Cache credentials in memory for a short period of time. See
        linkgit:git-credential-cache[1] for details.
 
+store::
+
+       Store credentials indefinitely on disk. See
+       linkgit:git-credential-store[1] for details.
+
 You may also have third-party helpers installed; search for
 `credential-*` in the output of `git help -a`, and consult the
 documentation of individual helpers.  Once you have selected a helper,
index 7c8ef2c0e5b177e49e476af8082e5f4105a95167..2222633a17a58f3b420053b40e1863245310f417 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -429,6 +429,7 @@ PROGRAM_OBJS += show-index.o
 PROGRAM_OBJS += upload-pack.o
 PROGRAM_OBJS += http-backend.o
 PROGRAM_OBJS += sh-i18n--envsubst.o
+PROGRAM_OBJS += credential-store.o
 
 PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS))
 
diff --git a/credential-store.c b/credential-store.c
new file mode 100644 (file)
index 0000000..26f7589
--- /dev/null
@@ -0,0 +1,157 @@
+#include "cache.h"
+#include "credential.h"
+#include "string-list.h"
+#include "parse-options.h"
+
+static struct lock_file credential_lock;
+
+static void parse_credential_file(const char *fn,
+                                 struct credential *c,
+                                 void (*match_cb)(struct credential *),
+                                 void (*other_cb)(struct strbuf *))
+{
+       FILE *fh;
+       struct strbuf line = STRBUF_INIT;
+       struct credential entry = CREDENTIAL_INIT;
+
+       fh = fopen(fn, "r");
+       if (!fh) {
+               if (errno != ENOENT)
+                       die_errno("unable to open %s", fn);
+               return;
+       }
+
+       while (strbuf_getline(&line, fh, '\n') != EOF) {
+               credential_from_url(&entry, line.buf);
+               if (entry.username && entry.password &&
+                   credential_match(c, &entry)) {
+                       if (match_cb) {
+                               match_cb(&entry);
+                               break;
+                       }
+               }
+               else if (other_cb)
+                       other_cb(&line);
+       }
+
+       credential_clear(&entry);
+       strbuf_release(&line);
+       fclose(fh);
+}
+
+static void print_entry(struct credential *c)
+{
+       printf("username=%s\n", c->username);
+       printf("password=%s\n", c->password);
+}
+
+static void print_line(struct strbuf *buf)
+{
+       strbuf_addch(buf, '\n');
+       write_or_die(credential_lock.fd, buf->buf, buf->len);
+}
+
+static void rewrite_credential_file(const char *fn, struct credential *c,
+                                   struct strbuf *extra)
+{
+       if (hold_lock_file_for_update(&credential_lock, fn, 0) < 0)
+               die_errno("unable to get credential storage lock");
+       if (extra)
+               print_line(extra);
+       parse_credential_file(fn, c, NULL, print_line);
+       if (commit_lock_file(&credential_lock) < 0)
+               die_errno("unable to commit credential store");
+}
+
+static void store_credential(const char *fn, struct credential *c)
+{
+       struct strbuf buf = STRBUF_INIT;
+
+       /*
+        * Sanity check that what we are storing is actually sensible.
+        * In particular, we can't make a URL without a protocol field.
+        * Without either a host or pathname (depending on the scheme),
+        * we have no primary key. And without a username and password,
+        * we are not actually storing a credential.
+        */
+       if (!c->protocol || !(c->host || c->path) ||
+           !c->username || !c->password)
+               return;
+
+       strbuf_addf(&buf, "%s://", c->protocol);
+       strbuf_addstr_urlencode(&buf, c->username, 1);
+       strbuf_addch(&buf, ':');
+       strbuf_addstr_urlencode(&buf, c->password, 1);
+       strbuf_addch(&buf, '@');
+       if (c->host)
+               strbuf_addstr_urlencode(&buf, c->host, 1);
+       if (c->path) {
+               strbuf_addch(&buf, '/');
+               strbuf_addstr_urlencode(&buf, c->path, 0);
+       }
+
+       rewrite_credential_file(fn, c, &buf);
+       strbuf_release(&buf);
+}
+
+static void remove_credential(const char *fn, struct credential *c)
+{
+       /*
+        * Sanity check that we actually have something to match
+        * against. The input we get is a restrictive pattern,
+        * so technically a blank credential means "erase everything".
+        * But it is too easy to accidentally send this, since it is equivalent
+        * to empty input. So explicitly disallow it, and require that the
+        * pattern have some actual content to match.
+        */
+       if (c->protocol || c->host || c->path || c->username)
+               rewrite_credential_file(fn, c, NULL);
+}
+
+static int lookup_credential(const char *fn, struct credential *c)
+{
+       parse_credential_file(fn, c, print_entry, NULL);
+       return c->username && c->password;
+}
+
+int main(int argc, const char **argv)
+{
+       const char * const usage[] = {
+               "git credential-store [options] <action>",
+               NULL
+       };
+       const char *op;
+       struct credential c = CREDENTIAL_INIT;
+       char *file = NULL;
+       struct option options[] = {
+               OPT_STRING(0, "file", &file, "path",
+                          "fetch and store credentials in <path>"),
+               OPT_END()
+       };
+
+       umask(077);
+
+       argc = parse_options(argc, argv, NULL, options, usage, 0);
+       if (argc != 1)
+               usage_with_options(usage, options);
+       op = argv[0];
+
+       if (!file)
+               file = expand_user_path("~/.git-credentials");
+       if (!file)
+               die("unable to set up default path; use --file");
+
+       if (credential_read(&c, stdin) < 0)
+               die("unable to read credential");
+
+       if (!strcmp(op, "get"))
+               lookup_credential(file, &c);
+       else if (!strcmp(op, "erase"))
+               remove_credential(file, &c);
+       else if (!strcmp(op, "store"))
+               store_credential(file, &c);
+       else
+               ; /* Ignore unknown operation. */
+
+       return 0;
+}
diff --git a/t/t0302-credential-store.sh b/t/t0302-credential-store.sh
new file mode 100755 (executable)
index 0000000..f61b40c
--- /dev/null
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+test_description='credential-store tests'
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-credential.sh
+
+helper_test store
+
+test_done