forked from NationTech/harmony
feat: Secret module works with infisical and local file storage backends
This commit is contained in:
166
harmony_secret/src/lib.rs
Normal file
166
harmony_secret/src/lib.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
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 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<dyn std::error::Error + Send + Sync>),
|
||||
}
|
||||
|
||||
// The trait is now async!
|
||||
#[async_trait]
|
||||
pub trait SecretStore: fmt::Debug + Send + Sync {
|
||||
async fn get_raw(&self, namespace: &str, key: &str) -> Result<Vec<u8>, 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<SecretManager> = 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<dyn SecretStore> = 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<dyn SecretStore>,
|
||||
}
|
||||
|
||||
impl SecretManager {
|
||||
fn new(namespace: String, store: Box<dyn SecretStore>) -> Self {
|
||||
Self { namespace, store }
|
||||
}
|
||||
|
||||
/// Retrieves and deserializes a secret.
|
||||
pub async fn get<T: Secret>() -> Result<T, SecretStoreError> {
|
||||
let manager = get_secret_manager().await;
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
/// Serializes and stores a secret.
|
||||
pub async fn set<T: Secret>(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::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
struct TestUserMeta {
|
||||
labels: Vec<String>,
|
||||
}
|
||||
|
||||
#[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 ç <20> <20> <20> <20> <20> <20> <20> <20> <20> <20> <20> <20> <20> 👩👩👧👦 /span> 👩👧👦 and why not emojis ",
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
SecretManager::set(&secret).await.unwrap();
|
||||
let value = SecretManager::get::<TestSecret>().await.unwrap();
|
||||
|
||||
assert_eq!(value, secret);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user