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