credential: apply helper config

The functionality for credential storage helpers is already there; we just need to give the users a way to turn it on. This patch provides a "credential.helper" configuration variable which allows the user to provide one or more helper strings. Rather than simply matching credential.helper, we will also compare URLs in subsection headings to the current context. This means you can apply configuration to a subset of credentials. For example: [credential "https://example.com"] helper = foo would match a request for "https://example.com/foo.git", but not one for "https://kernel.org/foo.git". This is overkill for the "helper" variable, since users are unlikely to want different helpers for different sites (and since helpers run arbitrary code, they could do the matching themselves anyway). However, future patches will add new config variables where this extra feature will be more useful. 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:31 UTC 118250728e1aa46c19d4d258950b2ba15cb6d5d2
4 files changed +119 -1
credential.c
+61
@@ -22,6 +22,61 @@ void credential_clear(struct credential *c)
22 credential_init(c);
23 }
24
25 +int credential_match(const struct credential *want,
26 + const struct credential *have)
27 +{
28 +#define CHECK(x) (!want->x || (have->x && !strcmp(want->x, have->x)))
29 + return CHECK(protocol) &&
30 + CHECK(host) &&
31 + CHECK(path) &&
32 + CHECK(username);
33 +#undef CHECK
34 +}
35 +
36 +static int credential_config_callback(const char *var, const char *value,
37 + void *data)
38 +{
39 + struct credential *c = data;
40 + const char *key, *dot;
41 +
42 + key = skip_prefix(var, "credential.");
43 + if (!key)
44 + return 0;
45 +
46 + if (!value)
47 + return config_error_nonbool(var);
48 +
49 + dot = strrchr(key, '.');
50 + if (dot) {
51 + struct credential want = CREDENTIAL_INIT;
52 + char *url = xmemdupz(key, dot - key);
53 + int matched;
54 +
55 + credential_from_url(&want, url);
56 + matched = credential_match(&want, c);
57 +
58 + credential_clear(&want);
59 + free(url);
60 +
61 + if (!matched)
62 + return 0;
63 + key = dot + 1;
64 + }
65 +
66 + if (!strcmp(key, "helper"))
67 + string_list_append(&c->helpers, value);
68 +
69 + return 0;
70 +}
71 +
72 +static void credential_apply_config(struct credential *c)
73 +{
74 + if (c->configured)
75 + return;
76 + git_config(credential_config_callback, c);
77 + c->configured = 1;
78 +}
79 +
80 static void credential_describe(struct credential *c, struct strbuf *out)
81 {
82 if (!c->protocol)
@@ -195,6 +250,8 @@ void credential_fill(struct credential *c)
250 if (c->username && c->password)
251 return;
252
253 + credential_apply_config(c);
254 +
255 for (i = 0; i < c->helpers.nr; i++) {
256 credential_do(c, c->helpers.items[i].string, "get");
257 if (c->username && c->password)
@@ -215,6 +272,8 @@ void credential_approve(struct credential *c)
272 if (!c->username || !c->password)
273 return;
274
275 + credential_apply_config(c);
276 +
277 for (i = 0; i < c->helpers.nr; i++)
278 credential_do(c, c->helpers.items[i].string, "store");
279 c->approved = 1;
@@ -224,6 +283,8 @@ void credential_reject(struct credential *c)
283 {
284 int i;
285
286 + credential_apply_config(c);
287 +
288 for (i = 0; i < c->helpers.nr; i++)
289 credential_do(c, c->helpers.items[i].string, "erase");
290
credential.h
+4 -1
@@ -5,7 +5,8 @@
5
6 struct credential {
7 struct string_list helpers;
8 - unsigned approved:1;
8 + unsigned approved:1,
9 + configured:1;
10
11 char *username;
12 char *password;
@@ -25,5 +26,7 @@ void credential_reject(struct credential *);
26
27 int credential_read(struct credential *, FILE *);
28 void credential_from_url(struct credential *, const char *url);
29 +int credential_match(const struct credential *have,
30 + const struct credential *want);
31
32 #endif /* CREDENTIAL_H */
t/t0300-credentials.sh
+42
@@ -192,4 +192,46 @@ test_expect_success 'internal getpass does not ask for known username' '
192 EOF
193 '
194
195 +HELPER="!f() {
196 + cat >/dev/null
197 + echo username=foo
198 + echo password=bar
199 + }; f"
200 +test_expect_success 'respect configured credentials' '
201 + test_config credential.helper "$HELPER" &&
202 + check fill <<-\EOF
203 + --
204 + username=foo
205 + password=bar
206 + --
207 + EOF
208 +'
209 +
210 +test_expect_success 'match configured credential' '
211 + test_config credential.https://example.com.helper "$HELPER" &&
212 + check fill <<-\EOF
213 + protocol=https
214 + host=example.com
215 + path=repo.git
216 + --
217 + username=foo
218 + password=bar
219 + --
220 + EOF
221 +'
222 +
223 +test_expect_success 'do not match configured credential' '
224 + test_config credential.https://foo.helper "$HELPER" &&
225 + check fill <<-\EOF
226 + protocol=https
227 + host=bar
228 + --
229 + username=askpass-username
230 + password=askpass-password
231 + --
232 + askpass: Username for '\''https://bar'\'':
233 + askpass: Password for '\''https://askpass-username@bar'\'':
234 + EOF
235 +'
236 +
237 test_done
t/t5550-http-fetch.sh
+12
@@ -101,6 +101,18 @@ test_expect_success 'http auth can request both user and pass' '
101 expect_askpass both user@host
102 '
103
104 +test_expect_success 'http auth respects credential helper config' '
105 + test_config_global credential.helper "!f() {
106 + cat >/dev/null
107 + echo username=user@host
108 + echo password=user@host
109 + }; f" &&
110 + >askpass-query &&
111 + echo wrong >askpass-response &&
112 + git clone "$HTTPD_URL/auth/repo.git" clone-auth-helper &&
113 + expect_askpass none
114 +'
115 +
116 test_expect_success 'fetch changes via http' '
117 echo content >>file &&
118 git commit -a -m two &&