diff options
| author | Robin Krahl <robin.krahl@ireas.org> | 2020-01-23 10:54:09 +0100 | 
|---|---|---|
| committer | Daniel Mueller <deso@posteo.net> | 2020-09-05 11:35:11 -0700 | 
| commit | 88b243bca17ab549342738bce98a1c678f98e754 (patch) | |
| tree | e1887917dc1eb94a23642ebe26b2f6c02b705188 /src | |
| parent | 887f3cb9aab12b390ae9efe09c1ff7f972e51c35 (diff) | |
| download | nitrocli-88b243bca17ab549342738bce98a1c678f98e754.tar.gz nitrocli-88b243bca17ab549342738bce98a1c678f98e754.tar.bz2 | |
Implement configuration handling
This patch implements basic configuration handling that reads a
configuration file and stores the parsed data in the ExecCtx and RunCtx
structs.  It supports three configuration items:
- model (previously only --model)
- no_cache (previously only NITROCLI_NO_CACHE)
- verbosity (previously only --verbose)
Diffstat (limited to 'src')
| -rw-r--r-- | src/args.rs | 15 | ||||
| -rw-r--r-- | src/commands.rs | 6 | ||||
| -rw-r--r-- | src/config.rs | 70 | ||||
| -rw-r--r-- | src/main.rs | 56 | ||||
| -rw-r--r-- | src/pinentry.rs | 2 | ||||
| -rw-r--r-- | src/tests/mod.rs | 5 | 
6 files changed, 126 insertions, 28 deletions
| diff --git a/src/args.rs b/src/args.rs index 0f548e4..7f2dc31 100644 --- a/src/args.rs +++ b/src/args.rs @@ -18,7 +18,7 @@  // *************************************************************************  /// Provides access to a Nitrokey device -#[derive(structopt::StructOpt)] +#[derive(Debug, structopt::StructOpt)]  #[structopt(name = "nitrocli")]  pub struct Args {    /// Increases the log level (can be supplied multiple times) @@ -57,6 +57,19 @@ impl From<DeviceModel> for nitrokey::Model {    }  } +impl<'de> serde::Deserialize<'de> for DeviceModel { +  fn deserialize<D>(deserializer: D) -> Result<DeviceModel, D::Error> +  where +    D: serde::Deserializer<'de>, +  { +    use serde::de::Error as _; +    use std::str::FromStr as _; + +    let s = String::deserialize(deserializer)?; +    DeviceModel::from_str(&s).map_err(D::Error::custom) +  } +} +  Command! {    /// A top-level command for nitrocli.    Command, [ diff --git a/src/commands.rs b/src/commands.rs index 1d59af5..090d532 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -39,7 +39,7 @@ use crate::ExecCtx;  /// Set `libnitrokey`'s log level based on the execution context's verbosity.  fn set_log_level(ctx: &mut ExecCtx<'_>) { -  let log_lvl = match ctx.verbosity { +  let log_lvl = match ctx.config.verbosity {      // The error log level is what libnitrokey uses by default. As such,      // there is no harm in us setting that as well when the user did not      // ask for higher verbosity. @@ -63,7 +63,7 @@ where    set_log_level(ctx); -  let device = match ctx.model { +  let device = match ctx.config.model {      Some(model) => manager.connect_model(model.into()).with_context(|| {        anyhow::anyhow!("Nitrokey {} device not found", model.as_user_facing_str())      })?, @@ -83,7 +83,7 @@ where    set_log_level(ctx); -  if let Some(model) = ctx.model { +  if let Some(model) = ctx.config.model {      if model != args::DeviceModel::Storage {        anyhow::bail!("This command is only available on the Nitrokey Storage");      } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..850d217 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,70 @@ +// config.rs + +// ************************************************************************* +// * Copyright (C) 2020 Daniel Mueller (deso@posteo.net)                   * +// *                                                                       * +// * This program is free software: you can redistribute it and/or modify  * +// * it under the terms of the GNU General Public License as published by  * +// * the Free Software Foundation, either version 3 of the License, or     * +// * (at your option) any later version.                                   * +// *                                                                       * +// * This program is distributed in the hope that it will be useful,       * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of        * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         * +// * GNU General Public License for more details.                          * +// *                                                                       * +// * You should have received a copy of the GNU General Public License     * +// * along with this program.  If not, see <http://www.gnu.org/licenses/>. * +// ************************************************************************* + +use std::fs; +use std::path; + +use crate::args; + +use anyhow::Context as _; + +/// The configuration for nitrocli, usually read from configuration +/// files and environment variables. +#[derive(Clone, Copy, Debug, Default, PartialEq, serde::Deserialize)] +pub struct Config { +  /// The model to connect to. +  pub model: Option<args::DeviceModel>, +  /// Whether to bypass the cache for all secrets or not. +  #[serde(default)] +  pub no_cache: bool, +  /// The log level. +  #[serde(default)] +  pub verbosity: u8, +} + +impl Config { +  pub fn load() -> anyhow::Result<Self> { +    load_user_config().map(|o| o.unwrap_or_default()) +  } + +  pub fn update(&mut self, args: &args::Args) { +    if args.model.is_some() { +      self.model = args.model; +    } +    if args.verbose > 0 { +      self.verbosity = args.verbose; +    } +  } +} + +fn load_user_config() -> anyhow::Result<Option<Config>> { +  let path = path::Path::new("config.toml"); +  if path.is_file() { +    read_config_file(&path).map(Some) +  } else { +    Ok(None) +  } +} + +pub fn read_config_file(path: &path::Path) -> anyhow::Result<Config> { +  let s = fs::read_to_string(path) +    .with_context(|| format!("Failed to read configuration file '{}'", path.display()))?; +  toml::from_str(&s) +    .with_context(|| format!("Failed to parse configuration file '{}'", path.display())) +} diff --git a/src/main.rs b/src/main.rs index a8c0a46..3535a91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,6 +69,7 @@ mod arg_util;  mod args;  mod commands; +mod config;  mod pinentry;  #[cfg(test)]  mod tests; @@ -108,8 +109,6 @@ where  /// the command execution.  #[allow(missing_debug_implementations)]  pub struct ExecCtx<'io> { -  /// The Nitrokey model to use. -  pub model: Option<args::DeviceModel>,    /// See `RunCtx::stdout`.    pub stdout: &'io mut dyn io::Write,    /// See `RunCtx::stderr`. @@ -124,10 +123,8 @@ pub struct ExecCtx<'io> {    pub new_user_pin: Option<ffi::OsString>,    /// See `RunCtx::password`.    pub password: Option<ffi::OsString>, -  /// See `RunCtx::no_cache`. -  pub no_cache: bool, -  /// The verbosity level to use for logging. -  pub verbosity: u64, +  /// See `RunCtx::config`. +  pub config: config::Config,  }  impl<'io> Stdio for ExecCtx<'io> { @@ -142,8 +139,9 @@ fn handle_arguments(ctx: &mut RunCtx<'_>, args: Vec<String>) -> anyhow::Result<(    match args::Args::from_iter_safe(args.iter()) {      Ok(args) => { +      let mut config = ctx.config; +      config.update(&args);        let mut ctx = ExecCtx { -        model: args.model,          stdout: ctx.stdout,          stderr: ctx.stderr,          admin_pin: ctx.admin_pin.take(), @@ -151,8 +149,7 @@ fn handle_arguments(ctx: &mut RunCtx<'_>, args: Vec<String>) -> anyhow::Result<(          new_admin_pin: ctx.new_admin_pin.take(),          new_user_pin: ctx.new_user_pin.take(),          password: ctx.password.take(), -        no_cache: ctx.no_cache, -        verbosity: args.verbose.into(), +        config,        };        args.cmd.execute(&mut ctx)      } @@ -187,8 +184,9 @@ pub(crate) struct RunCtx<'io> {    pub new_user_pin: Option<ffi::OsString>,    /// A password used by some commands, if provided through an environment variable.    pub password: Option<ffi::OsString>, -  /// Whether to bypass the cache for all secrets or not. -  pub no_cache: bool, +  /// The configuration, usually read from configuration files and environment +  /// variables. +  pub config: config::Config,  }  fn run<'ctx, 'io: 'ctx>(ctx: &'ctx mut RunCtx<'io>, args: Vec<String>) -> i32 { @@ -206,19 +204,33 @@ fn main() {    let mut stdout = io::stdout();    let mut stderr = io::stderr(); -  let args = env::args().collect::<Vec<_>>(); -  let ctx = &mut RunCtx { -    stdout: &mut stdout, -    stderr: &mut stderr, -    admin_pin: env::var_os(NITROCLI_ADMIN_PIN), -    user_pin: env::var_os(NITROCLI_USER_PIN), -    new_admin_pin: env::var_os(NITROCLI_NEW_ADMIN_PIN), -    new_user_pin: env::var_os(NITROCLI_NEW_USER_PIN), -    password: env::var_os(NITROCLI_PASSWORD), -    no_cache: env::var_os(NITROCLI_NO_CACHE).is_some(), + +  let rc = match config::Config::load() { +    Ok(mut config) => { +      if env::var_os(NITROCLI_NO_CACHE).is_some() { +        config.no_cache = true; +      } + +      let args = env::args().collect::<Vec<_>>(); +      let ctx = &mut RunCtx { +        stdout: &mut stdout, +        stderr: &mut stderr, +        admin_pin: env::var_os(NITROCLI_ADMIN_PIN), +        user_pin: env::var_os(NITROCLI_USER_PIN), +        new_admin_pin: env::var_os(NITROCLI_NEW_ADMIN_PIN), +        new_user_pin: env::var_os(NITROCLI_NEW_USER_PIN), +        password: env::var_os(NITROCLI_PASSWORD), +        config, +      }; + +      run(ctx, args) +    } +    Err(err) => { +      let _ = writeln!(stderr, "{:?}", err); +      1 +    }    }; -  let rc = run(ctx, args);    // We exit the process the hard way below. The problem is that because    // of this, buffered IO may not be flushed. So make sure to explicitly    // flush before exiting. Note that stderr is unbuffered, alleviating diff --git a/src/pinentry.rs b/src/pinentry.rs index 510d7b0..f538a47 100644 --- a/src/pinentry.rs +++ b/src/pinentry.rs @@ -238,7 +238,7 @@ where  {    let cache_id = entry      .cache_id() -    .and_then(|id| if ctx.no_cache { None } else { Some(id) }) +    .and_then(|id| if ctx.config.no_cache { None } else { Some(id) })      // "X" is a sentinel value indicating that no caching is desired.      .unwrap_or_else(|| "X".into())      .into(); diff --git a/src/tests/mod.rs b/src/tests/mod.rs index f26ab80..9477964 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -115,7 +115,10 @@ impl Nitrocli {        new_admin_pin: self.new_admin_pin.clone(),        new_user_pin: self.new_user_pin.clone(),        password: self.password.clone(), -      no_cache: true, +      config: crate::config::Config { +        no_cache: true, +        ..Default::default() +      },      };      (f(ctx, args), stdout, stderr) | 
