Skip to content

Commit a5533e0

Browse files
committed
Update the cli for attributes.
1 parent 694cb8c commit a5533e0

File tree

1 file changed

+143
-29
lines changed

1 file changed

+143
-29
lines changed

examples/cli.rs

Lines changed: 143 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
extern crate keyring;
22

33
use clap::Parser;
4+
use std::collections::HashMap;
45

56
use keyring::{Entry, Error, Result};
67

@@ -21,40 +22,57 @@ fn main() {
2122
};
2223
match &args.command {
2324
Command::Set { .. } => {
24-
let (secret, password) = args.get_password();
25+
let (secret, password, attributes) = args.get_password_and_attributes();
26+
if secret.is_none() && password.is_none() && attributes.is_none() {
27+
eprintln!("You must provide either a password or attributes to the set command");
28+
std::process::exit(1);
29+
}
2530
if let Some(secret) = secret {
2631
match entry.set_secret(&secret) {
27-
Ok(()) => args.success_message_for(Some(&secret), None),
32+
Ok(()) => args.success_message_for(Some(&secret), None, None),
2833
Err(err) => args.error_message_for(err),
2934
}
30-
} else if let Some(password) = password {
35+
}
36+
if let Some(password) = password {
3137
match entry.set_password(&password) {
32-
Ok(()) => args.success_message_for(None, Some(&password)),
38+
Ok(()) => args.success_message_for(None, Some(&password), None),
3339
Err(err) => args.error_message_for(err),
3440
}
35-
} else {
36-
if args.verbose {
37-
eprintln!("You must provide a password to the set command");
41+
}
42+
if let Some(attributes) = attributes {
43+
let attrs: HashMap<&str, &str> = attributes
44+
.iter()
45+
.map(|(key, value)| (key.as_str(), value.as_str()))
46+
.collect();
47+
match entry.update_attributes(&attrs) {
48+
Ok(()) => args.success_message_for(None, None, Some(attributes)),
49+
Err(err) => args.error_message_for(err),
3850
}
39-
std::process::exit(1)
4051
}
4152
}
4253
Command::Password => match entry.get_password() {
4354
Ok(password) => {
4455
println!("{password}");
45-
args.success_message_for(None, Some(&password));
56+
args.success_message_for(None, Some(&password), None);
4657
}
4758
Err(err) => args.error_message_for(err),
4859
},
4960
Command::Secret => match entry.get_secret() {
5061
Ok(secret) => {
5162
println!("{}", secret_string(&secret));
52-
args.success_message_for(Some(&secret), None);
63+
args.success_message_for(Some(&secret), None, None);
64+
}
65+
Err(err) => args.error_message_for(err),
66+
},
67+
Command::Attributes => match entry.get_attributes() {
68+
Ok(attributes) => {
69+
println!("{}", attributes_string(&attributes));
70+
args.success_message_for(None, None, Some(attributes));
5371
}
5472
Err(err) => args.error_message_for(err),
5573
},
5674
Command::Delete => match entry.delete_credential() {
57-
Ok(()) => args.success_message_for(None, None),
75+
Ok(()) => args.success_message_for(None, None, None),
5876
Err(err) => args.error_message_for(err),
5977
},
6078
}
@@ -87,8 +105,15 @@ pub struct Cli {
87105

88106
#[derive(Debug, Parser)]
89107
pub enum Command {
90-
/// Set the password in the secure store
108+
/// Set the password and, optionally, attributes in the secure store
91109
Set {
110+
#[clap(short, long, action)]
111+
/// The password is base64-encoded binary
112+
binary: bool,
113+
114+
#[clap(short, long, value_parser, default_value = "")]
115+
attributes: String,
116+
92117
#[clap(value_parser)]
93118
/// The password to set into the secure store.
94119
/// If it's a valid base64 encoding (with padding),
@@ -105,7 +130,8 @@ pub enum Command {
105130
/// Retrieve the (binary) secret from the secure store
106131
/// and write it in base64 encoding to the standard output.
107132
Secret,
108-
/// Delete the underlying credential from the secure store.
133+
/// Retrieve attributes available in the secure store.
134+
Attributes,
109135
Delete,
110136
}
111137

@@ -146,6 +172,9 @@ impl Cli {
146172
Command::Secret => {
147173
eprintln!("Couldn't get secret for '{description}': {err}");
148174
}
175+
Command::Attributes => {
176+
eprintln!("Couldn't get attributes for '{description}': {err}");
177+
}
149178
Command::Delete => {
150179
eprintln!("Couldn't delete credential for '{description}': {err}");
151180
}
@@ -155,7 +184,12 @@ impl Cli {
155184
std::process::exit(1)
156185
}
157186

158-
fn success_message_for(&self, secret: Option<&[u8]>, password: Option<&str>) {
187+
fn success_message_for(
188+
&self,
189+
secret: Option<&[u8]>,
190+
password: Option<&str>,
191+
attributes: Option<HashMap<String, String>>,
192+
) {
159193
if !self.verbose {
160194
return;
161195
}
@@ -169,6 +203,10 @@ impl Cli {
169203
let secret = secret_string(secret);
170204
eprintln!("Set secret for '{description}' to decode of '{secret}'");
171205
}
206+
if let Some(attributes) = attributes {
207+
eprintln!("Set attributes for '{description}' to:");
208+
eprint_attributes(attributes);
209+
}
172210
}
173211
Command::Password => {
174212
let pw = password.unwrap();
@@ -178,23 +216,48 @@ impl Cli {
178216
let secret = secret_string(secret.unwrap());
179217
eprintln!("Secret for '{description}' encodes as {secret}");
180218
}
219+
Command::Attributes => {
220+
let attributes = attributes.unwrap();
221+
if attributes.is_empty() {
222+
eprintln!("No attributes found for '{description}'");
223+
} else {
224+
eprintln!("Attributes for '{description}' are:");
225+
eprint_attributes(attributes);
226+
}
227+
}
181228
Command::Delete => {
182229
eprintln!("Successfully deleted credential for '{description}'");
183230
}
184231
}
185232
}
186233

187-
fn get_password(&self) -> (Option<Vec<u8>>, Option<String>) {
188-
match &self.command {
189-
Command::Set { password: Some(pw) } => password_or_secret(pw),
190-
Command::Set { password: None } => {
191-
if let Ok(password) = rpassword::prompt_password("Password: ") {
192-
password_or_secret(&password)
193-
} else {
194-
(None, None)
195-
}
196-
}
197-
_ => (None, None),
234+
fn get_password_and_attributes(
235+
&self,
236+
) -> (
237+
Option<Vec<u8>>,
238+
Option<String>,
239+
Option<HashMap<String, String>>,
240+
) {
241+
if let Command::Set {
242+
binary,
243+
attributes,
244+
password,
245+
} = &self.command
246+
{
247+
let secret = if *binary {
248+
Some(decode_secret(password))
249+
} else {
250+
None
251+
};
252+
let password = if !*binary {
253+
Some(read_password(password))
254+
} else {
255+
None
256+
};
257+
let attributes = parse_attributes(attributes);
258+
(secret, password, attributes)
259+
} else {
260+
panic!("Can't happen: asking for password and attributes on non-set command")
198261
}
199262
}
200263
}
@@ -205,11 +268,62 @@ fn secret_string(secret: &[u8]) -> String {
205268
BASE64_STANDARD.encode(secret)
206269
}
207270

208-
fn password_or_secret(input: &str) -> (Option<Vec<u8>>, Option<String>) {
271+
fn eprint_attributes(attributes: HashMap<String, String>) {
272+
for (key, value) in attributes {
273+
println!(" {key}: {value}");
274+
}
275+
}
276+
277+
fn decode_secret(input: &Option<String>) -> Vec<u8> {
209278
use base64::prelude::*;
210279

211-
match BASE64_STANDARD.decode(input) {
212-
Ok(secret) => (Some(secret), None),
213-
Err(_) => (None, Some(input.to_string())),
280+
let encoded = if let Some(input) = input {
281+
input.clone()
282+
} else {
283+
rpassword::prompt_password("Base64 encoding: ").unwrap_or_else(|_| String::new())
284+
};
285+
if encoded.is_empty() {
286+
return Vec::new();
287+
}
288+
match BASE64_STANDARD.decode(encoded) {
289+
Ok(secret) => secret,
290+
Err(err) => {
291+
eprintln!("Sorry, the provided secret data is not base64-encoded: {err}");
292+
std::process::exit(1);
293+
}
294+
}
295+
}
296+
297+
fn read_password(input: &Option<String>) -> String {
298+
let password = if let Some(input) = input {
299+
input.clone()
300+
} else {
301+
rpassword::prompt_password("Password: ").unwrap_or_else(|_| String::new())
302+
};
303+
password
304+
}
305+
306+
fn attributes_string(attributes: &HashMap<String, String>) -> String {
307+
let strings = attributes
308+
.iter()
309+
.map(|(k, v)| format!("{}={}", k, v))
310+
.collect::<Vec<_>>();
311+
strings.join(",")
312+
}
313+
314+
fn parse_attributes(input: &String) -> Option<HashMap<String, String>> {
315+
if input.is_empty() {
316+
return None;
317+
}
318+
let mut attributes = HashMap::new();
319+
let parts = input.split(',');
320+
for s in parts.into_iter() {
321+
let parts: Vec<&str> = s.split("=").collect();
322+
if parts.len() != 2 || parts[0].is_empty() {
323+
eprintln!("Sorry, this part of the attributes string is not a key=val pair: {s}");
324+
std::process::exit(1);
325+
}
326+
attributes.insert(parts[0].to_string(), parts[1].to_string());
214327
}
328+
Some(attributes)
215329
}

0 commit comments

Comments
 (0)