Skip to main content

gix_credentials/helper/
mod.rs

1use bstr::{BStr, BString};
2
3use crate::{Program, protocol, protocol::Context};
4
5/// A list of helper programs to run in order to obtain credentials.
6#[derive(Debug)]
7pub struct Cascade {
8    /// The programs to run in order to obtain credentials
9    pub programs: Vec<Program>,
10    /// If true, stderr is enabled when `programs` are run, which is the default.
11    pub stderr: bool,
12    /// If true, http(s) urls will take their path portion into account when obtaining credentials. Default is false.
13    /// Other protocols like ssh will always use the path portion.
14    pub use_http_path: bool,
15    /// Options controlling how credential contexts are encoded and decoded.
16    pub context_options: protocol::ContextOptions,
17    /// If true, default false, when getting credentials, we will set a bogus password to only obtain the user name.
18    /// Storage and cancellation work the same, but without a password set.
19    pub query_user_only: bool,
20}
21
22/// The outcome of the credentials helper [invocation][crate::helper::invoke()].
23#[derive(Debug, Clone, Eq, PartialEq)]
24pub struct Outcome {
25    /// The username to use in the identity, if set.
26    pub username: Option<String>,
27    /// The password to use in the identity, if set.
28    pub password: Option<String>,
29    /// An OAuth refresh token that may accompany a password. It is to be treated confidentially, just like the password.
30    pub oauth_refresh_token: Option<String>,
31    /// If set, the helper asked to stop the entire process, whether the identity is complete or not.
32    pub quit: bool,
33    /// A handle to the action to perform next in another call to [`helper::invoke()`][crate::helper::invoke()].
34    pub next: NextAction,
35}
36
37impl Outcome {
38    /// Try to fetch username _and_ password to form an identity. This will fail if one of them is not set.
39    ///
40    /// This does nothing if only one of the fields is set, or consume both.
41    pub fn consume_identity(&mut self) -> Option<gix_sec::identity::Account> {
42        if self.username.is_none() || self.password.is_none() {
43            return None;
44        }
45        self.username
46            .take()
47            .zip(self.password.take())
48            .map(|(username, password)| gix_sec::identity::Account {
49                username,
50                password,
51                oauth_refresh_token: self.oauth_refresh_token.take(),
52            })
53    }
54}
55
56/// The Result type used in [`invoke()`][crate::helper::invoke()].
57pub type Result = std::result::Result<Option<Outcome>, Error>;
58
59/// The error used in the [credentials helper invocation][crate::helper::invoke()].
60#[derive(Debug, thiserror::Error)]
61#[expect(missing_docs)]
62pub enum Error {
63    #[error(transparent)]
64    ContextDecode(#[from] protocol::context::decode::Error),
65    #[error("An IO error occurred while communicating to the credentials helper")]
66    Io(#[from] std::io::Error),
67    #[error(transparent)]
68    CredentialsHelperFailed { source: std::io::Error },
69}
70
71/// The action to perform by the credentials [helper][`crate::helper::invoke()`].
72#[derive(Clone, Debug)]
73pub enum Action {
74    /// Provide credentials using the given repository context, which must include the repository url.
75    Get(Context),
76    /// Approve the credentials as identified by the previous input provided as `BString`, containing information from [`Context`].
77    Store(BString),
78    /// Reject the credentials as identified by the previous input provided as `BString`. containing information from [`Context`].
79    Erase(BString),
80}
81
82/// Initialization
83impl Action {
84    /// Create a `Get` action with a default context containing only the given URL.
85    ///
86    /// This initializes [`Context::options`] with its default. Use it when only the URL is needed
87    /// and the default options are suitable. Otherwise, configure the context options afterwards
88    /// and before invoking a helper. Credential cascades do this automatically from
89    /// [`Cascade::context_options`].
90    pub fn get_for_url(url: impl Into<BString>) -> Action {
91        Action::Get(Context::from_url(url, protocol::ContextOptions::default()))
92    }
93}
94
95/// Access
96impl Action {
97    /// Return the payload of store or erase actions.
98    pub fn payload(&self) -> Option<&BStr> {
99        use bstr::ByteSlice;
100        match self {
101            Action::Get(_) => None,
102            Action::Store(p) | Action::Erase(p) => Some(p.as_bstr()),
103        }
104    }
105    /// Return the context of a get operation, or `None`.
106    ///
107    /// The opposite of [`payload`][Action::payload()].
108    pub fn context(&self) -> Option<&Context> {
109        match self {
110            Action::Get(ctx) => Some(ctx),
111            Action::Erase(_) | Action::Store(_) => None,
112        }
113    }
114
115    /// Return the mutable context of a get operation, or `None`.
116    pub fn context_mut(&mut self) -> Option<&mut Context> {
117        match self {
118            Action::Get(ctx) => Some(ctx),
119            Action::Erase(_) | Action::Store(_) => None,
120        }
121    }
122
123    /// Returns true if this action expects output from the helper.
124    pub fn expects_output(&self) -> bool {
125        matches!(self, Action::Get(_))
126    }
127
128    /// The name of the argument to describe this action. If `is_external` is true, the target program is
129    /// a custom credentials helper, not a built-in one.
130    pub fn as_arg(&self, is_external: bool) -> &str {
131        match self {
132            Action::Get(_) if is_external => "get",
133            Action::Get(_) => "fill",
134            Action::Store(_) if is_external => "store",
135            Action::Store(_) => "approve",
136            Action::Erase(_) if is_external => "erase",
137            Action::Erase(_) => "reject",
138        }
139    }
140}
141
142/// A handle to [store](NextAction::store()) or [erase](NextAction::erase()) the outcome of the initial action.
143#[derive(Clone, Debug, Eq, PartialEq)]
144pub struct NextAction {
145    previous_output: BString,
146    options: protocol::ContextOptions,
147}
148
149impl TryFrom<&NextAction> for Context {
150    type Error = protocol::context::decode::Error;
151
152    fn try_from(value: &NextAction) -> std::result::Result<Self, Self::Error> {
153        Context::from_bytes(value.previous_output.as_ref(), value.options)
154    }
155}
156
157impl From<Context> for NextAction {
158    fn from(ctx: Context) -> Self {
159        let options = ctx.options;
160        let mut buf = Vec::<u8>::new();
161        ctx.write_to(&mut buf).expect("cannot fail");
162        NextAction {
163            previous_output: buf.into(),
164            options,
165        }
166    }
167}
168
169impl NextAction {
170    /// Approve the result of the previous [Action] and store for lookup.
171    pub fn store(self) -> Action {
172        Action::Store(self.previous_output)
173    }
174    /// Reject the result of the previous [Action] and erase it as to not be returned when being looked up.
175    pub fn erase(self) -> Action {
176        Action::Erase(self.previous_output)
177    }
178}
179
180mod cascade;
181pub(crate) mod invoke;
182
183pub use invoke::invoke;