Skip to main content

fraiseql_server/secrets_manager/
types.rs

1// Phase 12.1 Cycle 1: Secrets Manager Types
2//! Core types for secrets management
3
4use std::fmt;
5
6use chrono::{DateTime, Utc};
7
8use super::SecretsError;
9
10/// Trait for different secrets backends
11///
12/// Implementations: Vault, Environment Variables, File-based
13#[async_trait::async_trait]
14pub trait SecretsBackend: Send + Sync {
15    /// Get secret by name
16    ///
17    /// # Arguments
18    /// * `name` - Secret identifier (path, env var name, etc.)
19    ///
20    /// # Returns
21    /// Secret value as String, or SecretsError if not found/error
22    async fn get_secret(&self, name: &str) -> Result<String, SecretsError>;
23
24    /// Get secret with expiry information
25    ///
26    /// Useful for dynamic credentials from Vault with lease durations
27    ///
28    /// # Returns
29    /// Tuple of (secret_value, expiry_datetime)
30    async fn get_secret_with_expiry(
31        &self,
32        name: &str,
33    ) -> Result<(String, DateTime<Utc>), SecretsError>;
34
35    /// Rotate secret to new value
36    ///
37    /// For backends supporting rotation (Vault), generates new credential
38    /// For static backends (env, file), may be no-op or return error
39    async fn rotate_secret(&self, name: &str) -> Result<String, SecretsError>;
40}
41
42/// Wrapper for secrets that redacts values in logs/debug output
43///
44/// Prevents accidental secret exposure through string formatting
45///
46/// # Example
47/// ```ignore
48/// let secret = Secret::new("password123".to_string());
49/// println!("{:?}", secret);  // Prints: Secret(***)
50/// let actual = secret.expose();  // Returns: "password123"
51/// ```
52#[derive(Clone)]
53pub struct Secret(String);
54
55impl Secret {
56    /// Create new Secret wrapper
57    #[must_use]
58    pub fn new(value: String) -> Self {
59        Secret(value)
60    }
61
62    /// Expose the actual secret value
63    ///
64    /// Should only be called when actually using the secret
65    /// Not called in logging or debugging code
66    #[must_use]
67    pub fn expose(&self) -> &str {
68        &self.0
69    }
70
71    /// Convert to owned String (consumes Secret)
72    #[must_use]
73    pub fn into_exposed(self) -> String {
74        self.0
75    }
76
77    /// Check if secret is empty
78    #[must_use]
79    pub fn is_empty(&self) -> bool {
80        self.0.is_empty()
81    }
82
83    /// Get length of secret
84    #[must_use]
85    pub fn len(&self) -> usize {
86        self.0.len()
87    }
88}
89
90/// Debug output redacts actual secret value
91impl fmt::Debug for Secret {
92    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
93        write!(f, "Secret(***)")
94    }
95}
96
97/// Display output redacts actual secret value
98impl fmt::Display for Secret {
99    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
100        write!(f, "***")
101    }
102}
103
104/// Partial equality that compares actual values (for testing)
105impl PartialEq for Secret {
106    fn eq(&self, other: &Self) -> bool {
107        self.0 == other.0
108    }
109}
110
111impl Eq for Secret {}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    /// Test Secret wrapper redacts in Debug output
118    #[test]
119    fn test_secret_debug_redaction() {
120        let secret = Secret::new("my_secret_password".to_string());
121        let debug_str = format!("{:?}", secret);
122
123        assert!(debug_str.contains("***"), "Debug should redact secret");
124        assert!(
125            !debug_str.contains("my_secret_password"),
126            "Debug should not contain actual value"
127        );
128        assert_eq!(debug_str, "Secret(***)");
129    }
130
131    /// Test Secret wrapper redacts in Display output
132    #[test]
133    fn test_secret_display_redaction() {
134        let secret = Secret::new("api_key_12345".to_string());
135        let display_str = format!("{}", secret);
136
137        assert_eq!(display_str, "***", "Display should only show ***");
138    }
139
140    /// Test Secret.expose() returns actual value
141    #[test]
142    fn test_secret_expose() {
143        let value = "actual_secret_value".to_string();
144        let secret = Secret::new(value.clone());
145
146        assert_eq!(secret.expose(), &value);
147    }
148
149    /// Test Secret.into_exposed() consumes and returns value
150    #[test]
151    fn test_secret_into_exposed() {
152        let value = "test_secret".to_string();
153        let secret = Secret::new(value.clone());
154
155        let exposed = secret.into_exposed();
156        assert_eq!(exposed, value);
157    }
158
159    /// Test Secret equality based on actual value
160    #[test]
161    fn test_secret_equality() {
162        let secret1 = Secret::new("same_value".to_string());
163        let secret2 = Secret::new("same_value".to_string());
164        let secret3 = Secret::new("different_value".to_string());
165
166        assert_eq!(secret1, secret2, "Secrets with same value should be equal");
167        assert_ne!(secret1, secret3, "Secrets with different values should not be equal");
168    }
169
170    /// Test Secret length and is_empty
171    #[test]
172    fn test_secret_properties() {
173        let secret = Secret::new("test".to_string());
174        assert_eq!(secret.len(), 4);
175        assert!(!secret.is_empty());
176
177        let empty = Secret::new(String::new());
178        assert_eq!(empty.len(), 0);
179        assert!(empty.is_empty());
180    }
181
182    /// Test SecretsBackend trait requirements
183    #[test]
184    fn test_secrets_backend_trait_definition() {
185        // Trait should require:
186        // 1. Send + Sync for thread safety
187        // 2. get_secret(&self, name: &str) -> Future<Result<String>>
188        // 3. get_secret_with_expiry(&self, name: &str) -> Future<Result<(String, DateTime<Utc>)>>
189        // 4. rotate_secret(&self, name: &str) -> Future<Result<String>>
190        // All methods async for I/O operations
191        assert!(true);
192    }
193}