Skip to content

Commit

Permalink
feat(get,totp,store): Group filter --group and --git-groups
Browse files Browse the repository at this point in the history
Filter login entries by group names provided by --group option. This
option can be repeated.

Since the store operation consists of getting existing entries then
update or create an entry, this option also applies to store. One minor
difference from get is that when a new entry is created, --group options
at the global position are ignored, i.e. it uses the first local --group
option if it's present, or the dedicated group created by
git-credential-keepassxc configure.

Also a new --git-groups option is added to filter login entries using
the names of the dedicated groups. Note that it filters all entries by
all groups from all databases, which can lead to unexpected results if a
user has more than one databases with different dedicated group names.
This is a limitation from KeePassXC as we don't get database UUIDs here.

Requires KeePassXC >= 2.6.0 [1]. These two options are ignored if used
with any older KeePassXC.

[1] keepassxreboot/keepassxc#4111
  • Loading branch information
Frederick888 committed Jan 27, 2023
1 parent 7065583 commit d33e1ec
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 101 deletions.
126 changes: 116 additions & 10 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,36 @@ 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, store and erase only.
/// Try unlocking database. Applies to get, totp, and store 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>,
/// Group(s) to get credentials from or the group to store the credential to
#[clap(long, value_parser)]
pub group: Vec<String>,
/// Get credentials from the dedicated group created by 'configure' subcommand
#[clap(long, value_parser)]
pub git_groups: bool,
/// Do not filter out entries with advanced field 'KPH: git' set to false
#[clap(long, value_parser)]
pub no_filter: bool,
/// Sets the level of verbosity (-v: WARNING; -vv: INFO; -vvv: DEBUG in debug builds)
#[clap(short, action(ArgAction::Count))]
pub verbose: u8,
#[clap(subcommand)]
pub command: Subcommands,
}

impl HasEntryFilters for MainArgs {
fn entry_filters(&self) -> EntryFilters {
EntryFilters {
kph: !self.no_filter,
groups: self.group.clone(),
git_groups: self.git_groups,
}
}
}

