rustic_rs/commands/
key.rs

1//! `key` subcommand
2
3use crate::{
4    Application, RUSTIC_APP, helpers::table_with_titles, repository::CliOpenRepo, status_err,
5};
6
7use std::path::PathBuf;
8
9use abscissa_core::{Command, Runnable, Shutdown};
10use anyhow::{Result, bail};
11use dialoguer::Password;
12use log::{info, warn};
13
14use rustic_core::{CommandInput, KeyOptions, RepositoryOptions, repofile::KeyFile};
15
16/// `key` subcommand
17#[derive(clap::Parser, Command, Debug)]
18pub(super) struct KeyCmd {
19    /// Subcommand to run
20    #[clap(subcommand)]
21    cmd: KeySubCmd,
22}
23
24impl Runnable for KeyCmd {
25    fn run(&self) {
26        self.cmd.run();
27    }
28}
29
30#[derive(clap::Subcommand, Debug, Runnable)]
31enum KeySubCmd {
32    /// Add a new key to the repository
33    Add(AddCmd),
34    /// List all keys in the repository
35    List(ListCmd),
36    /// Remove a key from the repository
37    Remove(RemoveCmd),
38    /// Change the password of a key
39    Password(PasswordCmd),
40}
41
42#[derive(clap::Parser, Debug)]
43pub(crate) struct NewPasswordOptions {
44    /// New password
45    #[clap(long)]
46    pub(crate) new_password: Option<String>,
47
48    /// File from which to read the new password
49    #[clap(long)]
50    pub(crate) new_password_file: Option<PathBuf>,
51
52    /// Command to get the new password from
53    #[clap(long)]
54    pub(crate) new_password_command: Option<CommandInput>,
55}
56
57impl NewPasswordOptions {
58    fn pass(&self, text: &str) -> Result<String> {
59        // create new Repository options which just contain password information
60        let mut pass_opts = RepositoryOptions::default();
61        pass_opts.password = self.new_password.clone();
62        pass_opts.password_file = self.new_password_file.clone();
63        pass_opts.password_command = self.new_password_command.clone();
64
65        let pass = pass_opts
66            .evaluate_password()
67            .map_err(Into::into)
68            .transpose()
69            .unwrap_or_else(|| -> Result<_> {
70                Ok(Password::new()
71                    .with_prompt(text)
72                    .allow_empty_password(true)
73                    .with_confirmation("confirm password", "passwords do not match")
74                    .interact()?)
75            })?;
76        Ok(pass)
77    }
78}
79
80#[derive(clap::Parser, Debug)]
81pub(crate) struct AddCmd {
82    /// New password options
83    #[clap(flatten)]
84    pub(crate) pass_opts: NewPasswordOptions,
85
86    /// Key options
87    #[clap(flatten)]
88    pub(crate) key_opts: KeyOptions,
89}
90
91impl Runnable for AddCmd {
92    fn run(&self) {
93        if let Err(err) = RUSTIC_APP
94            .config()
95            .repository
96            .run_open(|repo| self.inner_run(repo))
97        {
98            status_err!("{}", err);
99            RUSTIC_APP.shutdown(Shutdown::Crash);
100        };
101    }
102}
103
104impl AddCmd {
105    fn inner_run(&self, repo: CliOpenRepo) -> Result<()> {
106        if RUSTIC_APP.config().global.dry_run {
107            info!("adding no key in dry-run mode.");
108            return Ok(());
109        }
110        let pass = self.pass_opts.pass("enter password for new key")?;
111        let id = repo.add_key(&pass, &self.key_opts)?;
112        info!("key {id} successfully added.");
113
114        Ok(())
115    }
116}
117
118#[derive(clap::Parser, Debug)]
119pub(crate) struct ListCmd;
120
121impl Runnable for ListCmd {
122    fn run(&self) {
123        if let Err(err) = RUSTIC_APP
124            .config()
125            .repository
126            .run_open(|repo| self.inner_run(repo))
127        {
128            status_err!("{}", err);
129            RUSTIC_APP.shutdown(Shutdown::Crash);
130        };
131    }
132}
133
134impl ListCmd {
135    fn inner_run(&self, repo: CliOpenRepo) -> Result<()> {
136        let used_key = repo.key_id();
137        let keys = repo
138            .stream_files()?
139            .inspect(|f| {
140                if let Err(err) = f {
141                    warn!("{err:?}");
142                }
143            })
144            .filter_map(Result::ok);
145
146        let mut table = table_with_titles(["ID", "User", "Host", "Created"]);
147        _ = table.add_rows(keys.map(|key: (_, KeyFile)| {
148            [
149                format!("{}{}", if used_key == &key.0 { "*" } else { "" }, key.0),
150                key.1.username.unwrap_or_default(),
151                key.1.hostname.unwrap_or_default(),
152                key.1
153                    .created
154                    .map_or(String::new(), |time| format!("{time}")),
155            ]
156        }));
157        println!("{table}");
158        Ok(())
159    }
160}
161
162#[derive(clap::Parser, Debug)]
163pub(crate) struct RemoveCmd {
164    /// The keys to remove
165    ids: Vec<String>,
166}
167
168impl Runnable for RemoveCmd {
169    fn run(&self) {
170        if let Err(err) = RUSTIC_APP
171            .config()
172            .repository
173            .run_open(|repo| self.inner_run(repo))
174        {
175            status_err!("{}", err);
176            RUSTIC_APP.shutdown(Shutdown::Crash);
177        };
178    }
179}
180
181impl RemoveCmd {
182    fn inner_run(&self, repo: CliOpenRepo) -> Result<()> {
183        let repo_key = repo.key_id();
184        let ids: Vec<_> = repo.find_ids(&self.ids)?.collect();
185        if ids.iter().any(|id| id == repo_key) {
186            bail!("Cannot remove currently used key!");
187        }
188        if !RUSTIC_APP.config().global.dry_run {
189            for id in ids {
190                repo.delete_key(&id)?;
191                info!("key {id} successfully removed.");
192            }
193            return Ok(());
194        }
195
196        let keys = repo
197            .stream_files_list(&ids)?
198            .inspect(|f| {
199                if let Err(err) = f {
200                    warn!("{err:?}");
201                }
202            })
203            .filter_map(Result::ok);
204
205        let mut table = table_with_titles(["ID", "User", "Host", "Created"]);
206        _ = table.add_rows(keys.map(|key: (_, KeyFile)| {
207            [
208                key.0.to_string(),
209                key.1.username.unwrap_or_default(),
210                key.1.hostname.unwrap_or_default(),
211                key.1
212                    .created
213                    .map_or(String::new(), |time| format!("{time}")),
214            ]
215        }));
216        println!("would have removed the following keys:");
217        println!("{table}");
218        Ok(())
219    }
220}
221#[derive(clap::Parser, Debug)]
222pub(crate) struct PasswordCmd {
223    /// New password options
224    #[clap(flatten)]
225    pub(crate) pass_opts: NewPasswordOptions,
226}
227
228impl Runnable for PasswordCmd {
229    fn run(&self) {
230        if let Err(err) = RUSTIC_APP
231            .config()
232            .repository
233            .run_open(|repo| self.inner_run(repo))
234        {
235            status_err!("{}", err);
236            RUSTIC_APP.shutdown(Shutdown::Crash);
237        };
238    }
239}
240
241impl PasswordCmd {
242    fn inner_run(&self, repo: CliOpenRepo) -> Result<()> {
243        if RUSTIC_APP.config().global.dry_run {
244            info!("changing no password in dry-run mode.");
245            return Ok(());
246        }
247        let pass = self.pass_opts.pass("enter new password")?;
248        let old_key: KeyFile = repo.get_file(repo.key_id())?;
249        let key_opts = KeyOptions::default()
250            .hostname(old_key.hostname)
251            .username(old_key.username)
252            .with_created(old_key.created.is_some());
253        let id = repo.add_key(&pass, &key_opts)?;
254        info!("key {id} successfully added.");
255
256        let old_key = *repo.key_id();
257        // re-open repository using new password
258        let repo = repo.open_with_password(&pass)?;
259        repo.delete_key(&old_key)?;
260        info!("key {old_key} successfully removed.");
261
262        Ok(())
263    }
264}