1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9pub type SecretId = Uuid;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
14pub struct Secret {
15 #[cfg_attr(feature = "openapi", schema(value_type = String))]
17 pub id: SecretId,
18
19 pub name: String,
21
22 pub description: Option<String>,
24
25 #[serde(skip_serializing)]
27 pub encrypted_value: Vec<u8>,
28
29 pub encryption: EncryptionMetadata,
31
32 pub tags: Vec<String>,
34
35 #[cfg_attr(feature = "openapi", schema(value_type = String))]
37 pub owner_id: Uuid,
38
39 #[cfg_attr(feature = "openapi", schema(value_type = String))]
41 pub created_at: DateTime<Utc>,
42
43 #[cfg_attr(feature = "openapi", schema(value_type = String))]
45 pub updated_at: DateTime<Utc>,
46
47 #[cfg_attr(feature = "openapi", schema(value_type = String))]
49 pub last_accessed_at: Option<DateTime<Utc>>,
50
51 #[cfg_attr(feature = "openapi", schema(value_type = String))]
53 pub expires_at: Option<DateTime<Utc>>,
54
55 pub access_control: AccessControl,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
62pub struct EncryptionMetadata {
63 pub algorithm: String,
65
66 pub kdf: String,
68
69 pub salt: String,
71
72 pub iv: String,
74
75 pub key_version: u32,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
82#[derive(Default)]
83pub struct AccessControl {
84 pub allowed_workflows: Vec<Uuid>,
86
87 pub allowed_users: Vec<Uuid>,
89
90 pub ip_whitelist: Vec<String>,
92
93 pub require_mfa: bool,
95}
96
97impl Secret {
98 pub fn new(
100 name: String,
101 encrypted_value: Vec<u8>,
102 encryption: EncryptionMetadata,
103 owner_id: Uuid,
104 ) -> Self {
105 let now = Utc::now();
106 Self {
107 id: Uuid::new_v4(),
108 name,
109 description: None,
110 encrypted_value,
111 encryption,
112 tags: Vec::new(),
113 owner_id,
114 created_at: now,
115 updated_at: now,
116 last_accessed_at: None,
117 expires_at: None,
118 access_control: AccessControl::default(),
119 }
120 }
121
122 pub fn is_expired(&self) -> bool {
124 if let Some(expires_at) = self.expires_at {
125 Utc::now() > expires_at
126 } else {
127 false
128 }
129 }
130
131 pub fn can_access_workflow(&self, workflow_id: &Uuid) -> bool {
133 if self.access_control.allowed_workflows.is_empty() {
134 return true;
135 }
136 self.access_control.allowed_workflows.contains(workflow_id)
137 }
138
139 pub fn can_access_user(&self, user_id: &Uuid) -> bool {
141 if user_id == &self.owner_id {
142 return true;
143 }
144 if self.access_control.allowed_users.is_empty() {
145 return false;
146 }
147 self.access_control.allowed_users.contains(user_id)
148 }
149
150 pub fn mark_accessed(&mut self) {
152 self.last_accessed_at = Some(Utc::now());
153 }
154
155 pub fn to_safe_view(&self) -> SecretView {
157 SecretView {
158 id: self.id,
159 name: self.name.clone(),
160 description: self.description.clone(),
161 tags: self.tags.clone(),
162 owner_id: self.owner_id,
163 created_at: self.created_at,
164 updated_at: self.updated_at,
165 last_accessed_at: self.last_accessed_at,
166 expires_at: self.expires_at,
167 is_expired: self.is_expired(),
168 }
169 }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
175pub struct SecretView {
176 #[cfg_attr(feature = "openapi", schema(value_type = String))]
177 pub id: SecretId,
178 pub name: String,
179 pub description: Option<String>,
180 pub tags: Vec<String>,
181 #[cfg_attr(feature = "openapi", schema(value_type = String))]
182 pub owner_id: Uuid,
183 #[cfg_attr(feature = "openapi", schema(value_type = String))]
184 pub created_at: DateTime<Utc>,
185 #[cfg_attr(feature = "openapi", schema(value_type = String))]
186 pub updated_at: DateTime<Utc>,
187 #[cfg_attr(feature = "openapi", schema(value_type = String))]
188 pub last_accessed_at: Option<DateTime<Utc>>,
189 #[cfg_attr(feature = "openapi", schema(value_type = String))]
190 pub expires_at: Option<DateTime<Utc>>,
191 pub is_expired: bool,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct SecretReference {
197 pub identifier: String,
199
200 pub is_id: bool,
202
203 pub target_variable: String,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
210pub struct SecretAuditLog {
211 #[cfg_attr(feature = "openapi", schema(value_type = String))]
212 pub id: Uuid,
213
214 #[cfg_attr(feature = "openapi", schema(value_type = String))]
215 pub secret_id: SecretId,
216
217 #[cfg_attr(feature = "openapi", schema(value_type = String))]
218 pub user_id: Option<Uuid>,
219
220 #[cfg_attr(feature = "openapi", schema(value_type = String))]
221 pub workflow_id: Option<Uuid>,
222
223 pub action: SecretAction,
224
225 pub ip_address: Option<String>,
226
227 pub success: bool,
228
229 pub error_message: Option<String>,
230
231 #[cfg_attr(feature = "openapi", schema(value_type = String))]
232 pub timestamp: DateTime<Utc>,
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
237#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
238pub enum SecretAction {
239 Create,
240 Read,
241 Update,
242 Delete,
243 List,
244 Rotate,
245}
246
247impl std::fmt::Display for SecretAction {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 match self {
250 SecretAction::Create => write!(f, "CREATE"),
251 SecretAction::Read => write!(f, "READ"),
252 SecretAction::Update => write!(f, "UPDATE"),
253 SecretAction::Delete => write!(f, "DELETE"),
254 SecretAction::List => write!(f, "LIST"),
255 SecretAction::Rotate => write!(f, "ROTATE"),
256 }
257 }
258}
259
260#[derive(Debug, Serialize, Deserialize)]
262#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
263pub struct CreateSecretRequest {
264 pub name: String,
265 pub value: String,
266 pub description: Option<String>,
267 pub tags: Vec<String>,
268 #[cfg_attr(feature = "openapi", schema(value_type = String))]
269 pub expires_at: Option<DateTime<Utc>>,
270}
271
272#[derive(Debug, Serialize, Deserialize)]
274#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
275pub struct UpdateSecretRequest {
276 pub value: Option<String>,
277 pub description: Option<String>,
278 pub tags: Option<Vec<String>>,
279 #[cfg_attr(feature = "openapi", schema(value_type = String))]
280 pub expires_at: Option<DateTime<Utc>>,
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use chrono::Duration;
287
288 fn create_test_secret() -> Secret {
289 let encryption = EncryptionMetadata {
290 algorithm: "AES-256-GCM".to_string(),
291 kdf: "PBKDF2".to_string(),
292 salt: "base64salt".to_string(),
293 iv: "base64iv".to_string(),
294 key_version: 1,
295 };
296
297 Secret::new(
298 "test_secret".to_string(),
299 vec![1, 2, 3, 4, 5],
300 encryption,
301 Uuid::new_v4(),
302 )
303 }
304
305 #[test]
306 fn test_secret_creation() {
307 let secret = create_test_secret();
308 assert_eq!(secret.name, "test_secret");
309 assert_eq!(secret.encrypted_value, vec![1, 2, 3, 4, 5]);
310 assert_eq!(secret.encryption.algorithm, "AES-256-GCM");
311 assert_eq!(secret.tags.len(), 0);
312 assert_eq!(secret.description, None);
313 assert_eq!(secret.last_accessed_at, None);
314 assert_eq!(secret.expires_at, None);
315 }
316
317 #[test]
318 fn test_secret_not_expired_when_no_expiration() {
319 let secret = create_test_secret();
320 assert!(!secret.is_expired());
321 }
322
323 #[test]
324 fn test_secret_not_expired_when_future_expiration() {
325 let mut secret = create_test_secret();
326 secret.expires_at = Some(Utc::now() + Duration::days(1));
327 assert!(!secret.is_expired());
328 }
329
330 #[test]
331 fn test_secret_expired_when_past_expiration() {
332 let mut secret = create_test_secret();
333 secret.expires_at = Some(Utc::now() - Duration::days(1));
334 assert!(secret.is_expired());
335 }
336
337 #[test]
338 fn test_workflow_access_allowed_when_empty_list() {
339 let secret = create_test_secret();
340 let workflow_id = Uuid::new_v4();
341 assert!(secret.can_access_workflow(&workflow_id));
342 }
343
344 #[test]
345 fn test_workflow_access_allowed_when_in_list() {
346 let mut secret = create_test_secret();
347 let workflow_id = Uuid::new_v4();
348 secret.access_control.allowed_workflows.push(workflow_id);
349 assert!(secret.can_access_workflow(&workflow_id));
350 }
351
352 #[test]
353 fn test_workflow_access_denied_when_not_in_list() {
354 let mut secret = create_test_secret();
355 let allowed_workflow = Uuid::new_v4();
356 let other_workflow = Uuid::new_v4();
357 secret
358 .access_control
359 .allowed_workflows
360 .push(allowed_workflow);
361 assert!(!secret.can_access_workflow(&other_workflow));
362 }
363
364 #[test]
365 fn test_user_access_allowed_for_owner() {
366 let owner_id = Uuid::new_v4();
367 let encryption = EncryptionMetadata {
368 algorithm: "AES-256-GCM".to_string(),
369 kdf: "PBKDF2".to_string(),
370 salt: "base64salt".to_string(),
371 iv: "base64iv".to_string(),
372 key_version: 1,
373 };
374
375 let secret = Secret::new(
376 "test_secret".to_string(),
377 vec![1, 2, 3, 4, 5],
378 encryption,
379 owner_id,
380 );
381
382 assert!(secret.can_access_user(&owner_id));
383 }
384
385 #[test]
386 fn test_user_access_denied_for_non_owner_when_empty_list() {
387 let secret = create_test_secret();
388 let other_user = Uuid::new_v4();
389 assert!(!secret.can_access_user(&other_user));
390 }
391
392 #[test]
393 fn test_user_access_allowed_when_in_list() {
394 let mut secret = create_test_secret();
395 let user_id = Uuid::new_v4();
396 secret.access_control.allowed_users.push(user_id);
397 assert!(secret.can_access_user(&user_id));
398 }
399
400 #[test]
401 fn test_user_access_denied_when_not_in_list() {
402 let mut secret = create_test_secret();
403 let allowed_user = Uuid::new_v4();
404 let other_user = Uuid::new_v4();
405 secret.access_control.allowed_users.push(allowed_user);
406 assert!(!secret.can_access_user(&other_user));
407 }
408
409 #[test]
410 fn test_mark_accessed_updates_timestamp() {
411 let mut secret = create_test_secret();
412 assert_eq!(secret.last_accessed_at, None);
413
414 secret.mark_accessed();
415 assert!(secret.last_accessed_at.is_some());
416
417 let first_access = secret.last_accessed_at.unwrap();
418 std::thread::sleep(std::time::Duration::from_millis(10));
419
420 secret.mark_accessed();
421 let second_access = secret.last_accessed_at.unwrap();
422 assert!(second_access > first_access);
423 }
424
425 #[test]
426 fn test_safe_view_excludes_encrypted_value() {
427 let mut secret = create_test_secret();
428 secret.description = Some("Test description".to_string());
429 secret.tags.push("tag1".to_string());
430 secret.tags.push("tag2".to_string());
431
432 let view = secret.to_safe_view();
433
434 assert_eq!(view.id, secret.id);
435 assert_eq!(view.name, secret.name);
436 assert_eq!(view.description, secret.description);
437 assert_eq!(view.tags, secret.tags);
438 assert_eq!(view.owner_id, secret.owner_id);
439 assert_eq!(view.created_at, secret.created_at);
440 assert_eq!(view.updated_at, secret.updated_at);
441 assert_eq!(view.last_accessed_at, secret.last_accessed_at);
442 assert_eq!(view.expires_at, secret.expires_at);
443 assert_eq!(view.is_expired, secret.is_expired());
444 }
445
446 #[test]
447 fn test_safe_view_reflects_expiration_status() {
448 let mut secret = create_test_secret();
449 secret.expires_at = Some(Utc::now() - Duration::days(1));
450
451 let view = secret.to_safe_view();
452 assert!(view.is_expired);
453 }
454
455 #[test]
456 fn test_secret_action_display() {
457 assert_eq!(SecretAction::Create.to_string(), "CREATE");
458 assert_eq!(SecretAction::Read.to_string(), "READ");
459 assert_eq!(SecretAction::Update.to_string(), "UPDATE");
460 assert_eq!(SecretAction::Delete.to_string(), "DELETE");
461 assert_eq!(SecretAction::List.to_string(), "LIST");
462 assert_eq!(SecretAction::Rotate.to_string(), "ROTATE");
463 }
464
465 #[test]
466 fn test_access_control_default() {
467 let ac = AccessControl::default();
468 assert_eq!(ac.allowed_workflows.len(), 0);
469 assert_eq!(ac.allowed_users.len(), 0);
470 assert_eq!(ac.ip_whitelist.len(), 0);
471 assert!(!ac.require_mfa);
472 }
473
474 #[test]
475 fn test_encryption_metadata_fields() {
476 let encryption = EncryptionMetadata {
477 algorithm: "AES-256-GCM".to_string(),
478 kdf: "PBKDF2".to_string(),
479 salt: "base64salt".to_string(),
480 iv: "base64iv".to_string(),
481 key_version: 1,
482 };
483
484 assert_eq!(encryption.algorithm, "AES-256-GCM");
485 assert_eq!(encryption.kdf, "PBKDF2");
486 assert_eq!(encryption.salt, "base64salt");
487 assert_eq!(encryption.iv, "base64iv");
488 assert_eq!(encryption.key_version, 1);
489 }
490
491 #[test]
492 fn test_secret_reference() {
493 let reference = SecretReference {
494 identifier: "my-api-key".to_string(),
495 is_id: false,
496 target_variable: "api_key".to_string(),
497 };
498
499 assert_eq!(reference.identifier, "my-api-key");
500 assert!(!reference.is_id);
501 assert_eq!(reference.target_variable, "api_key");
502 }
503
504 #[test]
505 fn test_create_secret_request() {
506 let request = CreateSecretRequest {
507 name: "test_secret".to_string(),
508 value: "secret_value".to_string(),
509 description: Some("Test description".to_string()),
510 tags: vec!["tag1".to_string(), "tag2".to_string()],
511 expires_at: None,
512 };
513
514 assert_eq!(request.name, "test_secret");
515 assert_eq!(request.value, "secret_value");
516 assert_eq!(request.description, Some("Test description".to_string()));
517 assert_eq!(request.tags.len(), 2);
518 }
519
520 #[test]
521 fn test_update_secret_request() {
522 let request = UpdateSecretRequest {
523 value: Some("new_value".to_string()),
524 description: Some("New description".to_string()),
525 tags: Some(vec!["new_tag".to_string()]),
526 expires_at: None,
527 };
528
529 assert_eq!(request.value, Some("new_value".to_string()));
530 assert_eq!(request.description, Some("New description".to_string()));
531 assert!(request.tags.is_some());
532 }
533}