Merge branch 'mh/credential-erase-improvements'

* mh/credential-erase-improvements:
  credential: erase all matching credentials
  credential: avoid erasing distinct password
This commit is contained in:
Junio C Hamano 2023-06-23 11:21:16 -07:00
commit 5ee8fcdabc
7 changed files with 128 additions and 20 deletions

View file

@ -39,7 +39,7 @@ for later use.
If the action is `reject`, git-credential will send the description to
any configured credential helpers, which may erase any stored
credential matching the description.
credentials matching the description.
If the action is `approve` or `reject`, no output should be emitted.

View file

@ -260,7 +260,7 @@ appended to its command line, which is one of:
`erase`::
Remove a matching credential, if any, from the helper's storage.
Remove matching credentials, if any, from the helper's storage.
The details of the credential will be provided on the helper's stdin
stream. The exact format is the same as the input/output format of the

View file

@ -38,19 +38,22 @@ static struct credential_cache_entry *lookup_credential(const struct credential
int i;
for (i = 0; i < entries_nr; i++) {
struct credential *e = &entries[i].item;
if (credential_match(c, e))
if (credential_match(c, e, 0))
return &entries[i];
}
return NULL;
}
static void remove_credential(const struct credential *c)
static void remove_credential(const struct credential *c, int match_password)
{
struct credential_cache_entry *e;
e = lookup_credential(c);
if (e)
e->expiration = 0;
int i;
for (i = 0; i < entries_nr; i++) {
e = &entries[i];
if (credential_match(c, &e->item, match_password))
e->expiration = 0;
}
}
static timestamp_t check_expirations(void)
@ -151,14 +154,14 @@ static void serve_one_client(FILE *in, FILE *out)
exit(0);
}
else if (!strcmp(action.buf, "erase"))
remove_credential(&c);
remove_credential(&c, 1);
else if (!strcmp(action.buf, "store")) {
if (timeout < 0)
warning("cache client didn't specify a timeout");
else if (!c.username || !c.password)
warning("cache client gave us a partial credential");
else {
remove_credential(&c);
remove_credential(&c, 0);
cache_credential(&c, timeout);
}
}

View file

@ -13,7 +13,8 @@ static struct lock_file credential_lock;
static int parse_credential_file(const char *fn,
struct credential *c,
void (*match_cb)(struct credential *),
void (*other_cb)(struct strbuf *))
void (*other_cb)(struct strbuf *),
int match_password)
{
FILE *fh;
struct strbuf line = STRBUF_INIT;
@ -30,7 +31,7 @@ static int parse_credential_file(const char *fn,
while (strbuf_getline_lf(&line, fh) != EOF) {
if (!credential_from_url_gently(&entry, line.buf, 1) &&
entry.username && entry.password &&
credential_match(c, &entry)) {
credential_match(c, &entry, match_password)) {
found_credential = 1;
if (match_cb) {
match_cb(&entry);
@ -60,7 +61,7 @@ static void print_line(struct strbuf *buf)
}
static void rewrite_credential_file(const char *fn, struct credential *c,
struct strbuf *extra)
struct strbuf *extra, int match_password)
{
int timeout_ms = 1000;
@ -69,7 +70,7 @@ static void rewrite_credential_file(const char *fn, struct credential *c,
die_errno(_("unable to get credential storage lock in %d ms"), timeout_ms);
if (extra)
print_line(extra);
parse_credential_file(fn, c, NULL, print_line);
parse_credential_file(fn, c, NULL, print_line, match_password);
if (commit_lock_file(&credential_lock) < 0)
die_errno("unable to write credential store");
}
@ -91,7 +92,7 @@ static void store_credential_file(const char *fn, struct credential *c)
is_rfc3986_reserved_or_unreserved);
}
rewrite_credential_file(fn, c, &buf);
rewrite_credential_file(fn, c, &buf, 0);
strbuf_release(&buf);
}
@ -138,7 +139,7 @@ static void remove_credential(const struct string_list *fns, struct credential *
return;
for_each_string_list_item(fn, fns)
if (!access(fn->string, F_OK))
rewrite_credential_file(fn->string, c, NULL);
rewrite_credential_file(fn->string, c, NULL, 1);
}
static void lookup_credential(const struct string_list *fns, struct credential *c)
@ -146,7 +147,7 @@ static void lookup_credential(const struct string_list *fns, struct credential *
struct string_list_item *fn;
for_each_string_list_item(fn, fns)
if (parse_credential_file(fn->string, c, print_entry, NULL))
if (parse_credential_file(fn->string, c, print_entry, NULL, 0))
return; /* Found credential */
}

View file

@ -33,13 +33,14 @@ void credential_clear(struct credential *c)
}
int credential_match(const struct credential *want,
const struct credential *have)
const struct credential *have, int match_password)
{
#define CHECK(x) (!want->x || (have->x && !strcmp(want->x, have->x)))
return CHECK(protocol) &&
CHECK(host) &&
CHECK(path) &&
CHECK(username);
CHECK(username) &&
(!match_password || CHECK(password));
#undef CHECK
}
@ -102,7 +103,7 @@ static int match_partial_url(const char *url, void *cb)
warning(_("skipping credential lookup for key: credential.%s"),
url);
else
matches = credential_match(&want, c);
matches = credential_match(&want, c, 0);
credential_clear(&want);
return matches;

