Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Group filter and Git detection #51

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 117 additions & 11 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use clap::{
};
use std::{num, str::FromStr};

use crate::{git::GitCredentialMessage, utils::callers::CurrentCaller};

/// Helper that allows Git and shell scripts to use KeePassXC as credential store
#[derive(Parser)]
#[clap(author, version, about, long_about = None)]
Expand All @@ -16,10 +18,19 @@ 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 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>,
/// Do not filter out entries with advanced field 'KPH: git' set to false. Applies to get and store only
#[clap(long, value_parser, global = true)]
pub no_filter: bool,
/// Do not try using entries from dedicated group only for HTTP Git operations. Applies to get and store only
#[clap(long, value_parser, global = true)]
pub no_git_detection: bool,
/// Use specified KeePassXC group only, overrides Git detection. Applies to get and store only
#[clap(long, value_parser, global = true)]
pub group: Option<String>,
/// Sets the level of verbosity (-v: WARNING; -vv: INFO; -vvv: DEBUG in debug builds)
#[clap(short, action(ArgAction::Count))]
pub verbose: u8,
Expand Down Expand Up @@ -60,9 +71,16 @@ impl Subcommands {
}
}

pub trait GetOperation {
pub trait HasEntryFilters {
fn filters(
&self,
credential_message: &GitCredentialMessage,
current_caller: &CurrentCaller,
) -> EntryFilters;
}

pub trait GetOperation: HasEntryFilters {
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 @@ -77,6 +95,12 @@ pub struct SubGetArgs {
/// Do not filter out entries with advanced field 'KPH: git' set to false
#[clap(long, value_parser, conflicts_with = "raw")]
pub no_filter: bool,
/// Do not try using entries from dedicated group only for HTTP Git operations
#[clap(long, value_parser, conflicts_with = "raw")]
pub no_git_detection: bool,
/// Use specified KeePassXC group only, overrides Git detection
#[clap(long, value_parser, conflicts_with = "raw")]
pub group: Option<String>,
/// Print advanced fields
#[clap(long, value_parser, conflicts_with = "raw")]
pub advanced_fields: bool,
Expand All @@ -88,6 +112,25 @@ pub struct SubGetArgs {
pub raw: bool,
}

impl SubGetArgs {
fn group_filter(
&self,
credential_message: &GitCredentialMessage,
current_caller: &CurrentCaller,
) -> GroupFilter {
if let Some(group) = &self.group {
return GroupFilter::Argument(group.clone());
}
if self.no_git_detection {
return GroupFilter::Disabled;
}
if current_caller.may_be_git_http() && credential_message.is_http() {
return GroupFilter::Database;
}
GroupFilter::Disabled
}
}

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

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

fn advanced_fields(&self) -> bool {
self.advanced_fields
}
Expand All @@ -114,6 +153,19 @@ impl GetOperation for SubGetArgs {
}
}

impl HasEntryFilters for SubGetArgs {
fn filters(
&self,
credential_message: &GitCredentialMessage,
current_caller: &CurrentCaller,
) -> EntryFilters {
EntryFilters {
kph: !self.no_filter,
group: self.group_filter(credential_message, current_caller),
}
}
}

/// Get TOTP
#[derive(Args)]
pub struct SubTotpArgs {
Expand All @@ -130,10 +182,6 @@ impl GetOperation for SubTotpArgs {
GetMode::TotpOnly
}

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

fn advanced_fields(&self) -> bool {
false
}
Expand All @@ -147,12 +195,59 @@ impl GetOperation for SubTotpArgs {
}
}

impl HasEntryFilters for SubTotpArgs {
fn filters(&self, _: &GitCredentialMessage, _: &CurrentCaller) -> EntryFilters {
EntryFilters {
kph: true,
group: GroupFilter::Disabled,
}
}
}

/// Store credential (used by Git)
#[derive(Args)]
pub struct SubStoreArgs {
/// Do not filter out entries with advanced field 'KPH: git' set to false
#[clap(long, value_parser)]
pub no_filter: bool,
/// Do not try using entries from dedicated group only for HTTP Git operations
#[clap(long, value_parser)]
pub no_git_detection: bool,
/// Use specified KeePassXC group only, overrides Git detection
#[clap(long, value_parser)]
pub group: Option<String>,
}

impl SubStoreArgs {
fn group_filter(
&self,
credential_message: &GitCredentialMessage,
current_caller: &CurrentCaller,
) -> GroupFilter {
if let Some(group) = &self.group {
return GroupFilter::Argument(group.clone());
}
if self.no_git_detection {
return GroupFilter::Disabled;
}
if current_caller.may_be_git_http() && credential_message.is_http() {
return GroupFilter::Database;
}
GroupFilter::Disabled
}
}

impl HasEntryFilters for SubStoreArgs {
fn filters(
&self,
credential_message: &GitCredentialMessage,
current_caller: &CurrentCaller,
) -> EntryFilters {
EntryFilters {
kph: !self.no_filter,
group: self.group_filter(credential_message, current_caller),
}
}
}

/// [Not implemented] Erase credential (used by Git)
Expand Down Expand Up @@ -328,3 +423,14 @@ pub enum GetMode {
PasswordAndTotp,
TotpOnly,
}

pub struct EntryFilters {
pub kph: bool,
pub group: GroupFilter,
}

pub enum GroupFilter {
Argument(String),
Database,
Disabled,
}
52 changes: 52 additions & 0 deletions src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::{debug, error, info, warn};
use serde::Serialize;
use std::collections::HashMap;
use std::fmt;
use std::io::{self, Read};
use std::str::FromStr;

const KPXC_ADVANCED_FIELD_PREFIX: &str = "KPH: ";
Expand Down Expand Up @@ -109,6 +110,16 @@ message_from_to_string!(
);

impl GitCredentialMessage {
pub fn from_stdin() -> anyhow::Result<Self> {
let git_req = {
let mut git_req_string = String::with_capacity(256);
io::stdin().read_to_string(&mut git_req_string)?;
GitCredentialMessage::from_str(&git_req_string)?
};
debug!("Git credential request: {:?}", git_req);
Ok(git_req)
}

pub fn set_string_fields(&mut self, login_entry_fields: &[HashMap<String, String>]) {
let mut result = HashMap::new();
login_entry_fields.iter().for_each(|login_entry_field| {
Expand All @@ -127,6 +138,36 @@ impl GitCredentialMessage {
});
self.string_fields = Some(result);
}

pub fn get_url(&self) -> anyhow::Result<String> {
if let Some(ref url_string) = self.url {
Ok(url_string.clone())
} else {
if self.protocol.is_none() || self.host.is_none() {
return Err(anyhow::anyhow!(
"Protocol and host are both required when URL is not provided"
));
}
Ok(format!(
"{}://{}/{}",
self.protocol.as_deref().unwrap(),
self.host.as_deref().unwrap(),
self.path.as_deref().unwrap_or("")
))
}
}

pub fn is_http(&self) -> bool {
if let Some(protocol) = &self.protocol {
let protocol = protocol.to_ascii_lowercase();
return protocol == "http" || protocol == "https";
}
if let Some(url) = &self.url {
let url = url.to_ascii_lowercase();
return url.starts_with("http://") || url.starts_with("https://");
}
false
}
}

#[cfg(test)]
Expand Down Expand Up @@ -169,4 +210,15 @@ mod tests {
message.set_string_fields(&advanced_fields);
assert_eq!(string1 + &string2 + "\n", message.to_string());
}

#[test]
fn test_03_is_http() {
let string = "url=https://example.com\n".to_owned();
let message = GitCredentialMessage::from_str(string.as_str()).unwrap();
assert!(message.is_http());

let string = "protocol=http\nhost=example.com\n".to_owned();
let message = GitCredentialMessage::from_str(string.as_str()).unwrap();
assert!(message.is_http());
}
}
1 change: 1 addition & 0 deletions src/keepassxc/messages/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ impl GetLoginsRequest {

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct LoginEntry {
pub group: String,
pub login: String,
pub name: String,
pub password: String,
Expand Down
Loading