#[derive(Subcommand)]
pub enum Subcommands {
Get(SubGetArgs),
Expand Down Expand Up @@ -62,7 +81,6 @@ impl Subcommands {

pub trait GetOperation {
fn get_mode(&self) -> GetMode;
fn no_filter(&self) -> bool;
fn advanced_fields(&self) -> bool;
fn json(&self) -> bool;
fn raw(&self) -> bool;
Expand All @@ -74,6 +92,12 @@ pub struct SubGetArgs {
/// Try getting TOTP
#[clap(long, value_parser, conflicts_with = "raw")]
pub totp: bool,
/// Group(s) to get credentials from or the group to store the credential to
#[clap(long, value_parser, conflicts_with = "raw")]
pub group: Vec<String>,
/// Get credentials from the dedicated group created by 'configure' subcommand
#[clap(long, value_parser, conflicts_with = "raw")]
pub git_groups: bool,
/// Do not filter out entries with advanced field 'KPH: git' set to false
#[clap(long, value_parser, conflicts_with = "raw")]
pub no_filter: bool,
Expand All @@ -88,6 +112,18 @@ pub struct SubGetArgs {
pub raw: bool,
}

impl HasEntryFilters for SubGetArgs {
fn entry_filters(&self) -> EntryFilters {
EntryFilters {
kph: !self.no_filter,
groups: self.group.clone(),
git_groups: self.git_groups,
}
}
}

impl HasLocalEntryFilters for SubGetArgs {}

impl GetOperation for SubGetArgs {
fn get_mode(&self) -> GetMode {
if self.totp {
Expand All @@ -97,10 +133,6 @@ impl GetOperation for SubGetArgs {
}
}

fn no_filter(&self) -> bool {
self.no_filter
}

fn advanced_fields(&self) -> bool {
self.advanced_fields
}
Expand All @@ -117,6 +149,15 @@ impl GetOperation for SubGetArgs {
/// Get TOTP
#[derive(Args)]
pub struct SubTotpArgs {
/// Group(s) to get credentials from or the group to store the credential to
#[clap(long, value_parser)]
pub group: Vec<String>,
/// Get credentials from the dedicated group created by 'configure' subcommand
#[clap(long, value_parser)]
pub git_groups: bool,
/// Do not filter out entries with advanced field 'KPH: git' set to false
#[clap(long, value_parser)]
pub no_filter: bool,
/// Print JSON
#[clap(long, value_parser, conflicts_with = "raw")]
pub json: bool,
Expand All @@ -130,10 +171,6 @@ impl GetOperation for SubTotpArgs {
GetMode::TotpOnly
}

fn no_filter(&self) -> bool {
false
}

fn advanced_fields(&self) -> bool {
false
}
Expand All @@ -147,14 +184,44 @@ impl GetOperation for SubTotpArgs {
}
}

impl HasEntryFilters for SubTotpArgs {
fn entry_filters(&self) -> EntryFilters {
EntryFilters {
kph: !self.no_filter,
groups: self.group.clone(),
git_groups: self.git_groups,
}
}
}

impl HasLocalEntryFilters for SubTotpArgs {}

/// Store credential (used by Git)
#[derive(Args)]
pub struct SubStoreArgs {
/// Group(s) to get credentials from or the group to store the credential to
#[clap(long, value_parser)]
pub group: Vec<String>,
/// Get credentials from the dedicated group created by 'configure' subcommand
#[clap(long, value_parser)]
pub git_groups: bool,
/// Do not filter out entries with advanced field 'KPH: git' set to false
#[clap(long, value_parser)]
pub no_filter: bool,
}

impl HasEntryFilters for SubStoreArgs {
fn entry_filters(&self) -> EntryFilters {
EntryFilters {
kph: !self.no_filter,
groups: self.group.clone(),
git_groups: self.git_groups,
}
}
}

impl HasLocalEntryFilters for SubStoreArgs {}

/// [Not implemented] Erase credential (used by Git)
#[derive(Args)]
pub struct SubEraseArgs {}
Expand Down Expand Up @@ -328,3 +395,42 @@ pub enum GetMode {
PasswordAndTotp,
TotpOnly,
}

pub struct EntryFilters {
pub kph: bool,
pub groups: Vec<String>,
pub git_groups: bool,
}

impl EntryFilters {
pub fn has_non_default(&self) -> bool {
!self.kph || !self.groups.is_empty() || self.git_groups
}
}

impl Default for EntryFilters {
fn default() -> Self {
Self {
kph: true,
groups: vec![],
git_groups: false,
}
}
}

pub trait HasEntryFilters {
fn entry_filters(&self) -> EntryFilters;
}

pub trait HasLocalEntryFilters: HasEntryFilters {
fn local_entry_filters(&self, main_entry_filters: EntryFilters) -> EntryFilters {
let local_filters = self.entry_filters();
let mut effective_groups = main_entry_filters.groups;
effective_groups.extend_from_slice(&local_filters.groups);
EntryFilters {
kph: main_entry_filters.kph && local_filters.kph,
groups: effective_groups,
git_groups: main_entry_filters.git_groups || local_filters.git_groups,
}
}
}
24 changes: 24 additions & 0 deletions src/keepassxc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,28 @@ impl Group {
..Default::default()
}
}

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

#[derive(Clone, Serialize, Deserialize, Default, Debug)]
pub struct FlatGroup {
pub name: String,
pub uuid: String,
}

impl FlatGroup {
pub fn new<T: Into<String>>(name: T, uuid: T) -> Self {
Self {
name: name.into(),
uuid: uuid.into(),
}
}
}
90 changes: 51 additions & 39 deletions src/keepassxc/messages/structs.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::primitives::*;
use super::{super::errors::KeePassError, error_code::KeePassErrorCode};
use crate::keepassxc::{FlatGroup, Group};
use crate::utils::*;
#[allow(unused_imports)]
use crate::{debug, error, info, warn};
Expand Down Expand Up @@ -163,7 +164,7 @@ impl_cipher_text!([
(GetLoginsRequest, GetLoginsResponse),
(SetLoginRequest, SetLoginResponse),
(LockDatabaseRequest, LockDatabaseResponse),
// (GetDatabaseGroupsRequest, GetDatabaseGroupsResponse),
(GetDatabaseGroupsRequest, GetDatabaseGroupsResponse),
(CreateNewGroupRequest, CreateNewGroupResponse),
(GetTotpRequest, GetTotpResponse),
]);
Expand Down Expand Up @@ -446,6 +447,7 @@ impl GetLoginsRequest {

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct LoginEntry {
pub group: Option<String>,
pub login: String,
pub name: String,
pub password: String,
Expand Down Expand Up @@ -568,44 +570,54 @@ pub struct LockDatabaseResponse {
* https://github.com/keepassxreboot/keepassxc-browser/blob/develop/keepassxc-protocol.md#get-database-groups
*/

// #[derive(Serialize, Deserialize, Debug)]
// pub struct GetDatabaseGroupsRequest {
// action: KeePassAction,
// }
//
// impl GetDatabaseGroupsRequest {
// pub fn new() -> Self {
// Self {
// action: KeePassAction::GetDatabaseGroups,
// }
// }
// }
//
// #[derive(Serialize, Deserialize, Debug)]
// struct InnerGroups {
// pub groups: Vec<crate::keepassxc::Group>,
// }
//
// #[derive(Serialize, Deserialize, Debug)]
// pub struct GetDatabaseGroupsResponse {
// #[serde(rename = "defaultGroup")]
// pub default_group: Option<String>,
// #[serde(rename = "defaultGroupAlwaysAllow")]
// pub default_group_always_allow: Option<bool>,
// groups: InnerGroups,
// [> generic fields <]
// pub version: Option<String>,
// pub success: Option<KeePassBoolean>,
// pub error: Option<String>,
// #[serde(rename = "errorCode")]
// pub error_code: Option<KeePassErrorCode>,
// }
//
// impl GetDatabaseGroupsResponse {
// pub fn get_groups(&self) -> &[crate::keepassxc::Group] {
// &self.groups.groups
// }
// }
#[derive(Serialize, Deserialize, Debug)]
pub struct GetDatabaseGroupsRequest {
action: KeePassAction,
}

impl GetDatabaseGroupsRequest {
pub fn new() -> Self {
Self {
action: KeePassAction::GetDatabaseGroups,
}
}
}

#[derive(Serialize, Deserialize, Debug)]
struct InnerGroups {
pub groups: Vec<Group>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct GetDatabaseGroupsResponse {
#[serde(rename = "defaultGroup")]
pub default_group: Option<String>,
#[serde(rename = "defaultGroupAlwaysAllow")]
pub default_group_always_allow: Option<bool>,
groups: InnerGroups,
/* generic fields */
pub version: Option<String>,
pub success: Option<KeePassBoolean>,
pub error: Option<String>,
#[serde(rename = "errorCode")]
pub error_code: Option<KeePassErrorCode>,
}

impl GetDatabaseGroupsResponse {
pub fn get_groups(&self) -> &[Group] {
&self.groups.groups
}

pub fn get_flat_groups(&self) -> Vec<FlatGroup> {
self.get_groups()
.iter()
.map(Group::get_flat_groups)
.fold(vec![], |mut acc, mut x| {
acc.append(&mut x);
acc
})
}
}

/*
* create-new-group
Expand Down

0 comments on commit d33e1ec

Please sign in to comment.