Skip to content

Commit

Permalink
feat(groups): Add groups subcommand
Browse files Browse the repository at this point in the history
By default it lists the groups using tabwriter [1]. Option --raw is
still available.

This is added mainly because I plan to allow UUIDs as --group values.

[1] https://crates.io/crates/tabwriter
  • Loading branch information
Frederick888 committed Jan 28, 2023
1 parent 3fe4898 commit 9f45982
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 15 deletions.
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ slog = "2.5.2"
slog-term = "2.5.0"
strum = { version = "0.24.0", features = ["derive"] }
sysinfo = "0.26.2"
tabwriter = "1.2.1"
which = "4.0.2"
yubico_manager = { version = "0.9.0", optional = true }

Expand Down
12 changes: 11 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub struct MainArgs {
/// Specify KeePassXC socket path (environment variable: KEEPASSXC_BROWSER_SOCKET_PATH)
#[clap(short, long, value_parser)]
pub socket: Option<String>,
/// Try unlocking database. Applies to get, totp, and store only.
/// Try unlocking database. Applies to get, totp, store, and groups only.
/// Takes one argument in the format of [<MAX_RETRIES>[,<INTERVAL_MS>]]. Use 0 to retry indefinitely. The default interval is 1000ms.
#[clap(long, value_parser, verbatim_doc_comment)]
pub unlock: Option<UnlockOptions>,
Expand Down Expand Up @@ -53,6 +53,7 @@ pub enum Subcommands {
Store(SubStoreArgs),
Erase(SubEraseArgs),
Lock(SubLockArgs),
Groups(SubGroupsArgs),
GeneratePassword(SubGeneratePasswordArgs),
Configure(SubConfigureArgs),
Caller(SubCallerArgs),
Expand All @@ -69,6 +70,7 @@ impl Subcommands {
Self::Store(_) => "store",
Self::Erase(_) => "erase",
Self::Lock(_) => "lock",
Self::Groups(_) => "groups",
Self::GeneratePassword(_) => "generate-password",
Self::Configure(_) => "configure",
Self::Caller(_) => "caller",
Expand Down Expand Up @@ -233,6 +235,14 @@ pub struct SubEraseArgs {}
#[derive(Args)]
pub struct SubLockArgs {}

/// List KeePassXC database groups
#[derive(Args)]
pub struct SubGroupsArgs {
/// Show raw output from KeePassXC
#[clap(long, value_parser)]
pub raw: bool,
}

/// Generate a password
#[derive(Args)]
pub struct SubGeneratePasswordArgs {
Expand Down
23 changes: 13 additions & 10 deletions src/keepassxc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,30 @@ impl Group {
}
}

pub fn get_flat_groups(&self) -> Vec<FlatGroup> {
let flat_self = FlatGroup::new(&self.name, &self.uuid);
pub fn get_flat_groups<'a>(&'a self, mut parents: Vec<&'a str>) -> Vec<FlatGroup<'a>> {
let flat_self = FlatGroup::new(&self.name, &self.uuid, &parents);
parents.push(&self.name);
let mut flat_groups = vec![flat_self];
for child in &self.children {
flat_groups.extend(child.get_flat_groups());
flat_groups.extend(child.get_flat_groups(parents.clone()));
}
flat_groups
}
}

#[derive(Clone, Serialize, Deserialize, Default, Debug)]
pub struct FlatGroup {
pub name: String,
pub uuid: String,
pub struct FlatGroup<'a> {
pub name: &'a str,
pub uuid: &'a str,
pub parents: Vec<&'a str>,
}

impl FlatGroup {
pub fn new<T: Into<String>>(name: T, uuid: T) -> Self {
impl<'a> FlatGroup<'a> {
pub fn new(name: &'a str, uuid: &'a str, parents: &[&'a str]) -> Self {
Self {
name: name.into(),
uuid: uuid.into(),
name,
uuid,
parents: Vec::from(parents),
}
}
}
2 changes: 1 addition & 1 deletion src/keepassxc/messages/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,7 @@ impl GetDatabaseGroupsResponse {
pub fn get_flat_groups(&self) -> Vec<FlatGroup> {
self.get_groups()
.iter()
.map(Group::get_flat_groups)
.map(|g| g.get_flat_groups(vec![]))
.fold(vec![], |mut acc, mut x| {
acc.append(&mut x);
acc
Expand Down
54 changes: 51 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use std::thread;
use std::time::Duration;
use tabwriter::TabWriter;
use utils::callers::CurrentCaller;
use utils::*;

Expand Down Expand Up @@ -723,11 +724,11 @@ fn store_login<T: AsRef<Path>>(
let group_uuid = gg_resp
.get_flat_groups()
.iter()
.filter(|g| &g.name == group)
.map(|g| g.uuid.clone())
.filter(|g| g.name == group)
.map(|g| g.uuid)
.next()
.ok_or_else(|| anyhow!("Failed to find group {group}"))?;
(group.clone(), group_uuid)
(group.clone(), group_uuid.to_owned())
} else {
(database.group.clone(), database.group_uuid.clone())
};
Expand Down Expand Up @@ -768,6 +769,52 @@ fn lock_database<T: AsRef<Path>>(config_path: T) -> Result<()> {
ld_resp.check(&ld_req.get_action())
}

fn get_groups<T: AsRef<Path>>(
config_path: T,
unlock_options: &Option<UnlockOptions>,
args: &cli::SubGroupsArgs,
) -> Result<()> {
let config = Config::read_from(config_path.as_ref())?;
verify_caller(&config)?;
// start session
let (client_id, _, _) = start_session()?;

let _ = associated_databases(&config, &client_id, unlock_options)?;

let gg_req = GetDatabaseGroupsRequest::new();
let (gg_resp, gg_resp_raw) = gg_req.send(client_id, false)?;

if args.raw {
io::stdout().write_all(gg_resp_raw.as_bytes())?;
} else {
let mut tw = TabWriter::new(io::stdout());
tw.write_all("Parents\tName\tUUID\n".as_bytes())?;
tw.write_all("--\t--\t--\n".as_bytes())?;
let groups = gg_resp.get_flat_groups();
for group in groups {
let parents = group
.parents
.iter()
.map(|p| {
if p.contains("->") {
format!("\"{}\"", p.replace('"', "\\\""))
} else {
p.to_string()
}
})
.collect::<Vec<_>>()
.join(" -> ");
tw.write_fmt(format_args!(
"{}\t{}\t{}\n",
parents, group.name, group.uuid
))?;
}
tw.flush()?;
}

Ok(())
}

fn generate_password<T: AsRef<Path>>(
config_path: T,
args: &cli::SubGeneratePasswordArgs,
Expand Down Expand Up @@ -938,6 +985,7 @@ fn real_main() -> Result<()> {
}
cli::Subcommands::Erase(_) => erase_login(),
cli::Subcommands::Lock(_) => lock_database(config_path),
cli::Subcommands::Groups(groups_args) => get_groups(config_path, &args.unlock, groups_args),
cli::Subcommands::GeneratePassword(generate_password_args) => {
generate_password(config_path, generate_password_args)
}
Expand Down

0 comments on commit 9f45982

Please sign in to comment.