feat: WIP add secrets module and macro crate
This commit is contained in:
		
							parent
							
								
									67f3a23071
								
							
						
					
					
						commit
						2a6a233fb2
					
				
							
								
								
									
										58
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										58
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -378,7 +378,7 @@ dependencies = [ | |||||||
|  "serde_json", |  "serde_json", | ||||||
|  "serde_repr", |  "serde_repr", | ||||||
|  "serde_urlencoded", |  "serde_urlencoded", | ||||||
|  "thiserror 2.0.12", |  "thiserror 2.0.14", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "tokio-util", |  "tokio-util", | ||||||
|  "tower-service", |  "tower-service", | ||||||
| @ -473,7 +473,7 @@ dependencies = [ | |||||||
|  "semver", |  "semver", | ||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  "thiserror 2.0.12", |  "thiserror 2.0.14", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| @ -1823,12 +1823,23 @@ dependencies = [ | |||||||
|  "temp-dir", |  "temp-dir", | ||||||
|  "temp-file", |  "temp-file", | ||||||
|  "tempfile", |  "tempfile", | ||||||
|  |  "thiserror 2.0.14", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "tokio-util", |  "tokio-util", | ||||||
|  "url", |  "url", | ||||||
|  "uuid", |  "uuid", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "harmony-secrets-derive" | ||||||
|  | version = "0.1.0" | ||||||
|  | dependencies = [ | ||||||
|  |  "proc-macro-crate", | ||||||
|  |  "proc-macro2", | ||||||
|  |  "quote", | ||||||
|  |  "syn", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "harmony_cli" | name = "harmony_cli" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| @ -1962,7 +1973,7 @@ dependencies = [ | |||||||
|  "non-blank-string-rs", |  "non-blank-string-rs", | ||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  "thiserror 2.0.12", |  "thiserror 2.0.14", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| @ -2621,7 +2632,7 @@ dependencies = [ | |||||||
|  "pest_derive", |  "pest_derive", | ||||||
|  "regex", |  "regex", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  "thiserror 2.0.12", |  "thiserror 2.0.14", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| @ -2722,7 +2733,7 @@ dependencies = [ | |||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  "serde_yaml", |  "serde_yaml", | ||||||
|  "thiserror 2.0.12", |  "thiserror 2.0.14", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "tokio-tungstenite", |  "tokio-tungstenite", | ||||||
|  "tokio-util", |  "tokio-util", | ||||||
| @ -2747,7 +2758,7 @@ dependencies = [ | |||||||
|  "serde", |  "serde", | ||||||
|  "serde-value", |  "serde-value", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  "thiserror 2.0.12", |  "thiserror 2.0.14", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| @ -2785,7 +2796,7 @@ dependencies = [ | |||||||
|  "pin-project", |  "pin-project", | ||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  "thiserror 2.0.12", |  "thiserror 2.0.14", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "tokio-util", |  "tokio-util", | ||||||
|  "tracing", |  "tracing", | ||||||
| @ -3366,7 +3377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||||
| checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" | checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "memchr", |  "memchr", | ||||||
|  "thiserror 2.0.12", |  "thiserror 2.0.14", | ||||||
|  "ucd-trie", |  "ucd-trie", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| @ -3587,6 +3598,15 @@ dependencies = [ | |||||||
|  "elliptic-curve", |  "elliptic-curve", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "proc-macro-crate" | ||||||
|  | version = "3.3.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" | ||||||
|  | dependencies = [ | ||||||
|  |  "toml_edit", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "proc-macro2" | name = "proc-macro2" | ||||||
| version = "1.0.95" | version = "1.0.95" | ||||||
| @ -3720,7 +3740,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" | |||||||
| dependencies = [ | dependencies = [ | ||||||
|  "getrandom 0.2.16", |  "getrandom 0.2.16", | ||||||
|  "libredox", |  "libredox", | ||||||
|  "thiserror 2.0.12", |  "thiserror 2.0.14", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| @ -4016,7 +4036,7 @@ dependencies = [ | |||||||
|  "flurry", |  "flurry", | ||||||
|  "log", |  "log", | ||||||
|  "serde", |  "serde", | ||||||
|  "thiserror 2.0.12", |  "thiserror 2.0.14", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "tokio-util", |  "tokio-util", | ||||||
| ] | ] | ||||||
| @ -4579,7 +4599,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" | |||||||
| dependencies = [ | dependencies = [ | ||||||
|  "num-bigint", |  "num-bigint", | ||||||
|  "num-traits", |  "num-traits", | ||||||
|  "thiserror 2.0.12", |  "thiserror 2.0.14", | ||||||
|  "time", |  "time", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| @ -4763,9 +4783,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" | |||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "syn" | name = "syn" | ||||||
| version = "2.0.104" | version = "2.0.105" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" | checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "proc-macro2", |  "proc-macro2", | ||||||
|  "quote", |  "quote", | ||||||
| @ -4899,11 +4919,11 @@ dependencies = [ | |||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "thiserror" | name = "thiserror" | ||||||
| version = "2.0.12" | version = "2.0.14" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" | checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "thiserror-impl 2.0.12", |  "thiserror-impl 2.0.14", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| @ -4919,9 +4939,9 @@ dependencies = [ | |||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "thiserror-impl" | name = "thiserror-impl" | ||||||
| version = "2.0.12" | version = "2.0.14" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" | checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "proc-macro2", |  "proc-macro2", | ||||||
|  "quote", |  "quote", | ||||||
| @ -5251,7 +5271,7 @@ dependencies = [ | |||||||
|  "log", |  "log", | ||||||
|  "rand 0.9.1", |  "rand 0.9.1", | ||||||
|  "sha1", |  "sha1", | ||||||
|  "thiserror 2.0.12", |  "thiserror 2.0.14", | ||||||
|  "utf-8", |  "utf-8", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ members = [ | |||||||
|   "harmony_cli", |   "harmony_cli", | ||||||
|   "k3d", |   "k3d", | ||||||
|   "harmony_composer", |   "harmony_composer", | ||||||
|  |   "harmony_secrets_derive", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [workspace.package] | [workspace.package] | ||||||
|  | |||||||
| @ -67,6 +67,7 @@ bollard.workspace = true | |||||||
| tar.workspace = true | tar.workspace = true | ||||||
| base64.workspace = true | base64.workspace = true | ||||||
| once_cell = "1.21.3" | once_cell = "1.21.3" | ||||||
|  | thiserror = "2.0.14" | ||||||
| 
 | 
 | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| pretty_assertions.workspace = true | pretty_assertions.workspace = true | ||||||
|  | |||||||
							
								
								
									
										
											BIN
										
									
								
								harmony/harmony.rlib
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								harmony/harmony.rlib
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @ -9,3 +9,4 @@ pub mod inventory; | |||||||
| pub mod maestro; | pub mod maestro; | ||||||
| pub mod score; | pub mod score; | ||||||
| pub mod topology; | pub mod topology; | ||||||
|  | pub mod secrets; | ||||||
|  | |||||||
							
								
								
									
										265
									
								
								harmony/src/domain/secrets/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								harmony/src/domain/secrets/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,265 @@ | |||||||
|  | //! # Harmony Secrets Module
 | ||||||
|  | //!
 | ||||||
|  | //! This module provides core abstractions for type-safe secret management within the Harmony framework.
 | ||||||
|  | //!
 | ||||||
|  | //! ## Design Philosophy
 | ||||||
|  | //!
 | ||||||
|  | //! The design is centered around three key components:
 | ||||||
|  | //!
 | ||||||
|  | //! 1.  **The `Secret` Trait:** This is the heart of the module. Instead of using strings to identify
 | ||||||
|  | //!     secrets, we use dedicated, zero-sized types (ZSTs). Each ZST represents a single secret
 | ||||||
|  | //!     and implements the `Secret` trait to provide metadata (like its namespace and key) and
 | ||||||
|  | //!     its associated `Value` type. This enables full compile-time verification.
 | ||||||
|  | //!
 | ||||||
|  | //! 2.  **The `Secrets` Struct:** This is the primary user-facing API. It provides the `get` and `set`
 | ||||||
|  | //!     methods that are generic over any type implementing `Secret`. It's the high-level,
 | ||||||
|  | //!     easy-to-use entry point for all secret operations.
 | ||||||
|  | //!
 | ||||||
|  | //! 3.  **The `SecretStore` Trait:** This is the low-level backend interface. It defines the contract
 | ||||||
|  | //!     for how the `Secrets` struct will interact with an actual storage system (like Infisical,
 | ||||||
|  | //!     a local file, or a database). This decouples the high-level API from the implementation details.
 | ||||||
|  | //!
 | ||||||
|  | //! ## Example Usage
 | ||||||
|  | //!
 | ||||||
|  | //! ```
 | ||||||
|  | //! // In an external crate (e.g., harmony-okd):
 | ||||||
|  | //! use harmony_secrets::{Secret, StoreError};
 | ||||||
|  | //!
 | ||||||
|  | //! // 1. Define a zero-sized struct for each secret.
 | ||||||
|  | //! pub struct KubeadminPassword;
 | ||||||
|  | //!
 | ||||||
|  | //! // 2. Implement the `Secret` trait to provide metadata.
 | ||||||
|  | //! impl Secret for KubeadminPassword {
 | ||||||
|  | //!     // The associated type defines what you get back.
 | ||||||
|  | //!     type Value = String;
 | ||||||
|  | //!
 | ||||||
|  | //!     const NAMESPACE: &'static str = "okd-installation";
 | ||||||
|  | //!     const KEY: &'static str = "kubeadmin-password";
 | ||||||
|  | //! }
 | ||||||
|  | //!
 | ||||||
|  | //! // 3. Use it with the `Secrets` struct.
 | ||||||
|  | //! async fn example(secrets: &harmony_secrets::Secrets) -> Result<(), StoreError> {
 | ||||||
|  | //!     // The API is type-safe. The compiler knows what `Value` to expect.
 | ||||||
|  | //!     secrets.set::<KubeadminPassword>("password123".to_string()).await?;
 | ||||||
|  | //!     let password = secrets.get::<KubeadminPassword>().await?;
 | ||||||
|  | //!     assert_eq!(password, "password123");
 | ||||||
|  | //!     Ok(())
 | ||||||
|  | //! }
 | ||||||
|  | //! ```
 | ||||||
|  | 
 | ||||||
|  | use async_trait::async_trait; | ||||||
|  | use serde::{de::DeserializeOwned, Serialize}; | ||||||
|  | use std::sync::Arc; | ||||||
|  | use thiserror::Error; | ||||||
|  | 
 | ||||||
|  | /// Defines the set of errors that can occur during secret operations.
 | ||||||
|  | /// Using `thiserror` provides a great developer experience for error handling.
 | ||||||
|  | #[derive(Debug, Error)] | ||||||
|  | pub enum StoreError { | ||||||
|  |     #[error("Secret not found in store: namespace='{namespace}', key='{key}'")] | ||||||
|  |     NotFound { namespace: String, key: String }, | ||||||
|  | 
 | ||||||
|  |     #[error("Permission denied for secret: namespace='{namespace}', key='{key}'")] | ||||||
|  |     PermissionDenied { namespace: String, key: String }, | ||||||
|  | 
 | ||||||
|  |     #[error("Failed to deserialize secret value: {0}")] | ||||||
|  |     Deserialization(String), | ||||||
|  | 
 | ||||||
|  |     #[error("Failed to serialize secret value: {0}")] | ||||||
|  |     Serialization(String), | ||||||
|  | 
 | ||||||
|  |     #[error("A backend-specific error occurred: {0}")] | ||||||
|  |     Backend(String), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// A trait that marks a type as representing a single, retrievable secret.
 | ||||||
|  | ///
 | ||||||
|  | /// This trait should be implemented on a unique, zero-sized struct for each secret
 | ||||||
|  | /// your module needs to manage. This pattern ensures that all secret access is
 | ||||||
|  | /// validated at compile time.
 | ||||||
|  | pub trait Secret: 'static + Send + Sync { | ||||||
|  |     /// The data type of the secret's value. This ensures that `get` and `set`
 | ||||||
|  |     /// operations are fully type-safe. The value must be serializable.
 | ||||||
|  |     type Value: Serialize + DeserializeOwned + Send; | ||||||
|  | 
 | ||||||
|  |     /// A logical grouping for secrets, similar to a Kubernetes namespace or a
 | ||||||
|  |     /// directory path. This will be used by the `SecretStore` to organize data.
 | ||||||
|  |     const NAMESPACE: &'static str; | ||||||
|  | 
 | ||||||
|  |     /// The unique key for the secret within its namespace.
 | ||||||
|  |     const KEY: &'static str; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// The low-level storage trait that concrete secret backends must implement.
 | ||||||
|  | ///
 | ||||||
|  | /// This trait operates on raw bytes (`Vec<u8>`), keeping it decoupled from any
 | ||||||
|  | /// specific serialization format. The `Secrets` struct will handle the
 | ||||||
|  | //  serialization/deserialization boundary.
 | ||||||
|  | #[async_trait] | ||||||
|  | pub trait SecretStore: Send + Sync { | ||||||
|  |     /// Retrieves the raw byte value of a secret from the backend.
 | ||||||
|  |     async fn get(&self, namespace: &str, key: &str) -> Result<Vec<u8>, StoreError>; | ||||||
|  | 
 | ||||||
|  |     /// Saves the raw byte value of a secret to the backend.
 | ||||||
|  |     async fn set(&self, namespace: &str, key: &str, value: Vec<u8>) -> Result<(), StoreError>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// The primary, user-facing struct for interacting with secrets.
 | ||||||
|  | ///
 | ||||||
|  | /// It provides a high-level, type-safe API that is decoupled from the
 | ||||||
|  | /// underlying storage mechanism.
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct Secrets { | ||||||
|  |     /// A shared, thread-safe reference to the underlying secret store.
 | ||||||
|  |     store: Arc<dyn SecretStore>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Secrets { | ||||||
|  |     /// Creates a new `Secrets` instance with the given store implementation.
 | ||||||
|  |     pub fn new(store: Arc<dyn SecretStore>) -> Self { | ||||||
|  |         Self { store } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Retrieves a secret from the store in a fully type-safe manner.
 | ||||||
|  |     ///
 | ||||||
|  |     /// The type of the secret to retrieve is specified using a generic parameter `S`,
 | ||||||
|  |     /// which must implement the `Secret` trait. The method returns the `S::Value`
 | ||||||
|  |     /// associated type, ensuring you always get the data type you expect.
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Example
 | ||||||
|  |     /// `let admin_pass = secrets.get::<my_secrets::AdminPassword>().await?;`
 | ||||||
|  |     pub async fn get<S: Secret>(&self) -> Result<S::Value, StoreError> { | ||||||
|  |         let bytes = self | ||||||
|  |             .store | ||||||
|  |             .get(S::NAMESPACE, S::KEY) | ||||||
|  |             .await | ||||||
|  |             .map_err(|e| match e { | ||||||
|  |                 // Preserve the NotFound error for better diagnostics.
 | ||||||
|  |                 StoreError::NotFound { .. } => e, | ||||||
|  |                 _ => StoreError::Backend(e.to_string()), | ||||||
|  |             })?; | ||||||
|  | 
 | ||||||
|  |         // The public API uses JSON for serialization. It's robust and human-readable.
 | ||||||
|  |         serde_json::from_slice(&bytes) | ||||||
|  |             .map_err(|e| StoreError::Deserialization(e.to_string())) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Saves a secret to the store in a fully type-safe manner.
 | ||||||
|  |     ///
 | ||||||
|  |     /// The method is generic over the secret type `S`, and the `value` parameter
 | ||||||
|  |     /// must match the `S::Value` associated type, preventing type mismatch errors
 | ||||||
|  |     /// at compile time.
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Example
 | ||||||
|  |     /// `secrets.set::<my_secrets::AdminPassword>("new-password".to_string()).await?;`
 | ||||||
|  |     pub async fn set<S: Secret>(&self, value: S::Value) -> Result<(), StoreError> { | ||||||
|  |         let bytes = serde_json::to_vec(&value) | ||||||
|  |             .map_err(|e| StoreError::Serialization(e.to_string()))?; | ||||||
|  | 
 | ||||||
|  |         self.store | ||||||
|  |             .set(S::NAMESPACE, S::KEY, bytes) | ||||||
|  |             .await | ||||||
|  |             .map_err(|e| StoreError::Backend(e.to_string())) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use super::*; | ||||||
|  |     use std::collections::HashMap; | ||||||
|  |     use std::sync::Mutex; | ||||||
|  | 
 | ||||||
|  |     // Define a dummy secret for testing purposes.
 | ||||||
|  |     struct TestApiKey; | ||||||
|  |     impl Secret for TestApiKey { | ||||||
|  |         type Value = String; | ||||||
|  |         const NAMESPACE: &'static str = "global-tests"; | ||||||
|  |         const KEY: &'static str = "api-key"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     struct ComplexSecret; | ||||||
|  |     #[derive(Serialize, serde::Deserialize, PartialEq, Debug, Clone)] | ||||||
|  |     struct ComplexValue { | ||||||
|  |         user: String, | ||||||
|  |         permissions: Vec<String>, | ||||||
|  |     } | ||||||
|  |     impl Secret for ComplexSecret { | ||||||
|  |         type Value = ComplexValue; | ||||||
|  |         const NAMESPACE: &'static str = "complex-tests"; | ||||||
|  |         const KEY: &'static str = "user-data"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // A mock implementation of the `SecretStore` that uses an in-memory HashMap.
 | ||||||
|  |     #[derive(Default)] | ||||||
|  |     struct MockStore { | ||||||
|  |         data: Mutex<HashMap<String, Vec<u8>>>, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[async_trait] | ||||||
|  |     impl SecretStore for MockStore { | ||||||
|  |         async fn get(&self, namespace: &str, key: &str) -> Result<Vec<u8>, StoreError> { | ||||||
|  |             let path = format!("{}/{}", namespace, key); | ||||||
|  |             let data = self.data.lock().unwrap(); | ||||||
|  |             data.get(&path) | ||||||
|  |                 .cloned() | ||||||
|  |                 .ok_or_else(|| StoreError::NotFound { | ||||||
|  |                     namespace: namespace.to_string(), | ||||||
|  |                     key: key.to_string(), | ||||||
|  |                 }) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         async fn set(&self, namespace: &str, key: &str, value: Vec<u8>) -> Result<(), StoreError> { | ||||||
|  |             let path = format!("{}/{}", namespace, key); | ||||||
|  |             let mut data = self.data.lock().unwrap(); | ||||||
|  |             data.insert(path, value); | ||||||
|  |             Ok(()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[tokio::test] | ||||||
|  |     async fn test_set_and_get_simple_secret() { | ||||||
|  |         let store = Arc::new(MockStore::default()); | ||||||
|  |         let secrets = Secrets::new(store); | ||||||
|  | 
 | ||||||
|  |         let api_key_value = "secret-key-12345".to_string(); | ||||||
|  |         secrets | ||||||
|  |             .set::<TestApiKey>(api_key_value.clone()) | ||||||
|  |             .await | ||||||
|  |             .unwrap(); | ||||||
|  | 
 | ||||||
|  |         let retrieved_key = secrets.get::<TestApiKey>().await.unwrap(); | ||||||
|  |         assert_eq!(retrieved_key, api_key_value); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[tokio::test] | ||||||
|  |     async fn test_set_and_get_complex_secret() { | ||||||
|  |         let store = Arc::new(MockStore::default()); | ||||||
|  |         let secrets = Secrets::new(store); | ||||||
|  | 
 | ||||||
|  |         let complex_value = ComplexValue { | ||||||
|  |             user: "test-user".to_string(), | ||||||
|  |             permissions: vec!["read".to_string(), "write".to_string()], | ||||||
|  |         }; | ||||||
|  |         secrets | ||||||
|  |             .set::<ComplexSecret>(complex_value.clone()) | ||||||
|  |             .await | ||||||
|  |             .unwrap(); | ||||||
|  | 
 | ||||||
|  |         let retrieved_value = secrets.get::<ComplexSecret>().await.unwrap(); | ||||||
|  |         assert_eq!(retrieved_value, complex_value); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[tokio::test] | ||||||
|  |     async fn test_get_nonexistent_secret() { | ||||||
|  |         let store = Arc::new(MockStore::default()); | ||||||
|  |         let secrets = Secrets::new(store); | ||||||
|  | 
 | ||||||
|  |         let result = secrets.get::<TestApiKey>().await; | ||||||
|  |         assert!(matches!(result, Err(StoreError::NotFound { .. }))); | ||||||
|  | 
 | ||||||
|  |         if let Err(StoreError::NotFound { namespace, key }) = result { | ||||||
|  |             assert_eq!(namespace, TestApiKey::NAMESPACE); | ||||||
|  |             assert_eq!(key, TestApiKey::KEY); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -22,12 +22,18 @@ pub struct OPNSenseFirewall { | |||||||
|     host: LogicalHost, |     host: LogicalHost, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // TODO figure out a design to have a unique identifiere for this firewall
 | ||||||
|  | // I think a project identifier would be good enough, then the secrets module configuration will
 | ||||||
|  | // point to the project's vault and this opnsense modules doesn't need to know anything about it
 | ||||||
|  | const OPNSENSE_CREDENTIALS: &str = "OPNSENSE_CREDENTIALS"; | ||||||
|  | 
 | ||||||
| impl OPNSenseFirewall { | impl OPNSenseFirewall { | ||||||
|     pub fn get_ip(&self) -> IpAddress { |     pub fn get_ip(&self) -> IpAddress { | ||||||
|         self.host.ip |         self.host.ip | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub async fn new(host: LogicalHost, port: Option<u16>, username: &str, password: &str) -> Self { |     pub async fn new(host: LogicalHost, port: Option<u16>, username: &str, password: &str) -> Self { | ||||||
|  |         // let credentials = Secrets::get_by_name(OPNSENSE_CREDENTIALS)
 | ||||||
|         Self { |         Self { | ||||||
|             opnsense_config: Arc::new(RwLock::new( |             opnsense_config: Arc::new(RwLock::new( | ||||||
|                 opnsense_config::Config::from_credentials(host.ip, port, username, password).await, |                 opnsense_config::Config::from_credentials(host.ip, port, username, password).await, | ||||||
|  | |||||||
							
								
								
									
										51
									
								
								harmony/src/modules/tenant/credentials.rs.bak
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								harmony/src/modules/tenant/credentials.rs.bak
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | |||||||
|  | use async_trait::async_trait; | ||||||
|  | use chrono::{DateTime, Utc}; | ||||||
|  | use serde::Serialize; | ||||||
|  | 
 | ||||||
|  | use crate::{interpret::InterpretError, score::Score, topology::Topology}; | ||||||
|  | 
 | ||||||
|  | /// Create and manage Tenant Credentials.
 | ||||||
|  | ///
 | ||||||
|  | /// This is meant to be used by cluster administrators who need to provide their tenant users and
 | ||||||
|  | /// services with credentials to access their resources.
 | ||||||
|  | #[derive(Debug, Clone, Serialize)] | ||||||
|  | pub struct TenantCredentialScore; | ||||||
|  | 
 | ||||||
|  | impl<T: Topology + TenantCredentialManager> Score<T> for TenantCredentialScore { | ||||||
|  |     fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { | ||||||
|  |         todo!() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn name(&self) -> String { | ||||||
|  |         todo!() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[async_trait] | ||||||
|  | pub trait TenantCredentialManager { | ||||||
|  |     async fn create_user(&self) -> Result<TenantCredentialBundle, InterpretError>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | pub struct CredentialMetadata { | ||||||
|  |     pub tenant_id: String, | ||||||
|  |     pub credential_id: String, | ||||||
|  |     pub description: String, | ||||||
|  |     pub created_at: DateTime<Utc>, | ||||||
|  |     pub expires_at: Option<DateTime<Utc>>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | pub enum CredentialData { | ||||||
|  |     /// Used to store login instructions destined to a human. Akin to AWS login instructions email
 | ||||||
|  |     /// upon new console user creation.
 | ||||||
|  |     PlainText(String), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | pub struct TenantCredentialBundle { | ||||||
|  |     _metadata: CredentialMetadata, | ||||||
|  |    _content: CredentialData, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl TenantCredentialBundle {} | ||||||
							
								
								
									
										13
									
								
								harmony_secrets_derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								harmony_secrets_derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | [package] | ||||||
|  | name = "harmony-secrets-derive" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2024" | ||||||
|  | 
 | ||||||
|  | [lib] | ||||||
|  | proc-macro = true | ||||||
|  | 
 | ||||||
|  | [dependencies] | ||||||
|  | quote = "1.0" | ||||||
|  | proc-macro2 = "1.0" | ||||||
|  | proc-macro-crate = "3.3" | ||||||
|  | syn = "2.0" | ||||||
							
								
								
									
										152
									
								
								harmony_secrets_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								harmony_secrets_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,152 @@ | |||||||
|  | use syn::DeriveInput; | ||||||
|  | use syn::parse_macro_input; | ||||||
|  | use proc_macro::TokenStream; | ||||||
|  | use proc_macro_crate::{FoundCrate, crate_name}; | ||||||
|  | use quote::quote; | ||||||
|  | use syn::{ | ||||||
|  |     Ident, LitStr, Meta, Token, Type, | ||||||
|  |     parse::{Parse, ParseStream}, | ||||||
|  |     punctuated::Punctuated, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /// A helper struct to parse the contents of the `#[secret(...)]` attribute.
 | ||||||
|  | /// This makes parsing robust and allows for better error handling.
 | ||||||
|  | struct SecretAttributeArgs { | ||||||
|  |     namespace: LitStr, | ||||||
|  |     key: LitStr, | ||||||
|  |     value_type: Type, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Parse for SecretAttributeArgs { | ||||||
|  |     fn parse(input: ParseStream) -> syn::Result<Self> { | ||||||
|  |         // The attributes are parsed as a comma-separated list of `key = value` pairs.
 | ||||||
|  |         let parsed_args = Punctuated::<Meta, Token![,]>::parse_terminated(input)?; | ||||||
|  | 
 | ||||||
|  |         let mut namespace = None; | ||||||
|  |         let mut key = None; | ||||||
|  |         let mut value_type = None; | ||||||
|  | 
 | ||||||
|  |         for arg in parsed_args { | ||||||
|  |             if let Meta::NameValue(nv) = arg { | ||||||
|  |                 let ident_str = nv.path.get_ident().map(Ident::to_string); | ||||||
|  |                 match ident_str.as_deref() { | ||||||
|  |                     Some("namespace") => { | ||||||
|  |                         if let syn::Expr::Lit(expr_lit) = nv.value { | ||||||
|  |                             if let syn::Lit::Str(lit) = expr_lit.lit { | ||||||
|  |                                 namespace = Some(lit); | ||||||
|  |                                 continue; | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         return Err(syn::Error::new_spanned( | ||||||
|  |                             nv.value, | ||||||
|  |                             "Expected a string literal for `namespace`", | ||||||
|  |                         )); | ||||||
|  |                     } | ||||||
|  |                     Some("key") => { | ||||||
|  |                         if let syn::Expr::Lit(expr_lit) = nv.value { | ||||||
|  |                             if let syn::Lit::Str(lit) = expr_lit.lit { | ||||||
|  |                                 key = Some(lit); | ||||||
|  |                                 continue; | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         return Err(syn::Error::new_spanned( | ||||||
|  |                             nv.value, | ||||||
|  |                             "Expected a string literal for `key`", | ||||||
|  |                         )); | ||||||
|  |                     } | ||||||
|  |                     Some("value_type") => { | ||||||
|  |                         if let syn::Expr::Lit(expr_lit) = nv.value { | ||||||
|  |                             // This is the key improvement: parse the string literal's content as a Type.
 | ||||||
|  |                             if let syn::Lit::Str(lit) = expr_lit.lit { | ||||||
|  |                                 value_type = Some(lit.parse::<Type>()?); | ||||||
|  |                                 continue; | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         // This allows for the improved syntax: `value_type = String`
 | ||||||
|  |                         if let syn::Expr::Path(expr_path) = nv.value { | ||||||
|  |                             value_type = Some(Type::Path(expr_path.into())); | ||||||
|  |                             continue; | ||||||
|  |                         } | ||||||
|  |                         return Err(syn::Error::new_spanned( | ||||||
|  |                             nv.value, | ||||||
|  |                             "Expected a type path (e.g., `String` or `Vec<u8>`) for `value_type`", | ||||||
|  |                         )); | ||||||
|  |                     } | ||||||
|  |                     _ => {} | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             return Err(syn::Error::new_spanned( | ||||||
|  |                 arg, | ||||||
|  |                 "Unsupported attribute key. Must be `namespace`, `key`, or `value_type`.", | ||||||
|  |             )); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(SecretAttributeArgs { | ||||||
|  |             namespace: namespace.ok_or_else(|| { | ||||||
|  |                 syn::Error::new(input.span(), "Missing required attribute `namespace`") | ||||||
|  |             })?, | ||||||
|  |             key: key | ||||||
|  |                 .ok_or_else(|| syn::Error::new(input.span(), "Missing required attribute `key`"))?, | ||||||
|  |             value_type: value_type.ok_or_else(|| { | ||||||
|  |                 syn::Error::new(input.span(), "Missing required attribute `value_type`") | ||||||
|  |             })?, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[proc_macro_derive(Secret, attributes(secret))] | ||||||
|  | pub fn derive_secret(input: TokenStream) -> TokenStream { | ||||||
|  |     let input = parse_macro_input!(input as DeriveInput); | ||||||
|  | 
 | ||||||
|  |     // Ensure this is a unit struct (e.g., `struct MySecret;`)
 | ||||||
|  |     if !matches!(&input.data, syn::Data::Struct(s) if s.fields.is_empty()) { | ||||||
|  |         return syn::Error::new_spanned( | ||||||
|  |             &input.ident, | ||||||
|  |             "#[derive(Secret)] can only be used on unit structs.", | ||||||
|  |         ) | ||||||
|  |         .to_compile_error() | ||||||
|  |         .into(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Find the `#[secret(...)]` attribute.
 | ||||||
|  |     let secret_attr = input | ||||||
|  |         .attrs | ||||||
|  |         .iter() | ||||||
|  |         .find(|attr| attr.path().is_ident("secret")) | ||||||
|  |         .ok_or_else(|| syn::Error::new_spanned(&input.ident, "Missing `#[secret(...)]` attribute.")) | ||||||
|  |         .and_then(|attr| attr.parse_args::<SecretAttributeArgs>()); | ||||||
|  | 
 | ||||||
|  |     let args = match secret_attr { | ||||||
|  |         Ok(args) => args, | ||||||
|  |         Err(e) => return e.to_compile_error().into(), | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // Find the path to the `harmony_secrets` crate to make the macro work anywhere.
 | ||||||
|  |     let secret_crate_path = match crate_name("harmony-secrets") { | ||||||
|  |         Ok(FoundCrate::Itself) => quote!(crate), | ||||||
|  |         Ok(FoundCrate::Name(name)) => { | ||||||
|  |             let ident = Ident::new(&name, proc_macro2::Span::call_site()); | ||||||
|  |             quote!(::#ident) | ||||||
|  |         } | ||||||
|  |         Err(e) => { | ||||||
|  |             return syn::Error::new(proc_macro2::Span::call_site(), e.to_string()) | ||||||
|  |                 .to_compile_error() | ||||||
|  |                 .into(); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let struct_ident = &input.ident; | ||||||
|  |     let namespace = args.namespace; | ||||||
|  |     let key = args.key; | ||||||
|  |     let value_type = args.value_type; | ||||||
|  | 
 | ||||||
|  |     let expanded = quote! { | ||||||
|  |         impl #secret_crate_path::Secret for #struct_ident { | ||||||
|  |             type Value = #value_type; | ||||||
|  |             const NAMESPACE: &'static str = #namespace; | ||||||
|  |             const KEY: &'static str = #key; | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     TokenStream::from(expanded) | ||||||
|  | } | ||||||
							
								
								
									
										100
									
								
								harmony_secrets_derive/src/lib.rsglm45
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								harmony_secrets_derive/src/lib.rsglm45
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | |||||||
|  | use proc_macro::TokenStream; | ||||||
|  | use syn::{parse_macro_input, DeriveInput, Attribute, Meta}; | ||||||
|  | use quote::quote; | ||||||
|  | use proc_macro_crate::crate_name; | ||||||
|  | 
 | ||||||
|  | #[proc_macro_derive(Secret, attributes(secret))] | ||||||
|  | pub fn derive_secret(input: TokenStream) -> TokenStream { | ||||||
|  |     let input = parse_macro_input!(input as DeriveInput); | ||||||
|  |      | ||||||
|  |     // Verify this is a unit struct | ||||||
|  |     if !matches!(&input.data, syn::Data::Struct(data) if data.fields.is_empty()) { | ||||||
|  |         return syn::Error::new_spanned( | ||||||
|  |             input.ident, | ||||||
|  |             "#[derive(Secret)] only supports unit structs (e.g., `struct MySecret;`)", | ||||||
|  |         ) | ||||||
|  |         .to_compile_error() | ||||||
|  |         .into(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Parse the #[secret(...)] attribute | ||||||
|  |     let (namespace, key, value_type) = match parse_secret_attributes(&input.attrs) { | ||||||
|  |         Ok(attrs) => attrs, | ||||||
|  |         Err(e) => return e.into_compile_error().into(), | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // Get the path to the harmony_secrets crate | ||||||
|  |     let secret_crate_path = match crate_name("harmony-secrets") { | ||||||
|  |         Ok(proc_macro_crate::FoundCrate::Itself) => quote!(crate), | ||||||
|  |         Ok(proc_macro_crate::FoundCrate::Name(name)) => { | ||||||
|  |             let ident = quote::format_ident!("{}", name); | ||||||
|  |             quote!(::#ident) | ||||||
|  |         } | ||||||
|  |         Err(_) => { | ||||||
|  |             return syn::Error::new_spanned( | ||||||
|  |                 &input.ident, | ||||||
|  |                 "harmony-secrets crate not found in dependencies", | ||||||
|  |             ) | ||||||
|  |             .to_compile_error() | ||||||
|  |             .into(); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let struct_ident = input.ident; | ||||||
|  | 
 | ||||||
|  |     TokenStream::from(quote! { | ||||||
|  |         impl #secret_crate_path::Secret for #struct_ident { | ||||||
|  |             type Value = #value_type; | ||||||
|  |             const NAMESPACE: &'static str = #namespace; | ||||||
|  |             const KEY: &'static str = #key; | ||||||
|  |         } | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn parse_secret_attributes(attrs: &[Attribute]) -> syn::Result<(String, String, syn::Type)> { | ||||||
|  |     let secret_attr = attrs | ||||||
|  |         .iter() | ||||||
|  |         .find(|attr| attr.path().is_ident("secret")) | ||||||
|  |         .ok_or_else(|| { | ||||||
|  |             syn::Error::new_spanned( | ||||||
|  |                 attrs.first().unwrap_or_else(|| &attrs[0]), | ||||||
|  |                 "missing #[secret(...)] attribute", | ||||||
|  |             ) | ||||||
|  |         })?; | ||||||
|  | 
 | ||||||
|  |     let mut namespace = None; | ||||||
|  |     let mut key = None; | ||||||
|  |     let mut value_type = None; | ||||||
|  | 
 | ||||||
|  |     if let Meta::List(meta_list) = &secret_attr.parse_meta()? { | ||||||
|  |         for nested in &meta_list.nested { | ||||||
|  |             if let syn::NestedMeta::Meta(Meta::NameValue(nv)) = nested { | ||||||
|  |                 if nv.path.is_ident("namespace") { | ||||||
|  |                     if let syn::Lit::Str(lit) = &nv.lit { | ||||||
|  |                         namespace = Some(lit.value()); | ||||||
|  |                     } | ||||||
|  |                 } else if nv.path.is_ident("key") { | ||||||
|  |                     if let syn::Lit::Str(lit) = &nv.lit { | ||||||
|  |                         key = Some(lit.value()); | ||||||
|  |                     } | ||||||
|  |                 } else if nv.path.is_ident("value_type") { | ||||||
|  |                     if let syn::Lit::Str(lit) = &nv.lit { | ||||||
|  |                         value_type = Some(syn::parse_str::<syn::Type>(&lit.value())?); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Ok(( | ||||||
|  |         namespace.ok_or_else(|| { | ||||||
|  |             syn::Error::new_spanned(secret_attr, "missing `namespace` in #[secret(...)]") | ||||||
|  |         })?, | ||||||
|  |         key.ok_or_else(|| { | ||||||
|  |             syn::Error::new_spanned(secret_attr, "missing `key` in #[secret(...)]") | ||||||
|  |         })?, | ||||||
|  |         value_type.ok_or_else(|| { | ||||||
|  |             syn::Error::new_spanned(secret_attr, "missing `value_type` in #[secret(...)]") | ||||||
|  |         })?, | ||||||
|  |     )) | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user