Skip to main content

isothrum_runner/
secrets.rs

1//! Secret and input values: held by the embedder, handed in per operation, never stored, never
2//! printed. A value reaches only the steps its slot delivers it to, and is masked in every event.
3//! A required slot without a value refuses an operation that needs it.
4
5use std::collections::{BTreeMap, BTreeSet};
6use std::fmt;
7
8use isoloom_manifest::Manifest;
9
10use crate::scope::Scope;
11use crate::{Error, Result};
12
13/// A secret's value. Its `Debug` never shows it.
14#[derive(Clone, PartialEq, Eq)]
15pub struct SecretValue(String);
16
17impl SecretValue {
18    /// Holds `value`.
19    pub fn new(value: impl Into<String>) -> SecretValue {
20        SecretValue(value.into())
21    }
22
23    /// The value itself, for the step it is delivered to.
24    #[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/// The values of a run's secret and input slots, by slot name.
37#[derive(Debug, Clone, Default, PartialEq, Eq)]
38pub struct Secrets(BTreeMap<String, SecretValue>);
39
40impl Secrets {
41    /// No values.
42    #[must_use]
43    pub fn new() -> Secrets {
44        Secrets::default()
45    }
46
47    /// Sets slot `name`'s value.
48    pub fn insert(&mut self, name: impl Into<String>, value: SecretValue) {
49        self.0.insert(name.into(), value);
50    }
51
52    /// Slot `name`'s value, when given.
53    #[must_use]
54    pub fn get(&self, name: &str) -> Option<&SecretValue> {
55        self.0.get(name)
56    }
57
58    /// Every slot given a value, by name.
59    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
82/// Fails when a required secret slot of `scope` has no value.
83pub(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
91/// The values of the sensitive slots given, to mask.
92pub(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}