diff options
author | Robin Krahl <robin.krahl@ireas.org> | 2020-01-23 10:54:09 +0100 |
---|---|---|
committer | Daniel Mueller <deso@posteo.net> | 2020-09-01 21:12:43 -0700 |
commit | 3f430bac70a946c776a930fabc5b64a788113452 (patch) | |
tree | b1989fd259a159eb7982002155dc8b482948c1cd | |
parent | bb592ad9909c84c3c72252e1775b1cd2abc1e881 (diff) | |
download | nitrocli-3f430bac70a946c776a930fabc5b64a788113452.tar.gz nitrocli-3f430bac70a946c776a930fabc5b64a788113452.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)
-rw-r--r-- | CHANGELOG.md | 3 | ||||
-rw-r--r-- | Cargo.lock | 31 | ||||
-rw-r--r-- | Cargo.toml | 7 | ||||
-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 |
9 files changed, 167 insertions, 28 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index bdca896..21b5f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ Unreleased ---------- +- Added support for configuration files + - Added `toml` dependency in version `0.5.6` + - Added `serde` dependency in version `1.0.114` - Changed default OTP format from `hex` to `base32` - Improved error reporting format and fidelity - Added `anyhow` dependency in version `1.0.32` @@ -99,7 +99,9 @@ dependencies = [ "nitrokey-test", "nitrokey-test-state", "regex", + "serde", "structopt", + "toml", ] [[package]] @@ -212,6 +214,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" [[package]] +name = "serde" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "structopt" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -276,6 +298,15 @@ dependencies = [ ] [[package]] +name = "toml" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +dependencies = [ + "serde", +] + +[[package]] name = "unicode-segmentation" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -58,10 +58,17 @@ version = "0.2" [dependencies.nitrokey] version = "0.7.1" +[dependencies.serde] +version = "1.0" +features = ["derive"] + [dependencies.structopt] version = "0.3.7" default-features = false +[dependencies.toml] +version = "0.5.6" + [dev-dependencies.nitrokey-test] version = "0.4" 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) |