| 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, |
| ) |
| } |
| } |