View file

@ -211,6 +211,6 @@ void credential_from_url(struct credential *, const char *url);
int credential_from_url_gently(struct credential *, const char *url, int quiet);
int credential_match(const struct credential *want,
const struct credential *have);
const struct credential *have, int match_password);
#endif /* CREDENTIAL_H */

View file

@ -44,6 +44,10 @@ helper_test_clean() {
reject $1 https example.com user1
reject $1 https example.com user2
reject $1 https example.com user4
reject $1 https example.com user-distinct-pass
reject $1 https example.com user-overwrite
reject $1 https example.com user-erase1
reject $1 https example.com user-erase2
reject $1 http path.tld user
reject $1 https timeout.tld user
reject $1 https sso.tld
@ -167,6 +171,49 @@ helper_test() {
EOF
'
test_expect_success "helper ($HELPER) overwrites on store" '
check approve $HELPER <<-\EOF &&
protocol=https
host=example.com
username=user-overwrite
password=pass1
EOF
check approve $HELPER <<-\EOF &&
protocol=https
host=example.com
username=user-overwrite
password=pass2
EOF
check fill $HELPER <<-\EOF &&
protocol=https
host=example.com
username=user-overwrite
--
protocol=https
host=example.com
username=user-overwrite
password=pass2
EOF
check reject $HELPER <<-\EOF &&
protocol=https
host=example.com
username=user-overwrite
password=pass2
EOF
check fill $HELPER <<-\EOF
protocol=https
host=example.com
username=user-overwrite
--
protocol=https
host=example.com
username=user-overwrite
password=askpass-password
--
askpass: Password for '\''https://user-overwrite@example.com'\'':
EOF
'
test_expect_success "helper ($HELPER) can forget host" '
check reject $HELPER <<-\EOF &&
protocol=https
@ -221,6 +268,31 @@ helper_test() {
EOF
'
test_expect_success "helper ($HELPER) does not erase a password distinct from input" '
check approve $HELPER <<-\EOF &&
protocol=https
host=example.com
username=user-distinct-pass
password=pass1
EOF
check reject $HELPER <<-\EOF &&
protocol=https
host=example.com
username=user-distinct-pass
password=pass2
EOF
check fill $HELPER <<-\EOF
protocol=https
host=example.com
username=user-distinct-pass
--
protocol=https
host=example.com
username=user-distinct-pass
password=pass1
EOF
'
test_expect_success "helper ($HELPER) can forget user" '
check reject $HELPER <<-\EOF &&
protocol=https
@ -272,6 +344,37 @@ helper_test() {
EOF
'
test_expect_success "helper ($HELPER) erases all matching credentials" '
check approve $HELPER <<-\EOF &&
protocol=https
host=example.com
username=user-erase1
password=pass1
EOF
check approve $HELPER <<-\EOF &&
protocol=https
host=example.com
username=user-erase2
password=pass1
EOF
check reject $HELPER <<-\EOF &&
protocol=https
host=example.com
EOF
check fill $HELPER <<-\EOF
protocol=https
host=example.com
--
protocol=https
host=example.com
username=askpass-username
password=askpass-password
--
askpass: Username for '\''https://example.com'\'':
askpass: Password for '\''https://askpass-username@example.com'\'':
EOF
'
: ${GIT_TEST_LONG_CRED_BUFFER:=1024}
# 23 bytes accounts for "wwwauth[]=basic realm=" plus NUL
LONG_VALUE_LEN=$((GIT_TEST_LONG_CRED_BUFFER - 23))