gix_credentials/helper/
cascade.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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,
        )
    }
}