Skip to content

Commit

Permalink
fix: added MISE_SOPS_ROPS setting (#3603)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdx authored Dec 16, 2024
1 parent 3eb8fae commit 234975c
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 25 deletions.
4 changes: 4 additions & 0 deletions e2e/secrets/test_secrets
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ mise x -- sops encrypt -i --age "$age_pub" .env.yaml
export MISE_SOPS_AGE_KEY=
assert "mise set _.file=.env.yaml"
assert_contains "mise env" "export SECRET=mysecret"

# sops
mise settings set sops.rops 0
assert_contains "mise env" "export SECRET=mysecret"
5 changes: 5 additions & 0 deletions schema/mise.json
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,11 @@
"age_recipients": {
"description": "The age public keys to use for sops secret encryption.",
"type": "string"
},
"rops": {
"default": true,
"description": "Use rops to decrypt sops files. Disable to shell out to `sops` which will slow down mise but sops may offer features not available in rops.",
"type": "boolean"
}
}
},
Expand Down
6 changes: 6 additions & 0 deletions settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,12 @@ type = "String"
optional = true
description = "The age public keys to use for sops secret encryption."

[sops.rops]
env = "MISE_SOPS_ROPS"
type = "Bool"
default = true
description = "Use rops to decrypt sops files. Disable to shell out to `sops` which will slow down mise but sops may offer features not available in rops."

