isothrum_runner/
secrets.rs1use std::collections::{BTreeMap, BTreeSet};
6use std::fmt;
7
8use isoloom_manifest::Manifest;
9
10use crate::scope::Scope;
11use crate::{Error, Result};
12
13#[derive(Clone, PartialEq, Eq)]
15pub struct SecretValue(String);
16
17impl SecretValue {
18 pub fn new(value: impl Into<String>) -> SecretValue {
20 SecretValue(value.into())
21 }
22
23 #[must_use]
25 pub fn expose(&self) -> &str {
26 &self.0
27 }
28}
29
30impl fmt::Debug for SecretValue {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 f.write_str("SecretValue(<redacted>)")
33 }
34}
35
36#[derive(Debug, Clone, Default, PartialEq, Eq)]
38pub struct Secrets(BTreeMap<String, SecretValue>);
39
40impl Secrets {
41 #[must_use]
43 pub fn new() -> Secrets {
44 Secrets::default()
45 }
46
47 pub fn insert(&mut self, name: impl Into<String>, value: SecretValue) {
49 self.0.insert(name.into(), value);
50 }
51
52 #[must_use]
54 pub fn get(&self, name: &str) -> Option<&SecretValue> {
55 self.0.get(name)
56 }
57
58 pub fn iter(&self) -> impl Iterator<Item = (&str, &SecretValue)> {
60 self.0.iter().map(|(k, v)| (k.as_str(), v))
61 }
62}
63
64impl From<String> for SecretValue {
65 fn from(value: String) -> SecretValue {
66 SecretValue(value)
67 }
68}
69
70impl From<&str> for SecretValue {
71 fn from(value: &str) -> SecretValue {
72 SecretValue(value.to_owned())
73 }
74}
75
76impl<K: Into<String>, V: Into<SecretValue>> FromIterator<(K, V)> for Secrets {
77 fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Secrets {
78 Secrets(iter.into_iter().map(|(k, v)| (k.into(), v.into())).collect())
79 }
80}
81
82pub(crate) fn check_secrets(manifest: &Manifest, secrets: &Secrets, scope: &Scope) -> Result<()> {
84 let missing = |s: &&isoloom_manifest::SecretSlot| s.required && scope.admits(&BTreeSet::new(), &s.providers) && secrets.get(s.name.as_str()).is_none();
85 match manifest.secrets.iter().find(missing) {
86 Some(slot) => Err(Error::SecretMissing { name: slot.name.to_string() }),
87 None => Ok(()),
88 }
89}
90
91pub(crate) fn sensitive_values(manifest: &Manifest, secrets: &Secrets) -> Vec<String> {
93 manifest
94 .secrets
95 .iter()
96 .filter(|s| s.sensitive)
97 .filter_map(|s| secrets.get(s.name.as_str()))
98 .map(|v| v.expose().to_owned())
99 .collect()
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::testing::docker_manifest;
106
107 #[test]
108 fn a_secret_never_prints() {
109 let mut s = Secrets::new();
110 s.insert("CTF_FLAG_MAIN", SecretValue::new("flag{hunter2}"));
111 let shown = format!("{s:?}");
112 assert!(!shown.contains("hunter2") && shown.contains("<redacted>"), "{shown}");
113 assert_eq!(s.get("CTF_FLAG_MAIN").map(SecretValue::expose), Some("flag{hunter2}"));
114 }
115
116 fn scope() -> Scope {
117 Scope {
118 host: isoloom_manifest::HostOs::Linux,
119 provider: None,
120 }
121 }
122
123 #[test]
124 fn a_required_secret_must_be_given_and_sensitive_ones_are_masked() {
125 let mut m = docker_manifest();
126 m.secrets[0].required = true;
127 assert_eq!(
128 check_secrets(&m, &Secrets::new(), &scope()).unwrap_err().code(),
129 crate::ErrorCode::SecretMissing
130 );
131 let mut s = Secrets::new();
132 s.insert("CTF_FLAG_MAIN", SecretValue::new("flag{x}"));
133 check_secrets(&m, &s, &scope()).unwrap();
134 assert_eq!(sensitive_values(&m, &s), ["flag{x}"]);
135 }
136}