use super::package::ExtensionManifest;
use crate::connectors::ConnectionStore;
use anyhow::Result;
use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Clone, Default, PartialEq, Eq)]
pub struct Secret(String);
impl Secret {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn expose(&self) -> &str {
&self.0
}
}
impl std::fmt::Debug for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Secret(***)")
}
}
#[derive(Clone)]
pub struct ExtensionSecrets {
store: Arc<ConnectionStore>,
}
impl ExtensionSecrets {
pub fn new(store: Arc<ConnectionStore>) -> Self {
Self { store }
}
fn key(ext: &str) -> String {
format!("ext:{ext}")
}
pub fn get(&self, ext: &str, field: &str) -> Option<Secret> {
self.store
.get(&Self::key(ext))
.and_then(|conn| conn.fields.get(field).cloned())
.filter(|value| !value.trim().is_empty())
.map(Secret)
}
pub fn is_set(&self, ext: &str, field: &str) -> bool {
self.get(ext, field).is_some()
}
pub fn set(&self, ext: &str, field: &str, value: Secret) -> Result<()> {
let mut conn = self.store.get(&Self::key(ext)).unwrap_or_default();
conn.fields.insert(field.to_string(), value.0);
self.store.save(&Self::key(ext), conn)
}
pub fn clear(&self, ext: &str) -> Result<bool> {
self.store.clear(&Self::key(ext))
}
pub fn env_overrides(&self, manifest: &ExtensionManifest) -> BTreeMap<String, String> {
let mut env = BTreeMap::new();
for field in manifest.secret_fields() {
if let (Some(name), Some(secret)) =
(field.env.as_ref(), self.get(&manifest.name, &field.name))
{
env.insert(name.clone(), secret.expose().to_string());
}
}
env
}
#[cfg(test)]
pub fn open_at(path: std::path::PathBuf) -> Self {
Self::new(Arc::new(ConnectionStore::open(path)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn manifest_with_secret() -> ExtensionManifest {
crate::extensions::package::parse_manifest(
&json!({
"name": "logfire", "description": "t",
"yolop": {
"protocol_version": "1.0",
"capabilityServer": { "command": "x" },
"trace": true,
"config_schema": {
"type": "object",
"required": ["token"],
"properties": {
"token": { "type": "string", "secret": true, "env": "LOGFIRE_TOKEN" },
"region": { "type": "string", "default": "us" }
}
}
}
})
.to_string(),
)
.unwrap()
}
#[test]
fn secret_debug_is_redacted() {
let s = Secret::new("pylf_v1_supersecret");
assert_eq!(format!("{s:?}"), "Secret(***)");
assert!(!format!("{s:?}").contains("supersecret"));
assert_eq!(s.expose(), "pylf_v1_supersecret");
}
#[test]
fn set_get_and_env_overrides_roundtrip_via_disk() {
let tmp = tempfile::tempdir().unwrap();
let secrets = ExtensionSecrets::open_at(tmp.path().join("connections.toml"));
assert!(!secrets.is_set("logfire", "token"));
secrets
.set("logfire", "token", Secret::new("pylf_v1_abc"))
.unwrap();
assert!(secrets.is_set("logfire", "token"));
assert_eq!(
secrets.get("logfire", "token").unwrap().expose(),
"pylf_v1_abc"
);
let env = secrets.env_overrides(&manifest_with_secret());
assert_eq!(
env.get("LOGFIRE_TOKEN").map(String::as_str),
Some("pylf_v1_abc")
);
assert!(!env.contains_key("region"));
}
#[test]
fn config_fields_parse_secret_env_and_required() {
let manifest = manifest_with_secret();
let token = manifest
.config_fields()
.into_iter()
.find(|f| f.name == "token")
.unwrap();
assert!(token.secret);
assert!(token.required);
assert_eq!(token.env.as_deref(), Some("LOGFIRE_TOKEN"));
assert_eq!(manifest.secret_fields().len(), 1);
}
}