Skip to content

Commit e721289

Browse files
committed
Auto merge of #2407 - alexcrichton:better-failed-auth-error, r=brson
This commit is an attempt to improve the error message from failed authentication attempts as well as attempting more usernames. Right now we only attempt one username, but there are four different possible choices we could select (including $USER which we weren't previously trying). This commit tweaks a bunch of this logic and just in general refactors the with_authentication function. Closes #2399
2 parents 35480d7 + f66d716 commit e721289

File tree

2 files changed

+154
-49
lines changed

2 files changed

+154
-49
lines changed

src/cargo/sources/git/utils.rs

Lines changed: 153 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
use std::env;
12
use std::fmt;
2-
use std::path::{Path, PathBuf};
33
use std::fs::{self, File};
4+
use std::path::{Path, PathBuf};
45

56
use rustc_serialize::{Encodable, Encoder};
67
use url::Url;
@@ -358,66 +359,169 @@ impl<'a> GitCheckout<'a> {
358359
}
359360
}
360361

362+
/// Prepare the authentication callbacks for cloning a git repository.
363+
///
364+
/// The main purpose of this function is to construct the "authentication
365+
/// callback" which is used to clone a repository. This callback will attempt to
366+
/// find the right authentication on the system (without user input) and will
367+
/// guide libgit2 in doing so.
368+
///
369+
/// The callback is provided `allowed` types of credentials, and we try to do as
370+
/// much as possible based on that:
371+
///
372+
/// * Prioritize SSH keys from the local ssh agent as they're likely the most
373+
/// reliable. The username here is prioritized from the credential
374+
/// callback, then from whatever is configured in git itself, and finally
375+
/// we fall back to the generic user of `git`.
376+
///
377+
/// * If a username/password is allowed, then we fallback to git2-rs's
378+
/// implementation of the credential helper. This is what is configured
379+
/// with `credential.helper` in git, and is the interface for the OSX
380+
/// keychain, for example.
381+
///
382+
/// * After the above two have failed, we just kinda grapple attempting to
383+
/// return *something*.
384+
///
385+
/// If any form of authentication fails, libgit2 will repeatedly ask us for
386+
/// credentials until we give it a reason to not do so. To ensure we don't
387+
/// just sit here looping forever we keep track of authentications we've
388+
/// attempted and we don't try the same ones again.
361389
fn with_authentication<T, F>(url: &str, cfg: &git2::Config, mut f: F)
362390
-> CargoResult<T>
363391
where F: FnMut(&mut git2::Credentials) -> CargoResult<T>
364392
{
365-
// Prepare the authentication callbacks.
366-
//
367-
// We check the `allowed` types of credentials, and we try to do as much as
368-
// possible based on that:
369-
//
370-
// * Prioritize SSH keys from the local ssh agent as they're likely the most
371-
// reliable. The username here is prioritized from the credential
372-
// callback, then from whatever is configured in git itself, and finally
373-
// we fall back to the generic user of `git`.
374-
//
375-
// * If a username/password is allowed, then we fallback to git2-rs's
376-
// implementation of the credential helper. This is what is configured
377-
// with `credential.helper` in git, and is the interface for the OSX
378-
// keychain, for example.
379-
//
380-
// * After the above two have failed, we just kinda grapple attempting to
381-
// return *something*.
382-
//
383-
// Note that we keep track of the number of times we've called this callback
384-
// because libgit2 will repeatedly give us credentials until we give it a
385-
// reason to not do so. If we've been called once and our credentials failed
386-
// then we'll be called again, and in this case we assume that the reason
387-
// was because the credentials were wrong.
388393
let mut cred_helper = git2::CredentialHelper::new(url);
389394
cred_helper.config(cfg);
390-
let mut called = 0;
395+
396+
let mut attempted = git2::CredentialType::empty();
397+
let mut failed_cred_helper = false;
398+
399+
// We try a couple of different user names when cloning via ssh as there's a
400+
// few possibilities if one isn't mentioned, and these are used to keep
401+
// track of that.
402+
enum UsernameAttempt {
403+
Arg,
404+
CredHelper,
405+
Local,
406+
Git,
407+
}
408+
let mut username_attempt = UsernameAttempt::Arg;
409+
let mut username_attempts = Vec::new();
410+
391411
let res = f(&mut |url, username, allowed| {
392-
called += 1;
393-
if called >= 2 {
394-
return Err(git2::Error::from_str("no authentication available"))
412+
let allowed = allowed & !attempted;
413+
414+
// libgit2's "USERNAME" authentication actually means that it's just
415+
// asking us for a username to keep going. This is currently only really
416+
// used for SSH authentication and isn't really an authentication type.
417+
// The logic currently looks like:
418+
//
419+
// let user = ...;
420+
// if (user.is_null())
421+
// user = callback(USERNAME, null, ...);
422+
//
423+
// callback(SSH_KEY, user, ...)
424+
//
425+
// So if we have a USERNAME request we just pass it either `username` or
426+
// a fallback of "git". We'll do some more principled attempts later on.
427+
if allowed.contains(git2::USERNAME) {
428+
attempted = attempted | git2::USERNAME;
429+
return git2::Cred::username(username.unwrap_or("git"))
395430
}
396-
if allowed.contains(git2::SSH_KEY) ||
397-
allowed.contains(git2::USERNAME) {
398-
let user = username.map(|s| s.to_string())
399-
.or_else(|| cred_helper.username.clone())
400-
.unwrap_or("git".to_string());
401-
if allowed.contains(git2::USERNAME) {
402-
git2::Cred::username(&user)
403-
} else {
404-
git2::Cred::ssh_key_from_agent(&user)
431+
432+
// An "SSH_KEY" authentication indicates that we need some sort of SSH
433+
// authentication. This can currently either come from the ssh-agent
434+
// process or from a raw in-memory SSH key. Cargo only supports using
435+
// ssh-agent currently.
436+
//
437+
// We try a few different usernames here, including:
438+
//
439+
// 1. The `username` argument, if provided. This will cover cases where
440+
// the user was passed in the URL, for example.
441+
// 2. The global credential helper's username, if any is configured
442+
// 3. The local account's username (if present)
443+
// 4. Finally, "git" as it's a common fallback (e.g. with github)
444+
if allowed.contains(git2::SSH_KEY) {
445+
loop {
446+
let name = match username_attempt {
447+
UsernameAttempt::Arg => {
448+
username_attempt = UsernameAttempt::CredHelper;
449+
username.map(|s| s.to_string())
450+
}
451+
UsernameAttempt::CredHelper => {
452+
username_attempt = UsernameAttempt::Local;
453+
cred_helper.username.clone()
454+
}
455+
UsernameAttempt::Local => {
456+
username_attempt = UsernameAttempt::Git;
457+
env::var("USER").or_else(|_| env::var("USERNAME")).ok()
458+
}
459+
UsernameAttempt::Git => {
460+
attempted = attempted | git2::SSH_KEY;
461+
Some("git".to_string())
462+
}
463+
};
464+
if let Some(name) = name {
465+
let ret = git2::Cred::ssh_key_from_agent(&name);
466+
username_attempts.push(name);
467+
return ret
468+
}
405469
}
406-
} else if allowed.contains(git2::USER_PASS_PLAINTEXT) {
407-
git2::Cred::credential_helper(cfg, url, username)
408-
} else if allowed.contains(git2::DEFAULT) {
409-
git2::Cred::default()
410-
} else {
411-
Err(git2::Error::from_str("no authentication available"))
412470
}
471+
472+
// Sometimes libgit2 will ask for a username/password in plaintext. This
473+
// is where Cargo would have an interactive prompt if we supported it,
474+
// but we currently don't! Right now the only way we support fetching a
475+
// plaintext password is through the `credential.helper` support, so
476+
// fetch that here.
477+
if allowed.contains(git2::USER_PASS_PLAINTEXT) {
478+
attempted = attempted | git2::USER_PASS_PLAINTEXT;
479+
let r = git2::Cred::credential_helper(cfg, url, username);
480+
failed_cred_helper = r.is_err();
481+
return r
482+
}
483+
484+
// I'm... not sure what the DEFAULT kind of authentication is, but seems
485+
// easy to support?
486+
if allowed.contains(git2::DEFAULT) {
487+
attempted = attempted | git2::DEFAULT;
488+
return git2::Cred::default()
489+
}
490+
491+
// Whelp, we tried our best
492+
Err(git2::Error::from_str("no authentication available"))
413493
});
414-
if called > 0 {
415-
res.chain_error(|| {
416-
human("failed to authenticate when downloading repository")
417-
})
418-
} else {
419-
res
494+
495+
if attempted.bits() == 0 || res.is_ok() {
496+
return res
420497
}
498+
499+
// In the case of an authentication failure (where we tried something) then
500+
// we try to give a more helpful error message about precisely what we
501+
// tried.
502+
res.chain_error(|| {
503+
let mut msg = "failed to authenticate when downloading \
504+
repository".to_string();
505+
if attempted.contains(git2::SSH_KEY) {
506+
let names = username_attempts.iter()
507+
.map(|s| format!("`{}`", s))
508+
.collect::<Vec<_>>()
509+
.join(", ");
510+
msg.push_str(&format!("\nattempted ssh-agent authentication, but \
511+
none of the usernames {} succeeded", names));
512+
}
513+
if attempted.contains(git2::USER_PASS_PLAINTEXT) {
514+
if failed_cred_helper {
515+
msg.push_str("\nattempted to find username/password via \
516+
git's `credential.helper` support, but failed");
517+
} else {
518+
msg.push_str("\nattempted to find username/password via \
519+
`credential.helper`, but maybe the found \
520+
credentials were incorrect");
521+
}
522+
}
523+
human(msg)
524+
})
421525
}
422526

423527
pub fn fetch(repo: &git2::Repository, url: &str,

tests/test_cargo_build_auth.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ Caused by:
108108
109109
Caused by:
110110
failed to authenticate when downloading repository
111+
attempted to find username/password via `credential.helper`, but [..]
111112
112113
To learn more, run the command again with --verbose.
113114
",

0 commit comments

Comments
 (0)