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;