All checks were successful
Run Check Script / check (pull_request) Successful in 1m9s
167 lines
5.9 KiB
Rust
167 lines
5.9 KiB
Rust
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);
|
||
}
|
||
}
|