Skip to content

Commit 6a2eb48

Browse files
author
Marco Napetti
committed
fix: secret_providers config must be addictive
1 parent 80da1de commit 6a2eb48

3 files changed

Lines changed: 89 additions & 5 deletions

File tree

crates/firma-run/src/config.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,16 @@ impl Merge for ProfilePatch {
512512
allow_non_structural: higher.allow_non_structural.or(self.allow_non_structural),
513513
mask_home_paths: higher.mask_home_paths.or(self.mask_home_paths),
514514
ca_trust_mode: higher.ca_trust_mode.or(self.ca_trust_mode),
515-
secret_providers: higher.secret_providers.or(self.secret_providers),
515+
secret_providers: match (self.secret_providers, higher.secret_providers) {
516+
// Additive across layers (like `env_passthrough`); the later
517+
// (higher) entries come last so they win on name collision in
518+
// `resolve_secret_providers`.
519+
(Some(mut lower), Some(higher)) => {
520+
lower.extend(higher);
521+
Some(lower)
522+
}
523+
(lower, higher) => higher.or(lower),
524+
},
516525
}
517526
}
518527
}

crates/firma-run/src/secret/accept.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,43 @@ mod tests {
9696
(listener, addr)
9797
}
9898

99+
fn echo_spec() -> firma_secret_provider::spec::cli::CliIntegrationSpec<firma_core::SecretMatcher>
100+
{
101+
use firma_secret_provider::{
102+
non_empty::NonEmptyVec,
103+
spec::{MatcherRule, cli::CommandPattern},
104+
};
105+
106+
// Exact `SafeCommand` on the whole argv so `echo` is classified as a
107+
// known-safe passthrough (unconfigured binaries are rejected now).
108+
firma_secret_provider::spec::cli::CliIntegrationSpec::new(
109+
"echo".to_string(),
110+
"echo".to_string(),
111+
vec![],
112+
vec![],
113+
vec![],
114+
vec![MatcherRule::SafeCommand(CommandPattern::exact(
115+
NonEmptyVec::new(vec![
116+
String::from("hello"),
117+
String::from("from"),
118+
String::from("broker"),
119+
])
120+
.expect("non-empty argv"),
121+
))],
122+
)
123+
.unwrap_or_else(|error| panic!("valid spec: {error}"))
124+
}
125+
99126
#[tokio::test]
100127
async fn echo_end_to_end() {
101128
let (listener, addr) = bind_tcp().await;
102129
let store = Arc::new(RwLock::new(SecretStore::new()));
103130

131+
let spec = echo_spec();
104132
let server = tokio::spawn(serve_forever(
105133
listener,
106134
Arc::clone(&store),
107-
Arc::new(|_bin: &str| None),
135+
Arc::new(move |_bin: &str| Some(spec.clone())),
108136
None,
109137
));
110138

crates/firma-run/src/secret/serve.rs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,14 +287,42 @@ mod tests {
287287
RwLock::new(SecretStore::new())
288288
}
289289

290+
fn echo_spec() -> firma_secret_provider::spec::cli::CliIntegrationSpec<SecretMatcher> {
291+
use firma_secret_provider::{
292+
non_empty::NonEmptyVec,
293+
spec::{MatcherRule, cli::CommandPattern},
294+
};
295+
296+
// An exact `SafeCommand` match on the whole argv: passthrough with no
297+
// extraction, the only execution path still reachable for `echo` now
298+
// that unconfigured binaries are rejected.
299+
firma_secret_provider::spec::cli::CliIntegrationSpec::new(
300+
"echo".to_string(),
301+
"echo".to_string(),
302+
vec![],
303+
vec![],
304+
vec![],
305+
vec![MatcherRule::SafeCommand(CommandPattern::exact(
306+
NonEmptyVec::new(vec![
307+
String::from("hello"),
308+
String::from("from"),
309+
String::from("broker"),
310+
])
311+
.expect("non-empty argv"),
312+
))],
313+
)
314+
.unwrap_or_else(|error| panic!("valid spec: {error}"))
315+
}
316+
290317
#[tokio::test]
291-
async fn passthrough_without_spec_returns_raw_stdout() {
318+
async fn passthrough_safe_command_returns_raw_stdout() {
292319
let store = store();
293320
let request = BrokerRequest {
294321
bin: BinaryName::new("echo").expect("valid bin"),
295322
args: vec!["hello".into(), "from".into(), "broker".into()],
296323
};
297-
let response = serve_request(&request, None, &store, None, CAPTURE_LIMIT).await;
324+
let spec = echo_spec();
325+
let response = serve_request(&request, Some(&spec), &store, None, CAPTURE_LIMIT).await;
298326
let decoded = response.decode().expect("decode");
299327
match decoded {
300328
firma_secret_provider::broker::DecodedBrokerResponse::Executed(output) => {
@@ -315,6 +343,24 @@ mod tests {
315343
}
316344
}
317345

346+
#[tokio::test]
347+
async fn unconfigured_binary_is_rejected() {
348+
let store = store();
349+
let request = BrokerRequest {
350+
bin: BinaryName::new("echo").expect("valid bin"),
351+
args: vec!["hello".into()],
352+
};
353+
let response = serve_request(&request, None, &store, None, CAPTURE_LIMIT).await;
354+
assert!(
355+
matches!(
356+
response.decode().expect("decode"),
357+
firma_secret_provider::broker::DecodedBrokerResponse::Rejected(reason)
358+
if reason.contains("unconfigured binary echo")
359+
),
360+
"a binary with no resolved spec must be rejected, not executed"
361+
);
362+
}
363+
318364
#[tokio::test]
319365
async fn blocked_forbidden_option_is_rejected() {
320366
use firma_config_schema::secret_provider::cli::FlagSpec;
@@ -423,8 +469,9 @@ mod tests {
423469
bin: BinaryName::new("echo").expect("valid bin"),
424470
args: vec!["hello".into(), "from".into(), "broker".into()],
425471
};
472+
let spec = echo_spec();
426473
let tiny_limit = 4usize;
427-
let response = serve_request(&request, None, &store, None, tiny_limit).await;
474+
let response = serve_request(&request, Some(&spec), &store, None, tiny_limit).await;
428475
assert!(
429476
matches!(
430477
response.decode().expect("decode"),

0 commit comments

Comments
 (0)