From edb9c4efb955156a8095ce3b1cc0e54b6932bab2 Mon Sep 17 00:00:00 2001 From: eikek Date: Mon, 5 Feb 2024 19:59:34 +0100 Subject: [PATCH] Allow secrets to be given as files --- src/cli/cmd.rs | 43 +++++++++++++++++++ src/cli/cmd/cleanup.rs | 8 ++-- src/cli/cmd/file_exists.rs | 8 ++-- src/cli/cmd/open_item.rs | 7 ++- src/cli/cmd/upload.rs | 21 +++++---- src/cli/opts.rs | 87 ++++++++++++++++++++++++++++++++++---- src/error.rs | 3 ++ 7 files changed, 148 insertions(+), 29 deletions(-) diff --git a/src/cli/cmd.rs b/src/cli/cmd.rs index 8957910..8395592 100644 --- a/src/cli/cmd.rs +++ b/src/cli/cmd.rs @@ -134,27 +134,70 @@ fn proxy_settings(opts: &CommonOpts, cfg: &DsConfig) -> ProxySetting { #[derive(Debug, Snafu)] pub enum CmdError { + #[snafu(display("Bookmark - {}", source))] Bookmark { source: bookmark::Error }, + + #[snafu(display("ContextCreate - {}", source))] ContextCreate { source: http::Error }, + + #[snafu(display("Export - {}", source))] Export { source: export::Error }, + + #[snafu(display("Watch - {}", source))] Watch { source: watch::Error }, + + #[snafu(display("Upload - {}", source))] Upload { source: upload::Error }, + + #[snafu(display("Admin - {}", source))] Admin { source: admin::Error }, + + #[snafu(display("Cleanup - {}", source))] Cleanup { source: cleanup::Error }, + + #[snafu(display("Download - {}", source))] Download { source: download::Error }, + + #[snafu(display("FileExists - {}", source))] FileExists { source: file_exists::Error }, + + #[snafu(display("GenInvite - {}", source))] GenInvite { source: geninvite::Error }, + + #[snafu(display("Item - {}", source))] Item { source: item::Error }, + + #[snafu(display("Login - {}", source))] Login { source: login::Error }, + + #[snafu(display("Logout - {}", source))] Logout { source: logout::Error }, + + #[snafu(display("OpenItem - {}", source))] OpenItem { source: open_item::Error }, + + #[snafu(display("Register - {}", source))] Register { source: register::Error }, + + #[snafu(display("Search - {}", source))] Search { source: search::Error }, + + #[snafu(display("SearchSummary - {}", source))] SearchSummary { source: search_summary::Error }, + + #[snafu(display("Source - {}", source))] Source { source: source::Error }, + + #[snafu(display("Version - {}", source))] Version { source: version::Error }, + + #[snafu(display("View - {}", source))] View { source: view::Error }, + + #[snafu(display("WriteConfig - {}", source))] WriteConfig { source: ConfigError }, + + #[snafu(display("{}", source))] WriteSink { source: SinkError }, } diff --git a/src/cli/cmd/cleanup.rs b/src/cli/cmd/cleanup.rs index fc8d017..fb03dae 100644 --- a/src/cli/cmd/cleanup.rs +++ b/src/cli/cmd/cleanup.rs @@ -6,7 +6,7 @@ use super::{Cmd, Context}; use crate::http::Error as HttpError; use crate::util::{digest, file}; use crate::{ - cli::opts::{EndpointOpts, FileAction}, + cli::opts::{EndpointOpts, FileAction, FileAuthError}, util::file::FileActionResult, }; use crate::{cli::sink::Error as SinkError, http::payload::BasicResult}; @@ -61,8 +61,8 @@ pub enum Error { #[snafu(display("No action given. Use --move or --delete."))] NoAction, - #[snafu(display("A collective was not found and was not specified"))] - NoCollective, + #[snafu(display("Cannot get credentials: {}", source))] + CredentialsRead { source: FileAuthError }, #[snafu(display("The target '{}' is not a directory", path.display()))] TargetNotDirectory { path: PathBuf }, @@ -177,7 +177,7 @@ fn check_file_exists( .to_file_auth(ctx, &|| { file::collective_from_subdir(path, &dirs).unwrap_or(None) }) - .ok_or(Error::NoCollective)?; + .context(CredentialsReadSnafu)?; let hash = digest::digest_file_sha256(path).context(DigestFailSnafu { path })?; let result = ctx diff --git a/src/cli/cmd/file_exists.rs b/src/cli/cmd/file_exists.rs index 777446f..ba6273e 100644 --- a/src/cli/cmd/file_exists.rs +++ b/src/cli/cmd/file_exists.rs @@ -2,7 +2,7 @@ use clap::{Parser, ValueHint}; use snafu::{ResultExt, Snafu}; use std::path::{Path, PathBuf}; -use crate::cli::opts::EndpointOpts; +use crate::cli::opts::{EndpointOpts, FileAuthError}; use crate::cli::sink::Error as SinkError; use crate::http::payload::CheckFileResult; use crate::http::Error as HttpError; @@ -27,8 +27,8 @@ pub struct Input { #[derive(Debug, Snafu)] pub enum Error { - #[snafu(display("Collective must be present when using integration endpoint."))] - NoCollective, + #[snafu(display("Cannot get credentials: {}", source))] + CredentialsRead { source: FileAuthError }, #[snafu(display("Calculating digest of file {} failed: {}", path.display(), source))] DigestFail { @@ -68,7 +68,7 @@ pub fn check_file( ) -> Result { let fa = opts .to_file_auth(ctx, &|| None) - .ok_or(Error::NoCollective)?; + .context(CredentialsReadSnafu)?; let hash = digest::digest_file_sha256(file).context(DigestFailSnafu { path: file })?; let mut result = ctx.client.file_exists(hash, &fa).context(HttpClientSnafu)?; result.file = file.canonicalize().ok().map(|p| p.display().to_string()); diff --git a/src/cli/cmd/open_item.rs b/src/cli/cmd/open_item.rs index 930b3fe..d735a1b 100644 --- a/src/cli/cmd/open_item.rs +++ b/src/cli/cmd/open_item.rs @@ -5,10 +5,10 @@ use snafu::{ResultExt, Snafu}; use std::path::{Path, PathBuf}; use webbrowser; -use crate::cli::cmd; use crate::cli::opts::EndpointOpts; use crate::cli::sink::{Error as SinkError, Sink}; use crate::cli::table; +use crate::cli::{self, cmd}; use crate::http::payload::CheckFileResult; use crate::http::Error as HttpError; use crate::util::digest; @@ -49,6 +49,9 @@ pub enum Error { #[snafu(display("Error opening browser: {}", source))] Webbrowser { source: std::io::Error }, + + #[snafu(display("Cannot get credentials: {}", source))] + CredentialsRead { source: cli::opts::FileAuthError }, } #[derive(Debug, Serialize, Deserialize)] @@ -144,7 +147,7 @@ fn item_from_file( ) -> Result { let fa = opts .to_file_auth(ctx, &|| None) - .ok_or(Error::NoCollective)?; + .context(CredentialsReadSnafu)?; let hash = digest::digest_file_sha256(file).context(DigestFailSnafu { path: file })?; let mut result = ctx.client.file_exists(hash, &fa).context(HttpClientSnafu)?; result.file = file.canonicalize().ok().map(|p| p.display().to_string()); diff --git a/src/cli/cmd/upload.rs b/src/cli/cmd/upload.rs index ffb7c82..749da5c 100644 --- a/src/cli/cmd/upload.rs +++ b/src/cli/cmd/upload.rs @@ -3,7 +3,7 @@ use snafu::{ResultExt, Snafu}; use std::path::{Path, PathBuf}; use super::{Cmd, Context}; -use crate::cli::opts::{EndpointOpts, FileAction, UploadMeta}; +use crate::cli::opts::{EndpointOpts, FileAction, FileAuthError, UploadMeta}; use crate::cli::sink::Error as SinkError; use crate::http::payload::{BasicResult, StringList, UploadMeta as MetaRequest}; use crate::http::{Error as HttpError, FileAuth}; @@ -86,11 +86,11 @@ pub struct Input { #[derive(Debug, Snafu)] pub enum Error { - #[snafu(display( - "The collective is required when uploading: {}. It cannot be deduced from the path.", - path.display() - ))] - CollectiveNotGiven { path: PathBuf }, + #[snafu(display("Cannot get credentials (could not be deduced from {}): {}", path.display(), source))] + CredentialsRead { + source: FileAuthError, + path: PathBuf, + }, #[snafu(display("Unable to open file {}: {}", path.display(), source))] OpenFile { @@ -241,7 +241,7 @@ fn upload_traverse( file::collective_from_subdir(&child, &[path.to_path_buf()]) .unwrap_or(None) }) - .ok_or(Error::CollectiveNotGiven { + .context(CredentialsReadSnafu { path: child.clone(), })?; let exists = check_existence(&child, opts, ctx, &fauth)?; @@ -268,7 +268,7 @@ fn upload_traverse( let fauth = opts .endpoint .to_file_auth(ctx, &|| None) - .ok_or(Error::CollectiveNotGiven { path: path.clone() })?; + .context(CredentialsReadSnafu { path: path.clone() })?; let exists = check_existence(path, opts, ctx, &fauth)?; if !exists { eprintln!("Uploading file {}", path.display()); @@ -327,7 +327,7 @@ fn upload_single( let fauth = opts.endpoint .to_file_auth(ctx, &|| None) - .ok_or(Error::CollectiveNotGiven { + .context(CredentialsReadSnafu { path: opts.files[0].clone(), })?; @@ -351,10 +351,9 @@ fn upload_single( let fauth = opts.endpoint .to_file_auth(ctx, &|| None) - .ok_or(Error::CollectiveNotGiven { + .context(CredentialsReadSnafu { path: opts.files[0].clone(), })?; - eprintln!("Sending request …"); let result = ctx .client diff --git a/src/cli/opts.rs b/src/cli/opts.rs index 58245bf..4f55b07 100644 --- a/src/cli/opts.rs +++ b/src/cli/opts.rs @@ -9,6 +9,7 @@ use crate::{ }; use clap::{ArgAction, ArgGroup, Parser, ValueEnum, ValueHint}; use serde::{Deserialize, Serialize}; +use snafu::Snafu; use std::{path::PathBuf, str::FromStr}; /// This is a command line interface to the docspell server. Docspell @@ -225,6 +226,18 @@ pub enum Format { #[command(group = ArgGroup::new("int"))] #[command(group = ArgGroup::new("g_source"))] pub struct EndpointOpts { + /// Use the integration endpoint and provide the basic auth header + /// as credentials. This must be a `username:password` pair as the + /// first line not starting with '#'. + #[arg(long, group = "int", value_hint = ValueHint::FilePath)] + pub basic_file: Option, + + /// Use the integration endpoint and provide the http header as + /// credentials. This must be a `Header:Value` pair as the first + /// line not starting with '#'. + #[arg(long, group = "int", value_hint = ValueHint::FilePath)] + pub header_file: Option, + /// When using the integration endpoint, provides the Basic auth /// header as credential. This must be a `username:password` pair. #[arg(long, group = "int")] @@ -235,8 +248,9 @@ pub struct EndpointOpts { #[arg(long, group = "int")] pub header: Option, - /// Use the integration endpoint. Credentials `--header|--basic` - /// must be specified if applicable. + /// Use the integration endpoint. Credentials + /// `--header[-file]|--basic[-file]` must be specified if + /// applicable. #[arg(long, short, group = "g_source")] pub integration: bool, @@ -252,6 +266,21 @@ pub struct EndpointOpts { pub source: Option, } +#[derive(Debug, Snafu)] +pub enum FileAuthError { + #[snafu(display("Could not read file: {}", path.display()))] + FileRead { + path: PathBuf, + source: std::io::Error, + }, + + #[snafu(display("Could not parse name:value pair in '{}': {}", path.display(), message))] + NameValParse { path: PathBuf, message: String }, + + #[snafu(display("No collective specified"))] + NoCollective, +} + impl EndpointOpts { pub fn get_source_id(&self, cfg: &DsConfig) -> Option { self.source @@ -259,30 +288,72 @@ impl EndpointOpts { .or_else(|| cfg.default_source_id.clone()) } - /// When no result can be returned, the collective was not provided. + fn read_name_val(file: &PathBuf) -> Result { + let cnt = std::fs::read_to_string(file).map_err(|e| FileAuthError::FileRead { + path: file.to_path_buf(), + source: e, + })?; + + let line = cnt + .lines() + .filter(|s| !s.starts_with("#")) + .take(1) + .map(String::from) + .nth(0); + + match line { + Some(l) => NameVal::from_str(&l).map_err(|str| FileAuthError::NameValParse { + path: file.to_path_buf(), + message: str, + }), + None => Err(FileAuthError::NameValParse { + path: file.to_path_buf(), + message: "File is empty".to_string(), + }), + } + } + + /// Convert the options into a `FileAuth` object to be used with the http client pub fn to_file_auth( &self, ctx: &Context, fallback_cid: &dyn Fn() -> Option, - ) -> Option { + ) -> Result { if self.integration { - let cid = self.collective.clone().or_else(fallback_cid)?; + let cid = self + .collective + .clone() + .or_else(fallback_cid) + .ok_or(FileAuthError::NoCollective)?; let mut res = IntegrationData { collective: cid, auth: IntegrationAuth::None, }; + if let Some(header_file) = &self.header_file { + log::debug!( + "Reading file for integration header {}", + header_file.display() + ); + let np = Self::read_name_val(&header_file)?; + res.auth = IntegrationAuth::Header(np.name.clone(), np.value.clone()); + } + if let Some(basic_file) = &self.basic_file { + log::debug!("Reading file for basic auth {}", basic_file.display()); + let np = Self::read_name_val(&basic_file)?; + res.auth = IntegrationAuth::Basic(np.name.clone(), np.value.clone()); + } if let Some(basic) = &self.basic { res.auth = IntegrationAuth::Basic(basic.name.clone(), basic.value.clone()); } if let Some(header) = &self.header { res.auth = IntegrationAuth::Header(header.name.clone(), header.value.clone()); } - Some(FileAuth::Integration(res)) + Ok(FileAuth::Integration(res)) } else { let sid = self.get_source_id(ctx.cfg); match sid { - Some(id) => Some(FileAuth::from_source(id)), - None => Some(FileAuth::Session { + Some(id) => Ok(FileAuth::from_source(id)), + None => Ok(FileAuth::Session { token: ctx.opts.session.clone(), }), } diff --git a/src/error.rs b/src/error.rs index b42d66d..ae57482 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,7 +6,10 @@ use snafu::Snafu; #[derive(Debug, Snafu)] pub enum Error { + #[snafu(display("{}", source))] Cmd { source: cmd::CmdError }, + + #[snafu(display("Configuration error: {}", source))] Config { source: config::ConfigError }, }