Skip to main content

dataflow_rs/expr/
secrets.rs

1//! Secrets: values an expression may read but the engine never records.
2//!
3//! They live in the engine, not in a message's context, and are reached only
4//! through the reserved `{"secret": "name"}` operator. The operator is
5//! registered on every engine: in templating mode an unregistered name would
6//! echo back as literal data, so `{"secret": "k"}` on a plain engine would be
7//! handed on as an ordinary object.
8
9use super::path::get_dotted;
10use crate::base::{Error, ErrorKind, Result};
11use datalogic_rs::bumpalo::Bump;
12use datalogic_rs::operator::EvalContext;
13use datalogic_rs::{CustomOperator, DataValue, Error as LogicError};
14use datavalue::OwnedDataValue;
15use std::fmt;
16use std::sync::Arc;
17
18/// The reserved operator name. A host cannot register its own under it.
19pub(crate) const SECRET_OPERATOR: &str = "secret";
20
21/// An engine-scoped store of values readable through `{"secret": "name"}`.
22///
23/// Always an object; nested objects let a host namespace
24/// (`{"secret": "partner.hmac"}`). It is neither `Serialize` nor `Clone`, and
25/// its `Debug` prints key names with every value masked.
26pub struct Secrets {
27    root: OwnedDataValue,
28}
29
30impl Default for Secrets {
31    /// A store with nothing in it: the operator still exists, and every
32    /// lookup fails.
33    fn default() -> Self {
34        Self {
35            root: OwnedDataValue::Object(Vec::new()),
36        }
37    }
38}
39
40impl Secrets {
41    /// Wraps a host-supplied value. It must be an object, else
42    /// [`ErrorKind::Config`].
43    pub fn new(root: OwnedDataValue) -> Result<Self> {
44        if !root.is_object() {
45            return Err(Error::new(
46                ErrorKind::Config,
47                "secrets must be a JSON object of name -> value",
48            ));
49        }
50        Ok(Self { root })
51    }
52
53    /// Looks up a dotted path, by a context path's segment rules. The empty
54    /// path is `None`, never the whole store. A lookup allocates nothing.
55    pub fn get(&self, path: &str) -> Option<&OwnedDataValue> {
56        if path.is_empty() {
57            return None;
58        }
59        get_dotted(&self.root, path)
60    }
61
62    /// Top-level key names. Never values.
63    pub fn names(&self) -> impl Iterator<Item = &str> {
64        self.root
65            .as_object()
66            .into_iter()
67            .flatten()
68            .map(|(k, _)| k.as_str())
69    }
70}
71
72impl fmt::Debug for Secrets {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        let mut s = f.debug_struct("Secrets");
75        for name in self.names() {
76            s.field(name, &"******");
77        }
78        s.finish()
79    }
80}
81
82/// The `secret` operator: one string argument, a dotted path into the store.
83///
84/// Error text names the key, never a value and never the store.
85pub(crate) struct SecretOperator(pub(crate) Arc<Secrets>);
86
87impl CustomOperator for SecretOperator {
88    fn evaluate<'a>(
89        &self,
90        args: &[&'a DataValue<'a>],
91        _ctx: &mut EvalContext<'_, 'a>,
92        arena: &'a Bump,
93    ) -> datalogic_rs::Result<&'a DataValue<'a>> {
94        let key = match args {
95            [DataValue::String(s)] if !s.is_empty() => *s,
96            [DataValue::String(_)] => {
97                return Err(LogicError::invalid_arguments(
98                    "secret: the key must not be empty",
99                ));
100            }
101            [_] => {
102                return Err(LogicError::invalid_arguments(
103                    "secret: the key must be a string",
104                ));
105            }
106            _ => {
107                return Err(LogicError::invalid_arguments(format!(
108                    "secret: takes exactly one argument, got {}",
109                    args.len()
110                )));
111            }
112        };
113        match self.0.get(key) {
114            Some(value) => Ok(arena.alloc(value.to_arena(arena))),
115            None => Err(LogicError::variable_not_found(format!(
116                "secret '{key}' is not declared"
117            ))),
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::expr::ExprEngine;
126    use serde_json::json;
127
128    fn store() -> Secrets {
129        Secrets::new(OwnedDataValue::from(&json!({
130            "api_token": "tok-value-9f8e",
131            "partner": { "hmac": "hmac-value-1a2b" }
132        })))
133        .unwrap()
134    }
135
136    #[test]
137    fn debug_prints_names_and_masks_every_value() {
138        assert_eq!(
139            format!("{:?}", store()),
140            r#"Secrets { api_token: "******", partner: "******" }"#
141        );
142    }
143
144    #[test]
145    fn the_empty_path_is_not_the_whole_store() {
146        assert!(store().get("").is_none());
147        assert_eq!(
148            store().get("partner.hmac"),
149            Some(&OwnedDataValue::from(&json!("hmac-value-1a2b")))
150        );
151        assert!(store().get("partner.nope").is_none());
152    }
153
154    #[test]
155    fn only_objects_are_accepted() {
156        let err = Secrets::new(OwnedDataValue::from(&json!(["a"]))).unwrap_err();
157        assert_eq!(err.kind(), &ErrorKind::Config);
158        assert!(Secrets::new(OwnedDataValue::from(&json!("s"))).is_err());
159        assert_eq!(Secrets::default().names().count(), 0);
160    }
161
162    #[test]
163    fn the_operator_reads_a_nested_secret_and_refuses_an_undeclared_one() {
164        let engine = ExprEngine::builder().secrets(store()).build().unwrap();
165        assert_eq!(
166            engine.eval_json(&json!({"secret": "partner.hmac"})),
167            json!("hmac-value-1a2b")
168        );
169        let err = engine
170            .try_eval_json(&json!({"secret": "nope"}))
171            .unwrap_err();
172        assert_eq!(err.tag(), "VariableNotFound");
173        assert!(!err.to_string().contains("hmac-value"));
174        // With no store configured the operator is still live, and refuses.
175        let bare = ExprEngine::default();
176        assert!(bare.try_eval_json(&json!({"secret": "x"})).is_err());
177    }
178}