dataflow_rs/expr/
secrets.rs1use 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
18pub(crate) const SECRET_OPERATOR: &str = "secret";
20
21pub struct Secrets {
27 root: OwnedDataValue,
28}
29
30impl Default for Secrets {
31 fn default() -> Self {
34 Self {
35 root: OwnedDataValue::Object(Vec::new()),
36 }
37 }
38}
39
40impl Secrets {
41 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 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 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
82pub(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 let bare = ExprEngine::default();
176 assert!(bare.try_eval_json(&json!({"secret": "x"})).is_err());
177 }
178}