rustic_rs/commands/
key.rs1use 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#[derive(clap::Parser, Command, Debug)]
18pub(super) struct KeyCmd {
19 #[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(AddCmd),
34 List(ListCmd),
36 Remove(RemoveCmd),
38 Password(PasswordCmd),
40}
41
42#[derive(clap::Parser, Debug)]
43pub(crate) struct NewPasswordOptions {
44 #[clap(long)]
46 pub(crate) new_password: Option<String>,
47
48 #[clap(long)]
50 pub(crate) new_password_file: Option<PathBuf>,
51
52 #[clap(long)]
54 pub(crate) new_password_command: Option<CommandInput>,
55}
56
57impl NewPasswordOptions {
58 fn pass(&self, text: &str) -> Result<String> {
59 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 #[clap(flatten)]
84 pub(crate) pass_opts: NewPasswordOptions,
85
86 #[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 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 #[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 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}