aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobin Krahl <robin.krahl@ireas.org>2020-01-23 10:54:09 +0100
committerDaniel Mueller <deso@posteo.net>2020-09-01 21:12:43 -0700
commit3f430bac70a946c776a930fabc5b64a788113452 (patch)
treeb1989fd259a159eb7982002155dc8b482948c1cd
parentbb592ad9909c84c3c72252e1775b1cd2abc1e881 (diff)
downloadnitrocli-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.md3
-rw-r--r--Cargo.lock31
-rw-r--r--Cargo.toml7
-rw-r--r--src/args.rs15
-rw-r--r--src/commands.rs6
-rw-r--r--src/config.rs70
-rw-r--r--src/main.rs56
-rw-r--r--src/pinentry.rs2
-rw-r--r--src/tests/mod.rs5
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`
diff --git a/Cargo.lock b/Cargo.lock
index 810b868..22c764b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 961e82f..d652cba 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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)