pub mod config; mod store; use crate::config::SECRET_NAMESPACE; use async_trait::async_trait; use config::INFISICAL_CLIENT_ID; use config::INFISICAL_CLIENT_SECRET; use config::INFISICAL_ENVIRONMENT; use config::INFISICAL_PROJECT_ID; use config::INFISICAL_URL; use config::SECRET_STORE; use log::debug; use serde::{Serialize, de::DeserializeOwned}; use std::fmt; use store::InfisicalSecretStore; use store::LocalFileSecretStore; use thiserror::Error; use tokio::sync::OnceCell; pub use harmony_secret_derive::Secret; // The Secret trait remains the same. pub trait Secret: Serialize + DeserializeOwned + Sized { const KEY: &'static str; } // The error enum remains the same. #[derive(Debug, Error)] pub enum SecretStoreError { #[error("Secret not found for key '{key}' in namespace '{namespace}'")] NotFound { namespace: String, key: String }, #[error("Failed to deserialize secret for key '{key}': {source}")] Deserialization { key: String, source: serde_json::Error, }, #[error("Failed to serialize secret for key '{key}': {source}")] Serialization { key: String, source: serde_json::Error, }, #[error("Underlying storage error: {0}")] Store(#[from] Box), } // The trait is now async! #[async_trait] pub trait SecretStore: fmt::Debug + Send + Sync { async fn get_raw(&self, namespace: &str, key: &str) -> Result, SecretStoreError>; async fn set_raw( &self, namespace: &str, key: &str, value: &[u8], ) -> Result<(), SecretStoreError>; } // Use OnceCell for async-friendly, one-time initialization. static SECRET_MANAGER: OnceCell = OnceCell::const_new(); /// Initializes and returns a reference to the global SecretManager. async fn get_secret_manager() -> &'static SecretManager { SECRET_MANAGER.get_or_init(init_secret_manager).await } /// The async initialization function for the SecretManager. async fn init_secret_manager() -> SecretManager { let default_secret_score = "infisical".to_string(); let store_type = SECRET_STORE.as_ref().unwrap_or(&default_secret_score); let store: Box = match store_type.as_str() { "file" => Box::new(LocalFileSecretStore::default()), "infisical" | _ => { let store = InfisicalSecretStore::new( INFISICAL_URL.clone().expect("Infisical url must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_URL"), INFISICAL_PROJECT_ID.clone().expect("Infisical project id must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_PROJECT_ID"), INFISICAL_ENVIRONMENT.clone().expect("Infisical environment must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_ENVIRONMENT"), INFISICAL_CLIENT_ID.clone().expect("Infisical client id must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_CLIENT_ID"), INFISICAL_CLIENT_SECRET.clone().expect("Infisical client secret must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_CLIENT_SECRET"), ) .await .expect("Failed to initialize Infisical secret store"); Box::new(store) } }; SecretManager::new(SECRET_NAMESPACE.clone(), store) } /// Manages the lifecycle of secrets, providing a simple static API. #[derive(Debug)] pub struct SecretManager { namespace: String, store: Box, } impl SecretManager { fn new(namespace: String, store: Box) -> Self { Self { namespace, store } } /// Retrieves and deserializes a secret. pub async fn get() -> Result { let manager = get_secret_manager().await; debug!("Getting secret ns {} key {}", &manager.namespace, T::KEY); let raw_value = manager.store.get_raw(&manager.namespace, T::KEY).await?; serde_json::from_slice(&raw_value).map_err(|e| SecretStoreError::Deserialization { key: T::KEY.to_string(), source: e, }) } pub async fn get_or_prompt() -> Result { let secret = Self::get::().await; let manager = get_secret_manager().await; let prompted = secret.is_err(); let secret = secret.or_else(|e| -> Result { debug!("Could not get secret : {e}"); let ns = &manager.namespace; let key = T::KEY; let secret_json = inquire::Text::new(&format!( "Secret not found for {} {}, paste the JSON here :", ns, key )) .prompt() .map_err(|e| { SecretStoreError::Store(format!("Failed to prompt secret {ns} {key} : {e}").into()) })?; let secret: T = serde_json::from_str(&secret_json).map_err(|e| { SecretStoreError::Deserialization { key: T::KEY.to_string(), source: e, } })?; Ok(secret) })?; if prompted { Self::set(&secret).await?; } Ok(secret) } /// Serializes and stores a secret. pub async fn set(secret: &T) -> Result<(), SecretStoreError> { let manager = get_secret_manager().await; let raw_value = serde_json::to_vec(secret).map_err(|e| SecretStoreError::Serialization { key: T::KEY.to_string(), source: e, })?; manager .store .set_raw(&manager.namespace, T::KEY, &raw_value) .await } } #[cfg(test)] mod test { use super::*; #[cfg(secrete2etest)] use pretty_assertions::assert_eq; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, PartialEq)] struct TestUserMeta { labels: Vec, } #[derive(Secret, Serialize, Deserialize, Debug, PartialEq)] struct TestSecret { user: String, password: String, metadata: TestUserMeta, } #[cfg(secrete2etest)] #[tokio::test] async fn set_and_retrieve_secret() { let secret = TestSecret { user: String::from("user"), password: String::from("password"), metadata: TestUserMeta { labels: vec![ String::from("label1"), String::from("label2"), String::from( "some longet label with \" special @#%$)(udiojcia[]]] \"'asdij'' characters Nдs はにほへとちり าฟันพัฒนา yağız şoföre ç � � � � � � � � � � � � � 👩‍👩‍👧‍👦 /span> 👩‍👧‍👦 and why not emojis ", ), ], }, }; SecretManager::set(&secret).await.unwrap(); let value = SecretManager::get::().await.unwrap(); assert_eq!(value, secret); } }