forked from NationTech/harmony
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_repr",
|
||||
"serde_urlencoded",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.14",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
@ -473,7 +473,7 @@ dependencies = [
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1823,12 +1823,23 @@ dependencies = [
|
||||
"temp-dir",
|
||||
"temp-file",
|
||||
"tempfile",
|
||||
"thiserror 2.0.14",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "harmony-secrets-derive"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "harmony_cli"
|
||||
version = "0.1.0"
|
||||
@ -1962,7 +1973,7 @@ dependencies = [
|
||||
"non-blank-string-rs",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2621,7 +2632,7 @@ dependencies = [
|
||||
"pest_derive",
|
||||
"regex",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2722,7 +2733,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.14",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-util",
|
||||
@ -2747,7 +2758,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde-value",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2785,7 +2796,7 @@ dependencies = [
|
||||
"pin-project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.14",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
@ -3366,7 +3377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.14",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
@ -3587,6 +3598,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
@ -3720,7 +3740,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4016,7 +4036,7 @@ dependencies = [
|
||||
"flurry",
|
||||
"log",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.14",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
@ -4579,7 +4599,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.14",
|
||||
"time",
|
||||
]
|
||||
|
||||
@ -4763,9 +4783,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.104"
|
||||
version = "2.0.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||
checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -4899,11 +4919,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
version = "2.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.12",
|
||||
"thiserror-impl 2.0.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4919,9 +4939,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
version = "2.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -5251,7 +5271,7 @@ dependencies = [
|
||||
"log",
|
||||
"rand 0.9.1",
|
||||
"sha1",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.14",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ members = [
|
||||
"harmony_cli",
|
||||
"k3d",
|
||||
"harmony_composer",
|
||||
"harmony_secrets_derive",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
||||
@ -67,6 +67,7 @@ bollard.workspace = true
|
||||
tar.workspace = true
|
||||
base64.workspace = true
|
||||
once_cell = "1.21.3"
|
||||
thiserror = "2.0.14"
|
||||
|
||||
[dev-dependencies]
|
||||
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 score;
|
||||
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,
|
||||
}
|
||||
|
||||
// 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 {
|
||||
pub fn get_ip(&self) -> IpAddress {
|
||||
self.host.ip
|
||||
}
|
||||
|
||||
pub async fn new(host: LogicalHost, port: Option<u16>, username: &str, password: &str) -> Self {
|
||||
// let credentials = Secrets::get_by_name(OPNSENSE_CREDENTIALS)
|
||||
Self {
|
||||
opnsense_config: Arc::new(RwLock::new(
|
||||
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