diff --git a/docs/ragenix.1 b/docs/ragenix.1 index 78ab95e..58e6f38 100644 --- a/docs/ragenix.1 +++ b/docs/ragenix.1 @@ -1,6 +1,6 @@ -.\" generated with Ronn-NG/v0.9.1 -.\" http://github.com/apjanke/ronn-ng/tree/0.9.1 -.TH "RAGENIX" "1" "January 2022" "" +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "RAGENIX" "1" "January 1980" "" .SH "NAME" \fBragenix\fR \- age\-encrypted secrets for Nix .SH "SYNOPSIS" @@ -30,8 +30,8 @@ Use the given \fIPROGRAM\fR to open the decrypted file for editing\. Defaults to .IP Giving the special token \fB\-\fR as a \fIPROGRAM\fR causes \fBragenix\fR to read from standard input\. In this case, \fBragenix\fR stream\-encrypts data from standard input only and does not open the file for editing\. .TP -\fB\-r\fR, \fB\-\-rekey\fR -Decrypt all secrets given in the rules configuration file and encrypt them with the defined public keys\. If a secret file does not exist yet, it is ignored\. This option is useful to grant a new recipient access to one or multiple secrets\. +\fB\-r\fR, \fB\-\-rekey\fR [PATH] +Decrypt secrets given in the rules configuration file and encrypt them with the defined public keys\. If no paths are given, rekey all secrets\. If no paths are given and a secret file does not exist yet, it is ignored\. This option is useful to grant a new recipient access to one or multiple secrets\. .IP If the \fB\-\-identity\fR option is not given, \fBragenix\fR tries to decrypt \fIPATH\fR with the default SSH private keys\. See \fB\-\-identity\fR for details\. .IP diff --git a/docs/ragenix.1.html b/docs/ragenix.1.html index 5959865..48f7bbf 100644 --- a/docs/ragenix.1.html +++ b/docs/ragenix.1.html @@ -127,10 +127,10 @@

OPTIONS

input only and does not open the file for editing.

--r, --rekey -
-
Decrypt all secrets given in the rules configuration file and encrypt them - with the defined public keys. If a secret file does not exist yet, it is +-r, --rekey [PATH] +
Decrypt secrets given in the rules configuration file and encrypt them + with the defined public keys. If no paths are given, rekey all secrets. + If no paths are given and a secret file does not exist yet, it is ignored. This option is useful to grant a new recipient access to one or multiple secrets. @@ -268,7 +268,7 @@

AUTHORS

  1. -
  2. January 2022
  3. +
  4. January 1980
  5. ragenix(1)
