gix_credentials/helper/cascade.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
use crate::{helper, helper::Cascade, protocol, protocol::Context, Program};
impl Default for Cascade {
fn default() -> Self {
Cascade {
programs: Vec::new(),
stderr: true,
use_http_path: false,
query_user_only: false,
}
}
}
/// Initialization
impl Cascade {
/// Return the programs to run for the current platform.
///
/// These are typically used as basis for all credential cascade invocations, with configured programs following afterwards.
///
/// # Note
///
/// These defaults emulate what typical git installations may use these days, as in fact it's a configurable which comes
/// from installation-specific configuration files which we cannot know (or guess at best).
/// This seems like an acceptable trade-off as helpers are ignored if they fail or are not existing.
pub fn platform_builtin() -> Vec<Program> {
if cfg!(target_os = "macos") {
Some("osxkeychain")
} else if cfg!(target_os = "linux") {
Some("libsecret")
} else if cfg!(target_os = "windows") {
Some("manager-core")
} else {
None
}
.map(|name| vec![Program::from_custom_definition(name)])
.unwrap_or_default()
}
}
/// Builder
impl Cascade {
/// Extend the list of programs to run `programs`.
pub fn extend(mut self, programs: impl IntoIterator<Item = Program>) -> Self {
self.programs.extend(programs);
self
}
/// If `toggle` is true, http(s) urls will use the path portions of the url to obtain a credential for.
///
/// Otherwise, they will only take the user name into account.
pub fn use_http_path(mut self, toggle: bool) -> Self {
self.use_http_path = toggle;
self
}
/// If `toggle` is true, a bogus password will be provided to prevent any helper program from prompting for it, nor will
/// we prompt for the password. The resulting identity will have a bogus password and it's expected to not be used by the
/// consuming transport.
pub fn query_user_only(mut self, toggle: bool) -> Self {
self.query_user_only = toggle;
self
}
}
/// Finalize
impl Cascade {
/// Invoke the cascade by `invoking` each program with `action`, and configuring potential prompts with `prompt` options.
/// The latter can also be used to disable the prompt entirely when setting the `mode` to [`Disable`][gix_prompt::Mode::Disable];=.
///
/// When _getting_ credentials, all programs are asked until the credentials are complete, stopping the cascade.
/// When _storing_ or _erasing_ all programs are instructed in order.
#[allow(clippy::result_large_err)]
pub fn invoke(&mut self, mut action: helper::Action, mut prompt: gix_prompt::Options<'_>) -> protocol::Result {
let mut url = action
.context_mut()
.map(|ctx| {
ctx.destructure_url_in_place(self.use_http_path).map(|ctx| {
if self.query_user_only && ctx.password.is_none() {
ctx.password = Some("".into());
}
ctx
})
})
.transpose()?
.and_then(|ctx| ctx.url.take());
for program in &mut self.programs {
program.stderr = self.stderr;
match helper::invoke::raw(program, &action) {
Ok(None) => {}
Ok(Some(stdout)) => {
let ctx = Context::from_bytes(&stdout)?;
if let Some(dst_ctx) = action.context_mut() {
if let Some(src) = ctx.path {
dst_ctx.path = Some(src);
}
for (src, dst) in [
(ctx.protocol, &mut dst_ctx.protocol),
(ctx.host, &mut dst_ctx.host),
(ctx.username, &mut dst_ctx.username),
(ctx.password, &mut dst_ctx.password),
] {
if let Some(src) = src {
*dst = Some(src);
}
}
if let Some(src) = ctx.url {
dst_ctx.url = Some(src);
url = dst_ctx.destructure_url_in_place(self.use_http_path)?.url.take();
}
if dst_ctx.username.is_some() && dst_ctx.password.is_some() {
break;
}
if ctx.quit.unwrap_or_default() {
dst_ctx.quit = ctx.quit;
break;
}
}
}
Err(helper::Error::CredentialsHelperFailed { .. }) => continue, // ignore helpers that we can't call
Err(err) if action.context().is_some() => return Err(err.into()), // communication errors are fatal when getting credentials
Err(_) => {} // for other actions, ignore everything, try the operation
}
}
if prompt.mode != gix_prompt::Mode::Disable {
if let Some(ctx) = action.context_mut() {
ctx.url = url;
if ctx.username.is_none() {
let message = ctx.to_prompt("Username");
prompt.mode = gix_prompt::Mode::Visible;
ctx.username = gix_prompt::ask(&message, &prompt)
.map_err(|err| protocol::Error::Prompt {
prompt: message,
source: err,
})?
.into();
}
if ctx.password.is_none() {
let message = ctx.to_prompt("Password");
prompt.mode = gix_prompt::Mode::Hidden;
ctx.password = gix_prompt::ask(&message, &prompt)
.map_err(|err| protocol::Error::Prompt {
prompt: message,
source: err,
})?
.into();
}
}
}
protocol::helper_outcome_to_result(
action.context().map(|ctx| helper::Outcome {
username: ctx.username.clone(),
password: ctx.password.clone(),
quit: ctx.quit.unwrap_or(false),
next: ctx.to_owned().into(),
}),
action,
)
}
}