Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: added MISE_SOPS_ROPS setting #3603

Merged
merged 4 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading