diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:35 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:35 +0000 |
commit | 7e5d7eea9c580ef4b41a765bde624af431942b96 (patch) | |
tree | 2c0d9ca12878fc4525650aa4e54d77a81a07cc09 /vendor/gix-credentials/src/helper | |
parent | Adding debian version 1.70.0+dfsg1-9. (diff) | |
download | rustc-7e5d7eea9c580ef4b41a765bde624af431942b96.tar.xz rustc-7e5d7eea9c580ef4b41a765bde624af431942b96.zip |
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/gix-credentials/src/helper')
-rw-r--r-- | vendor/gix-credentials/src/helper/cascade.rs | 161 | ||||
-rw-r--r-- | vendor/gix-credentials/src/helper/invoke.rs | 66 | ||||
-rw-r--r-- | vendor/gix-credentials/src/helper/mod.rs | 174 |
3 files changed, 401 insertions, 0 deletions
diff --git a/vendor/gix-credentials/src/helper/cascade.rs b/vendor/gix-credentials/src/helper/cascade.rs new file mode 100644 index 000000000..9ec251161 --- /dev/null +++ b/vendor/gix-credentials/src/helper/cascade.rs @@ -0,0 +1,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, + ) + } +} diff --git a/vendor/gix-credentials/src/helper/invoke.rs b/vendor/gix-credentials/src/helper/invoke.rs new file mode 100644 index 000000000..a543ec761 --- /dev/null +++ b/vendor/gix-credentials/src/helper/invoke.rs @@ -0,0 +1,66 @@ +use std::io::Read; + +use crate::helper::{Action, Context, Error, NextAction, Outcome, Result}; + +impl Action { + /// Send ourselves to the given `write` which is expected to be credentials-helper compatible + pub fn send(&self, mut write: impl std::io::Write) -> std::io::Result<()> { + match self { + Action::Get(ctx) => ctx.write_to(write), + Action::Store(last) | Action::Erase(last) => { + write.write_all(last)?; + write.write_all(&[b'\n']) + } + } + } +} + +/// Invoke the given `helper` with `action` in `context`. +/// +/// Usually the first call is performed with [`Action::Get`] to obtain `Some` identity, which subsequently can be used if it is complete. +/// Note that it may also only contain the username _or_ password, and should start out with everything the helper needs. +/// On successful usage, use [`NextAction::store()`], otherwise [`NextAction::erase()`], which is when this function +/// returns `Ok(None)` as no outcome is expected. +pub fn invoke(helper: &mut crate::Program, action: &Action) -> Result { + match raw(helper, action)? { + None => Ok(None), + Some(stdout) => { + let ctx = Context::from_bytes(stdout.as_slice())?; + Ok(Some(Outcome { + username: ctx.username, + password: ctx.password, + quit: ctx.quit.unwrap_or(false), + next: NextAction { + previous_output: stdout.into(), + }, + })) + } + } +} + +pub(crate) fn raw(helper: &mut crate::Program, action: &Action) -> std::result::Result<Option<Vec<u8>>, Error> { + let (stdin, stdout) = helper.start(action)?; + if let (Action::Get(_), None) = (&action, &stdout) { + panic!("BUG: `Helper` impls must return an output handle to read output from if Action::Get is provided") + } + action.send(stdin)?; + let stdout = stdout + .map(|mut stdout| { + let mut buf = Vec::new(); + stdout.read_to_end(&mut buf).map(|_| buf) + }) + .transpose() + .map_err(|err| Error::CredentialsHelperFailed { source: err })?; + helper.finish().map_err(|err| { + if err.kind() == std::io::ErrorKind::Other { + Error::CredentialsHelperFailed { source: err } + } else { + err.into() + } + })?; + + match matches!(action, Action::Get(_)).then(|| stdout).flatten() { + None => Ok(None), + Some(stdout) => Ok(Some(stdout)), + } +} diff --git a/vendor/gix-credentials/src/helper/mod.rs b/vendor/gix-credentials/src/helper/mod.rs new file mode 100644 index 000000000..107d6db7a --- /dev/null +++ b/vendor/gix-credentials/src/helper/mod.rs @@ -0,0 +1,174 @@ +use std::convert::TryFrom; + +use bstr::{BStr, BString}; + +use crate::{protocol, protocol::Context, Program}; + +/// A list of helper programs to run in order to obtain credentials. +#[allow(dead_code)] +#[derive(Debug)] +pub struct Cascade { + /// The programs to run in order to obtain credentials + pub programs: Vec<Program>, + /// If true, stderr is enabled when `programs` are run, which is the default. + pub stderr: bool, + /// If true, http(s) urls will take their path portion into account when obtaining credentials. Default is false. + /// Other protocols like ssh will always use the path portion. + pub use_http_path: bool, + /// If true, default false, when getting credentials, we will set a bogus password to only obtain the user name. + /// Storage and cancellation work the same, but without a password set. + pub query_user_only: bool, +} + +/// The outcome of the credentials helper [invocation][crate::helper::invoke()]. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Outcome { + /// The username to use in the identity, if set. + pub username: Option<String>, + /// The password to use in the identity, if set. + pub password: Option<String>, + /// If set, the helper asked to stop the entire process, whether the identity is complete or not. + pub quit: bool, + /// A handle to the action to perform next in another call to [`helper::invoke()`][crate::helper::invoke()]. + pub next: NextAction, +} + +impl Outcome { + /// Try to fetch username _and_ password to form an identity. This will fail if one of them is not set. + /// + /// This does nothing if only one of the fields is set, or consume both. + pub fn consume_identity(&mut self) -> Option<gix_sec::identity::Account> { + if self.username.is_none() || self.password.is_none() { + return None; + } + self.username + .take() + .zip(self.password.take()) + .map(|(username, password)| gix_sec::identity::Account { username, password }) + } +} + +/// The Result type used in [`invoke()`][crate::helper::invoke()]. +pub type Result = std::result::Result<Option<Outcome>, Error>; + +/// The error used in the [credentials helper invocation][crate::helper::invoke()]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + ContextDecode(#[from] protocol::context::decode::Error), + #[error("An IO error occurred while communicating to the credentials helper")] + Io(#[from] std::io::Error), + #[error(transparent)] + CredentialsHelperFailed { source: std::io::Error }, +} + +/// The action to perform by the credentials [helper][`crate::helper::invoke()`]. +#[derive(Clone, Debug)] +pub enum Action { + /// Provide credentials using the given repository context, which must include the repository url. + Get(Context), + /// Approve the credentials as identified by the previous input provided as `BString`, containing information from [`Context`]. + Store(BString), + /// Reject the credentials as identified by the previous input provided as `BString`. containing information from [`Context`]. + Erase(BString), +} + +/// Initialization +impl Action { + /// Create a `Get` action with context containing the given URL. + /// Note that this creates an `Action` suitable for the credential helper cascade only. + pub fn get_for_url(url: impl Into<BString>) -> Action { + Action::Get(Context { + url: Some(url.into()), + ..Default::default() + }) + } +} + +/// Access +impl Action { + /// Return the payload of store or erase actions. + pub fn payload(&self) -> Option<&BStr> { + use bstr::ByteSlice; + match self { + Action::Get(_) => None, + Action::Store(p) | Action::Erase(p) => Some(p.as_bstr()), + } + } + /// Return the context of a get operation, or `None`. + /// + /// The opposite of [`payload`][Action::payload()]. + pub fn context(&self) -> Option<&Context> { + match self { + Action::Get(ctx) => Some(ctx), + Action::Erase(_) | Action::Store(_) => None, + } + } + + /// Return the mutable context of a get operation, or `None`. + pub fn context_mut(&mut self) -> Option<&mut Context> { + match self { + Action::Get(ctx) => Some(ctx), + Action::Erase(_) | Action::Store(_) => None, + } + } + + /// Returns true if this action expects output from the helper. + pub fn expects_output(&self) -> bool { + matches!(self, Action::Get(_)) + } + + /// The name of the argument to describe this action. If `is_external` is true, the target program is + /// a custom credentials helper, not a built-in one. + pub fn as_arg(&self, is_external: bool) -> &str { + match self { + Action::Get(_) if is_external => "get", + Action::Get(_) => "fill", + Action::Store(_) if is_external => "store", + Action::Store(_) => "approve", + Action::Erase(_) if is_external => "erase", + Action::Erase(_) => "reject", + } + } +} + +/// A handle to [store][NextAction::store()] or [erase][NextAction::erase()] the outcome of the initial action. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct NextAction { + previous_output: BString, +} + +impl TryFrom<&NextAction> for Context { + type Error = protocol::context::decode::Error; + + fn try_from(value: &NextAction) -> std::result::Result<Self, Self::Error> { + Context::from_bytes(value.previous_output.as_ref()) + } +} + +impl From<Context> for NextAction { + fn from(ctx: Context) -> Self { + let mut buf = Vec::<u8>::new(); + ctx.write_to(&mut buf).expect("cannot fail"); + NextAction { + previous_output: buf.into(), + } + } +} + +impl NextAction { + /// Approve the result of the previous [Action] and store for lookup. + pub fn store(self) -> Action { + Action::Store(self.previous_output) + } + /// Reject the result of the previous [Action] and erase it as to not be returned when being looked up. + pub fn erase(self) -> Action { + Action::Erase(self.previous_output) + } +} + +mod cascade; +pub(crate) mod invoke; + +pub use invoke::invoke; |