Skip to content

Commit

Permalink
Merge pull request #425 from docspell/file-secrets
Browse files Browse the repository at this point in the history
Allow secrets to be given as files
  • Loading branch information
eikek authored Feb 5, 2024
2 parents d71ac6e + edb9c4e commit 7fc7d4a
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 29 deletions.
43 changes: 43 additions & 0 deletions src/cli/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
}

Expand Down
8 changes: 4 additions & 4 deletions src/cli/cmd/cleanup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions src/cli/cmd/file_exists.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -68,7 +68,7 @@ pub fn check_file(
) -> Result<CheckFileResult, Error> {
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());
Expand Down
7 changes: 5 additions & 2 deletions src/cli/cmd/open_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -144,7 +147,7 @@ fn item_from_file(
) -> Result<CheckFileResult, Error> {
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());
Expand Down
21 changes: 10 additions & 11 deletions src/cli/cmd/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)?;
Expand All @@ -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());
Expand Down Expand Up @@ -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(),
})?;

Expand All @@ -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
Expand Down
87 changes: 79 additions & 8 deletions src/cli/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<PathBuf>,

/// 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<PathBuf>,

/// When using the integration endpoint, provides the Basic auth
/// header as credential. This must be a `username:password` pair.
#[arg(long, group = "int")]
Expand All @@ -235,8 +248,9 @@ pub struct EndpointOpts {
#[arg(long, group = "int")]
pub header: Option<NameVal>,

/// 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,

Expand All @@ -252,37 +266,94 @@ pub struct EndpointOpts {
pub source: Option<String>,
}

#[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<String> {
self.source
.clone()
.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<NameVal, FileAuthError> {
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("#"))

Check warning on line 299 in src/cli/opts.rs

View workflow job for this annotation

GitHub Actions / clippy

single-character string constant used as pattern

warning: single-character string constant used as pattern --> src/cli/opts.rs:299:40 | 299 | .filter(|s| !s.starts_with("#")) | ^^^ help: try using a `char` instead: `'#'` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#single_char_pattern = note: `#[warn(clippy::single_char_pattern)]` on by default
.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<String>,
) -> Option<FileAuth> {
) -> Result<FileAuth, FileAuthError> {
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)?;

Check warning on line 337 in src/cli/opts.rs

View workflow job for this annotation

GitHub Actions / clippy

this expression creates a reference which is immediately dereferenced by the compiler

warning: this expression creates a reference which is immediately dereferenced by the compiler --> src/cli/opts.rs:337:46 | 337 | let np = Self::read_name_val(&header_file)?; | ^^^^^^^^^^^^ help: change this to: `header_file` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow = note: `#[warn(clippy::needless_borrow)]` on by default
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)?;

Check warning on line 342 in src/cli/opts.rs

View workflow job for this annotation

GitHub Actions / clippy

this expression creates a reference which is immediately dereferenced by the compiler

warning: this expression creates a reference which is immediately dereferenced by the compiler --> src/cli/opts.rs:342:46 | 342 | let np = Self::read_name_val(&basic_file)?; | ^^^^^^^^^^^ help: change this to: `basic_file` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow
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(),
}),
}
Expand Down
3 changes: 3 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
}

Expand Down

0 comments on commit 7fc7d4a

Please sign in to comment.