diff --git a/docs/ragenix.1.ronn b/docs/ragenix.1.ronn index 9d77c9d..22b7957 100644 --- a/docs/ragenix.1.ronn +++ b/docs/ragenix.1.ronn @@ -45,9 +45,10 @@ store. standard input. In this case, `ragenix` stream-encrypts data from standard input only and does not open the file for editing. -* `-r`, `--rekey`: - Decrypt all secrets given in the rules configuration file and encrypt them - with the defined public keys. If a secret file does not exist yet, it is +* `-r`, `--rekey` [PATH]: + Decrypt secrets given in the rules configuration file and encrypt them + with the defined public keys. If no paths are given, rekey all secrets. + If no paths are given and a secret file does not exist yet, it is ignored. This option is useful to grant a new recipient access to one or multiple secrets. diff --git a/src/cli.rs b/src/cli.rs index 252c1b7..c50d0e9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -12,7 +12,7 @@ pub(crate) struct Opts { pub edit: Option, pub editor: Option, pub identities: Option>, - pub rekey: bool, + pub rekey: Option>, pub rules: String, pub schema: bool, pub verbose: bool, @@ -35,10 +35,12 @@ fn build() -> Command { ) .arg( Arg::new("rekey") - .help("re-encrypts all secrets with specified recipients") + .help("re-encrypts secrets with specified recipients") .long("rekey") .short('r') - .action(ArgAction::SetTrue), + .num_args(0..) + .value_name("FILE") + .value_hint(ValueHint::FilePath), ) .arg( Arg::new("identity") @@ -107,7 +109,9 @@ where identities: matches .get_many::("identity") .map(|vals| vals.cloned().collect::>()), - rekey: matches.get_flag("rekey"), + rekey: matches + .get_many::("rekey") + .map(|vals| vals.cloned().collect::>()), rules: matches .get_one::("rules") .cloned() diff --git a/src/main.rs b/src/main.rs index cbad641..1a00d73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use color_eyre::eyre::{eyre, Result}; -use std::{env, fs, path::Path, process}; +use std::{env, path::PathBuf, process}; mod age; mod cli; @@ -29,10 +29,7 @@ fn main() -> Result<()> { let identities = opts.identities.unwrap_or_default(); if let Some(path) = &opts.edit { - let path_normalized = util::normalize_path(Path::new(path)); - let edit_path = std::env::current_dir() - .and_then(fs::canonicalize) - .map(|p| p.join(path_normalized))?; + let edit_path = util::canonicalize_rule_path(path)?; let rule = rules .into_iter() .find(|x| x.path == edit_path) @@ -41,8 +38,22 @@ fn main() -> Result<()> { // `EDITOR`/`--editor` is mandatory if action is `--edit` let editor = &opts.editor.unwrap(); ragenix::edit(&rule, &identities, editor, &mut std::io::stdout())?; - } else if opts.rekey { - ragenix::rekey(&rules, &identities, &mut std::io::stdout())?; + } else if let Some(paths) = opts.rekey { + if paths.is_empty() { + // Option passed but no files specified - rekey all + ragenix::rekey(&rules, &identities, true, &mut std::io::stdout())?; + } else { + let paths_normalized = paths + .into_iter() + .map(util::canonicalize_rule_path) + .collect::>>()?; + let chosen_rules = rules + .into_iter() + .filter(|x| paths_normalized.contains(&x.path)) + .collect::>(); + + ragenix::rekey(&chosen_rules, &identities, false, &mut std::io::stdout())?; + } } } diff --git a/src/ragenix/mod.rs b/src/ragenix/mod.rs index 7b14a8a..f066f45 100644 --- a/src/ragenix/mod.rs +++ b/src/ragenix/mod.rs @@ -148,6 +148,7 @@ pub(crate) fn parse_rules>(rules_path: P) -> Result Result<()> { let identities = age::get_identities(identities)?; @@ -155,8 +156,10 @@ pub(crate) fn rekey( if entry.path.exists() { writeln!(writer, "Rekeying {}", entry.path.display())?; age::rekey(&entry.path, &identities, &entry.public_keys)?; - } else { + } else if no_exist_ok { writeln!(writer, "Does not exist, ignored: {}", entry.path.display())?; + } else { + return Err(eyre!("Does not exist: {}", entry.path.display())); } } Ok(()) diff --git a/src/util.rs b/src/util.rs index e6e96c6..7f6d5d1 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,7 @@ //! Util functions use std::{ - fs::File, + fs::{self, File}, io, path::{Component, Path, PathBuf}, }; @@ -49,6 +49,14 @@ pub(crate) fn normalize_path(path: &Path) -> PathBuf { ret } +/// Make an input path absolute relative to the current directory (rules format) +pub(crate) fn canonicalize_rule_path>(path: P) -> Result { + let path_normalized = normalize_path(path.as_ref()); + Ok(std::env::current_dir() + .and_then(fs::canonicalize) + .map(|p| p.join(path_normalized))?) +} + /// Hash a file using SHA-256 pub(crate) fn sha256>(path: P) -> Result> { let mut file = File::open(path)?; diff --git a/tests/ragenix.rs b/tests/ragenix.rs index bc03b93..abe2059 100644 --- a/tests/ragenix.rs +++ b/tests/ragenix.rs @@ -367,6 +367,88 @@ fn rekeying_fails_no_valid_identites() -> Result<()> { Ok(()) } +#[test] +#[cfg_attr(not(feature = "recursive-nix"), ignore)] +fn rekeying_one_works() -> Result<()> { + let (_dir, path) = copy_example_to_tmpdir()?; + + let files = &["root.passwd.age"]; + let expected = files + .iter() + .map(|s| path.join(s)) + .map(|p| format!("Rekeying {}", p.display())) + .collect::>() + .join("\n") + + "\n"; + + let mut cmd = Command::cargo_bin(crate_name!())?; + let assert = cmd + .current_dir(&path) + .arg("--rekey") + .arg(files[0]) + .arg("--identity") + .arg("keys/id_ed25519") + .assert(); + + assert.success().stdout(expected); + + Ok(()) +} + +#[test] +#[cfg_attr(not(feature = "recursive-nix"), ignore)] +fn rekeying_one_multiple_works() -> Result<()> { + let (_dir, path) = copy_example_to_tmpdir()?; + + let files = &["github-runner.token.age", "root.passwd.age"]; + let expected = files + .iter() + .map(|s| path.join(s)) + .map(|p| format!("Rekeying {}", p.display())) + .collect::>() + .join("\n") + + "\n"; + + let mut cmd = Command::cargo_bin(crate_name!())?; + let assert = cmd + .current_dir(&path) + .arg("--rekey") + .arg(files[0]) + .arg(files[1]) + .arg("--identity") + .arg("keys/id_ed25519") + .assert(); + + assert.success().stdout(expected); + + Ok(()) +} + +#[test] +#[cfg_attr(not(feature = "recursive-nix"), ignore)] +fn rekeying_one_fails_not_existing_files() -> Result<()> { + let (_dir, path) = copy_example_to_tmpdir()?; + + let missing_file = path.join("root.passwd.age"); + fs::remove_file(&missing_file)?; + + let mut cmd = Command::cargo_bin(crate_name!())?; + let assert = cmd + .current_dir(&path) + .arg("--rekey") + .arg(&missing_file) + .arg("--identity") + .arg("keys/id_ed25519") + .assert(); + + assert.failure().stderr(predicate::str::contains(format!( + "Does not exist: {}", + missing_file.display() + ))); + + Ok(()) +} + #[test] fn prints_schema() -> Result<()> { let mut cmd = Command::cargo_bin(crate_name!())?;