[status.missing_tools]
env = "MISE_STATUS_MESSAGE_MISSING_TOOLS"
type = "String"
Expand Down
4 changes: 2 additions & 2 deletions src/config/env_directive/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl EnvResults {
if let Ok(raw) = file::read_to_string(p) {
let mut f: Env<serde_json::Value> = serde_json::from_str(&raw).wrap_err_with(errfn)?;
if !f.sops.is_empty() {
let raw = sops::decrypt::<_, JsonFileFormat>(&raw, parse_template)?;
let raw = sops::decrypt::<_, JsonFileFormat>(&raw, parse_template, "json")?;
f = serde_json::from_str(&raw).wrap_err_with(errfn)?;
}
f.env
Expand Down Expand Up @@ -87,7 +87,7 @@ impl EnvResults {
if let Ok(raw) = file::read_to_string(p) {
let mut f: Env<serde_yaml::Value> = serde_yaml::from_str(&raw).wrap_err_with(errfn)?;
if !f.sops.is_empty() {
let raw = sops::decrypt::<_, YamlFileFormat>(&raw, parse_template)?;
let raw = sops::decrypt::<_, YamlFileFormat>(&raw, parse_template, "yaml")?;
f = serde_yaml::from_str(&raw).wrap_err_with(errfn)?;
}
f.env
Expand Down
2 changes: 1 addition & 1 deletion src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ impl Config {
.get_or_try_init(|| ToolRequestSetBuilder::new().build())
}

pub fn get_toolset(&self) -> eyre::Result<&Toolset> {
pub fn get_toolset(&self) -> Result<&Toolset> {
self.toolset.get_or_try_init(|| {
let mut ts = Toolset::from(self.get_tool_request_set()?.clone());
ts.resolve()?;
Expand Down
83 changes: 66 additions & 17 deletions src/sops.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::config::SETTINGS;
use crate::config::{Config, SETTINGS};
use crate::file::replace_path;
use crate::{dirs, file, result};
use eyre::WrapErr;
Expand All @@ -7,14 +7,16 @@ use rops::cryptography::hasher::SHA512;
use rops::file::state::EncryptedFile;
use rops::file::RopsFile;
use std::env;
use std::sync::{Mutex, OnceLock};

pub fn decrypt<PT, F>(input: &str, parse_template: PT) -> result::Result<String>
pub fn decrypt<PT, F>(input: &str, parse_template: PT, format: &str) -> result::Result<String>
where
PT: Fn(String) -> result::Result<String>,
F: rops::file::format::FileFormat,
{
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
static AGE_KEY: OnceLock<Option<String>> = OnceLock::new();
static MUTEX: Mutex<()> = Mutex::new(());
let age = AGE_KEY.get_or_init(|| {
let p = SETTINGS
.sops
.age_key_file
Expand All @@ -24,29 +26,76 @@ where
Ok(p) => p,
Err(e) => {
warn!("failed to parse sops age key file: {}", e);
return;
return None;
}
});
if let Some(age_key) = &SETTINGS.sops.age_key {
if !age_key.is_empty() {
return Some(age_key.clone());
}
}
if p.exists() {
if let Ok(raw) = file::read_to_string(p) {
let key = raw
.trim()
.lines()
.filter(|l| !l.starts_with('#'))
.collect::<String>();
env::set_var("ROPS_AGE", key);
}
}
if let Some(age_key) = &SETTINGS.sops.age_key {
if !age_key.is_empty() {
env::set_var("ROPS_AGE", age_key);
if !key.trim().is_empty() {
return Some(key);
}
}
}
None
});
let f = input
.parse::<RopsFile<EncryptedFile<AES256GCM, SHA512>, F>>()
.wrap_err("failed to parse sops file")?;
Ok(f.decrypt::<F>()
.wrap_err("failed to decrypt sops file")?
.to_string())
let _lock = MUTEX.lock().unwrap(); // prevent multiple threads from using the same age key
let age_env_key = if SETTINGS.sops.rops {
"ROPS_AGE"
} else {
"SOPS_AGE_KEY"
};
let prev_age = env::var(age_env_key).ok();
if let Some(age) = &age {
env::set_var(age_env_key, age.trim());
}
let output = if SETTINGS.sops.rops {
input
.parse::<RopsFile<EncryptedFile<AES256GCM, SHA512>, F>>()
.wrap_err("failed to parse sops file")?
.decrypt::<F>()
.wrap_err("failed to decrypt sops file")?
.to_string()
} else {
let config = Config::get();
let mut ts = config
.get_tool_request_set()
.cloned()
.unwrap_or_default()
.filter_by_tool(["sops".into()].into())
.into_toolset();
ts.resolve()?;
let sops = ts
.which_bin("sops")
.map(|s| s.to_string_lossy().to_string())
.unwrap_or("sops".into());
// TODO: this obviously won't work on windows
cmd!(
sops,
"--input-type",
format,
"--output-type",
format,
"-d",
"/dev/stdin"
)
.stdin_bytes(input.as_bytes())
.read()?
};

if let Some(age) = prev_age {
env::set_var(age_env_key, age);
} else {
env::remove_var(age_env_key);
}
Ok(output)
}
8 changes: 4 additions & 4 deletions src/toolset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,10 +538,10 @@ impl Toolset {
pub fn which(&self, bin_name: &str) -> Option<(Arc<dyn Backend>, ToolVersion)> {
self.list_current_installed_versions()
.into_par_iter()
.find_first(|(p, tv)| {
if let Ok(x) = p.which(tv, bin_name) {
x.is_some()
} else {
.find_first(|(p, tv)| match p.which(tv, bin_name) {
Ok(x) => x.is_some(),
Err(e) => {
debug!("Error running which: {:#}", e);
false
}
})
Expand Down
6 changes: 5 additions & 1 deletion src/toolset/tool_request_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::cli::args::{BackendArg, ToolArg};
use crate::config::{Config, Settings};
use crate::env;
use crate::registry::REGISTRY;
use crate::toolset::{ToolRequest, ToolSource};
use crate::toolset::{ToolRequest, ToolSource, Toolset};

#[derive(Debug, Default, Clone)]
pub struct ToolRequestSet {
Expand Down Expand Up @@ -86,6 +86,10 @@ impl ToolRequestSet {
.map(|(fa, trl, ts)| (fa.clone(), trl.clone(), ts.clone()))
.collect::<ToolRequestSet>()
}

pub fn into_toolset(self) -> Toolset {
self.into()
}
}

impl Display for ToolRequestSet {
Expand Down

0 comments on commit 234975c

Please sign in to comment.