use std::fmt;
use serde_json::{Map, Value};
use crate::config::SecretsConfig;
use crate::errors::OrionError;
pub struct ResolvedSecrets {
root: Value,
}
impl ResolvedSecrets {
pub fn empty() -> Self {
Self {
root: Value::Object(Map::new()),
}
}
pub async fn resolve(declared: &SecretsConfig) -> Result<Self, OrionError> {
let resolvers = crate::connector::secrets::default_resolvers();
let mut root = Map::with_capacity(declared.0.len());
for (name, reference) in declared.iter() {
let mut value = Value::String(reference.clone());
crate::connector::secrets::resolve_in_place(
&mut value,
&resolvers,
&format!("secrets.{name}"),
)
.await?;
root.insert(name.clone(), value);
}
Ok(Self {
root: Value::Object(root),
})
}
pub fn from_values(values: Map<String, Value>) -> Self {
Self {
root: Value::Object(values),
}
}
pub fn is_empty(&self) -> bool {
self.names().next().is_none()
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.root
.as_object()
.into_iter()
.flatten()
.map(|(name, _)| name.as_str())
}
pub(crate) fn as_json(&self) -> &Value {
&self.root
}
}
impl fmt::Debug for ResolvedSecrets {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("ResolvedSecrets");
for name in self.names() {
s.field(name, &"******");
}
s.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn store() -> ResolvedSecrets {
let mut map = Map::new();
map.insert("partner_hmac".into(), json!("hmac-value-1a2b"));
ResolvedSecrets::from_values(map)
}
#[test]
fn debug_names_the_keys_and_masks_every_value() {
let rendered = format!("{:?}", store());
assert!(rendered.contains("partner_hmac"), "{rendered}");
assert!(!rendered.contains("hmac-value-1a2b"), "{rendered}");
}
#[test]
fn an_empty_store_declares_nothing() {
assert!(ResolvedSecrets::empty().is_empty());
assert_eq!(ResolvedSecrets::empty().names().count(), 0);
}
#[tokio::test]
async fn an_unset_reference_fails_rather_than_resolving_to_its_own_text() {
let declared = SecretsConfig(
[(
"partner_hmac".to_string(),
"env://ORION_SECRET_DEFINITELY_NOT_SET".to_string(),
)]
.into_iter()
.collect(),
);
let err = ResolvedSecrets::resolve(&declared)
.await
.expect_err("an unset variable must not boot");
let text = err.to_string();
assert!(text.contains("secrets.partner_hmac"), "{text}");
}
#[tokio::test]
async fn nothing_declared_resolves_to_an_empty_store() {
let store = ResolvedSecrets::resolve(&SecretsConfig::default())
.await
.expect("an empty section resolves");
assert!(store.is_empty());
}
}