summaryrefslogtreecommitdiffstats
path: root/vendor/gix-credentials/src/helper
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:35 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:35 +0000
commit7e5d7eea9c580ef4b41a765bde624af431942b96 (patch)
tree2c0d9ca12878fc4525650aa4e54d77a81a07cc09 /vendor/gix-credentials/src/helper
parentAdding debian version 1.70.0+dfsg1-9. (diff)
downloadrustc-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.rs161
-rw-r--r--vendor/gix-credentials/src/helper/invoke.rs66
-rw-r--r--vendor/gix-credentials/src/helper/mod.rs174
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;