credentials: add "store" helper

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>

Jeff King committed Dec 10, 2011 at 05:34 UTC 71e1b4b6bf7f274f0b3bf45e7325c04abb405b8a
6 files changed +248
.gitignore
+1
@@ -32,6 +32,7 @@
32 /git-count-objects
33 /git-credential-cache
34 /git-credential-cache--daemon
35 +/git-credential-store
36 /git-cvsexportcommit
37 /git-cvsimport
38 /git-cvsserver
Documentation/git-credential-store.txt new
+75
@@ -0,0 +1,75 @@
1 +git-credential-store(1)
2 +=======================
3 +
4 +NAME
5 +----
6 +git-credential-store - helper to store credentials on disk
7 +
8 +SYNOPSIS
9 +--------
10 +-------------------
11 +git config credential.helper 'store [options]'
12 +-------------------
13 +
14 +DESCRIPTION
15 +-----------
16 +
17 +NOTE: Using this helper will store your passwords unencrypted on disk,
18 +protected only by filesystem permissions. If this is not an acceptable
19 +security tradeoff, try linkgit:git-credential-cache[1], or find a helper
20 +that integrates with secure storage provided by your operating system.
21 +
22 +This command stores credentials indefinitely on disk for use by future
23 +git programs.
24 +
25 +You probably don't want to invoke this command directly; it is meant to
26 +be used as a credential helper by other parts of git. See
27 +linkgit:gitcredentials[7] or `EXAMPLES` below.
28 +
29 +OPTIONS
30 +-------
31 +
32 +--store=<path>::
33 +
34 + Use `<path>` to store credentials. The file will have its
35 + filesystem permissions set to prevent other users on the system
36 + from reading it, but will not be encrypted or otherwise
37 + protected. Defaults to `~/.git-credentials`.
38 +
39 +EXAMPLES
40 +--------
41 +
42 +The point of this helper is to reduce the number of times you must type
43 +your username or password. For example:
44 +
45 +------------------------------------------
46 +$ git config credential.helper store
47 +$ git push http://example.com/repo.git
48 +Username: <type your username>
49 +Password: <type your password>
50 +
51 +[several days later]
52 +$ git push http://example.com/repo.git
53 +[your credentials are used automatically]
54 +------------------------------------------
55 +
56 +STORAGE FORMAT
57 +--------------
58 +
59 +The `.git-credentials` file is stored in plaintext. Each credential is
60 +stored on its own line as a URL like:
61 +
62 +------------------------------
63 +https://user:pass@example.com
64 +------------------------------
65 +
66 +When git needs authentication for a particular URL context,
67 +credential-store will consider that context a pattern to match against
68 +each entry in the credentials file. If the protocol, hostname, and
69 +username (if we already have one) match, then the password is returned
70 +to git. See the discussion of configuration in linkgit:gitcredentials[7]
71 +for more information.
72 +
73 +GIT
74 +---
75 +Part of the linkgit:git[1] suite
Documentation/gitcredentials.txt
+5
@@ -71,6 +71,11 @@ cache::
71 Cache credentials in memory for a short period of time. See
72 linkgit:git-credential-cache[1] for details.
73
74 +store::
75 +
76 + Store credentials indefinitely on disk. See
77 + linkgit:git-credential-store[1] for details.
78 +
79 You may also have third-party helpers installed; search for
80 `credential-*` in the output of `git help -a`, and consult the
81 documentation of individual helpers. Once you have selected a helper,
Makefile
+1
@@ -429,6 +429,7 @@ PROGRAM_OBJS += show-index.o
429 PROGRAM_OBJS += upload-pack.o
430 PROGRAM_OBJS += http-backend.o
431 PROGRAM_OBJS += sh-i18n--envsubst.o
432 +PROGRAM_OBJS += credential-store.o
433
434 PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS))
435
credential-store.c new
+157
@@ -0,0 +1,157 @@
1 +#include "cache.h"
2 +#include "credential.h"
3 +#include "string-list.h"
4 +#include "parse-options.h"
5 +
6 +static struct lock_file credential_lock;
7 +
8 +static void parse_credential_file(const char *fn,
9 + struct credential *c,
10 + void (*match_cb)(struct credential *),
11 + void (*other_cb)(struct strbuf *))
12 +{
13 + FILE *fh;
14 + struct strbuf line = STRBUF_INIT;
15 + struct credential entry = CREDENTIAL_INIT;
16 +
17 + fh = fopen(fn, "r");
18 + if (!fh) {
19 + if (errno != ENOENT)
20 + die_errno("unable to open %s", fn);
21 + return;
22 + }
23 +
24 + while (strbuf_getline(&line, fh, '\n') != EOF) {
25 + credential_from_url(&entry, line.buf);
26 + if (entry.username && entry.password &&
27 + credential_match(c, &entry)) {
28 + if (match_cb) {
29 + match_cb(&entry);
30 + break;
31 + }
32 + }
33 + else if (other_cb)
34 + other_cb(&line);
35 + }
36 +
37 + credential_clear(&entry);
38 + strbuf_release(&line);
39 + fclose(fh);
40 +}
41 +
42 +static void print_entry(struct credential *c)
43 +{
44 + printf("username=%s\n", c->username);
45 + printf("password=%s\n", c->password);
46 +}
47 +
48 +static void print_line(struct strbuf *buf)
49 +{
50 + strbuf_addch(buf, '\n');
51 + write_or_die(credential_lock.fd, buf->buf, buf->len);
52 +}
53 +
54 +static void rewrite_credential_file(const char *fn, struct credential *c,
55 + struct strbuf *extra)
56 +{
57 + if (hold_lock_file_for_update(&credential_lock, fn, 0) < 0)
58 + die_errno("unable to get credential storage lock");
59 + if (extra)
60 + print_line(extra);
61 + parse_credential_file(fn, c, NULL, print_line);
62 + if (commit_lock_file(&credential_lock) < 0)
63 + die_errno("unable to commit credential store");
64 +}
65 +
66 +static void store_credential(const char *fn, struct credential *c)
67 +{
68 + struct strbuf buf = STRBUF_INIT;
69 +
70 + /*
71 + * Sanity check that what we are storing is actually sensible.
72 + * In particular, we can't make a URL without a protocol field.
73 + * Without either a host or pathname (depending on the scheme),
74 + * we have no primary key. And without a username and password,
75 + * we are not actually storing a credential.
76 + */
77 + if (!c->protocol || !(c->host || c->path) ||
78 + !c->username || !c->password)
79 + return;
80 +
81 + strbuf_addf(&buf, "%s://", c->protocol);
82 + strbuf_addstr_urlencode(&buf, c->username, 1);
83 + strbuf_addch(&buf, ':');
84 + strbuf_addstr_urlencode(&buf, c->password, 1);
85 + strbuf_addch(&buf, '@');
86 + if (c->host)
87 + strbuf_addstr_urlencode(&buf, c->host, 1);
88 + if (c->path) {
89 + strbuf_addch(&buf, '/');
90 + strbuf_addstr_urlencode(&buf, c->path, 0);
91 + }
92 +
93 + rewrite_credential_file(fn, c, &buf);
94 + strbuf_release(&buf);
95 +}
96 +
97 +static void remove_credential(const char *fn, struct credential *c)
98 +{
99 + /*
100 + * Sanity check that we actually have something to match
101 + * against. The input we get is a restrictive pattern,
102 + * so technically a blank credential means "erase everything".
103 + * But it is too easy to accidentally send this, since it is equivalent
104 + * to empty input. So explicitly disallow it, and require that the
105 + * pattern have some actual content to match.
106 + */
107 + if (c->protocol || c->host || c->path || c->username)
108 + rewrite_credential_file(fn, c, NULL);
109 +}
110 +
111 +static int lookup_credential(const char *fn, struct credential *c)
112 +{
113 + parse_credential_file(fn, c, print_entry, NULL);
114 + return c->username && c->password;
115 +}
116 +
117 +int main(int argc, const char **argv)
118 +{
119 + const char * const usage[] = {
120 + "git credential-store [options] <action>",
121 + NULL
122 + };
123 + const char *op;
124 + struct credential c = CREDENTIAL_INIT;
125 + char *file = NULL;
126 + struct option options[] = {
127 + OPT_STRING(0, "file", &file, "path",
128 + "fetch and store credentials in <path>"),
129 + OPT_END()
130 + };
131 +
132 + umask(077);
133 +
134 + argc = parse_options(argc, argv, NULL, options, usage, 0);
135 + if (argc != 1)
136 + usage_with_options(usage, options);
137 + op = argv[0];
138 +
139 + if (!file)
140 + file = expand_user_path("~/.git-credentials");
141 + if (!file)
142 + die("unable to set up default path; use --file");
143 +
144 + if (credential_read(&c, stdin) < 0)
145 + die("unable to read credential");
146 +
147 + if (!strcmp(op, "get"))
148 + lookup_credential(file, &c);
149 + else if (!strcmp(op, "erase"))
150 + remove_credential(file, &c);
151 + else if (!strcmp(op, "store"))
152 + store_credential(file, &c);
153 + else
154 + ; /* Ignore unknown operation. */
155 +
156 + return 0;
157 +}
t/t0302-credential-store.sh new
+9
@@ -0,0 +1,9 @@
1 +#!/bin/sh
2 +
3 +test_description='credential-store tests'
4 +. ./test-lib.sh
5 +. "$TEST_DIRECTORY"/lib-credential.sh
6 +
7 +helper_test store
8 +
9 +test_done